最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。
前言
Vue有两个核心思想,一个是数据驱动,简单来说就是通过模板和数据渲染成最终的 DOM ,具体是如何实现在上一篇🚩Vue源码——模板和数据如何渲染成最终的DOM中详细地介绍过了。
另外一个是组件化,谓组件化,就是把一个页面拆分成多个组件,这些组件是独立的,可复用的,可嵌套的,等这些组件开发完成后,像搭积木一样拼装成一个页面。
本文会在上一篇的基础上来详细介绍在 Vue 中组件如何渲染成最终的 DOM ,其过程与通过模板和数据渲染成最终的 DOM 有何不同。
先创建一个简单的 demo ,基于这个设定的场景来研究。
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const aa ={
template:'<div><p>{{aa}}<span>{{bb}}</span></p></div>',
data(){
return{
aa:'欢迎',
bb:'Vue'
}
}
}
var app = new Vue({
el: '#app',
render: h =>h(aa)
})
</script>
</html>
回顾上一篇模板和数据渲染成最终的 DOM 的逻辑流程图
跟组件渲染成最终的 DOM 的逻辑流程图对比。
从
new Vue() 到new Watcher()过程都是一样的。因为在上一篇 demo 中 render 方法是编译生成,在本文 demo 中 render 方法是用户手写的render: h =>h(aa),所以从vm.render开始不一样,那么从vm.render开始介绍组件如何渲染成最终的 DOM 。
一、vm._render
vm._update(vm._render(), hydrating),在此处打个断点,按F11进入vm._render()方法中**。
Vue.prototype._render = function() {
var vm = this;
var ref = vm.$options;
var render = ref.render;
var _parentVnode = ref._parentVnode;
vm.$vnode = _parentVnode;
var vnode;
try {
currentRenderingInstance = vm;
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
//...
} finally {
currentRenderingInstance = null;
}
return vnode;
}
此时的vnode = render.call(vm._renderProxy, vm.$createElement)中的render是用户手写的render方法render: h =>h(aa),其中h是vm.$createElement。
vm.$createElement是在 Vue 初始化中通过initRender(vm)定义的。
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);
};
}
可以看到调用vm.$createElement实际上是调用createElement(vm, a, b, c, d, true)。
vnode = render.call(vm._renderProxy, vm.$createElement) 在此处打个断点,按三次F11进入createElement方法中。
1、_createElement
var SIMPLE_NORMALIZE = 1;
var ALWAYS_NORMALIZE = 2;
function createElement(context, tag, data, children, normalizationType, alwaysNormalize) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType)
}
要注意参数alwaysNormalize为true,故normalizationType有值为2。
最后调用_createElement,此处打个断点,按F11进入_createElement方法中。
function _createElement(context, tag, data, children, normalizationType) {
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children);
}
var vnode;
if (typeof tag === 'string') {
//...
} else {
vnode = createComponent(tag, data, context, children);
}
return vnode
}
从设定的场景中,从render: h =>h(aa)可以得知,参数data、参数children为 undefined ,故即是参数normalizationType为 2 等于ALWAYS_NORMALIZE,也可以忽略children = normalizeChildren(children)的逻辑过程。
参数tag为组件 aa 的选项对象。
const aa ={
template:'<div><p>{{aa}}<span>{{bb}}</span></p></div>',
data(){
return{
aa:'欢迎',
bb:'Vue'
}
}
}
故直接走 else 中的逻辑,调用createComponent生成组件类型的vnode。vnode = createComponent(tag, data, context, children),在此处打个断点,按F11进入createComponent方法中。
2、createComponent
function createComponent(Ctor, data, context, children, tag) {
var baseCtor = context.$options._base;
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
data = data || {};
installComponentHooks(data);
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context, {
Ctor: Ctor,
tag: tag,
children: children
}
);
return vnode
}
以上代码有三个关键步骤,构造子类构造函数,安装组件钩子函数和实例化 VNode 。
1、构造子类构造函数baseCtor.extend
var baseCtor = context.$options._base;
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
_base在initGlobalAPI函数中定义。
function initGlobalAPI(Vue) {
Vue.options._base = Vue;
}
又在this._init(options)中,把options合并到$options上。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
所以baseCtor实际上就是 Vue 构造函数,再来看一下Vue.extend函数的定义。
var ASSET_TYPES = ['component','directive','filter'];
Vue.extend = function(extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
var name = extendOptions.name || Super.options.name;
if (name) {
validateComponentName(name);
}
var Sub = function VueComponent(options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
Sub.options = mergeOptions(
Super.options,
extendOptions
);
Sub['super'] = Super;
if (Sub.options.props) {
initProps$1(Sub);
}
if (Sub.options.computed) {
initComputed$1(Sub);
}
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.use = Super.use;
ASSET_TYPES.forEach(function(type) {
Sub[type] = Super[type];
});
if (name) {
Sub.options.components[name] = Sub;
}
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);
cachedCtors[SuperId] = Sub;
return Sub
}
Vue.extend的作用就是创建一个 Vue 的子类 Sub。
var Sub = function VueComponent(options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
这里采用原型链继承和借用构造函数继承的组合继承方式,创建一个继承于 Vue 的子类 Sub 并返回。
借用构造函数继承好理解,在子类 Sub 的构造函数VueComponent(options)中调用父类 Vue 的初始化方法this._init(options)。这样实例化子类 Sub 时就会执行this._init(options),就再次走到 Vue 的初始化过程。
原型链继承为什么不采用Sub.prototype = new Vue(),因为这样做,有个缺点创建子类 Sub 实例时,要调用两次父类 Vue。
采用Sub.prototype = Object.create(Super.prototype)来实现原型链继承,不会再去调用父类 Vue。然后再把子类 Sub 的构造器constructor重新指向Sub。
var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
Sub.cid = cid++;
cachedCtors[SuperId] = Sub;
return Sub
在创建过程中,对子类 Sub 做了缓存,避免多次执行Vue.extend时对同一个组件重复创建子类 Sub。
2、安装组件钩子函数installComponentHooks
data = data || {};
installComponentHooks(data);
installComponentHooks此处打个断点,按F11进入installComponentHooks方法中。
var componentVNodeHooks = {
init: function init(vnode, hydrating) {
//...
},
prepatch: function prepatch(oldVnode, vnode) {
//...
},
insert: function insert(vnode) {
//...
},
destroy: function destroy(vnode) {
//...
}
};
var hooksToMerge = Object.keys(componentVNodeHooks);
function mergeHook(f1, f2) {
var merged = function(a, b) {
f1(a, b);
f2(a, b);
};
merged._merged = true;
return merged
}
function installComponentHooks(data) {
var hooks = data.hook || (data.hook = {});
for (var i = 0; i < hooksToMerge.length; i++) {
var key = hooksToMerge[i];
var existing = hooks[key];
var toMerge = componentVNodeHooks[key];
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
}
}
}
整个installComponentHooks的过程就是把componentVNodeHooks中的钩子函数合并到data.hook中,在合并过程中,如果某个钩子函数已经存在data.hook中,通过mergeHook方法做合并,在mergeHook方法中,返回一个依次执行这两个钩子函数的函数,即完成合并。
此小节要记住data.hook存储了组件的钩子函数。
3、new VNode
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context, {
Ctor: Ctor,
tag: tag,
children: children
}
);
return vnode
先来看一下 VNode类的构造函数,忽略掉跟设定的场景无关的代码。
var VNode = function VNode(tag, data, children, text, elm, context, componentOptions, asyncFactory) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.context = context;
this.key = data && data.key;
this.componentOptions = componentOptions;
this.componentInstance = undefined;
}
需要注意组件的vnode和普通元素节点的vnode不同,组件的vnode是没有children的。
执行完vm._render()生成vnode,回到vm._update中,分析vnode如何生成真实 DOM 。
二、vm._update
Vue.prototype._update = function(vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
} else {
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
}
执行var prevVnode = vm._vnode,vm._vnode是当前 Vue 实例生成的 Virtual DOM ,在设定的场景中是首次渲染,此时vm._vnode为 undefined ,故prevVnode为 undefined ,再执行vm._vnode = vnode,把当前 Vue 实例生成的 Virtual DOM 赋值给vm._vnode。
因为prevVnode为 undefined ,故执行vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false).
在上一篇文章中,介绍了vm.__patch__是如何定义的。
执行vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false),最终是调用patch方法,在此处打个断点,按F11进入patch方法中。
1、patch
function patch(oldVnode, vnode, hydrating, removeOnly) {
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {} else {
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(vnode, insertedVnodeQueue, parentElm, nodeOps.nextSibling(oldElm))
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {}
}
}
return vnode.elm
}
- 参数
oldVnode:上一次的 Virtual DOM ,设定的场景中的值为vm.$el,是<div id="app"></div>DOM 对象; - 参数
vnode:这一次的 Virtual DOM ; - 参数
hydrating:在非服务端渲染情况下为false,可以忽略; - 参数
removeOnly: 是在transition-group场景下用,设定场景中没有,为false,可以忽略。
如果oldVnode不是 Virtual DOM 而是 DOM 对象,要把oldVnode用emptyNodeAt转成一个 Virtual DOM,并在其属性elm赋值上被转换的 DOM 对象,所以oldElm等同vm.$el,在用nodeOps.parentNode(oldElm)获取oldElm的父级 DOM 节点,此时为 body。
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
//nodeOps.parentNode
function parentNode (node) {
return node.parentNode
}
执行createElm(vnode, insertedVnodeQueue, parentElm, nodeOps.nextSibling(oldElm))生成真实 DOM ,在此处打个断点,按F11进入createElm方法中,nodeOps.nextSibling(oldElm)取oldElm的下一个兄弟节点。
2、createElm
function createElm( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
}
- 参数
vnode: Virtual DOM; - 参数
insertedVnodeQueue:钩子函数队列; - 参数
parentElm: 参数vnode对应 DOM 对象的父节点 DOM 对象; - 参数
refElm: 占位节点对象,例如,参数vnode对应 DOM 对象的下个兄弟节点;
在设定场景中,是要把组件渲染成 DOM ,会调用createComponent方法,在此处打个断点,按F11进入createComponent方法中。
3、createComponent
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false);
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
执行完var i = vnode.data;isDef(i = i.hook) && isDef(i = i.init),此时i 为vnode.data.hook中的init方法。init方法在componentVNodeHooks中定义,通过installComponentHooks方法将其合并到data.hook中。
i(vnode, false),在此处打个断点,按F11进入init方法中。
此外还有注意到insert(parentElm, vnode.elm, refElm)这句代码,先提一下这句代码的作用是把组件内容生成的 DOM 插入父节点中,后面会详细介绍。
4、componentVNodeHooks.init
var componentVNodeHooks = {
init: function init(vnode, hydrating) {
if (vnode.componentInstance
&&!vnode.componentInstance._isDestroyed
&& vnode.data.keepAlive) {
//...
} else {
var child = vnode.componentInstance =
createComponentInstanceForVnode( vnode, activeInstance);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
}
vnode.componentInstance的含义是组件实例,此时组件实例还没创建,故vnode.componentInstance为 undefined,走 else 部分逻辑。通过createComponentInstanceForVnode方法创建一个 Vue 实例 child,调用$mount方法挂载组件。
createComponentInstanceForVnode( vnode, activeInstance)在此处打个断点,按F11进入createComponentInstanceForVnode方法。
5、createComponentInstanceForVnode
function createComponentInstanceForVnode(vnode, parent) {
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
var inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
return new vnode.componentOptions.Ctor(options)
}
- 参数
vnode: 要渲染的组件生成的 Virtual DOM。 - 参数
parent: 要渲染的组件的父 Vue 实例,也就是上下文环境。
还记得在createComponent方法中,通过Ctor = baseCtor.extend(Ctor)创建了一个 Vue 的子类(组件)构造函数赋值给Ctor,然后在实例化 Vnode 中,通过 Vnode 构造函数的参数传递给vnode的属性componentOptions。所以vnode.componentOptions.Ctor就是要渲染组件的构造函数,new一下来生成组件实例。
new vnode.componentOptions.Ctor(options),在此处打个断点,按F11进入。会发现走到Vue.extend方法中的
var Sub = function VueComponent (options) {
this._init(options);
}
执行this._init(options),进行组件构造函数的初始化,又回到 Vue 构造函数的初始化,按F11进入this._init中,开始介绍组件内容的渲染过程
6、组件内容的渲染过程
1、this._init
Vue.prototype._init = function(options) {
var vm = this;
if (options && options._isComponent) {
initInternalComponent(vm, options);
} else {
//...
}
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}
因为在createComponentInstanceForVnode方法中设置了options = {_isComponent : true},
故options._isComponent为true,执行initInternalComponent(vm, options)来合并options,在此次打个断点,进入initInternalComponent方法中。
2、initInternalComponent
function initInternalComponent(vm, options) {
var opts = vm.$options = Object.create(vm.constructor.options);
var parentVnode = options._parentVnode;
opts.parent = options.parent;
opts._parentVnode = parentVnode;
var vnodeComponentOptions = parentVnode.componentOptions;
opts.propsData = vnodeComponentOptions.propsData;
opts._parentListeners = vnodeComponentOptions.listeners;
opts._renderChildren = vnodeComponentOptions.children;
opts._componentTag = vnodeComponentOptions.tag;
if (options.render) {
opts.render = options.render;
opts.staticRenderFns = options.staticRenderFns;
}
}
还记得用Vue.extend方法创建组件的构造函数时,有执行以下一段代码
Vue.extend = function(extendOptions){
Sub.options = mergeOptions(
Super.options,
extendOptions
);
}
在createComponent方法中执行Ctor = baseCtor.extend(Ctor)中调用Vue.extend,参数Ctor为 demo 中 aa 组件的选项对象,即extendOptions的值为
{
template:'<div><p>{{aa}}<span>{{bb}}</span></p></div>',
data(){
return{
aa:'欢迎',
bb:'Vue'
}
}
}
经过mergeOptions方法合并,可以通过构造函数的属性options访问到 demo 中 aa 组件的选项对象。那么执行var opts = vm.$options = Object.create(vm.constructor.options)就可以用vm.$options获取到 demo 中 aa 组件的选项对象。
执行opts.parent = options.parent;opts._parentVnode = parentVnode;把之前通过createComponentInstanceForVnode函数传入的几个参数合并到vm.$options。
vm.$options.parentVnode: 要渲染的组件生成的 Virtual DOM。vm.$options.parent: 要渲染的组件的父 Vue 实例,也就是上下文环境。
合并options完毕后回到this._init中执行if (vm.$options.el){vm.$mount(vm.$options.el)},由于组件选项对象中没有el,在这里不执行vm.$mount挂载。
回到componentVNodeHooks.init钩子函数中,执行child.$mount(hydrating ? vnode.elm : undefined, hydrating) ,这里不是服务端,故hydrating为false,相当于执行child.$mount(undefined, false),对组件进行挂载,在此打个断点,按F11进入child.$mount方法中。
3、child.$mount
var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el, hydrating) {
var options = this.$options;
if (!options.render) {
var template = options.template;
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
//...
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
//...
}
} else if (el) {
template = getOuterHTML(el);
}
if (template) {
var ref = compileToFunctions(template, {
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render;
options.staticRenderFns = staticRenderFns;
}
}
return mount.call(this, el, hydrating)
};
在initInternalComponent方法中,把组件选项对象合并到this.$options,执行var template = options.template,
template为<p>{{aa}}<span>{{bb}}</span></p>。因options.render为 undefined ,故调用compileToFunctions方法把template转成成 render 方法,并挂载到this.$options上。
执行return mount.call(this, el, hydrating),在此处打个断点,按F11进入mount方法。
Vue.prototype.$mount = function (el,hydrating) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
这里要注意此时的el为 undefined。执行return mountComponent(this, el, hydrating),在此处打个断点,按F11进入mountComponent方法。
4、mountComponent
function mountComponent(vm, el, hydrating) {
vm.$el = el;
var updateComponent;
updateComponent = function() {
vm._update(vm._render(), hydrating);
};
new Watcher(vm, updateComponent, noop, {
before: function before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true);
hydrating = false;
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
此时vm代表的是组件实例,不是Vue实例。另外此时el为 undefined,故vm.$el为 undefined,这个要记住后面过程中会用到。
实例化一个渲染Watcher,初始化的时候会执行回调函数,即执行vm._update(vm._render(), hydrating),按F11进入vm._render方法。
5、vm._render
Vue.prototype._render = function() {
var vm = this;
var ref = vm.$options;
var render = ref.render;
var _parentVnode = ref._parentVnode;
vm.$vnode = _parentVnode;
var vnode;
vnode = render.call(vm._renderProxy, vm.$createElement);
vnode.parent = _parentVnode;
return vnode
};
vm.$vnode表示 Vue 实例的父 Virtual DOM,其值vm.$options._parentVnode是在执行createComponentInstanceForVnode(vnode, parent)时,内部有段代码var options = { _parentVnode: vnode,},再通过initInternalComponent合并到vm.$options上,其中的vnode是当前要渲染的组件生成的 Virtual DOM,那么相对于组件的内容就是父 Virtual DOM,也可以叫作组件实例的父 Virtual DOM,如下图所示。
此时的 render 方法,如下所示
(function anonymous() {
with(this) {
return _c('div', [_c('p', [_v(_s(aa)), _c('span', [_v(_s(bb))])])])
}
})
执行vnode = render.call(vm._renderProxy, vm.$createElement),把组件的内容生成vnode(Virtual DOM) 树,其过程可以看上一篇文章。
执行vnode.parent = _parentVnode把组件内容的父 Virtual DOM,赋值给vnode.parent,最后返回vnode,如下图所示。
按F11进入vm._update方法。
6、vm._update
Vue.prototype._update = function(vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
} else {
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
};
首次渲染,执行 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false),
注意此时,传入vm.__patch__的参数vm.$el是 undefined,最终是调用patch方法,按F11进入patch方法中。
7、patch
function patch(oldVnode, vnode, hydrating, removeOnly) {
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
oldVnode 为 undefined,故执行createElm(vnode, insertedVnodeQueue),在此处打个断点,按F11进入createElm方法。
8、createElm
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
vnode.elm = nodeOps.createElement(tag, vnode);
createChildren(vnode, children, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
} else if (isTrue(vnode.isComment)) {
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
createElm方法用来创建真实的 DOM 节点,并插入对应的父节点。详解介绍可以上看上一篇内容。
执行createChildren(vnode, children, insertedVnodeQueue),按F11进入createChildren方法。
9、createChildren
function createChildren(vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
checkDuplicateKeys(children);
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
}
}
createChildren的逻辑很简单,实际上是遍历vnode子 Virtual DOM,递归调用createElm,这是一种常用的深度优先的遍历算法,在遍历过程中会把vnode.elm作为 的 children[i](Virtual DOM)对应真实 DOM 的父节点传入。
当children不是数组时。判断vnode.text是否是基础类型,若是调用nodeOps.createTextNode生成一个纯文本节点,再调用nodeOps.appendChild插入到vnode.elm中。
递归调用createElm中如果当前已经没有子 Virtual DOM,执行insert(parentElm, vnode.elm, refElm)把生成的 DOM (vnode.elm) 插入到对应父节点(parentElm)中,因为是递归调用,子 Virtual DOM 会优先调用insert,所以整个 Virtual DOM 树生成真实 DOM 后的插入顺序是先子后父。
在insert(parentElm, vnode.elm, refElm)处打个断点,按F11进入insert方法。
10、insert
function insert(parent, elm, ref$$1) {
if (isDef(parent)) {
if (isDef(ref$$1)) {
if (nodeOps.parentNode(ref$$1) === parent) {
nodeOps.insertBefore(parent, elm, ref$$1);
}
} else {
nodeOps.appendChild(parent, elm);
}
}
}
- 参数
parent:要插入节点的父节点 - 参数
elm: 要插入节点 - 参数
ref$$1:参考节点,会在参考节点前插入
nodeOps.insertBefore对应insertBefore方法,nodeOps.appendChild对应appendChild方法,
function insertBefore (parentNode, newNode, referenceNode) {
parentNode.insertBefore(newNode, referenceNode);
}
function appendChild (node, child) {
node.appendChild(child);
}
insertBefore方法和appendChild方法其实就是调用原生 DOM 的 API 进行 DOM 操作。
11、回到createElm
等遍历完vnode所有子 Virtual DOM,执行insert(parentElm, vnode.elm, refElm)时,因为在patch中是这么调用createElm,执行createElm(vnode, insertedVnodeQueue),只传递两个参数,故parentElm 为 undefined,那么生成真实的 DOM 树要怎么插到对应的父节点呢?
7、回到createComponent
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);
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
在执行i(vnode, false)中,执行了var child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance),所以vnode.componentInstance有值为当前组件实例。
执行initComponent(vnode, insertedVnodeQueue),按F11进入initComponent方法。
function initComponent(vnode, insertedVnodeQueue) {
vnode.elm = vnode.componentInstance.$el;
}
vnode.componentInstance.$el是在vm._updat方法中执行vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)赋值的,vm为当前组件实例。
这里的vnode为组件生成的 Virtual DOM ,不是组件内容生成的 Virtual DOM 。
执行insert(parentElm, vnode.elm, refElm),parentElm为组件的父节点,这里为 body,这样就把组件内容生成的 DOM 树插入到body中。
三、总结
对比一下数据和模板(普通元素标签)渲染成 DOM 的流程,在组件渲染成 DOM 的流程中,执行vm.render过程中,调用createComponent方法生成一个组件类型的vnode,在其中调用Vue.extend创建一个继承于 Vue 的组件构造函数。执行vm.updata过程中,调用createElm方法将这个 Virtual DOM 生产真实的 DOM ,在其中执行createComponent方法时执行组件的钩子函数init将组件构造函数实例化,重新走渲染成 DOM 的流程。如果组件的内容都是普通元素标签时,则走数据和模板渲染成 DOM 的流程,在生成真实的 DOM 树后要回到createComponent方法中调用insert方法插入到组件的父节点中渲染到页面上。如果组件的内容包含组件标签则又开始走组件渲染成 DOM 的流程,直到所有子组件的内容都是普通元素标签时,才会回到createComponent方法中调用insert方法插入到父组件的父节点中渲染到页面上。