Vue源码学习之VNode

Vue源码学习之VNode

前面三章只是介绍Vue是基于getter/setter依赖收集的机制以及简述了渲染(render)流程;但是对于Vue的虚拟DOM生成和DOM的更新这一个过程还没有探究完成,接下来继续学习探究。

模板tempalte编译

在看Vue的面试题的时候,都会讲述Vue中是怎么实现template编译的?我现在的回答还不是很完善的。

Vue中template通过compile编译成AST,会经过generate函数生成render,执行最后的render后返回VNode

从理论上说,这个答案说的没错;但是里面的是如何实现?还是需要进一步探究。那先从template编译生成AST开始说起:

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
35
36
37
38
39
40
41
42
43
44
45
46
// 首先创建编译函数
/* 编译,将模板template编译成AST */
function compile (template,options) {
var finalOptions = Object.create(baseOptions);
var errors = [];
var tips = [];
finalOptions.warn = function (msg, tip) {
(tip ? tips : errors).push(msg);
};

if (options) {
// merge custom modules
/*合并modules*/
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules);
}
// merge custom directives
/*合并directives*/
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
);
}
// copy other options
/*合并其余的配置项options*/
for (var key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key];
}
}
}
/*模板编译生成AST,AST通过generate得到编译结果*/
var compiled = baseCompile(template, finalOptions);

compiled.errors = errors;
compiled.tips = tips;
return compiled
}

return {
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}

从compile函数中可以看出,其主要的作用是: 一是合并option,二是通过baseCompile函数进行模板template编译。接下来看一下baseCompile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function baseCompile (template,options) {
/*通过parse得到AST*/
var ast = parse(template.trim(), options);

if (options.optimize !== false) {
optimize(ast, options);
}
/*根据generate函数把AST生成code(里面包含render与staticRenderFns)*/
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}

baseCompile会先将模板template进行parse操作得到AST,经过optimize进行优化一波,最后通过generate函数将AST得到renderstaticRenderFns。在后面的过程中会通过函数createFunction生成真正所需的render函数。

虚拟DOM生成

前一章我们提到$mount将Vue实例vm挂载DOM节点上,里面会创建一个用于更新视图的Watcher。其中最重要代码:

1
vm._watcher = new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);

当前的Watcher就是用于当数据发生改变通知视图更新的,在构造函数中,当前的Watcher会执行getter,也就是执行vm._update函数去更新视图。VNode的生成是在vm._render函数执行得到的结果。

1
2
3
4
5
6
7
8
9
10
function initRender(vm){
/*...*/
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
/*...*/
}

// _render函数中代码核心代码 在render函数中会执行到vm._c函数去创建函数,最终调用的函数式_createElement去创建VNode
vnode = render.call(vm._renderProxy, vm.$createElement);

Vue初始化时,会执行initRender函数给vm._c或者wm.$createElement赋值,最终在函数_createElement中完成生成VNode的过程。

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
35
36
37
38
39
/*context Vue实例上下文 tag 标签名 children 子元素 data attributes被解析出来的配置*/
function _createElement (context,tag,data,children,normalizationType) {
/* 省略大段 */
var vnode, ns;
if (typeof tag === 'string') {
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
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 = new VNode(
tag, data, children,
undefined, undefined, context
);
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) { applyNS(vnode, ns); }
if (isDef(data)) { registerDeepBindings(data); }
return vnode
} else {
return createEmptyVNode()
}
}

在创建虚拟DOM中,如果是静态的Tag,就直接创建VNode对象;倘若是自定义组件,那么需要函数createComponent去创建VNode。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
function createComponent (Ctor,data,context,children,tag) {
if (isUndef(Ctor)) {
return
}

var baseCtor = context.$options._base;

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}

// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
return
}

// async component
var asyncFactory;
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor;
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context);
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}

data = data || {};

// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor);

// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data);
}

// extract props
var propsData = extractPropsFromVNodeData(data, Ctor, tag);

// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}

// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
var listeners = data.on;
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn;

if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot

// work around flow
var slot = data.slot;
data = {};
if (slot) {
data.slot = slot;
}
}

// install component management hooks onto the placeholder node
installComponentHooks(data);

// return a placeholder vnode
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);

return vnode
}

