Vue.prototype._render = function(): VNode {
const vm: Component = this;
const { render, _parentVnode } = vm.$options;
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode;
// render self
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`);
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production" && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(
vm._renderProxy,
vm.$createElement,
e
);
} catch (e) {
handleError(e, vm, `renderError`);
vnode = vm._vnode;
}
} else {
vnode = vm._vnode;
}
} finally {
currentRenderingInstance = null;
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0];
}
// return empty vnode in case the render function errored out
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();
}
// set parent
vnode.parent = _parentVnode;
return vnode;
};
// src/core/instance/proxy.js
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
);
};
// src/core/instance/proxy.js
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
)
}
function initRender(vm: Component) {
// ...
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
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);
// ...
}
// src/core/instance/render.js
Vue.prototype._render = function(): VNode {
const vm: Component = this;
const { render, _parentVnode } = vm.$options;
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode;
// render self
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`);
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production" && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(
vm._renderProxy,
vm.$createElement,
e
);
} catch (e) {
handleError(e, vm, `renderError`);
vnode = vm._vnode;
}
} else {
vnode = vm._vnode;
}
} finally {
currentRenderingInstance = null;
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0];
}
// return empty vnode in case the render function errored out
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();
}
// set parent
vnode.parent = _parentVnode;
return vnode;
};
引言
在上一篇文章的结尾,我们提到了在
$mount
函数的最后调用了mountComponent
函数,而mountComponent
函数内又定义了updateComponent
函数:这里面涉及到
_update
和_render
两个函数。本篇文章我们先来分析一下_render
函数。_render
Vue
的_render
方法是实例的一个私有方法,它用来把实例渲染成一个虚拟Node
。定义在src/core/instance/render.js
文件中:这段代码最关键的是
render
方法的调用。我们先来看一下这段代码:这里的
vm._renderProxy
是什么呢?vm._renderProxy
在之前的文章中,我有介绍
_init
函数,其中有这么一段代码:表示在生产环境下,
vm._renderProxy
就是vm
本身;在开发环境下则调用initProxy
方法,将vm
作为参数传入,来看下initProxy
函数:hasProxy
是什么呢?看下对它的定义:很简单,就是判断一下浏览器是否支持
Proxy
。如果支持就创建一个
Proxy
对象赋给vm._renderProxy
;不支持就和生产环境一样直接使用vm._renderProxy
。如果是在开发环境下并且浏览器支持
Proxy
的情况下,会创建一个Proxy
对象,这里的第二个参数handlers
,它的定义是:我们来看下
hasHandler
:hasHandler
对象里面定义了一个has
函数。has
函数的执行逻辑是求出属性查询的结果然后存入has
,下面的isAllowed
涉及到一个函数allowedGlobals
,来看看这个函数:这里传入了各种
js
的全局属性、函数作为makeMap
的参数,其实很容易看出来,allowedGlobals
就是检查key
是不是这些全局的属性、函数其中的任意一个。所以
isAllowed
为true
的条件就是key
是js全局关键字
或者非vm.$data
下的以_
开头的字符串。如果
!has
(访问的key
在vm
不存在)和!isAllowed
同时成立的话,进入if
语句。这里面有两种情况,分别对应两个不同的警告,先来看第一个:警告信息的大致意思是: 在
Vue
中,以$
或_
开头的属性不会被代理,因为有可能与内置属性产生冲突。如果你设置的属性以$
或_
开头,那么不能直接通过vm.key
这种形式访问,而是需要通过vm.$data.key
来访问。第二个警告是针对我们的
key
没有在data
中定义:这个报错信息,我想你一定不陌生。就是这种:
到这里,我们就大致把
vm._renderProxy
分析完成了,回到上文中这一行代码:我们再来看下
vm.$createElement
。vm.\$createElement
vm.$createElement
的定义是在initRender
函数中:这里我们先省略其他部分代码,只关注中间这两行。这两行是分别给实例
vm
加上_c
和$createElement
方法。这两个方法都调用了createElement
方法,只是最后一个参数值不同。从注释可以很清晰的看出两者的不同,
vm._c
是内部函数,它是被模板编译成的render
函数使用;而vm.$createElement
是提供给用户编写的render
函数使用。为了更好的理解这两个函数,下面看两个例子:
如果我们手动编写
render
函数,通常是这样写的:这里我们编写的
render
函数的参数createElement
其实就是 vm.\$createElement,所以我也可以这么写:如果我们使用字符串模版,那么是这样写的:
这种使用字符串模板的情况,使用的就是
vm._c
了。我们重新回顾下
_render
函数:这里
vm.$createElement
被作为参数给了render
函数,最后会返回一个VNode
,我们直接跳过catch
和finally
,来到最后。判断
vnode
是数组并且长度为 1 的情况下,直接取第一项。如果
vnode
不是VNode
类型(一般是由于用户编写不规范导致渲染函数出错),就去判断vnode
是不是数组,如果是的话抛出警告(说明用户的template
包含了多个根节点)。并创建一个空的VNode
给到vnode
。最后返回vnode
。总结
到这里,
_render
函数的大致流程就分析完成了。vm._render
最终是通过执行createElement
方法并返回的是vnode
,它是一个虚拟 Node
。Vue 2.0
相比Vue 1.0
最大的升级就是利用了Virtual DOM
。最后呢,我先抛出一个问题给到大家:为什么
Vue
要限制template
只能有一个根节点呢?其实这个问题是与上文最后提到的
VNode
和Virtual DOM
相关的。下一篇文章中呢,我将带大家一块来看下Virtual DOM
相关部分的源码。