汪图南
  • RAG

    • RAG
  • 快速入门
  • 高级技巧
前端面试之道
  • 打包工具

    • Webpack
    • Rollup
  • TypeScript

    • TypeScript基础
    • TypeScript类型挑战
  • CSS预编译器

    • SASS
  • 自动化测试

    • Vue应用测试
  • Vue2.0源码分析
  • Vue3.0源码分析
  • 数据结构和算法(基础)
  • LeetCode(刷题)
  • JavaScript书籍

    • 你不知道的JavaScript(上)
    • 你不知道的JavaScript(中下)
    • JavaScript数据结构和算法
    • JavaScript设计模式与开发实践
    • 深入理解ES6
  • Git书籍

    • 精通Git
Github
  • RAG

    • RAG
  • 快速入门
  • 高级技巧
前端面试之道
  • 打包工具

    • Webpack
    • Rollup
  • TypeScript

    • TypeScript基础
    • TypeScript类型挑战
  • CSS预编译器

    • SASS
  • 自动化测试

    • Vue应用测试
  • Vue2.0源码分析
  • Vue3.0源码分析
  • 数据结构和算法(基础)
  • LeetCode(刷题)
  • JavaScript书籍

    • 你不知道的JavaScript(上)
    • 你不知道的JavaScript(中下)
    • JavaScript数据结构和算法
    • JavaScript设计模式与开发实践
    • 深入理解ES6
  • Git书籍

    • 精通Git
Github
  • 介绍

    • 介绍和参考
  • 源码目录设计和架构设计

    • 设计
  • Rollup构建版本

    • Rollup基础知识
    • Vue中的Rollup构建
  • 从入口到构造函数整体流程

    • 整体流程
    • initGlobalAPI流程
    • initMixin流程
    • stateMixin流程
    • eventsMixin流程
    • lifecycleMixin流程
    • renderMixin流程
  • 响应式原理

    • 介绍
    • 前置核心概念
    • props处理
    • methods处理
    • data处理
    • computed处理
    • watch处理
    • 深入响应式原理
    • 依赖收集
    • 派发更新
    • nextTick实现原理
    • 变化侦测注意事项
    • 变化侦测API实现
  • 虚拟DOM和VNode

    • 虚拟DOM
    • VNode介绍
    • Diff算法
  • 组件化

    • 介绍
    • $mount方法
    • render和renderProxy
    • createElement
    • createComponent
    • 合并策略
    • update和patch
    • 组件生命周期
    • 组件注册
  • 编译原理

    • 介绍
    • compileToFunctions
    • parse模板解析
    • optimize优化
    • codegen代码生成
  • 扩展

    • 扩展
    • directive指令
    • filter过滤器
    • event事件处理
    • v-model
    • 插槽
    • Keep-Alive
    • Transition
    • Transition-Group
    • Vue.use插件机制
  • Vue-Router

    • 介绍
    • 路由安装
    • matcher介绍
    • 路由切换
    • 内置组件
    • 路由hooks钩子函数
  • Vuex

    • 介绍
    • Vuex安装
    • Vuex初始化
    • Vuex辅助API
    • Store实例API

render和renderProxy

介绍完$mount后,我们来看一下render以及renderProxy相关的逻辑,这一节的主要目标是:弄清楚renderProxy的作用以及render的实现原理。

renderProxy

我们在之前介绍的initMixin方法中,有下面这样一段代码:

import { initProxy } from './proxy'
export default initMixin (Vue) {
  Vue.prototype._init = function () {
    // ...
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // ...
  }
}

initProxy是定义在src/core/instance/proxy.js文件中的一个方法,其代码如下:

let initProxy
initProxy = function initProxy (vm) {
  if (hasProxy) {
    // determine which proxy handler to use
    const options = vm.$options
    const handlers = options.render && options.render._withStripped
      ? getHandler
      : hasHandler
    vm._renderProxy = new Proxy(vm, handlers)
  } else {
    vm._renderProxy = vm
  }
}

代码分析:

  • 这个方法首先判断了当前环境是否支持原生Proxy,如果支持则创建一个Proxy代理,其中hasProxy是一个boolean值,它的实现逻辑如下:
const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy)
  • 然后根据options.render和options.render._withStripped的值来选择使用getHandler还是hasHandler,当使用vue-loader解析.vue文件时,这个时候options.render._withStripped为真值,因此选用getHandler。当选择使用compiler版本的Vue.js时,我们的入口文件中根实例是这样定义的:
import Vue from 'vue'
import App from './App'
new Vue({
  el: '#app',
  components: { App },
  template: '<App/>'
})

这个时候,对于根实例而言其options.render._withStripped为undefined,因此使用hasHandler。在搞清楚什么时候使用getHandler和hasHandler后,我们可能会有另外的问题: getHandler和hasHandler是干什么的?怎么触发?

