上一篇 vue 原理解析(四): 虚拟Dom 是怎么生成的
再有一颗树形结构的Javascript对象后, 我们需要做的就是讲这棵树跟真实Dom树形成映射关系。我们先回顾之前的mountComponnet 方法:
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, 不过它有两个执行时机:
首次渲染
更新页面
先来看看vm._update方法的定义:
Vue.prototype._update = function(vnode) {
... 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode) // 覆盖原来的vm.$el
...
}
这里的 vm. e l 是 之 前 在 = = m o u n t C o m p o n e n t = = 方 法 内 就 挂 载 的 , 一 个 真 实 的 = = D o m = = 元 素 。 首 次 渲 染 会 传 入 v m . el 是之前在 ==mountComponent== 方法内就挂载的, 一个真实的==Dom==元素。 首次渲染会传入 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 = isDef(oldVnode.nodeType) // 是否是真实Dom
if(isRealElement) { // $el是真实Dom
oldVnode = emptyNodeAt(oldVnode) // 转为VNode格式覆盖自己
}
...
}
}
首次渲染时没有oldVnode, oldVnode 就是 $el, 一个真实的dom, 经过emptyNodeAt(odVnode) 方法包装:
function emptyNodeAt(elm) {
return new VNode(
nodeOps.tagName(elm).toLowerCase(), // 对应tag属性
{}, // 对应data
[], // 对应children
undefined, //对应text
elm // 真实dom赋值给了elm属性
)
}
包装后的:
{
tag: 'div',
elm: '' // 真实dom
}
-------------------------------------------------------
nodeOps:
export function tagName (node) { // 返回节点的标签名
return node.tagName
}
在将传入的==$el== 属性转为了VNode 格式之后,我们继续:
export function createPatchFunction(backend) {
...
return function (oldVnode, vnode) { // 接收新旧vnode
const insertedVnodeQueue = []
...
const oldElm = oldVnode.elm //包装后的真实Dom
const parentElm = nodeOps.parentNode(oldElm) // 首次父节点为
createElm( // 创建真实Dom
vnode, // 第二个参数
insertedVnodeQueue, // 空数组
parentElm, //
nodeOps.nextSibling(oldElm) // 下一个节点
)
return vnode.elm // 返回真实Dom覆盖vm.$el
}
}
------------------------------------------------------
nodeOps:
export function parentNode (node) { // 获取父节点
return node.parentNode
}
export function nextSibling(node) { // 获取下一个节点
return node.nextSibing
}
createElm 方法开始生成真实的Dom, VNode 生成真实的Dom 的方式还是分为元素节点和组件两种方式, 所以我们使用上一章生成的VNode分别说明。
1. 元素节点生成Dom
{ // 元素节点VNode
tag: 'div',
children: [{
tag: 'h1',
children: [
{text: 'title h1'}
]
}, {
tag: 'h2',
children: [
{text: 'title h2'}
]
}, {
tag: 'h3',
children: [
{text: 'title h3'}
]
}
]
}
大家可以先看下这个流程图有个印象即可, 再接下来看具体实现时思路会清晰很多(这里先借用网上的一张图):
开始Dom, 来看下它的定义:
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
...
const children = vnode.children // [VNode, VNode, VNode]
const tag = vnode.tag // div
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return // 如果是组件结果返回true,不会继续,之后详解createComponent
}
if(isDef(tag)) { // 元素节点
vnode.elm = nodeOps.createElement(tag) // 创建父节点
createChildren(vnode, children, insertedVnodeQueue) // 创建子节点
insert(parentElm, vnode.elm, refElm) // 插入
} 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) // 插入到父节点
}
...
}
------------------------------------------------------------------
nodeOps:
export function createElement(tagName) { // 创建节点
return document.createElement(tagName)
}
export function createComment(text) { //创建注释节点
return document.createComment(text)
}
export function createTextNode(text) { // 创建文本节点
return document.createTextNode(text)
}
function insert (parent, elm, ref) { //插入dom操作
if (isDef(parent)) { // 有父节点
if (isDef(ref)) { // 有参考节点
if (ref.parentNode === parent) { // 参考节点的父节点等于传入的父节点
nodeOps.insertBefore(parent, elm, ref) // 在父节点内的参考节点之前插入elm
}
} else {
nodeOps.appendChild(parent, elm) // 添加elm到parent内
}
} // 没有父节点什么都不做
}
这算一个比较重要的方法,因为很多地方会用到。
依次判断是否是元素节点, 注释节点,文本节点, 分别创建它们然后插入到父节点里面, 这里主要介绍创建元素节点, 另外两个并没有复杂的逻辑。 我们接下来看下:createChild 方法定义:
function createChild(vnode, children, insertedVnodeQueue) {
if(Array.isArray(children)) { // 是数组
for(let i = 0; i < children.length; ++i) { // 遍历vnode每一项
createElm( // 递归调用
children[i],
insertedVnodeQueue,
vnode.elm,
null,
true, // 不是根节点插入
children,
i
)
}
} else if(isPrimitive(vnode.text)) { //typeof为string/number/symbol/boolean之一
nodeOps.appendChild( // 创建并插入到父节点
vnode.elm,
nodeOps.createTextNode(String(vnode.text))
)
}
}
-------------------------------------------------------------------------------
nodeOps:
export default appendChild(node, child) { // 添加子节点
node.appendChild(child)
}
开始创建子节点, 遍历VNode 的每一项, 每一项还是使用之前的createElm方法创建Dom。 如果某一项又是数组,继续调用createChild创建某一项的子节点; 如果某一项不是数组, 创建文本节点并将它添加到父节点内。 像这样使用递归的形式将嵌套的VNode全部创建为真实的Dom。
在看一遍流程图, 应该就能减少大家很多疑惑了(这里先借用网上一章图):
简单来说就是由里向外的挨个创建出真实的Dom, 然后插入到它的父节点内,最后将创建好的Dom插入到body内, 完成创建的过程, 元素节点的创建还是比较简单的, 接下来看下组件式怎么创建的。
组件VNode生成Dom
{ // 组件VNode
tag: 'vue-component-1-app',
context: {...},
componentOptions: {
Ctor: function(){...}, // 子组件构造函数
propsData: undefined,
children: undefined,
tag: undefined
},
data: {
on: undefined, // 原生事件
hook: { // 组件钩子
init: function(){...},
insert: function(){...},
prepatch: function(){...},
destroy: function(){...}
}
}
}
-------------------------------------------
// app组件内模板
app text
首先看张简易流程图, 留个影响即可,方便理清之后的逻辑顺序(这里借用网上一张图):
使用上一章组件生成VNode , 看下在createElm 内创建组件Dom分支逻辑是怎么样的:
function createElm(vnode, insertedVnodeQueue, parentElm, refElm) {
...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { // 组件分支
return
}
...
执行createComponent 方法, 如果是元素节点不会返回任何东西,所以是undefined , 会继续走接下来的创建元节点的逻辑。 现在是组件, 我们看下createComponent 的实现:
function createComponent(vnode, insertedVnodeQueue, 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 = vnode.componentInstance =
createComponentInstanceForVnode(vnode, activeInstance)
...
}
activeInstance 是一个全局的变量, 再update 方法内赋值为当前实例, 再当前实例做 patch 的过程中作为了组件的父实例传入, 在子组件的initLifecycle时构建组件关系。 将createComponentInsanceForVnode 执行的结果赋值给了vnode.componentInstance, 所以看下它的返回的结果是什么:
export createComponentInstanceForVnode(vnode, parent) { // parent为全局变量activeInstance
const options = { // 组件的options
_isComponent: true, // 设置一个标记位,表明是组件
_parentVnode: vnode,
parent // 子组件的父vm实例,让初始化initLifecycle可以建立父子关系
}
return new vnode.componentOptions.Ctor(options) // 子组件的构造函数定义为Ctor
}
再组件的init 方法内首先执行craeeteComponentInstanceForVnode方法, 这个方法的内部就会将子组件的构造函数实例化, 因为子组件的构造函数继承了基类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 = Object.create(vm.constructor.options)
opts.parent = options.parent // 组件init赋值,全局变量activeInstance
opts._parentVnode = options._parentVnode // 组件init赋值,组件的vnode
...
}
前面都还是执行的好好的, 最后却因为没有el属性, 所以没有挂载,createComponentInstanceForVnode 方法执行完毕。 这个时候我们回到组件的init方法, 补全剩下的逻辑:
const init = vnode => {
const child = vnode.componentInstance = // 得到组件的实例
createComponentInstanceForVnode(vnode, activeInstance)
child.$mount(undefined) // 那就手动挂载呗
}
我们在init 方法内手动挂载这个组件, 接着又会执行组件的==render()== 方法得到组件内元素节点VNode , 然后执行vm._update(), 执行组件的 patch 方法, 因为 $mount 方法传入的是 undefined, oldVnode 也是 undefinned, 会执行__patch_ 内的这段逻辑:
return function patch(oldVnode, vnode) {
...
if (isUndef(oldVnode)) {
createElm(vnode, insertedVnodeQueue)
}
...
}
这次执行createElm 是没有传入第三个参数父节点的, 那组件创建好的Dom放哪生效了? 没有父节点页要生成Dom不是, 这个时候执行的是组件的 patch , 所以参数vnode 就是组件内元素节点的vnode了:
// app组件内模板
app text
-------------------------
{ // app内元素vnode
tag: 'div',
children: [
{text: app text}
],
parent: { // 子组件_init时执行initLifecycle建立的关系
tag: 'vue-component-1-app',
componentOptions: {...}
}
}
很明显这个时候不是组件了, 即使是组件也没关系, 大不了还是执行一遍createComponent 创建组件的逻辑, 因为总会有组件是由元素节点组成的。 这个时候我们执行一遍创建元素节点的逻辑, 因为没有第三个参数父节点, 所以组件的Dom虽然创建好了, 并不会在这里插入。 请注意这个时候组件的init 已经完成, 但是组件的createComponent 方法并没有完成, 我们补全它的逻辑:
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data;
if (isDef(i)) {
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode) // init已经完成
}
if (isDef(vnode.componentInstance)) { // 执行组件init时被赋值
initComponent(vnode) // 赋值真实dom给vnode.elm
insert(parentElm, vnode.elm, refElm) // 组件Dom在这里插入
...
return true // 所以会直接return
}
}
}
-----------------------------------------------------------------------
function initComponent(vnode) {
...
vnode.elm = vnode.componentInstance.$el // __patch__返回的真实dom
...
}
无论是嵌套多么深的组件, 遇到组件后就执行 init, 在init 的 patch 过程中又遇到嵌套组件, 那就再执行嵌套组件的init, 嵌套组件完成 __patch__后将真是的Dom插入到它的父节点内, 接着执行完外层组件的 patch 又插入到它的父几点内, 最后插入到body 内, 完成嵌套组件的创建过程, 总之还是一个由里及外的过程。
在回过头看这张图, 相信会很好理解了:
再将本章最初的mountComponent 之后的逻辑补全:
export function mountComponent(vm, el) {
...
const updateComponent = () => {
vm._update(vm._render())
}
new Watcher(vm, updateComponent, noop, {
before() {
if(vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true)
...
callHook(vm, 'mounted')
return vm
}
接下来会将 updateComponent 传入到一个Watcher 的类中, 这个类是干嘛的,我们下一章在介绍。 接下来执行mounted 钩子方法。 至此new vue 的整个流程就全部走完了。 我们回顾下从new Vue 开始执行的顺序:
new Vue ==> vm._init() ==> vm.$mount(el) ==> vm._render() ==> vm.update(vnode)
最后我们以一个问题来结束本章的内容:
解答:
parent beforeCreate
parent created
parent beforeMounte
child beforeCreate
child created
child beforeMounte
child mounted
parent mounted
下一篇:vue 原理解析(六): 理解响应式原理(上): 对象