在函数createComponent中区分了两种方式的标签,一种是静态的标签,另一种是自定义组件;根据不同的分类通过VNode的构造函数生成不同的VNode,有组件的VNode和原生DOM的VNode;组件级别的VNode会增加data.hook以及componentOptions,其中Ctor为创建组件的constructor,可通过Vue.extend(options)得到。最后Vue.prototype._render完成了VNode的构造。

虚拟DOM转化到DOM更新的过程

下面通过_update函数将VNode转换成真实的DOM

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
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};

里面会调用vm._ptach_函数,将虚拟DOM转换成真实的DOM,在初始化,内部会直接会创建新的节点,调用函数createEle

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// 省略
/*如果是组件虚拟DOM,会用组件的方式创建真实DOM节点*/
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// 获取当前标签的属性
const data = vnode.data
// 获取当前标签的子元素
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// 调用对象nodeOps去创建节点
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// 设置css的权限范围
setScope(vnode)

/* istanbul ignore if */
if (__WEEX__) {
//省略weex
} else {
// 创建子元素,内部递归调用 createElm
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// 调用create钩子 内部调用 cbs.create对象中的方法
// 将虚拟DOM中的attr、class、DOMListener、style等属性映射到真实DOM中
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 调用 nodeOps.appendChild 或者 nodeOps.insertBefore
insert(parentElm, vnode.elm, refElm)
}

if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {// 文本节点
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}

如果是一个组件,那么函数createComponent就会返回true,因此不会进行下面的操作;如果不是,就会进行节点创建工作,同时对子元素进行递归调用创建DOM节点。在创建节点的步骤:

  1. 判断是否为原生虚拟DOM节点,如果不是,就按照组件VNode来创建节点,调用函数createComponent
  2. 如果是,则会通过nodeOps.createElement函数创建DOM节点,
  3. 调用createChildren,递归创建子元素,
  4. 调用invokeCreateHooks,将VNode的attrs、class、DOMListener、style等属性映射到真实DOM上
  5. 调用insert函数,将节点插入到指定的DOM节点上。

那我们看看组件虚拟DOM是怎样创建DOM真实节点的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */);
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
// 会调用data.init.hook,会创建一个子组件的vm实例,同时会将vm实例挂载到vnode.componentInstance上
// 再通过$mount挂载,对页面进行渲染
// 最终子组件生成的DOM 可通过 vnode.componentInstance.$el 拿到
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}

子组件VNode生成真实DOM过程中,需要创建vm实例,同时会对组件进一步的操作,$mount会生成一个监听视图更新的Watcher,同时生成子组件VNode渲染成真实DOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
vnode.data.pendingInsert = null;
}
// 将子组件的真实DOM挂载到vnode.elm上
vnode.elm = vnode.componentInstance.$el;
if (isPatchable(vnode)) {
// 将虚拟DOM中的attr、class、DOMListener、style等属性映射到真实DOM中
invokeCreateHooks(vnode, insertedVnodeQueue);
setScope(vnode);
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode);
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode);
}
}

执行完initComponent,子组件的DOM基本构建完毕,但在函数中主要是将虚拟DOM中的attr、class、DOMListener、style等属性映射到真实DOM中。

在整个Vue的虚拟DOM生成和DOM的更新过程中,首先通过baseCompile函数将模板tempalte先编译成AST,通过generate解析成带render函数的code,最后通过createFunction生成真正所需的render函数。通过_render函数,创建更新视图的RenderWatcher,通过监听调用_update来更新视图,将VNode生成真实的DOM节点。再生成真实的DOM节点中,通过createElmcreateChildren,createComponent等一系列的方法的递归调用,完成了真实DOM的构造,再将DOM将入到#app的DOM节点中。

总结

概述的说,Vue从初始化到视图渲染的过程中主要分为以下步骤:

  1. 通过_render函数获取VNode
  2. 通过createElm函数,调用nodeOps的函数和invokeCreateHooks将各种属性映射到真实DOM
  3. 如果是子组件VNode,将会从第一步开始创建真实DOM
  4. 通过之后一系列的方法完成DOM的构建过程,将DOM挂载在el上。

本文由 Abert 创作,采用 知识共享署名 4.0 国际许可协议。

本站文章除注明转载/出处外,均为本站原创或翻译,转载请务必署名。