想了解一下虚拟DOM是怎么生成的。
我之前简单介绍过DOM是如何解析模板然后渲染的,如果你还不太清楚的话可以跳到我的这篇文章了解一下:《Vue相关指令(一)》,其中有一小节拓展了一下从模板到抽象语法树的过程,然后最那一小节的最后面讲到了:抽象语法树会变成render函数代码字符串。也就是说后面的工作都交给render来做了,render会给我们返回一个虚拟的DOM。
还是先简单解释一下render是干啥的吧~反正我以后也会单独开一篇文章来讲(其实我的文章都是按自己记笔记的顺序来讲的嘿嘿,就一边复习,想到啥就查一查然后写进来,很随意的那种,我怎么又讲废话了..)
在我印象里render就是个又长又臭的东西,当时学他学的很痛苦。对于我们使用者而言他其实和<template>是差不多的功能,但是<template>使用标签的方式去告诉VUE我们要生成什么样的DOM结构,而render是用函数的方式去生成虚拟DOM结构,然后VUE会根据这些虚拟DOM生成我们想要的DOM结构。
例如在模板中:
<h1>{{ blogTitle }}</h1>
在render中就要写成这样的格式:
var vm = new Vue({
el:...,
data:{
blogTitle : "haha"
},
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
})
看起来有点麻烦对吧,但是人家还是有好处的,这里先了解一下他是什么东西就好了。
其实render有两种格式,我们自己写的时候是这样的格式,但是在VUE内部编译的时候往往是那种大长串的代码字符串,在前面的文章中我们已经体验过了..就是用_l,_c拼拼凑凑生成的代码字符串。
那么_render又是什么东西呢?他是怎么生成虚拟DOM的呢?来看看它的源码,这段代码已经被我精简了,提取了核心代码:
//vm._render()调用render函数,会返回一个VNode,在生成VNode的过程中,会动态计算getter,同时推入到dep里面
Vue.prototype._render = function (): VNode {
const vm: Component = this
//拿到选项中的render函数 这里的_parentVnode就是当前组件的父 VNode
const { render, _parentVnode } = vm.$options
//处理作用域插槽
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
vm.$vnode = _parentVnode
let vnode
//执行render函数 得到虚拟节点 其实render函数就是那些代码字符串
try {
//其中render是就是之前讲了几次的,那些_l,_c拼拼凑凑形成的代码字符串啦。或者是我们自己在创建实例的时候写的render函数。
//vm._renderProxy把vm做了一层代理 vm里面有_c等函数,还有data啥的。
//$createElement是给我们自己在data中写render属性的时候用的,我们自己写render的时候第一个参数就是他啦。
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
//...在生成虚拟节点的时候可能会出现一些错误,需要做一些错误处理
}
//挂载vnode父节点,最后返回vnode
vnode.parent = _parentVnode
return vnode
}
执行了render就等于是拿到了虚拟DOM。render已经没啥好讲的了,不是我们自己写的函数就是之前讲过的拼拼凑凑的函数,我们都知道他的真面目的。vm._renderProxy和vm.$createElement还没见过,那就看一看这两个东西~
vm._renderProxy
当我们一执行Vue,就会调用initMixin(initMixin是VUE初始化的入口,初始化一些实例属性和事件,生命周期啥的)对实例进行初始化,这个函数的代码很长,唯一与_renderProxy相关的一句就是initProxy(vm);。所以我们跳过initMixin,直接来看初始化initProxy函数。
initProxy = function initProxy(vm) {
//如果支持proxy
if (hasProxy) {
var options = vm.$options;
//看看有没有用户手写的render函数
//手写的函数中当存在_withStripped时,使用getHandler,否则hasHandler
var handlers = options.render && options.render._withStripped ?
getHandler :
hasHandler;
//操作_renderProxy的时候要先经过代理的拦截
//这层代理会在模板渲染时对一些非法或者不存在的字符串进行判断,做数据的过滤筛选。
vm._renderProxy = new Proxy(vm, handlers);
//如果不支持
} else {
//直接就把vm赋值给_renderProx
vm._renderProxy = vm;
}
};
有时候使用类似webpack这样的打包工具时,我们将使用vue-loader进行模板编译,这个时候options.render 是存在的,并且_withStripped的属性也会设置为true。
这里我们不讨论这种情况哈,我们看看getHandler和hasHandler。
hasHandler
其实这两个东西的名字很直白,handler中的钩子是has就叫hasHandler,钩子是get就叫getHandler。先讲一下hasHandler,他能判断一个对象是否拥有一个属性。
has钩子可以用来拦截with语句下的作用对象,例如:
var obj = {
a: 1
}
var nObj = new Proxy(obj, {
has(target, key) {
console.log(target) // { a: 1 }
console.log(key) // a
return true
}
})
with(nObj) {
a = 2
}
所以我们的render函数执行的时候,由于里面有with函数,函数里面操作vm实例的属性的时候也会被他拦截。
但是只有render函数是自动生成的时候才有with呀,如果是我们手写的呢?我测试了一下,当render是我们手写的时候,除非用了xxx in this; Reflect.has();这样的语句,否则是不会触发拦截器的,所以说拦截器主要是针对于编译生成的render代码。
那他有啥用嘞?先看看他的代码:
const hasHandler = {
has (target, key) {
// 先得到in出来的结果
const has = key in target
// 如果key在allowedGlobals(里面定义了一些全局变量)之内,或者key是以下划线 _ 开头的字符串,则为真
const isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_')
// 如果has和isAllowed都为假,说明真的找不到,这时候使用warnNonPresent函数打印错误
if (!has && !isAllowed) {
// warnNonPresent会通过warn打印一段警告信息说"在渲染的时候引用了key,但是在实例对象上并没有定义 key 这个属性或方法"
warnNonPresent(target, key)
}
// 返回has,没有就返回!isAllowed,因为对于全局对象即使没有在原型链上找到也不需要报错
return has || !isAllowed
}
所以我们举个简单的例子,来演示hasHandler触发的过程。
const vm = new Vue({
el: '#app',
//使用了a
template: '<div>{{a}}</div>',
//但是没有定义a
data: {}
})
//编译的过程中可以看见他输出错误信息
VUE处理template的时候,就会得到一个render渲染函数,大概长这个样子:
vm.$options.render = function () {
// render 函数的 this 指向实例的 _renderProxy
with(this){
return _c('div', [_v(_s(a))]) // 在这里访问 a,相当于访问 vm._renderProxy.a
}
}
然后我们在Vue.prototype._render中执行了render,返回一个VNode。
//Vue.prototype._render
vnode = render.call(vm._renderProxy, vm.$createElement)
再执行render的过程中,由于call的作用,with绑定的作用域变成了vm._renderProxy,所以我们在with中访问的变量都会经过vm._renderProxy的handler,也就是hasHandler。
当在with中访问变量a的时候,出发了hasHandler,由于他真的找不到这个变量,所以执行了warnNonPresent,输出错误。
getHandler
var handlers = options.render && options.render._withStripped ?
getHandler :
hasHandler;
根据上面这段代码,我们可以知道:这个函数会在我们手写了render函数,而且函数中有_withStripped属性的时候才赋值给handlers。也就是说,只有当我们手动设置这个_withStripped属性为true的时候才会触发。
所以说,对于这么一段代码:
var vm = new Vue({
el: '#app',
data: {
test: 1
},
render: function (h) {
return h('div', this.a)
}
})
不会触发hasHandler的拦截,也不会触发getHandler的拦截。既不会报错,也看不到结果。

如果想要在render中得到警告,就要手动设置render._withStripped为true。
const render = function (h) {
return h('div', this.a)
}
render._withStripped = true
var vm = new Vue({
el: '#app',
render,
data: {
test: 1
}
})

这个getHandler函数的代码也是差不多的,都是用来报错的:
const getHandler = {
get (target, key) {
if (typeof key === 'string' && !(key in target)) {
warnNonPresent(target, key)
}
return target[key]
}
}
为什么要这么做呢?因为在webpack配合vue-loader的环境中,vue-loader会借助工具将template编译成不用with语句包裹的形式,然后设置render._withStripped = true。由于他编译生成的render函数都是用"."的方式去访问vm中的变量的,所以无法触发hasHandler函数(就像我们自己写的render一样无法触发),这时候只能设置_withStripped去触发getHandler了。
好了,终于讲完这个vm._renderProxy了,接下来我们看vm.$createElement。
vm.$createElement
我们在源代码中直接找到vm.$createElement定义,在initRender方法中,这个方法也是一个用来初始化VUE实例上的一些属性的方法。里面的东西有点多,有机会再讲吧,就先把createElement挑出来讲。
// 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)
这两个方法都是我们眼熟的,其中_c就是之前拼凑render代码字符串的时候常见的。内个$createElement嘞,是我们手写render的时候传入的参数。
我们之前手写render的时候一般是这样写的:
render: function (createElement) {
return createElement('h2', 'Title')
}
其实我们也可以这样写:
render: function () {
return this.$createElement('h2', 'Title')
}
这只是一个小小的扩展。我们可以看到这两种格式的render都交给createElement这个函数来处理了,只有最后一个参数不一样。
那我们就具体看看createElement是什么吧~
createElement
function createElement(
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
//检测data的类型,判断data是不是数组,是不是基本类型,看看data有没有传入。
if (Array.isArray(data) || isPrimitive(data)) {
//把children移到normalizationType参数的位置
normalizationType = children;
//把data移到children参数的位置
children = data;
//把data赋值为undefined
data = undefined;
}
//当这个参数为true的时候说明是用户手写的
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
//context表示vnode上下文环境,也就是vm实例
//tag是个标签,告诉他希望生成什么节点,可以为字符串,组件,以及函数。
//data代表是vnode数据。
//children表示当前vnode子节点。但是需要被规范为标准的vnode数组。
return _createElement(context, tag, data, children, normalizationType)
}
说实话这个操作把我给看懵了,干啥呀这是,移来移去的,而且看了好几篇文章都说是"把参数往前移动",看得我有点怀疑人生,这不是往后移动吗?哎,真是把我看蒙了。
然后我们又要讲回内个手写的render了,本来其实不是很想讲,但是通过这个理解传参似乎比较快一点。你看呀,不管是手写的render和VUE自动拼接形成的render不都是这么几个参数嘛。所以我们可以直接通过手写render的时候传递的参数来看看,那些a,b,c,d都是些什么。
现在我们来了解在render中调用createElement的时候的完整参数:
createElement(标签名(必需), 与模板中属性对应的数据对象(可选), 子级虚拟节点(可选));
我们之前了解过render的其中一种手写方式,如下:
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
//这个时候传递的参数是createElement(vm, 'h1', this.blogTitle, undefined, undefined, true)
现在看看完整版:
render: function (createElement) {
return createElement('h1',{
//在attrs中,我们可以传入一些写在HTML标签中的特性
attrs: {
id: 'foo'
}
} ,this.blogTitle)
}
//这个时候传递的参数是createElement(vm, 'h1', attrs:{id: 'foo'}, this.blogTitle, undefined, true)
//生成的代码是<h1 id = "foo"> blogTitle对应的值 </h1>
中间的这个参数对象在VUE中叫做数据对象,主要是写一些DOM节点上附加的属性,里面可以写的东西有很多,我就不说了,想看的可以点击链接感受一下:深入数据对象
好啦,我们知道这个数据对象可以传也可以不传之后,就能知道那里移来移去是想干什么了,如果我们没有传递数据对象的话,第二个参数写的就是子节点;如果传递了数据对象的话,第二个参数就是数据对象。所以这里主要是做一个参数的兼容,判断有没有传递数据对象,如果没有传递,就把这样的参数格式:
createElement(vm, 'h1', this.blogTitle, undefined, undefined, true)
变成这样的参数格式:
createElement(vm, 'h1', undefined, this.blogTitle, undefined, true)
这样就能正确调用我们的这个函数,参数都能一一对应:
_createElement(context, tag, data, children, normalizationType)
okk,这些a,b,c搞定了,那第四个参数d是什么东西?这个参数手写render的时候是不会传递的,只有当VUE自动拼接render的时候才会传递,请看我之前那一节举的例子:
_c(
//第一个参数
'div',
//第二个参数
{
attrs: {
"id": "app"
}
},
//第三个参数
_l(
(infos),
function (item, key, index) {
return _c('p', [_v(_s(index) + ":" + _s(key) + ":" + _s(item))])
}),
//第四个参数
0
)
好,知道他会传递了,那他是干嘛的呢?
不急不急~我们先看看_createElement
_createElement
这个函数太长了,我们分开一段一段看:
function _createElement(
context,
tag,
data,
children,
normalizationType
) {
//对data进行校验
//isDef是isDefined的缩写,看他有没有被定义,v !== undefined && v !== null
//看看有没有定义(data).__ob__这个属性
if (isDef(data) && isDef((data).__ob__)) {
warn(
"Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
'Always create fresh vnode data objects in each render!',
context
);
//然后返回一个空节点
return createEmptyVNode()
}
}
__ob__ 会指向一个Observer对象,每个被双向绑定的对象元素(数组也是对象)都会有一个_ob_。所以说一旦我们传入的数据对象是一个已经被双向绑定的对象,就会报错,然后创造一个空节点。
//看看有没有is属性,如果传递了is属性就用is作为标签名字
//但是注释上这里写的是v-bind相关的语法,有点摸不着头脑
if (isDef(data) && isDef(data.is)) {
tag = data.is;
}
//如果没有设置标签的名字,就返回一个空节点
if (!tag) {
return createEmptyVNode()
}
//如果定义了key值而且key值不是基本类型的值,提示:替换成字符串或数字
if (isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
{
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
);
}
}
// support single function children as default scoped slot
// 这一步的意义我还没有弄懂...
// 应该是说支持把数组中第一个函数当作默认作用域插槽,其他的都无视?
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
//如果没传数据就用一个空对象
data = data || {};
//设置为默认作用域插槽
data.scopedSlots = {
default: children[0]
};
//清空子节点
children.length = 0;
}
上面的都是一些七七八八的过滤和处理,接下来是才是重点
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children);
}
simpleNormalizeChildren
export function simpleNormalizeChildren (children: any) {
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
这个方法比较简单,主要就是判断children中的子级是否有数组,有就把children给拍平成一维数组返回。
这个方法会在render函数是通过VUE编译生成的时候调用,主要是针对函数式组件。如果使用了函数式组件,那时候得到的子节点就是一个数组了(一般的组件返回的都是一个根节点),所以会通过数组concat方法把children拍平成一维数组返回。
normalizeChildren
normalizeChildren的调用场景有两种:
- render函数是用户手写的。
- 编译slot、v-for 的时候会产生嵌套数组的情况,就会调用这个方法。
export function normalizeChildren (children: any): ?Array<VNode> {
//判断子节点是不是基本类型
return isPrimitive(children)
//是的话就如果创建一个文字vnode返回
? [createTextVNode(children)]
//判断是不是数组类型
: Array.isArray(children)
//是的话就返回调用normalizeArrayChildren
? normalizeArrayChildren(children)
//否则就返回一个undefined
: undefined
}
createTextVNode方法能创建一个文字vnode返回:
function createTextVNode(val) {
return new VNode(undefined, undefined, undefined, String(val))
}
normalizeArrayChildren,长代码段预警:
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
const res = []
let i, c, lastIndex, last
//遍历处理每一个子节点
for (i = 0; i < children.length; i++) {
c = children[i]
//如果没定义或者是布尔值就跳过
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
if (Array.isArray(c)) {
// 当前子节点为数组
if (c.length > 0) {
//递归调用normalizeArrayChildren,遍历处理他的子节点
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
// 当前子节点和上一个处理的子节点,都是文本节点,就合并成一个
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
} else if (isPrimitive(c)) {
// 当前子节点为基础类型 判断上一个处理的节点是否为文本
if (isTextNode(last)) {
// 是就合并成一个
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
// 不是而且不为空就把这个基本类型转换为文本节点并且push
res.push(createTextVNode(c))
}
} else {
..
}
return res
}
为什么要合并文本节点呢?举个例子来说明:
var app = new Vue({
data: {},
el: '#app',
render(createElement) {
return createElement('div', [1, 2, 3])
}
})
对于这种情况,数组里面有三个元素,这些元素会变成文本节点,作为div对应的文本节点,最终生成<div>123</div>这样的格式。也就是说文本节点是不能单独作为一个节点存在的,所以如果有多个文本节点(1,2,3)一定要拼接在一起,作为一个文本节点(123),然后再交给一个标签(div),作为他的子节点。
所以当子节点是数组的时候,也要考虑合并文本节点的情况,场景如下:
render(createElement) {
//子节点是一个数组
return createElement('div', [1, 2, 3].map((item) => {
//数组里面都是p节点,节点的内容是文本的数组,要被合并成一个文本节点
return createElement("p", [item, item, item])
}))
}
最终生成的结果如下:
<p>111</p>
<p>222</p>
<p>333</p>
okk,回到之前的代码中。接下来要处理一些其他情况,如果当前子节点为vnode节点或者是一个普通对象就会进入到这个else块:
} else {
// 判断子节点是否为文本节点 上一个处理的是否为文本节点
if (isTextNode(c) && isTextNode(last)) {
// 是就合并成一个
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// default key for nested array children (likely generated by v-for)
// 判断该节点的属性 并且为他生成默认的key值
// 判断有没有_isVList标记,表示renderList处理成功
if (isTrue(children._isVList) &&
//定义了标签
isDef(c.tag) &&
//没有定义key值
isUndef(c.key) &&
//传递了nestedIndex
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__`
}
//将该节点加入到结果中
res.push(c)
}
}
}
针对上面这块代码有几点要讲一下:
-
为什么上面已经处理过了基本类型的文本节点,这里还要再判断一次文本节点呢?其实主要是处理这种情况:如果该节点是一个普通对象,那么如果里面写了text属性,则渲染结果就是text属性,如下:
var app = new Vue({ data: { items: { text: "xixi" } }, el: '#app', render(createElement) { return createElement('div', [this.items]) } }) //最终渲染结果为xixi -
_isVList其实就是在执行renderList函数的时候的标记。renderList的代码我们之前已经看过了,就是处理v-for的处理函数,也就是"_l"。如果renderList处理成功就会添加一个_isVList标记,值为true。
-
nestedIndex是在遍历的子节点是数组的时候传入的,他表示嵌套的索引,对应的代码如下:c = normalizeArrayChildren(c, ${nestedIndex || ''}_${i})最后生成的key值是这样的形式:
key: "__vlist_1_2__" //表示在vlist渲染列表中的第1个子节点的第2个元素最后,经过对children的规范化,children变成了一个里面全是VNode的数组。接下来我们讨论下VNode的创建。
我们回到_createElement来,看看他如何生成vnode节点:
if (typeof tag === 'string') {
let Ctor
// 命名空间处理
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 判断 tag 是否为html原生的保留标签
if (config.isReservedTag(tag)) {
vnode = new VNode(
//platform built-in elements 创建平台保留标签
config.parsePlatformTagName(tag),
data, children, undefined, undefined, context
)
// 是否能从 vm.$options.components 中获取组件相关信息
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 创建component组件 这个createComponent先不研究了,以后有时间再补充
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
// 未知标签 创建vnode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
}
而 tag、data、children就是createElement方法中传递的参数。
VNode
参考文章:【Vue原理】VNode - 源码版
我们发现几乎所有的节点都是交给VNode来生成的,所以我们可以看一下VNode这个构造函数:
class VNode {
constructor (
tag,
data,
children,
text,
elm,
context,
componentOptions,
asyncFactory
) {
// 当前节点标签名
this.tag = tag
// 当前节点数据对象
this.data = data
// 当前节点子节点
this.children = children
// 当前节点文本
this.text = text
// 当前节点对应的真实DOM节点
this.elm = elm
// 当前节点命名空间
this.ns = undefined
// 当前节点上下文
this.context = context
// 函数化组件上下文
this.fnContext = undefined
// 函数化组件配置项
this.fnOptions = undefined
// 函数化组件ScopeId
this.fnScopeId = undefined
// 子节点key属性
this.key = data && data.key
// 组件配置项
this.componentOptions = componentOptions
// 保存组件生成的实例
this.componentInstance = undefined
// 当前节点父节点
this.parent = undefined
// 是否为原生HTML或只是普通文本
this.raw = false
// 静态节点标志
// 当数据变化的时候,可以忽略去比对他,以提高比对效率
this.isStatic = false
// 是否作为根节点插入
this.isRootInsert = true
// 是否为注释节点
this.isComment = false
// 是否为克隆节点
this.isCloned = false
// 是否有v-once指令
this.isOnce = false
// 异步组件的工厂方法
this.asyncFactory = asyncFactory
// 异步源
this.asyncMeta = undefined
// 是否异步的预赋值
this.isAsyncPlaceholder = false
}
}
反正就是存储一些节点信息的对吧~举个例子来看看他
<div class="parent" style="height:0" href="2222">
111111
</div>
对应的(简洁版)VNode如下,主要提取了一些关键信息。
{
tag: 'div',
data: {
attrs:{href:"2222"},
staticClass: "parent",
staticStyle: {
height: "0"
}
},
children: [{
tag: undefined,
text: "111111"
}]
//elm: div#app.parent 为什么要打注释呢 因为这里在markdown上会报错
}
好啦,这样就可以描述这些节点了。就可以根据这些信息生成真实的DOM节点了。
再解释一下几个组件相关的属性:
-
parent,表示是组件的外壳节点,例如:
components: { test: { template: "<div>haha</div>" } }页面中使用组件:
<div> <test></test> </div>这时候会生成两种VNode,其中页面生成的VNode长这样:
{ tag: "test", children: undefined } //对应的html是<test></test>组件内部生成的VNode长这样:
{ tag: "h2", children: [{ tag: undefined, text: "haha" }] } //对应的html是<div>haha</div>这时候,第一个VNode就是第二个VNode的外壳节点
//组件内部的VNode的parent属性 parent: VNode { tag: "vue-component-1-test" }第一个VNode会作为父组件和子组件的关联,用于保存一些父组件传给子组件的数据。
-
componentOptions,放了prop,事件,插槽之类的东西。
<div> <test @event="name = 1" :name = "name">1111</test> </div>//形成的componentOptions如下 componentOptions: Ctor: f VueComponent(options) //保存slot children: [VNode] //保存事件 listeners: {event: f} //保存 props propsData: {name: "111"} tag: "test"
VNode的存放
在实例的_vnode属性中,存放了VUE生成的VNode节点。他可以用来比对更新,如果我们的数据变化了,会生成一个新的VNode,然后和这个旧的_vnode进行对比,就能得到要更新的节点。
在组件实例中会有一个$vnode,因为如果是组件实例的话,_vnode中存放的是外壳节点。
捋一遍流程
最后看一看整个流程吧,对于下面的例子,他是怎么生成虚拟DOM的?
<div id="app">
<div href="xxxx">{{test}}</div>
</div>
-
在
_render函数中,执行render.call根据前面几篇文章的描述,我们已经了解render代码字符串是怎么生成的了,这里就直接拿了。针对上面的标签,会生成这样的render代码:
with(this){ return _c('div', {attrs:{"href":"xxxx"}}, ["1"] ) }从这里开始执行_c函数。
-
我们知道_c函数其实就是
createElement函数,所以开始执行:createElement(vm, 'div', {attrs:{"href":"xxxx"}}, ["1"], undefined, false) -
由于我们传参没毛病,所以直接执行
_createElement(vm, 'div', {attrs:{"href":"xxxx"}}, ["1"], undefined) -
由于我们的tag是一个字符串,所以执行了new VNode
vnode = new VNode( 'div', {attrs:{"href":"xxxx"}}, ["1"], undefined, undefined, vm ); -
生成VNode虚拟节点并且进行挂载