Appearance
实现
javascript
// 通过 updateComponent 更新视图,内部先调用 vm._render 生成新的 vnode,再调用
// vm._update,内部会先 vm._vnode 取出上次的 vnode,称为 prevVnode,
// 再 vm._vnode = vnode 将 vnode 修改为新生成的 vnode,如果 prevVnode 未定义,说明是
// 初始化渲染,直接 patch(vm.$el, vnode),否则是更新,直接 patch(prevVnode,vnode)
// 进行 diff,patch 内部先判断 oldVnode.nodeType 是否有定义,有说明是初始化渲染,
// 否则如果 oldVnode 和 vnode 满足 sameVnode 条件,执行 patchVnode 进行 diff。
Vue.prototype._update = function (vnode: VNode) {
const vm: Component = this
// 之前的 vnode
const prevVnode = vm._vnode
// 新的 vnode
vm._vnode = vnode
if (!prevVnode) {
// 初始化渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 后续更新,diff
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (sameVnode(oldVnode, vnode)) {
// 进行 diff
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
}javascript
// patchVnode通过比较oldVnode和vnode的差异从而更新真实DOM,并通过const elm = vnode.elm = oldVnode.elm实现DOM的复用。在进行同级比较前通过updateStyle和updateAttrs完成样式和属性的更新,先取出各自的vnode.data.staticStyle,即oldStyle和newStyle,遍历oldStyle所有属性,如果newStyle[name]未定义,说明该样式被移除了,设置el.style[name] = '',再遍历newStyle所有属性,如果属性值和oldStyle[name]不相等,说明该样式被更新了,以新的为准,设置el.style[name] = newStyle[name],updateAttrs的流程和updateStyle流程一样,通过removeAttribute和setAttribute完成属性的移除和更新javascript
function updateStyle (oldVnode, vnode) {
const data = vnode.data || {}
const oldData = oldVnode.data || {}
if (isUndef(data.staticStyle) && isUndef(oldData.staticStyle)) {
return
}
let cur, name
const el = vnode.elm
const oldStyle = oldData.staticStyle || {}
const newStyle = data.staticStyle || {} // 先子后父 确保父组件能重写子组件样式
for (name in oldStyle) { // 遍历旧的样式
if (isUndef(newStyle[name])) { // 新的没有旧的属性
el.style[name] = '' // dom移除该属性
}
}
for (name in newStyle) { // 遍历新的样式
cur = newStyle[name]
if (cur !== oldStyle[name]) { // 新的和旧的不相等 => 更新 / 删除
el.style[name] = cur // 以新的为准
}
}
}javascript
// diff,首先 oldVnode 和 vnode 一定是相同类型的虚拟节点,因为 patch 中只有
// oldVnode 和 vnode 满 足sameVnode 才进入 patchVnode。
// 内部先复用 DOM,即 vnode.elm = oldVnode.elm
// 再先判断 vnode.text 是否有定义,如果有定义说明它们都是文本 vnode,
// 如果 oldVnode.text !== vnode.text,则 elm.textContent = vnode.text 进行更新。
// 如果 vnode.text 未定义说明它们都是元素 vnode,对它们的 children 进行diff,
// 如果只有 oldVnode 有 children,说明子元素都被移除了,直接 elm.innerHTML = '',
// 如果只有 vnode 有 children,说明原来没有子元素,现在添加了子元素,直接遍历 children
// 调用 createElm 生成 DOM 并添加到 elm 中,即
// vnode.elm.appendChild(createElm(ch[i]))
// 如果 oldVnode 和 vnode 都有 children,进行 updateChildren,进行真正的 diff。
pacthVnode(oldVnode, vnode) {
let elm = (vnode.elm = oldVnode.elm) // 复用真实DOM
let oldCh = oldVnode.children
let ch = vnode.children
updateProps(oldVnode, vnode)
// oldVnode, vnode是相同类型节点
if (isUndef(vnode.text)) {
// 都是元素节点
if (isDef(oldCh) && isDef(ch) && oldCh !== ch) {
// 都有子元素, 去 diff
updateChildren(elm, oldCh, ch)
} else if (isDef(ch)) {
// 添加子元素
elm.appendChild(createElm(ch[i]))
} else if (isDef(oldCh)) {
// 移除子元素
elm.innerHTML = ''
}
} else if (oldVnode.text !== vnode.text) {
// 都是文本节点
elm.textContent = vnode.text
}
}javascript
// updateChildren 中,通过 oldStartIdx、oldEndIdx、newStartIdx、newEndIdx 四个指针
// 分别标识 oldCh 的头和尾、ch 的头和尾,当
// oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,即新旧指针都没越界时不断
// 进行比较,对 parentElm 进行对应更新。
// 先通过 oldCh 和 ch 头尾两两比较来实现更新
// - 旧头和新头,命中说明 DOM 位置不用改变,递归调用 patchVnode 对它们内部进行diff,
// 最后 oldStartIdx 和 newStartIdx 都右移
// - 旧尾和新尾,命中说明 DOM 位置不用改变,递归调用 patchVnode 对它们内部进行diff,
// 最后 oldEndIdx 和 newEndIdx 都左移
// - 旧头和新尾,命中说明旧头对应的 DOM 元素被移动到后面了,递归调用 patchVnode 对
// 它们内部进行 diff,再将 oldStartIdx 对应的 elm 移动到 oldEndVnode 对应的 elm 的
// nextSibling 的前面,最后 oldStartIdx 右移,newEndIdx 左移
// - 旧尾和新头,命中说明旧尾对应的 DOM 元素被移动到前面了,递归调用 patchVnode 对
// 它们内部进行 diff,再将 oldEndIdx 对应的 elm 移动到 oldStartVnode 对应的elm的前面,
// 最后 oldEndIdx 左移,newStartIdx 右移
// 如果上面四种情况都不命中,说明有可能出现了新增的 DOM,用一张表 oldKeyToIdx 记录
// oldStartIdx 到 oldEndIdx 中有定义 key 的 vnode 在 oldCh 中的位置,之后尝试进行
// DOM 的复用,先判断 newStartVnode 的 key 是否有定义,如果有定义去表中取这个 key
// 对应的值,即被用来复用的 vnode 在 oldCh 中的下标,如果没有定义也要尽最大能力复用,
// 遍历 oldStartIdx 到 oldEndIdx 的每个 oldVnode,如果 oldVnode 有定义
// (可能因为移动被置为了 undefined)并且 newStartVnode 和该 oldVnode 满足
// sameVnode,就可以进行复用,返回这个 oldVnode 在 oldCh 中的下标,通过 idxInOld
// 保存下标,如果 idxInOld 未定义,说明没法复用,根据 newStartVnode 生成 DOM 并插入
// 到 oldStartVnode.elm 的前面,有定义,说明可以复用,oldCh[idxInOld] 找出该 vnode
// 并用 vnodeToMove 接收,判断 vnodeToMove 和 newStartVnode 是否满足 sameVnode
// (如果这个 idxInOld 是尽最大能力复用得到的必然满足,但如果是表中找到的只能保证它们
// 的 key 相同),如果不满足直接当作新节点处理,根据 newStartVnode 生成 DOM 并插入
// 到 oldStartVnode.elm 的前面。如果满足则递归调用 patchVnode 对它们内部进行 diff,
// 再将 oldCh[idxInOld] 置为undefined,表示该 vnode 已经处理过了,被移动到了其他位置,
// 再将 vnodeToMove 对应的 elm 移动到 newStartVnode 对应的 elm 的前面,最后
// newStartIdx 右移。同时因为产生了 undefined,条件中增加如果 oldStartVnode 未定义,
// oldStartIdx 就右移,如果oldEndVnode未定义,oldEndIdx 就左移。
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) { // 跳过oldCh中因移动产生的undefined
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else { // 都不命中 用一张表记录oldVnode中有key的vnode在oldCh中的位置
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
)
}
// 复用
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
)
if (isUndef(idxInOld)) { // 是新增节点
parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode)
oldCh[idxInOld] = undefined // 置为undefined, 表示处理过了
parentElm.insertBefore(vnodeToMove.elm, newStartVnode.elm)
} else {
parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm)
}
}
newStartVnode = newCh[++newStartIdx]
}
}javascript
// 当指针越界后跳出循环,此时如果 oldStartIdx > oldEndIdx,说明旧的处理完了,而新的
// 没处理完,我们要以新的为准,说明 newStartIdx 到 newEndIdx 都是新增,如果
// newCh[newEndIdx + 1] 未定义,createElm 生成的 DOM 都插入到最后。有定义说明
// 新增的 DOM 位置在 newCh[newEndIdx + 1].elm 的前面,让新增的 DOM 都插入到这个
// vnode 对应 elm 之前。
// 如果 newStartIdx > newEndIdx,说明新的处理完了,而旧的没处理完,
// 我们要以新的为准,说明 oldStartIdx 到 oldEndIdx 都是要删除的 DOM,遍历每一项,如果
// 有定义就通过 removeChild 删除该 vnode 对应的 elm
if (oldStartIdx > oldEndIdx) { // 旧的结束了, 新的没结束, 说明有新增
// newStartIdx - newEndIdx 都是新增的节点
// newCh[newEndIdx + 1] 是 vnode, 新增到这个 vnode 对应的 elm 的前面
// newCh[newEndIdx + 1] 不是 vnode, 新增到最后后面
let refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
for (let i = newStartIdx; i < newEndIdx; i++) {
parentElm.insertBefore(createElm(newCh[i]), refElm)
}
} else if (newStartIdx > newEndIdx) { // 新的结束了, 旧的没结束, 说明有删除
for (let i = oldStartIdx; i < oldEndIdx; i++) {
if (oldCh[i]) {
parentElm.removeChild(oldCh[i].elm)
}
}
}