深入解读vue源码(六)render

基本介绍

所谓render其实是vue的一个私有方法,这个方法用来将实例vm转换成虚拟的node,这个方法的定义在src/core/instance/render.js文件中。


render的实现

首先在源码中的src/core/instance/index.js方法中,能够看到关于renderMixin这个方法。

1
2
3
import { renderMixin } from './render'
...
renderMixin(Vue)

通过导入我们能够找到renderMixin方法的定义,在这个方法里面能够看到Vue原型链上的_render这个私有方法。

1
2
3
Vue.prototype._render = function (): VNode {
...
}

在这段代码中我们能够看到它不接收任何参数,但是它会返回一个VNodeVNodevue在编译后生成的虚拟节点,又叫Virtual DOM

在这个私有方法中,我们来查看他的具体实现方式。

1
2
const vm: Component = this // 从this中拿到vm实例
const { render, _parentVnode } = vm.$options // 从vm.$options中拿到render

继续往下看能够看到_parentVnode私有方法和vm.$vnode的定义,这些在下一篇文章中会详细的介绍,在这里我们只需关注render方法。

对于这个render生成的方式有两种,一种是用户自己去写一个,另一种是通过编译生成一个。继续往下看代码。

1
2
3
4
5
6
7
8
9
let vnode
try {
...
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
...
} finally {
...
}

这段代码中render方法调用call方法的同时传递两个参数,这里我们要知道call方法的第一个参数是上下文环境,在生产环境中,这个call方法的第一个参数代指的就是vm,而vm我们就可以理解为this

call方法中传递的第二个参数在vue源码中是一个方法,这个方法在当前文件的initRender中有定义。

1
2
3
4
5
6
export function initRender (vm: Component) {
...
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // 用于自动编译生成render
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) // 用于手动编写生成render
...
}

initRender中,能够看到这两个方法的定义,通过观察这两个方法都是在调用createElement方法的时候最后一个参数有所不同,通过最后一个参数的不同,决定了方法的作用不同。

这里我将通过vue的实际使用方式来介绍什么是自动编译和手动编写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 自动生成render
<div id="app">
{{msg}}
</div>
<script>
var app = new Vue({
el: '#app',
data() {
return {
msg: 'Hello Vue!'
}
}
})
</script>
// 手动编写render
<div id="app"></div>
<script>
var app = new Vue({
el: '#app',
render(createElement) {
return createElement('div', {
attrs: {
id: 'app1'
}
}, this.msg)
}
data() {
return {
msg: 'Hello Vue!'
}
}
})
</script>

这里有一点需要注意,在上一篇文章中,我们提到了挂载实例是会被替换掉的,所以不能挂载到标签为body和标签为html的节点上的,如下图所示,我们的dom结构中的id属性是为app1,而并不是我们之前定义好的app

回归源码,我们通过实际编写来介绍了createElement方法,接下来我们继续分析vm._renderProxy方法,对于这个方法的入手点,还是我们老生常谈的src/core/instance/index.js这个文件,在这个文件中,能够看到这段代码。

1
import { initMixin } from './init'

点击跳转到src/core/instance/init.js文件中的initMixin下的_init原型链的私有方法,在介绍《深入解读vue源码(四)new Vue》这篇文章的时候,我们基本了解了这个原型链的私有方法,但是还有这样一段代码我们没有分析。

1
2
3
4
5
if (process.env.NODE_ENV !== 'production') {
initProxy(vm) // 开发环境
} else {
vm._renderProxy = vm // 生产环境
}

这里通过对运行环境的判断来进行一些操作,对于生产环境来说_renderProxy就是vm,我们继续分析开发环境下的initProxy方法都做了什么。

首先initProxy方法是定义在src/core/instance/proxy.js中的,在这个文件的底部,我们能看到这样的代码。

1
2
3
4
5
6
7
8
9
10
11
initProxy = function initProxy (vm) {
if (hasProxy) { // 判断浏览器是否支持proxy
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}

这段代码中通过hasProxy来判断浏览器是否支持proxyproxy的作用简单来讲就是对对象访问做一个劫持,在当前主流的浏览器中,基本都是支持proxy的,继续往下看,我们来分析下getHandler这个对象。

1
2
3
4
5
6
7
8
9
10
11
12
const hasHandler = {
has (target, key) {
const has = key in target // 判断key在不在target中,返回布尔类型
const isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)) // allowedGlobals是一些全局的属性和方法,这里是判断key是否属于allowedGlobals中的属性或方法
if (!has && !isAllowed) { // 都不满足的情况下
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key) // 调用warnNonPresent函数,这个函数抛出的是一个警告
}
return has || !isAllowed
}
}

关于warnNonPresent有一个值得讲解的问题,首先我们来看下这个函数。

1
2
3
4
5
6
7
8
9
10
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
)
}

这个函数内的报错信息大家应该不会陌生,这个报错就是因为在template中使用了一个在datapropscomputed中没有声明过的变量。

回到src/core/instance/render.js文件中,我们继续看这段代码。

1
2
3
4
5
6
7
8
9
10
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}

这段代码通过判断上面trycatch语句块中生成的vnode是否是VNode类型,然后继续判断如果vnode是一个数组类型则会抛出一个警告信息,如果为数组类型则代表着有多个VNode的节点生成。


总结

vm._render最终是通过执行createElement方法并返回一个vnode,它是一个Virtual DOM,这个概念也是vue2对比于vue1的升级点。


相关链接

------本文结束,感谢您的阅读,如有问题请通过邮件方式联系作者------