彻底搞懂虚拟Dom到真实Dom的⽣成过程
再有⼀棵树形结构的JavaScript对象后,我们现在需要做的就是将这棵树跟真实的Dom树形成映射关系,⾸先简单回顾之前遇到的mountComponent⽅法:
export function mountComponent(vm, el) {
vm.$el = el
...
callHook(vm, 'beforeMount')
...
const updateComponent = function () {
vm._update(vm._render())
}
...
}
我们已经执⾏完了vm._render⽅法拿到了VNode,现在将它作为参数传给vm._update⽅法并执⾏。vm._update这个⽅法的作⽤就是就是将VNode转为真实的Dom,不过它有两个执⾏的时机:
⾸次渲染
当执⾏new vue到此时就是⾸次渲染了,会将传⼊的VNode对象映射为真实的Dom。
更新页⾯
数据变化会驱动页⾯发⽣变化,这也是vue最独特的特性之⼀,数据改变之前和之后会⽣成两份VNode进⾏⽐较,⽽怎么样在旧的VNode上做最⼩的改动去渲染页⾯,这样⼀个diff算法还是挺复杂的。如再没有先说清楚数据响应式是怎么回事之前,⽽直接讲diff对理解vue的整体流程并不太好。所以我们这章分析完⾸次渲染后,下⼀章就是数据响应式,之后才是diff⽐对,如此排序,万望理解。
我们现在先来看下vm._update⽅法的定义:
Vue.prototype._update = function(vnode) {
... ⾸次渲染
vm.$el = vm.__patch__(vm.$el, vnode) // 覆盖原来的vm.$el
...
}
这⾥的vm.$el是之前在mountComponent⽅法内就挂载的,⼀个真实Dom元素。⾸次渲染会传⼊vm.$el以及得到的VNode,所以看下vm.__patch__定义:
Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules })
__patch__是createPatchFunction⽅法内部返回的⼀个⽅法,它接受⼀个对象:
nodeOps属性:封装了操作原⽣Dom的⼀些⽅法的集合,如创建、插⼊、移除这些,再使⽤到的地⽅再详解。
modules属性:创建真实Dom也需要⽣成它的如class/attrs/style等属性。modules是⼀个数组集合,数组的每⼀项都是这些属性对应的钩⼦⽅法,这些属性的创建、更新、销毁等都有对应钩⼦⽅法,当某⼀时刻需要做某件事,执⾏对应的钩⼦即可。⽐如它们都有create这个钩⼦⽅法,如将这些create钩⼦收集到⼀个数组内,需要在真实Dom上创建这些属性时,依次执⾏数组的每⼀项,也就是依次创建了它们。
Ps: 这⾥modules属性内的钩⼦⽅法是区分平台的,web、weex以及SSR它们调⽤VNode⽅法⽅式并不相同,所以vue在这⾥⼜使⽤了函数柯⾥化这个骚操作,在createPatchFunction内将平台的差异化抹平,从⽽__patch__⽅法只⽤接收新旧node即可。
⽣成Dom
惠州翻译这⾥⼤家记住⼀句话即可,⽆论VNode是什么类型的节点,只有三种类型的节点会被创建并插⼊到的Dom中:元素节点、注释节点、和⽂本节点。
我们接着来看下createPatchFunction它究竟返回⼀个什么样的⽅法:
export function createPatchFunction(backend) {
...
const { modules, nodeOps } = backend // 解构出传⼊的集合
return function (oldVnode, vnode) { // 接收新旧vnode
...
const isRealElement = deType) // 是否是真实Dom
if(isRealElement) { // $el是真实Dom
oldVnode = emptyNodeAt(oldVnode) // 转为VNode格式覆盖⾃⼰
}
...
}
}
⾸次渲染时没有oldVnode,oldVnode就是$el,⼀个真实的dom,经过emptyNodeAt(oldVnode)⽅法包装:
function emptyNodeAt(elm) {
return new VNode(
memorial day
nodeOps.tagName(elm).toLowerCa(), // 对应tag属性
{}, // 对应data
[], // 对应children
undefined, //对应text
elm // 真实dom赋值给了elm属性
)
}
包装后的:
{
tag: 'div',
elm: '<div id="app"></div>' // 真实dom
}
-
------------------------------------------------------
nodeOps:
export function tagName (node) { // 返回节点的标签名
return node.tagName
}
再将传⼊的$el属性转为了VNode格式之后,我们继续:
export function createPatchFunction(backend) {
...
return function (oldVnode, vnode) { // 接收新旧vnode
const inrtedVnodeQueue = []
...
const oldElm = oldVnode.elm //包装后的真实Dom <div id='app'></div>
const parentElm = nodeOps.parentNode(oldElm) // ⾸次⽗节点为<body></body>lo过去式
createElm( // 创建真实Dom
vnode, // 第⼆个参数
inrtedVnodeQueue, // 空数组
parentElm, // <body></body>
)
return vnode.elm // 返回真实Dom覆盖vm.$el
}
}
-
-----------------------------------------------------
nodeOps:
export function parentNode (node) { // 获取⽗节点
return node.parentNode
}
export function nextSibling(node) { // 获取下⼀个节点
Sibing
}
createElm⽅法开始⽣成真实的Dom,VNode⽣成真实的Dom的⽅式还是分为元素节点和组件两种⽅式,所以我们使⽤上⼀章⽣成的VNode 分别说明。
1. 元素节点⽣成Dom
{ // 元素节点VNode
tag: 'div',
家长会校长发言稿children: [{
tag: 'h1',
children: [
{text: 'title h1'}
]
}, {
2011格莱美
tag: 'h2',
children: [
{text: 'title h2'}
]
}, {
tag: 'h3',
children: [
{text: 'title h3'}
}
⼤家可以先看下这个流程图有⼀个印象即可,接下来再看具体实现时相信思路会清晰很多:
开始创建Dom,我们来看下它的定义:
function createElm(vnode, inrtedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
...
const children = vnode.children // [VNode, VNode, VNode]
const tag = vnode.tag // div
if (createComponent(vnode, inrtedVnodeQueue, parentElm, refElm)) {
return // 如果是组件结果返回true,不会继续,之后详解createComponent
}
if(isDef(tag)) { // 元素节点
vnode.elm = ateElement(tag) // 创建⽗节点
createChildren(vnode, children, inrtedVnodeQueue) // 创建⼦节点
inrt(parentElm, vnode.elm, refElm) // 插⼊
} el if(isTrue(vnode.isComment)) { // 注释节点
vnode.elm = ) // 创建注释节点
inrt(parentElm, vnode.elm, refElm); // 插⼊到⽗节点
} el { // ⽂本节点
vnode.elm = ) // 创建⽂本节点
inrt(parentElm, vnode.elm, refElm) // 插⼊到⽗节点
}
...
}
------------------------------------------------------------------
nodeOps:
export function createElement(tagName) { // 创建节点
ateElement(tagName)
}
export function createComment(text) { //创建注释节点
怎么快速祛斑ateComment(text)
}
export function createTextNode(text) { // 创建⽂本节点
ateTextNode(text)
}
function inrt (parent, elm, ref) { //插⼊dom操作
if (isDef(parent)) { // 有⽗节点
if (isDef(ref)) { // 有参考节点
photograph
if (ref.parentNode === parent) { // 参考节点的⽗节点等于传⼊的⽗节点
nodeOps.inrtBefore(parent, elm, ref) // 在⽗节点内的参考节点之前插⼊elm
}
} el {
nodeOps.appendChild(parent, elm) // 添加elm到parent内
}
} // 没有⽗节点什么都不做
}
这算⼀个⽐较重要的⽅法,因为很多地⽅会⽤到。
王长喜英语预测试卷
依次判断是否是元素节点、注释节点、⽂本节点,分别创建它们然后插⼊到⽗节点⾥⾯,这⾥主要介绍创建元素节点,另外两个并没有复杂的逻辑。我们来看下createChild⽅法定义:
function createChild(vnode, children, inrtedVnodeQueue) {
if(Array.isArray(children)) { // 是数组
for(let i = 0; i < children.length; ++i) { // 遍历vnode每⼀项
createElm( // 递归调⽤
children[i],
inrtedVnodeQueue,
vnode.elm,
null,
汽车装潢培训true, // 不是根节点插⼊
children,
i
)
}
} el if()) { //typeof为string/number/symbol/boolean之⼀
nodeOps.appendChild( // 创建并插⼊到⽗节点
vnode.elm,
-------------------------------------------------------------------------------
nodeOps:
export default appendChild(node, child) { // 添加⼦节点
node.appendChild(child)
}
开始创建⼦节点,遍历VNode的每⼀项,每⼀项还是使⽤之前的createElm⽅法创建Dom。如果某⼀项⼜是数组,继续调⽤createChild创建某⼀项的⼦节点;如果某⼀项不是数组,创建⽂本节点并将它添加到⽗节点内。像这样使⽤递归的形式将嵌套的VNode全部创建为真实的Dom。
再看⼀遍流程图,相信⼤家疑惑已经减少很多:
简单来说就是由⾥向外的挨个创建出真实的Dom,然后插⼊到它的⽗节点内,最后将创建好的Dom插
⼊到body内,完成创建的过程,元素节点的创建还是⽐较简单的,我们接下来看下组件是怎么创建的。
2. 组件VNode⽣成Dom
{ // 组件VNode
tag: 'vue-component-1-app',
context: {...},
componentOptions: {
Ctor: function(){...}, // ⼦组件构造函数
propsData: undefined,
children: undefined,
tag: undefined,
children: undefined
},
data: {
on: undefined, // 原⽣事件
hook: { // 组件钩⼦
init: function(){...},
inrt: function(){...},
prepatch: function(){...},
destroy: function(){...}
}
}
}
-
------------------------------------------
<template> // app组件内模板
<div>app text</div>
</template>
⾸先还是看张简易流程图,留个印象即可,⽅便理清之后的逻辑顺序:
我们使⽤上⼀章组件⽣成的VNode,看下在createElm内创建组件Dom分⽀逻辑是怎么样的:
function createElm(vnode, inrtedVnodeQueue, parentElm, refElm) {
...
if (createComponent(vnode, inrtedVnodeQueue, parentElm, refElm)) { // 组件分⽀
return
}
.
..
执⾏createComponent⽅法,如果是元素节点不会返回任何东西,所以是undefined,会继续⾛接下来的创建元素节点的逻辑。现在是组件,我们看下createComponent的实现:
function createComponent(vnode, inrtedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if(isDef(i)) {
if(isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode) // 执⾏init⽅法
}
...
}
}
⾸先会将组件的vnode.data赋值给i,是否有这个属性就能判断是否是组件vnode。之后的if(isDef(i = i.hook) && isDef(i = i.init))集判断和赋值为⼀体,if内的i(vnode)就是执⾏的组件init(vnode)⽅法。这个时候我们来看下组件的init钩⼦⽅法做了什么:
import activeInstance // 全局变量
const init = vnode => {
const child = ponentInstance =
createComponentInstanceForVnode(vnode, activeInstance)
...
}
activeInstance是⼀个全局的变量,再update⽅法内赋值为当前实例,再当前实例做__patch__的过程中作为⼦组件的⽗实例传⼊,在⼦组件的initLifecycle时构建组件关系。将createComponentInstanceForVnode执⾏的结果赋值给了ponentInstance,所以看下它的返回的结果是什么:
export createComponentInstanceForVnode(vnode, parent) { // parent为全局变量activeInstance
const options = { // 组件的options
_isComponent: true, // 设置⼀个标记位,表明是组件
_parentVnode: vnode,
parent // ⼦组件的⽗vm实例,让初始化initLifecycle可以建⽴⽗⼦关系
}
return ponentOptions.Ctor(options) // ⼦组件的构造函数定义为Ctor
}
再组件的init⽅法内⾸先执⾏createComponentInstanceForVnode⽅法,这个⽅法的内部就会将⼦组件的构造函数实例化,因为⼦组件的构造函数继承了基类Vue的所有能⼒,这个时候相当于执⾏new Vue({...}),接下来⼜会执⾏_init⽅法进⾏⼀系列的⼦组件的初始化逻辑,我们回到_init⽅法内,因为它们之间还是有些不同的地⽅:
Vue.prototype._init = function(options) {
if(options && options._isComponent) { // 组件的合并options,_isComponent为之前定义的标记位
initInternalComponent(this, options) // 区分是因为组件的合并项会简单很多
}
initLifecycle(vm) // 建⽴⽗⼦关系
...
callHook(vm, 'created')
if (vm.$options.el) { // 组件是没有el属性的,所以到这⾥咋然⽽⽌
vm.$mount(vm.$options.el)
}
}
-
---------------------------------------------------------------------------------------
function initInternalComponent(vm, options) { // 合并⼦组件options
const opts = vm.$options = structor.options)
opts.parent = options.parent // 组件init赋值,全局变量activeInstance
opts._parentVnode = options._parentVnode // 组件init赋值,组件的vnode
...
}
前⾯都还执⾏的好好的,最后却因为没有el属性,所以没有挂载,createComponentInstanceForVnode⽅法执⾏完毕。这个时候我们回到组件的init⽅法,补全剩下的逻辑:
const init = vnode => {
const child = ponentInstance = // 得到组件的实例
createComponentInstanceForVnode(vnode, activeInstance)
child.$mount(undefined) // 那就⼿动挂载呗
}
我们在init⽅法内⼿动挂载这个组件,接着⼜会执⾏组件的_render()⽅法得到组件内元素节点VNode,然后执⾏vm._update(),执⾏组件的__patch__⽅法,因为$mount⽅法传⼊的是undefined,oldVnode也是undefined,会执⾏__patch__内的这段逻辑:
return function patch(oldVnode, vnode) {
...
if (isUndef(oldVnode)) {
createElm(vnode, inrtedVnodeQueue)
}
...
}
这次执⾏createElm时没有传⼊第三个参数⽗节点的,那组件创建好的Dom放哪⽣效了?没有⽗节点也要⽣成Dom不是,这个时候执⾏的是组件的__patch__,所以参数vnode就是组件内元素节点的vnode了:
<template> // app组件内模板
<div>app text</div>
</template>
-------------------------
{ // app内元素vnode
tag: 'div',
children: [
{text: app text}
],