上一篇:Vue源码解析(二):Vue是如何挂载Dom的(render篇) 上一节通过
_render
方法已经把render
函数转换成VNode
了,那VNode
又是怎么样转成真实Dom
的呢?
export function mountComponent(vm, el) {
vm.$el = el
...
callHook(vm, 'beforeMount')
...
const updateComponent = function () {
vm._update(vm._render())
}
...
}
我们知道vm._render()
返回的是一个VNode
,而updateComponent
方法里的_update
方法就是把传进去的VNode
转为真实Dom
,它里面有两种场景:
- 首次渲染
当执行
new Vue
到此时就是首次渲染了,会将传入的VNode
对象映射为真实的Dom
。 - 更新页面
数据变化会驱动页面发生变化,这也是
vue
最独特的特性之一,数据改变之前和之后会生成两份VNode
进行比较,然后再做最小的改动去渲染页面,这样一个diff
算法还是挺复杂的,后面再看。
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el // 上一次的 $el
const prevVnode = vm._vnode // 上一次的 vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode // 再把最新的重新赋值到_vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
... 首次渲染
if (!prevVnode) { // 没有上一次的vnode,则说明是首次渲染
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates // 否则是更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
...
}
patch
patch的作用
主要是比较VNode
和oldVNode
,再以VNode
为标准的情况下在oldVNode
上做小的改动,完成VNode
对应的Dom
渲染。
继续,这里的vm.$el
是之前在mountComponent
方法内挂载的,是一个真实Dom
元素。vm.__patch__
返回的也是一个真实Dom
元素,把之前那个给覆盖掉了。
首次渲染会传入vm.$el
以及得到的VNode
,所以看下vm.__patch__
定义:
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
Ps: 这里modules属性内的钩子方法是区分平台的,web、weex以及SSR它们调用VNode方法方式并不相同,比如这里是web, 所以vue在这里又使用了函数柯里化这个骚操作,在createPatchFunction内将平台的差异化抹平,从而__patch__方法只用接收新旧node即可。
// platformModules = [
// attrs,
// klass,
// events,
// domProps,
// style,
// transition
// ]
// baseModules = [
// ref,
// directives
// ]
const modules = platformModules.concat(baseModules)
Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules })
__patch__
是createPatchFunction
方法内部返回的一个方法,它接受一个对象:
-
nodeOps
属性:封装了操作原生Dom的一些方法的集合,如创建、插入、移除这些,再使用到的地方再详解。 -
modules
属性:创建真实Dom也需要生成它的如class/attrs/style等属性。modules是一个数组集合,数组的每一项都是这些属性对应的钩子方法,这些属性的创建、更新、销毁等都有对应钩子方法,当某一时刻需要做某件事,执行对应的钩子即可。比如它们都有create这个钩子方法,如将这些create钩子收集到一个数组内,需要在真实Dom上创建这些属性时,依次执行数组的每一项,也就是依次创建了它们
来看一下createPatchFunction
返回的patch
方法是什么样的:
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend // 结构出 modules nodeOps
// const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
// modules = [
// {create: ƒ, update: ƒ}
// {create: ƒ, update: ƒ}
// {create: ƒ, update: ƒ}
// {create: ƒ, update: ƒ}
// {create: ƒ, update: ƒ}
// {create: ƒ, activate: ƒ, remove: ƒ}
// {create: ƒ, update: ƒ, destroy: ƒ}
// {create: ƒ, update: ƒ, destroy: ƒ}
// ] 顺序可以看上一段代码传进来之前用concat拼接的
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// 经过处理后的cbs长这样
// {
// activate: [ƒ]
// create: (8) [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ]
// destroy: (2) [ƒ, ƒ]
// remove: [ƒ]
// update: (7) [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ]
// }
...
...
...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // 没有新节点,有老节点,说明老节点被删除了,则直接销毁
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) { // 有新节点,没有老节点,则直接创建新节点
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType) // 老节点是否是真实dom
if (!isRealElement && sameVnode(oldVnode, vnode)) { // 不是真实dom且老节点与新节点是同一个节点,则进行 patchVnode
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
首次渲染 走这里
if (isRealElement) { // 首次渲染传的是一个真实dom
...
oldVnode = emptyNodeAt(oldVnode) // 转为VNode格式覆盖自己
}
// replacing existing element
const oldElm = oldVnode.elm // 获取包装后的真实 Dom <div id='app'></div>
const parentElm = nodeOps.parentNode(oldElm) // 首次父节点为<body></body>
...
// create new node
createElm( // 创建真实Dom
vnode, // 新 vnode
insertedVnodeQueue, // 空数组
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm) // 下一个节点
)
// 将旧的节点从父节点中移除
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
...
return vnode.elm
}
}
简单解释一下返回的patch
方法里代码:
1、patch逻辑
- 如果
vnode
不存在,而oldVnode
存在,则调用invodeDestoryHook
进行销毁旧的节点 - 如果
oldVnode
不存在,而vnode
存在,则调用createElm
创建新的节点 - 如果
oldVnode
和vnode
都存在 1)如果oldVnode
不是真实节点且和vnode
是相同节点(调用sameVnode
比较),则调用patchVnode
进行patch
2)如果oldVnode
是真实DOM
节点,则先把真实DOM
节点转为Vnode
,再调用createElm
创建新的DOM
节点,并插入到真实的父节点中,同时调用removeVnodes
将旧的节点从父节点中移除。
人话就是,在patch
方法里,之前有的东西现在没了,就删除;之前没的东西,现在有了,就创建;之前有现在也有的,就判断是否是相同的节点,不是相同的节点则创建新节点把之前老节点废弃;是相同节点则进入patchVode
进行patch
。
判断两个节点是否为同一个节点,内部是这样定义的:
function sameVnode (a, b) { // 是否是相同的VNode节点
return (
a.key === b.key && ( // 如平时v-for内写的key
(
a.tag === b.tag && // tag相同
a.isComment === b.isComment && // 注释节点
isDef(a.data) === isDef(b.data) && // 都有data属性
sameInputType(a, b) // 相同的input类型
) || (
isTrue(a.isAsyncPlaceholder) && // 是异步占位符节点
a.asyncFactory === b.asyncFactory && // 异步工厂方法
isUndef(b.asyncFactory.error)
)
)
)
}
2.patchVnode逻辑
相同的节点会通过patchVnode
来处理,那patchVnode
里面是做啥。
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 如果这两个节点完全相同,则没必要patch了 直接返回
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
// 静态节点把组件实例覆盖就行
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
// 调用组件的prepatch方法
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// 是否可以 patch 里面主要判断vnode.tag是否存在
// 这里做了一个 data 属性的全量更新
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
// vnode没有text属性
if (isDef(oldCh) && isDef(ch)) {
// 都有子节点,且子节点不同,则调updateChildren更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 只有新节点有子节点
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
// 新节点没有 text,老节点有text,则text更新设置为空
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 向oldVnode空的标签内插入vnode的children的真实dom
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 只有老节点有子节点,则把老节点全部移除
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 新节点没有text,老节点有text,则text更新设置为空
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 老节点的text属性不等于新的text属性,则用新的替换掉老的
nodeOps.setTextContent(elm, vnode.text)
}
}
所以归纳一下patchVnode
逻辑:
- 如果
vnode
和oldVnode
完全一致,则什么都不做处理,直接返回 - 如果
oldVnode
和vnode
都是静态节点,且具有相同的key
,并且当vnode
是克隆节点或是v-once
指令控制的节点时,只需要把oldVnode
的elm
和oldVnode.children
都复制到vnode
上即可 - 如果
vnode
不是文本节点或注释节点 1)如果vnode
的children
和oldVnode
的children
都存在,且不完全相等,则调用updateChildren
更新子节点 2)如果只有vnode
存在子节点,则调用addVnodes
添加这些子节点 3)如果只有oldVnode
存在子节点,则调用removeVnodes
移除这些子节点 4)如果oldVnode
和vnode
都不存在子节点,但是oldVnode
为文本节点或注释节点,则把oldVnode.elm
的文本内容置为空 - 如果
vnode
是文本节点或注释节点,并且vnode.text
和oldVnode.text
不相等,则更新oldVnode
的文本内容为vnode.text
3.updateChildren逻辑(重点中的重点)
如果vnode
不是文本节点或注释节点,并且vnode
的children
和oldVnode
的children
都存在且不完全相等,则调用updateChildren
更新子节点。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
// 定义了4种猜想,分别是:
// 1.老节点的头和新节点的头是同一个节点
// 2.老节点的尾和新节点的尾是同一个节点
// 3.老节点的头和新节点的尾是同一个节点
// 4.老节点的尾和新节点的头是同一个节点
// 只要这4种情况命中,就能少一次while循环。如果都没命中,就只能双循环去找了
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) { // 老开始节点和新开始节点相同 则patch这两个节点
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// oldKeyToIdx = {
// '节点中的key0':0,
// '节点中的key1':1,
// ...
// }
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 通过key 去找新子节点在老子节点中的位置
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key] // 新子节点在老子节点中的位置,也可能是 undefined
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 遍历找新子节点在老子节点中的位置
if (isUndef(idxInOld)) { // New element
// 没有找到新子节点在老子节点里的位置,则代表没有,执行创建
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 找到了新子节点在老子节点里的位置
vnodeToMove = oldCh[idxInOld]
// 然后对比老子节点和新子节点是否是同一个节点,是就patch,不是就创建,如此递归
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined // 找到了对应的旧子节点 旧子节点中设置为 undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) // 把旧的子节点插入到后面
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
// 收尾工作,如果老子节点先循环完,则剩下的新子节点直接添加到父节点
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 如果新子节点先循环完,则移除剩下的老子节点
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
具体的演示流程推荐看这篇文章:Vue原理解析(八):一起搞明白令人头疼的diff算法
作者的图画得非常的清晰明了!
以上。
对数据渲染的过程有了更深的一层理解,从new Vue()开始,创建了一个vue是对象,会先进行
init初始化——>$mount()——>compile(若已经是render则该过程不需要)——>render——>创建VNode——>patch过程——>生成真实的DOM