前言 前面我們簡單的瞭解了 vue 初始化時的一些大概的流程,這裡我們擴展下 Vue 的 patch。 內容 這一塊主要圍繞 vue 中的__patch__進行剖析。 __patch__ Vue.prototype.__patch__的方法位於scr/platforms/web/runtime/in ...
前言
前面我們簡單的瞭解了 vue 初始化時的一些大概的流程,這裡我們擴展下 Vue 的 patch。
內容
這一塊主要圍繞 vue 中的__patch__
進行剖析。
__patch__
Vue.prototype.__patch__
的方法位於scr/platforms/web/runtime/index.ts
中;
// install platform patch function
// 判斷是否是瀏覽器環境,是就賦予patch否則就賦予空函數
Vue.prototype.__patch__ = inBrowser ? patch : noop
patch.ts
patch.ts
位於src/platforms/web/runtime/patch.ts
?> 虛擬 DOM 演算法基於snabbdom進行修改
// the directive module should be applied last, after all
// built-in modules have been applied.
// 在應用了所有內置模塊之後,最後再應用指令模塊
const modules = platformModules.concat(baseModules)
// 創建patch函數
export const patch: Function = createPatchFunction({ nodeOps, modules })
nodeOps
nodeOps
引入於src/platforms/web/runtime/node-ops.ts
,封裝了 DOM 操作的 API;
import VNode from 'core/vdom/vnode'
import { namespaceMap } from 'web/util/index'
// 創建一個由標簽名稱 tagName 指定的 HTML 元素
// https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElement
export function createElement(tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
// false or null will remove the attribute but undefined will not
// false或者null將會移除這個屬性但是undefined不會
// vnode?.data?.attrs?.multiple !== undefined
if (
vnode.data &&
vnode.data.attrs &&
vnode.data.attrs.multiple !== undefined
) {
// select元素增加multiple屬性
elm.setAttribute('multiple', 'multiple')
}
return elm
}
// 創建一個具有指定的命名空間 URI 和限定名稱的元素
// https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElementNS
export function createElementNS(namespace: string, tagName: string): Element {
return document.createElementNS(namespaceMap[namespace], tagName)
}
// 創建一個新的文本節點。這個方法可以用來轉義 HTML 字元。
// https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createTextNode
export function createTextNode(text: string): Text {
return document.createTextNode(text)
}
// 創建一個註釋節點
// https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createComment
export function createComment(text: string): Comment {
return document.createComment(text)
}
// 在參考節點之前插入一個擁有指定父節點的子節點
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/insertBefore
export function insertBefore(
parentNode: Node,
newNode: Node,
referenceNode: Node
) {
// referenceNode 引用節點不是可選參數——你必須顯式傳入一個 Node 或者 null。
// 如果不提供節點或者傳入無效值,在不同的瀏覽器中會有不同的表現
parentNode.insertBefore(newNode, referenceNode)
}
// 從 DOM 中刪除一個子節點。會返回刪除的節點。
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/removeChild
export function removeChild(node: Node, child: Node) {
node.removeChild(child)
}
// 將一個節點附加到指定父節點的子節點列表的末尾處會返回附加的節點對象
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
// 這裡有一個新的方法ParentNode.append()
// 兩者不同之處
// Element.append() allows you to also append string objects, whereas Node.appendChild() only accepts Node objects.
// Element.append()允許添加DOMString 對象,而 Node.appendChild() 只接受 Node 對象
// Element.append() has no return value, whereas Node.appendChild() returns the appended Node object.
// Element.append() 沒有返回值,而 Node.appendChild() 返回追加的 Node 對象。
// Element.append() can append several nodes and strings, whereas Node.appendChild() can only append one node.
// Element.append() 可以追加多個節點和字元串,而 Node.appendChild() 只能追加一個節點。
export function appendChild(node: Node, child: Node) {
node.appendChild(child)
}
// 返回指定的節點在 DOM 樹中的父節點
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/parentNode
export function parentNode(node: Node) {
return node.parentNode
}
// 返回其父節點的 childNodes 列表中緊跟在其後面的節點,其實就是返回指定節點的兄弟節點
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nextSibling
export function nextSibling(node: Node) {
return node.nextSibling
}
// 返回指定節點的標簽名
// https://developer.mozilla.org/zh-CN/docs/Web/API/Element/tagName
export function tagName(node: Element): string {
return node.tagName
}
// 為指定節點設置文本內容
// https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent
// 比 innerHTML更好的性能(因為不會解析html)而且可以防止xss的攻擊
// textContent 和 innerText 的區別
// textContent 會獲取所有元素的內容,包括 <script> 和 <style> 元素, innerText 只展示給人看的元素。
// textContent 會返回節點中的每一個元素。相反,innerText 受 CSS 樣式的影響,並且不會返回隱藏元素的文本,
// 此外,由於 innerText 受 CSS 樣式的影響,它會觸發迴流( reflow )去確保是最新的計算樣式。(迴流在計算上可能會非常昂貴,因此應儘可能避免。)
// 與 textContent 不同的是,在 Internet Explorer (小於和等於 11 的版本) 中對 innerText 進行修改,
// 不僅會移除當前元素的子節點,而且還會永久性地破壞所有後代文本節點。在之後不可能再次將節點再次插入到任何其他元素或同一元素中。
export function setTextContent(node: Node, text: string) {
node.textContent = text
}
// 為指定節點設置scopeId屬性
// https://developer.mozilla.org/zh-CN/docs/Web/API/Element/setAttribute
export function setStyleScope(node: Element, scopeId: string) {
node.setAttribute(scopeId, '')
}
modules
主要是內置的一些模塊方法,如:
attrs
klass
,events
,domProps
,style
,transition
,ref
,directives
createPatchFunction
createPatchFunction
中包含了emptyNodeAt
,createRmCb
,removeNode
,isUnknownElement
,createElm
,createComponent
,initComponent
,reactivateComponent
,insert
,createChildren
,isPatchable
,invokeCreateHooks
,setScope
,addVnodes
,invokeDestroyHook
,removeVnodes
,removeAndInvokeRemoveHook
,updateChildren
,checkDuplicateKeys
,findIdxInOld
,patchVnode
,invokeInsertHook
,isRenderedModule
,hydrate
,assertNodeMatch
,patch
共 26 個函數;
鉤子遍歷
// hooks ['create', 'activate', 'update', 'remove', 'destroy']
// modules [attrs, klass, events, domProps, style, transition, ref, directives]
// 遍歷hooks鉤子併在modules中判斷是否存在對應的方法
// 存在就push到cbs中
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]])
}
}
}
emptyNodeAt
// 創建一個vnode節點
// 獲取傳入元素的小寫標簽名並創建對應空的虛擬DOM
function emptyNodeAt(elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
createRmCb
// 創建remove函數
// 要移除某個節點需先把監聽器全部移除
function createRmCb(childElm, listeners) {
function remove() {
if (--remove.listeners === 0) {
removeNode(childElm)
}
}
remove.listeners = listeners
return remove
}
removeNode
// 移除節點
function removeNode(el) {
// 找到指定節點的父節點
const parent = nodeOps.parentNode(el)
// element may have already been removed due to v-html / v-text
// 元素可能已經由於v-html/v-text被刪除
// 父節點存在則移除父節點的下該節點
if (isDef(parent)) {
nodeOps.removeChild(parent, el)
}
}
isUnknownElement
// 是否是未知的元素標簽
// 如果自定元素存在ignoredElementse就返回false不使用isUnknownElement進行校驗
// https://v2.cn.vuejs.org/v2/api/#ignoredElements
function isUnknownElement(vnode, inVPre) {
return (
!inVPre &&
!vnode.ns &&
!(
config.ignoredElements.length &&
config.ignoredElements.some(ignore => {
return isRegExp(ignore) ? ignore.test(vnode.tag) : ignore === vnode.tag
})
) &&
config.isUnknownElement(vnode.tag)
)
}
createElm
// 創建元素
function createElm(
vnode,
insertedVnodeQueue,
parentElm?: any,
refElm?: any,
nested?: any,
ownerArray?: any,
index?: any
) {
// 節點已經被渲染,克隆節點
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
// 此vnode已在以前的渲染中使用!
// 現在它被用作一個新節點,當它被用作插入參考節點時,覆蓋它的elm將導致潛在的補丁錯誤。
// 相反,我們在為節點創建關聯的DOM元素之前按需克隆節點。
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
// 創建組件
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
// 如果是創建組件節點且成功創建,createComponent返回 true。createElm直接return。
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// 存在tag的情況
// 開發環境下會對tag進行校驗
if (__DEV__) {
// 跳過這個元素和它的子元素的編譯過程。可以用來顯示原始 Mustache 標簽。跳過大量沒有指令的節點會加快編譯。
// https://v2.cn.vuejs.org/v2/api/#v-pre
if (data && data.pre) {
// 節點上存在pre屬性就對creatingElmInVPre標識進行+1操作
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' +
tag +
'> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
// 通過傳入節點的tag,創建相應的標簽元素,賦值給 vnode.elm 進行占位
// 如果存在命名空間就調用createElementNS創建帶有命名空間元素否則就調用createElement創建正常元素
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// 設置CSS作用域
setScope(vnode)
// 創建子節點
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// 調用create鉤子
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 插入父元素
insert(parentElm, vnode.elm, refElm)
if (__DEV__ && data && data.pre) {
creatingElmInVPre--
}
} 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)
}
}
createComponent
// 創建組件
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// vnode.data存在
// 存在組件實例且為keep-alive組件
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 子組件調用init執行初始化,創建子組件實例進行子組件掛載
if (isDef((i = i.hook)) && isDef((i = i.init))) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
//在調用init鉤子之後,如果vnode是一個子組件,它應該創建一個子實例並掛載它。該子組件還設置了占位符vnode的elm。
//在這種情況下,我們可以返回元素並完成。
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
// 將子組件節點插入父元素中
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
// 如果是keep-alive組件則激活組件
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
initComponent
// 初始化組件
function initComponent(vnode, insertedVnodeQueue) {
// 存在pendingInsert就插入vnode隊列中
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
//賦予vnode.elm進行占位
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
// 觸發create鉤子
invokeCreateHooks(vnode, insertedVnodeQueue)
// 設置CSS作用域
setScope(vnode)
} else {
// empty component root.
// 空的根組件
// skip all element-related modules except for ref (#3455)
// 跳過除ref之外的所有與元素相關的模塊
// 註冊ref
registerRef(vnode)
// make sure to invoke the insert hook
// 確保調用了insert鉤子
insertedVnodeQueue.push(vnode)
}
}
reactivateComponent
// 激活組件 | 針對keep-alive組件
function reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i
// hack for #4339: a reactivated component with inner transition
// does not trigger because the inner node's created hooks are not called
// again. It's not ideal to involve module-specific logic in here but
// there doesn't seem to be a better way to do it.
// #4339的破解:具有內部轉換的重新激活組件不會觸發,
// 因為內部節點創建的鉤子不會被再次調用。
// 在這裡涉及特定於模塊的邏輯並不理想,但似乎沒有更好的方法。
let innerNode = vnode
while (innerNode.componentInstance) {
innerNode = innerNode.componentInstance._vnode
// 調用activate鉤子方法
if (isDef((i = innerNode.data)) && isDef((i = i.transition))) {
for (i = 0; i < cbs.activate.length; ++i) {
cbs.activate[i](emptyNode, innerNode)
}
// 將節點推入隊列
insertedVnodeQueue.push(innerNode)
break
}
}
// unlike a newly created component,
// a reactivated keep-alive component doesn't insert itself
// 不同於新創建的組件, 重新激活的keep-alive組件不會插入自身
insert(parentElm, vnode.elm, refElm)
}
insert
function insert(parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
createChildren
// 創建子節點
function createChildren(vnode, children, insertedVnodeQueue) {
if (isArray(children)) {
if (__DEV__) {
checkDuplicateKeys(children)
}
for (let i = 0; i < children.length; ++i) {
// 子節點是數組話就遍歷調用createElm
createElm(
children[i],
insertedVnodeQueue,
vnode.elm,
null,
true,
children,
i
)
}
} else if (isPrimitive(vnode.text)) {
// 文本節點是直接追加
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
isPatchable
function isPatchable(vnode) {
// 存在組件實例就將組件實例下的_vnode賦值給vnode
while (vnode.componentInstance) {
vnode = vnode.componentInstance._vnode
}
// 返回標簽名
return isDef(vnode.tag)
}
// 執行所有傳入節點下的create方法
function invokeCreateHooks(vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
setScope
// set scope id attribute for scoped CSS.
// this is implemented as a special case to avoid the overhead
// of going through the normal attribute patching process.
// 設置CSS的作用域id屬性。
// 這是作為一種特殊情況來實現的,以避免經過正常屬性修補過程的開銷。
function setScope(vnode) {
let i
if (isDef((i = vnode.fnScopeId))) {
nodeOps.setStyleScope(vnode.elm, i)
} else {
let ancestor = vnode
while (ancestor) {
if (isDef((i = ancestor.context)) && isDef((i = i.$options._scopeId))) {
nodeOps.setStyleScope(vnode.elm, i)
}
ancestor = ancestor.parent
}
}
// for slot content they should also get the scopeId from the host instance.
// 對於插槽內容,他們還應該從主機實例中獲取scopeId。
if (
isDef((i = activeInstance)) &&
i !== vnode.context &&
i !== vnode.fnContext &&
isDef((i = i.$options._scopeId))
) {
nodeOps.setStyleScope(vnode.elm, i)
}
}
addVnodes
// 在指定索引範圍內添加節點
function addVnodes(
parentElm,
refElm,
vnodes,
startIdx,
endIdx,
insertedVnodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(
vnodes[startIdx],
insertedVnodeQueue,
parentElm,
refElm,
false,
vnodes,
startIdx
)
}
}
invokeDestroyHook
// 銷毀節點,其實就是執行destroy鉤子方法
function invokeDestroyHook(vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
// 如果存在子節點就遞歸調用invokeDestroyHook
if (isDef((i = vnode.children))) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
removeVnodes
// 移除指定索引範圍內的節點
function removeVnodes(vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
} else {
// Text node
removeNode(ch.elm)
}
}
}
}
removeAndInvokeRemoveHook
// 移除並調用remove的鉤子方法
function removeAndInvokeRemoveHook(vnode, rm?: any) {
if (isDef(rm) || isDef(vnode.data)) {
let i
const listeners = cbs.remove.length + 1
if (isDef(rm)) {
// we have a recursively passed down rm callback
// increase the listeners count
rm.listeners += listeners
} else {
// directly removing
rm = createRmCb(vnode.elm, listeners)
}
// recursively invoke hooks on child component root node
// 遞歸調用子組件根節點商的鉤子
if (
isDef((i = vnode.componentInstance)) &&
isDef((i = i._vnode)) &&
isDef(i.data)
) {
removeAndInvokeRemoveHook(i, rm)
}
for (i = 0; i < cbs.remove.length; ++i) {
// 調用remove鉤子
cbs.remove[i](vnode, rm)
}
if (isDef((i = vnode.data.hook)) && isDef((i = i.remove))) {
i(vnode, rm)
} else {
rm()
}
} else {
removeNode(vnode.elm)
}
}
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]
// key和索引的映射關係 | 新節點對應的老節點的oldIdx | 需要移動的老節點 | 錨點
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
// removeOnly是一個特殊標誌,僅在<transition group>使用,以確保移除的元素在離開過渡期間保持在正確的相對位置
const canMove = !removeOnly
// dev環境下會檢查組件的新節點的key是否存在重覆的情況
if (__DEV__) {
checkDuplicateKeys(newCh)
}
// 對新老節點進行遍歷,任意一個遍歷完成就結束遍歷
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)) {
// 新老節點尾節點為同一個節點,執行patch
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 新老節點前移(左移)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 老節點頭節點和新節點尾節點是同一個節點,執行patch
// Vnode moved right
patchVnode(
oldStartVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
// 針對transition group情況進行處理
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
)
// 老節點右移
oldStartVnode = oldCh[++oldStartIdx]
// 新節點左移
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 老節點尾節點和新節點頭節點是同一節點,執行patch
// Vnode moved left
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
// 針對transition group情況進行處理
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// 老節點左移
oldEndVnode = oldCh[--oldEndIdx]
// 新節點右移
newStartVnode = newCh[++newStartIdx]
} else {
// 老節點下的key和索引的關係映射
if (isUndef(oldKeyToIdx))
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 在映射中找到新節點在老節點的索引
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// 新元素執行創建
// New element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
} else {
// 找到對應的節點開始進行對比
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 兩個節點是同一個執行patch
patchVnode(
vnodeToMove,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
// patch之後將老節點設置為undefined
oldCh[idxInOld] = 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)
}
}
checkDuplicateKeys
// 檢查傳入節點的key是否重覆
function checkDuplicateKeys(children) {
const seenKeys = {}
for (let i = 0; i < children.length; i++) {
const vnode = children[i]
const key = vnode.key
if (isDef(key)) {
if (seenKeys[key]) {
warn(
`Duplicate keys detected: '${key}'. This may cause an update error.`,
vnode.context
)
} else {
seenKeys[key] = true
}
}
}
}
findIdxInOld
function findIdxInOld(node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
}
patchVnode
// 更新節點
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly?: any
) {
// 新老節點一致不錯處理直接返回
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = (vnode.elm = oldVnode.elm)
// 非同步占位符
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
// 均是靜態節點
// key相同
// 新節點被克隆或存在v-once指令
// 將實例賦予新節點
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)
}
// 老節點的children
const oldCh = oldVnode.children
// 新節點的children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// 調用update鉤子
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)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
// 新老節點都存在children,則進行遞歸diff
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 僅新節點存在children
if (__DEV__) {
// 開發環境下檢查新節點下的children key值是否重覆
checkDuplicateKeys(ch)
}
// 老節點是文本節點則進行重置,文本內容置空
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 添加新節點創建元素
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 僅老節點存在children,新節點不存在
// 移除老節點的children
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 老節點是文本節點則置空
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 更新文本節點
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
// 調用postpatch鉤子
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode)
}
}
invokeInsertHook
// 調用insert鉤子函數
function invokeInsertHook(vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
// 延遲組件根節點的insert鉤子函數,在真正插入元素後調用它們
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
assertNodeMatch
// 判斷節點是否匹配
function assertNodeMatch(node, vnode, inVPre) {
if (isDef(vnode.tag)) {
return (
vnode.tag.indexOf('vue-component') === 0 ||
(!isUnknownElement(vnode, inVPre) &&
vnode.tag.toLowerCase() ===
(node.tagName && node.tagName.toLowerCase()))
)
} else {
return node.nodeType === (vnode.isComment ? 8 : 3)
}
}
assertNodeMatch
// 判斷節點是否匹配 (ssr) | dev環境下進行的檢查
function assertNodeMatch(node, vnode, inVPre) {
if (isDef(vnode.tag)) {
return (
vnode.tag.indexOf('vue-component') === 0 ||
(!isUnknownElement(vnode, inVPre) &&
vnode.tag.toLowerCase() ===
(node.tagName && node.tagName.toLowerCase()))
)
} else {
return node.nodeType === (vnode.isComment ? 8 : 3)
}
}
patch
// 返回一個patch函數對後續的節點進行patch操作 || todo
return function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
// 如果新節點不存在, 但老節點存在, 則調用destroy鉤子函數對老節點進行銷毀
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
// 如果新節點不存在, 老節點也不存在,直接return不用做任何操作
return
}
// 是否是初始化patch的標識
let isInitialPatch = false
// 插入虛擬節點的隊列
const insertedVnodeQueue: any[] = []
// 判斷是否存在老節點
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
// 空掛載(類型於組件),創建一個新的根元素
// 不存在老節點
isInitialPatch = true
// 首次初始化patch
createElm(vnode, insertedVnodeQueue)
} else {
// 判斷老節點是否是真實元素
const isRealElement = isDef(oldVnode.nodeType)
// 不是真實元素且為同一節點
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
// 掛載到一個真實的元素,
// 檢查這是否是服務端渲染(SSR)的內容,
// 以及我們是否可以成功執行hydration。
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
// 是服務端渲染且hydrate成功
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
// 調用insert鉤子
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (__DEV__) {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// 不是服務端渲染或者hydration失敗
// create an empty node and replace it
// 創建一個空節點代替老節點
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
// 獲取老節點元素
const oldElm = oldVnode.elm
// 獲取老節點的父元素
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// 基於新節點創建一個dom樹
createElm(
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)
// 極為罕見的邊緣情況:如果舊元素處於離開過渡,則不要插入。
// 只有當結合transition + keep-alive + HOCs時才會發生。(#4590)
oldElm._leaveCb ? null : parentElm,
// 獲取兄弟節點
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
// 遞歸更新父占位符節點元素
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
// 調用destroy鉤子
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
// 調用create鉤子
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
// 調用可能已通過創建鉤子合併的插入鉤子。
// 例如,對於使用“inserted”鉤子的指令。
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
// 從索引1開始,避免重新調用組件的mounted鉤子
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
// 註冊ref
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
// 銷毀老節點
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
// 調用insert鉤子
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
學無止境,謙卑而行.