在 上一章中我们知道了 updateComponent 其实是先调用 vm._render 生成 VNode,最终调用 vm._update 更新 DOM。
在分析 _render 函数之前,我们先来看看如何在 Vue 中使用 render 函数的。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode。
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
}
正文开始
vm._render 定义在 src/core/instance/render.js 文件中
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
// slot相关逻辑
}
// 允许 render 函数能访问到占位符vnode 的 data 数据
vm.$vnode = _parentVnode
let vnode
try {
// 无需维护一个栈,因为所有的 render 函数是彼此分开调用的
// 嵌套组件的 render 函数在父组件 patched 时被调用
currentRenderingInstance = vm
// 核心:调用 render 函数
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// 错误处理...
} finally {
currentRenderingInstance = null
}
// 允许返回的数组仅包含一个节点
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// set parent
vnode.parent = _parentVnode
return vnode
}
核心逻辑就是调用了 render 函数,传入 vm.$createElement 参数,然后将 this 绑定为 vm._renderProxy,最终返回一个 vnode。
下面来介绍一下 vm._renderProxy 和 vm.$createElement
1. vm._renderProxy
vm._renderProxy 如果在生产环境下,其实就是 vm ,如果在开发环境下,就是 Proxy 对象。其在 _init 方法中定义。
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
// ...
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// ...
}
生产环境下调用了 initProxy 函数. initProxy 专门定义在 src/core/instance/proxy.js中
// src/core/instance/proxy.js
initProxy = function initProxy (vm) {
// 判断浏览器是否支持proxy
if (hasProxy) {
// 根据 _withStripped 来决定使用哪个 proxy handler
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
- 首先判断浏览器是否支持
Proxy,如果支持的话,判断_withStripped属性_withStripped为真:Proxy handler采用getHandler方法_withStripped为假:Proxy handler采用hasHandler方法。
- 不支持的话直接将
vm赋值给_renderProxy
1.1. getHandler
// src/core/instance/proxy.js
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]
}
}
主要作用当读取代理对象的属性时,如果属性不在真实的 vm 实例中,针对不同的情况抛出警告
- 情况一:
key不在vm中,但是在vm.$data中,说明我们自定义的数据以$或_开头,这种情况是不会被数据代理,也就是直接通过this.$xx访问会得到undefined(关于数据代理这里有介绍) - 情况二:确实不在
vm中,就抛出一个警告
1.2. hasHandler
// src/core/instance/proxy.js
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
}
}
- 首先使用
in操作符判断该属性是否在vm实例上存在。 - 判断属性是否可用:当是 特殊属性(Number,Array等) 或者 下划线开头的且不在$data中的字符串 则为可用。
- 如果属性在
vm中不存在,且属性不可用,则抛出警告。 - 返回
has || !isAllowed,!isAllowed表示如果属性不可用的话就当成属性存在。
1.3. 为什么根据 _withStripped 来使用hasHandler或getHandler呢?
这其实是为了处理不同情况下的 render 函数
在这之前,我们先明确两点,当
proxy.foo访问时会触发Proxy的get,当with(proxy) { (foo); }会触发Proxy的has。详细查看 MDN
情况一
对于非单文件组件,使用 el 或者 templete 来创建组件的方式,vue 会解析 template 生成 render,形如
vm.$options.render = function () {
with (this) {
// 这里的 _c 是 vm._c,下文有介绍
return _c(/*...*/)
}
}
我们访问 _c 会触发 Proxy 的 has,也就是上面的 hasHandler
情况二
但是对于 单文件组件(SFC) 而言,vue-loader 工具将 template 编译成严格模式下是不包含 with 的代码,但是会为编译后的 render 设置 render._withStripped = true,问题,编译后的 render 长这样:
var render = function() {
var _vm = this;
var _h = _vm.$createElement;
var _c = _vm._self._c || _h;
return _c(...)
}
这时我们通过 _vm.xx 的形式访问属性,则会触发 Proxy 的 get,也就是 getHandler
2. vm.$createElement
render 函数中的 createElement 方法就是 vm.$createElement 方法,定义在src/core/instance/render.js
// src/core/instance/render.js
export function initRender (vm: Component) {
// ...
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// ...
}
vm._c是内部函数,它是被模板编译成的render函数使用vm.$createElement是提供给用户编写的render函数使用的
他们两个其实都是执行了 createElement 函数,有关此函数将单独放到一个章节来介绍
总结
vm._render 最终是通过执行 createElement 方法并返回的是 vnode
