blog
blog copied to clipboard
Vue源码探秘(_render 函数)
引言
在上一篇文章的结尾,我们提到了在$mount
函数的最后调用了mountComponent
函数,而mountComponent
函数内又定义了updateComponent
函数:
// src/core/instance/lifecycle.js
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
这里面涉及到_update
和_render
两个函数。本篇文章我们先来分析一下_render
函数。
_render
Vue
的 _render
方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node
。定义在 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;
};
这段代码最关键的是render
方法的调用。我们先来看一下这段代码:
vnode = render.call(vm._renderProxy, vm.$createElement);
这里的vm._renderProxy
是什么呢?
vm._renderProxy
在之前的文章中,我有介绍_init
函数,其中有这么一段代码:
// src/core/instance/init.js
Vue.prototype._init = function(options?: Object) {
//...
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production") {
initProxy(vm);
} else {
vm._renderProxy = vm;
}
// ...
};
表示在生产环境下,vm._renderProxy
就是vm
本身;在开发环境下则调用initProxy
方法,将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;
}
};
hasProxy
是什么呢?看下对它的定义:
// src/core/instance/proxy.js
const hasProxy = typeof Proxy !== "undefined" && isNative(Proxy);
很简单,就是判断一下浏览器是否支持Proxy
。
如果支持就创建一个Proxy
对象赋给vm._renderProxy
;不支持就和生产环境一样直接使用vm._renderProxy
。
如果是在开发环境下并且浏览器支持Proxy
的情况下,会创建一个Proxy
对象,这里的第二个参数handlers
,它的定义是:
// src/core/instance/proxy.js
const handlers =
options.render && options.render._withStripped ? getHandler : hasHandler;
handlers
,是负责定义代理行为的对象。options.render._withStripped
的取值一般情况下都是false
,所以handlers
的取值为hasHandler
我们来看下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;
}
};
hasHandler
对象里面定义了一个has
函数。has
函数的执行逻辑是求出属性查询的结果然后存入 has
,下面的 isAllowed
涉及到一个函数 allowedGlobals
,来看看这个函数:
// src/core/instance/proxy.js
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
);
这里传入了各种js
的全局属性、函数作为makeMap
的参数,其实很容易看出来,allowedGlobals
就是检查key
是不是这些全局的属性、函数其中的任意一个。
所以isAllowed
为true
的条件就是key
是js全局关键字
或者非vm.$data
下的以_
开头的字符串。
如果!has
(访问的key
在vm
不存在)和!isAllowed
同时成立的话,进入if
语句。这里面有两种情况,分别对应两个不同的警告,先来看第一个:
// 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
);
};
警告信息的大致意思是: 在Vue
中,以$
或_
开头的属性不会被代理,因为有可能与内置属性产生冲突。如果你设置的属性以$
或_
开头,那么不能直接通过vm.key
这种形式访问,而是需要通过vm.$data.key
来访问。
第二个警告是针对我们的key
没有在data
中定义:
// 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
)
}
这个报错信息,我想你一定不陌生。就是这种:
到这里,我们就大致把vm._renderProxy
分析完成了,回到上文中这一行代码:
vnode = render.call(vm._renderProxy, vm.$createElement);
我们再来看下vm.$createElement
。
vm.$createElement
vm.$createElement
的定义是在initRender
函数中:
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);
// ...
}
这里我们先省略其他部分代码,只关注中间这两行。这两行是分别给实例vm
加上_c
和$createElement
方法。这两个方法都调用了createElement
方法,只是最后一个参数值不同。
从注释可以很清晰的看出两者的不同,vm._c
是内部函数,它是被模板编译成的 render
函数使用;而 vm.$createElement
是提供给用户编写的 render
函数使用。
为了更好的理解这两个函数,下面看两个例子:
如果我们手动编写render
函数,通常是这样写的:
<div id="app"></div>
<script>
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
},
data() {
return {
message: '森林小哥哥'
}
}
</script>
这里我们编写的 render
函数的参数 createElement
其实就是 vm.$createElement,所以我也可以这么写:
render: function () {
return this.$createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
},
data() {
return {
message: '森林小哥哥'
}
}
如果我们使用字符串模版,那么是这样写的:
<div id="app">{{ message }}</div>
<script>
var app = new Vue({
el: "#app",
data() {
return {
message: "森林小哥哥"
};
}
});
</script>
这种使用字符串模板的情况,使用的就是vm._c
了。
使用字符串模板的话,在相关代码执行完前,会先在页面显示
{{ message }}
,然后再展示森林小哥哥
;而我们手动编写render
函数的话,根据上一节的分析,内部就不用执行把字符串模板转换成render
函数这个操作,并且是空白页面之后立即就显示森林小哥哥
,用户体验会更好。
我们重新回顾下_render
函数:
// 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;
};
这里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
相关部分的源码。
........所以为啥写了两遍
........所以为啥写了两遍
已经去除重复部分了🐶