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方法的实现原理。