在回答第一个问题之前,我们先来看一下getHandler和hasHandler的定义:

const allowedGlobals = makeMap(
  'Infinity,undefined,NaN,isFinite,isNaN,' +
  'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
  'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
  'require' // for Webpack/Browserify
)

const warnNonPresent = (target, key) => {
  warn(
    `Property or method "${key}" is not defined on the instance but ` +
    'referenced during render. Make sure that this property is reactive, ' +
    'either in the data option, or for class-based components, by ' +
    'initializing the property. ' +
    'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
    target
  )
}

const warnReservedPrefix = (target, key) => {
  warn(
    `Property "${key}" must be accessed with "$data.${key}" because ` +
    'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
    'prevent conflicts with Vue internals. ' +
    'See: https://vuejs.org/v2/api/#data',
    target
  )
}

const hasHandler = {
  has (target, key) {
    const has = key in target
    const isAllowed = allowedGlobals(key) ||
      (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
    if (!has && !isAllowed) {
      if (key in target.$data) warnReservedPrefix(target, key)
      else warnNonPresent(target, key)
    }
    return has || !isAllowed
  }
}

const getHandler = {
  get (target, key) {
    if (typeof key === 'string' && !(key in target)) {
      if (key in target.$data) warnReservedPrefix(target, key)
      else warnNonPresent(target, key)
    }
    return target[key]
  }
}

我们可以看到,getHandler和hasHandler所做的事情几乎差不多,都是在渲染阶段对不合法的数据做判断和处理。对于warnNonPresent而言,它提示我们在模板中使用了未定义的变量;对于warnReservedPrefix而言,它提示我们不能定义带$或者_开头的变量,因为这样容易和一些内部的属性相互混淆。

<template>
  {{msg1}}
  {{$age}}
</template>
<script>
// msg1报错
// $age报错
export default {
  data () {
    return {
      msg: 'message',
      $age: 23
    }
  }
}
</script>

紧接着,我们第二个问题:getHandler和hasHandler如何触发?这其实涉及到一点ES6 Proxy方面的知识,我们以下面这段代码为例来进行说明:

const obj = {
  a: 1,
  b: 2,
  c: 3
}
const proxy = new Proxy(obj, {
  has (target, key) {
    console.log(key)
    return key in target
  },
  get (target, key) {
    console.log(key)
    return target[key]
  }
})

// 触发getHandler,输出a
proxy.a 

// 触发hasHandler,输出 b c
with(proxy){
  const d = b + c
}

在以上代码中,我们定义了一个proxy代理,当我们访问proxy.a的时候,根据Proxy相关的知识会触发getHandler,因此会输出a。当我们使用with访问proxy的时候,在其中任何属性的访问都会触发hasHandler,因此会输出b和c。

在以上代码分析完毕后,我们就可以对initProxy的作用进行一个总结:在渲染阶段对不合法的数据做判断和处理。

render

在之前的代码中,我们在mountComponent中遇到过下面这样一段代码:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

在这一节,我们来分析一下_render函数的实现,它其实是在src/core/instance/render.js文件中被定义:

export function renderMixin (Vue) {
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    // ...省略代码
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
    }
    // ...省略代码
    vnode.parent = _parentVnode
    return vnode
  }
}

其中通过$options解构出来的render,就是我们实例化的时候提供的render选择或者通过template编译好的render函数。在_render代码中,最重要的一步是render.call函数的调用,render函数执行后会返回VNode,VNode会在之后的处理过程中使用到。

我们在render.call方法调用的时候,除了传递我们的renderProxy代理,还传递了一个$createElement函数,其中这个函数是在initRender方法中被定义:

export function initRender (vm) {
  // ...省略代码
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  // ...省略代码
}

我们发现,vm.$createElement和vm._c的函数定义是差不多的,唯一的区别是在调用createElement方法的时候,传递的最后一个参数不相同。$createElement和_c方法虽然方法定义差不多,但使用场景是不一样的,$createElement通常是用户手动提供的render来使用,而_c方法通常是模板编译生成的render来使用的。

根据render函数的定义,我们可以把template例子改写成使用render的形式:

<template>
  <div id="app">
    {{msg}}
  </div>
</template>
<script>
export default () {
  data () {
    return {
      msg: 'message'
    }
  }
}
</script>

render改写后:

export default {
  data () {
    return {
      msg: 'message'
    }
  },
  render: ($createElement) {
    return  $createElement('div', {
      attrs: {
        id: 'app'
      }
    }, this.message)
  }
}

在这一小节,我们分析了render的实现,在下一小节我们将深入学习createElement方法的实现原理。

最后更新时间: 2025/5/6 15:36
贡献者: wangtunan
Prev
$mount方法
Next
createElement