很早以前,我曾寫過一篇文章,分析並實現過一版簡易的 vdom
。想看的能夠點擊 傳送門javascript
聊聊爲何又想着寫這麼一篇文章,實在是項目裏,無論本身仍是同事,都或多或少會遇到這塊的坑。因此這裏當給小夥伴們再作一次總結吧,但願大夥看完,能對 vue
中的 vdom
有一個更好的認知。好了,接下來直接開始吧html
在開始以前,我先拋出一個問題,你們能夠先思考,而後再接着閱讀後面的篇幅。先上下代碼前端
<template>
<el-select class="test-select" multiple filterable remote placeholder="請輸入關鍵詞" :remote-method="remoteMethod" :loading="loading" @focus="handleFoucs" v-model="items">
<!-- 這裏 option 的 key 直接綁定 vfor 的 index -->
<el-option v-for="(item, index) in options" :key="index" :label="item.label" :value="item.value">
<el-checkbox :label="item.value" :value="isChecked(item.value)">
{{ item.label }}
</el-checkbox>
</el-option>
</el-select>
</template>
<script lang="ts"> import { Component, Vue } from 'vue-property-decorator' @Component export default class TestSelect extends Vue { options: Array<{ label: string, value: string }> = [] items: Array<string> = [] list: Array<{ label: string, value: string }> = [] loading: boolean = false states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'] mounted () { this.list = this.states.map(item => { return { value: item, label: item } }) } remoteMethod (query) { if (query !== '') { this.loading = true setTimeout(() => { this.loading = false this.options = this.list.filter(item => { return item.label.toLowerCase() .indexOf(query.toLowerCase()) > -1 }) }, 200) } else { this.options = this.list } } handleFoucs (e) { this.remoteMethod(e.target.value) } isChecked (value: string): boolean { let checked = false this.items.forEach((item: string) => { if (item === value) { checked = true } }) return checked } } </script>
複製代碼
輸入篩選後效果圖以下vue
而後我在換一個關鍵詞進行搜索,結果就會出現如下展現的問題java
我並無進行選擇,可是 select 選擇框中展現的值卻發生了變動。老司機可能一開始看代碼,就知道問題所在了。其實把 option 裏面的 key
綁定換一下就OK,換成以下的node
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
<el-checkbox :label="item.value" :value="isChecked(item.value)">
{{ item.label }}
</el-checkbox>
</el-option>
複製代碼
那麼問題來了,這樣能夠避免問題,可是爲何能夠避免呢?其實,這塊就牽扯到 vdom 裏 patch 相關的內容了。接下來我就帶着你們從新把 vdom 再撿起來一次web
開始以前,看幾個下文中常常出現的 API正則表達式
isDef()
export function isDef (v: any): boolean %checks {
return v !== undefined && v !== null
}
複製代碼
isUndef()
export function isUndef (v: any): boolean %checks {
return v === undefined || v === null
}
複製代碼
isTrue()
export function isTrue (v: any): boolean %checks {
return v === true
}
複製代碼
開篇前,先講一下 VNode ,vue
中的 vdom
其實就是一個 vnode
對象。express
對 vdom
稍做了解的同窗都應該知道,vdom
建立節點的核心首先就是建立一個對真實 dom 抽象的 js 對象樹,而後經過一系列操做(後面我再談具體什麼操做)。該章節咱們就只談 vnode
的實現數組
首先,咱們能夠先看看, VNode 這個類對咱們這些使用者暴露了哪些屬性出來,挑一些咱們常見的看
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component
) {
this.tag = tag // 節點的標籤名
this.data = data // 節點的數據信息,如 props,attrs,key,class,directives 等
this.children = children // 節點的子節點
this.text = text // 節點對應的文本
this.elm = elm // 節點對應的真實節點
this.context = context // 節點上下文,爲 Vue Component 的定義
this.key = data && data.key // 節點用做 diff 的惟一標識
}
複製代碼
如今,咱們舉個例子,假如我須要解析下面文本
<template>
<div class="vnode" :class={ 'show-node': isShow } v-show="isShow">
This is a vnode.
</div>
</template>
複製代碼
使用 js 進行抽象就是這樣的
function render () {
return new VNode(
'div',
{
// 靜態 class
staticClass: 'vnode',
// 動態 class
class: {
'show-node': isShow
},
/**
* directives: [
* {
* rawName: 'v-show',
* name: 'show',
* value: isShow
* }
* ],
*/
// 等同於 directives 裏面的 v-show
show: isShow,
[ new VNode(undefined, undefined, undefined, 'This is a vnode.') ]
}
)
}
複製代碼
轉換成 vnode 後的表現形式以下
{
tag: 'div',
data: {
show: isShow,
// 靜態 class
staticClass: 'vnode',
// 動態 class
class: {
'show-node': isShow
},
},
text: undefined,
children: [
{
tag: undefined,
data: undefined,
text: 'This is a vnode.',
children: undefined
}
]
}
複製代碼
而後我再看一個稍微複雜一點的例子
<span v-for="n in 5" :key="n">{{ n }}</span>
複製代碼
假如讓你們使用 js 對其進行對象抽象,你們會如何進行呢?主要是裏面的 v-for 指令,你們能夠先本身帶着思考試試。
OK,不賣關子,咱們如今直接看看下面的 render 函數對其的抽象處理,其實就是循環 render 啦!
function render (val, keyOrIndex, index) {
return new VNode(
'span',
{
directives: [
{
rawName: 'v-for',
name: 'for',
value: val
}
],
key: val,
[ new VNode(undefined, undefined, undefined, val) ]
}
)
}
function renderList ( val: any, render: ( val: any, keyOrIndex: string | number, index?: number ) => VNode ): ?Array<VNode> {
// 僅考慮 number 的狀況
let ret: ?Array<VNode>, i, l, keys, key
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
return ret
}
renderList(5)
複製代碼
轉換成 vnode 後的表現形式以下
[
{
tag: 'span',
data: {
key: 1
},
text: undefined,
children: [
{
tag: undefined,
data: undefined,
text: 1,
children: undefined
}
]
}
// 依次循環
]
複製代碼
咱們看完了 VNode Ctor
的一些屬性,也看了一下對於真實 dom vnode 的轉換形式,這裏咱們就稍微補個漏,看看基於 VNode
作的一些封裝給咱們暴露的一些方法
// 建立一個空節點
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
// 建立一個文本節點
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
// 克隆一個節點,僅列舉部分屬性
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children,
vnode.text
)
cloned.key = vnode.key
cloned.isCloned = true
return cloned
}
複製代碼
捋清楚 VNode
相關方法,下面的章節,將介紹 vue
是如何將 vnode
渲染成真實 dom
在看 vue 中 createElement 的實現前,咱們先看看同文件下私有方法 _createElement
的實現。其中是對 tag 具體的一些邏輯斷定
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
複製代碼
if (!tag) {
return createEmptyVNode()
}
複製代碼
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
複製代碼
createComponent()
建立一個 Component 對象vnode = createComponent(tag, data, context, children)
複製代碼
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
// namespace 相關處理
if (isDef(ns)) applyNS(vnode, ns)
// 進行 Observer 相關綁定
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
複製代碼
createElement()
則是執行 _createElement()
返回 vnode
return _createElement(context, tag, data, children, normalizationType)
複製代碼
這裏咱們先總體看下,掛載在 Vue.prototype
上的都有哪些 render 相關的方法
export function installRenderHelpers (target: any) {
target._o = markOnce // v-once render 處理
target._n = toNumber // 值轉換 Number 處理
target._s = toString // 值轉換 String 處理
target._l = renderList // v-for render 處理
target._t = renderSlot // slot 槽點 render 處理
target._q = looseEqual // 判斷兩個對象是否大致相等
target._i = looseIndexOf // 對等屬性索引,不存在則返回 -1
target._m = renderStatic // 靜態節點 render 處理
target._f = resolveFilter // filters 指令 render 處理
target._k = checkKeyCodes // checking keyCodes from config
target._b = bindObjectProps // v-bind render 處理,將 v-bind="object" 的屬性 merge 到VNode屬性中
target._v = createTextVNode // 建立文本節點
target._e = createEmptyVNode // 建立空節點
target._u = resolveScopedSlots // scopeSlots render 處理
target._g = bindObjectListeners // v-on render 處理
}
複製代碼
而後在 renderMixin()
方法中,對 Vue.prototype
進行 init 操做
export function renderMixin (Vue: Class<Component>) {
// render helps init 操做
installRenderHelpers(Vue.prototype)
// 定義 vue nextTick 方法
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {
// 此處定義 vm 實例,以及 return vnode。具體代碼此處忽略
}
}
複製代碼
到目前爲止,咱們看到的 render
相關的操做都是返回一個 vnode
對象,而真實節點的渲染以前,vue 會對 template 模板中的字符串進行解析,將其轉換成 AST 抽象語法樹,方便後續的操做。關於這塊,咱們直接來看看 vue 中在 flow 類型裏面是如何定義 ASTElement
接口類型的,既然是開篇拋出的問題是由 v-for
致使的,那麼這塊,咱們就僅僅看看 ASTElement
對其的定義,看完以後記得觸類旁通去源碼裏面理解其餘的定義哦💪
declare type ASTElement = {
tag: string; // 標籤名
attrsMap: { [key: string]: any }; // 標籤屬性 map
parent: ASTElement | void; // 父標籤
children: Array<ASTNode>; // 子節點
for?: string; // 被 v-for 的對象
forProcessed?: boolean; // v-for 是否須要被處理
key?: string; // v-for 的 key 值
alias?: string; // v-for 的參數
iterator1?: string; // v-for 第一個參數
iterator2?: string; // v-for 第二個參數
};
複製代碼
renderList
在看 render function
字符串轉換以前,先看下 renderList
的參數,方便後面的閱讀
export function renderList ( val: any, render: ( val: any, keyOrIndex: string | number, index?: number ) => VNode ): ?Array<VNode> {
// 此處爲 render 相關處理,具體細節這裏就不列出來了,上文中有列出 number 狀況的處理
}
複製代碼
genFor
上面看完定義,緊接着咱們再來看看,generate
是如何將 AST 轉換成 render function 字符串的,這樣同理咱們就看對 v-for
相關的處理
function genFor ( el: any, state: CodegenState, altGen?: Function, altHelper?: string ): string {
const exp = el.for // v-for 的對象
const alias = el.alias // v-for 的參數
const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' // v-for 第一個參數
const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' // v-for 第二個參數
el.forProcessed = true // 指令須要被處理
// return 出對應 render function 字符串
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
複製代碼
genElement
這塊集成了各個指令對應的轉換邏輯
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) { // 靜態節點
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) { // v-once 處理
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // v-for 處理
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // v-if 處理
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) { // template 根節點處理
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') { // slot 節點處理
return genSlot(el, state)
} else {
// component or element 相關處理
}
}
複製代碼
generate
generate
則是將以上全部的方法集成到一個對象中,其中 render
屬性對應的則是 genElement
相關的操做,staticRenderFns
對應的則是字符串數組。
export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`, // render
staticRenderFns: state.staticRenderFns // render function 字符串數組
}
}
複製代碼
看了上面這麼多,對 vue 不太瞭解的一些小夥伴可能會以爲有些暈,這裏直接舉一個 v-for
渲染的例子給你們來理解。
<div class="root">
<span v-for="n in 5" :key="n">{{ n }}</span>
</div>
複製代碼
這塊首先會被解析成 html 字符串
let html = `<div class="root"> <span v-for="n in 5" :key="n">{{ n }}</span> </div>`
複製代碼
拿到 template 裏面的 html 字符串以後,會對其進行解析操做。具體相關的正則表達式在 src/compiler/parser/html-parser.js
裏面有說起,如下是相關的一些正則表達式以及 decoding map
的定義。
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
const decodingMap = {
'<': '<',
'>': '>',
'"': '"',
'&': '&',
' ': '\n',
'	': '\t'
}
const encodedAttr = /&(?:lt|gt|quot|amp);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#10|#9);/g
複製代碼
vue
解析 template 都是使用 while
循環進行字符串匹配的,往往解析完一段字符串都會將已經匹配完的部分去除掉,而後 index
索引會直接對剩下的部分繼續進行匹配。具體有關 parseHTML
的定義以下,因爲文章到這篇幅已經比較長了,我省略掉了正則循環匹配指針的一些邏輯,想要具體瞭解的小夥伴能夠自行研究或者等我下次再出一篇文章詳談這塊的邏輯。
export function parseHTML (html, options) {
const stack = [] // 用來存儲解析好的標籤頭
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0 // 匹配指針索引
let last, lastTag
while (html) {
// 此處是對標籤進行正則匹配的邏輯
}
// 清理剩餘的 tags
parseEndTag()
// 循環匹配相關處理
function advance (n) {
index += n
html = html.substring(n)
}
// 起始標籤相關處理
function parseStartTag () {
let match = {
tagName: start[1],
attrs: [],
start: index
}
// 一系列匹配操做,而後對 match 進行賦值
return match
}
function handleStartTag (match) {}
// 結束標籤相關處理
function parseEndTag (tagName, start, end) {}
}
複製代碼
通過 parseHTML()
進行一系列正則匹配處理以後,會將字符串 html 解析成如下 AST 的內容
{
'attrsMap': {
'class': 'root'
},
'staticClass': 'root', // 標籤的靜態 class
'tag': 'div', // 標籤的 tag
'children': [{ // 子標籤數組
'attrsMap': {
'v-for': "n in 5",
'key': n
},
'key': n,
'alias': "n", // v-for 參數
'for': 5, // 被 v-for 的對象
'forProcessed': true,
'tag': 'span',
'children': [{
'expression': '_s(item)', // toString 操做(上文有說起)
'text': '{{ n }}'
}]
}]
}
複製代碼
到這裏,再結合上面的 generate
進行轉換即是 render
這塊的邏輯了。
哎呀,終於到 diff 和 patch 環節了,想一想仍是很雞凍呢。
看進行具體 diff 以前,咱們先看看在 platforms/web/runtime/node-ops.js
中定義的一些建立真實 dom 的方法,正好溫習一下 dom
相關操做的 API
createElement()
建立由 tagName 指定的 HTML 元素export function createElement (tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
複製代碼
createTextNode()
建立文本節點export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
複製代碼
createComment()
建立一個註釋節點export function createComment (text: string): Comment {
return document.createComment(text)
}
複製代碼
insertBefore()
在參考節點以前插入一個擁有指定父節點的子節點export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
複製代碼
removeChild()
從 DOM 中刪除一個子節點export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
複製代碼
appendChild()
將一個節點添加到指定父節點的子節點列表末尾export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
複製代碼
parentNode()
返回父節點export function parentNode (node: Node): ?Node {
return node.parentNode
}
複製代碼
nextSibling()
返回兄弟節點export function nextSibling (node: Node): ?Node {
return node.nextSibling
}
複製代碼
tagName()
返回節點標籤名export function tagName (node: Element): string {
return node.tagName
}
複製代碼
setTextContent()
設置節點文本內容export function setTextContent (node: Node, text: string) {
node.textContent = text
}
複製代碼
提示:上面咱們列出來的 API 都掛在了下面的 nodeOps
對象中了
createElm()
建立節點function createElm (vnode, parentElm, refElm) {
if (isDef(vnode.tag)) { // 建立標籤節點
vnode.elm = nodeOps.createElement(tag, vnode)
} else if (isDef(vnode.isComment)) { // 建立註釋節點
vnode.elm = nodeOps.createComment(vnode.text)
} else { // 建立文本節點
vnode.elm = nodeOps.createTextNode(vnode.text)
}
insert(parentElm, vnode.elm, refElm)
}
複製代碼
insert()
指定父節點下插入子節點function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) { // 插入到指定 ref 的前面
if (ref.parentNode === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else { // 直接插入到父節點後面
nodeOps.appendChild(parent, elm)
}
}
}
複製代碼
addVnodes()
批量調用 createElm()
來建立節點function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], parentElm, refElm)
}
}
複製代碼
removeNode()
移除節點function removeNode (el) {
const parent = nodeOps.parentNode(el)
if (isDef(parent)) {
nodeOps.removeChild(parent, el)
}
}
複製代碼
removeNodes()
批量移除節點function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
removeNode(ch.elm)
}
}
}
複製代碼
sameVnode()
是否爲相同節點function sameVnode (a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
}
複製代碼
sameInputType()
是否有相同的 input typefunction sameInputType (a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB
}
複製代碼
談到這,先挪(盜)用下我之前文章中相關的兩張圖
看過我之前文章的小夥伴都應該知道,我以前文章中關於 diff 和 patch 是分紅兩個步驟來實現的。而 vue
中則是將 diff 和 patch 操做合二爲一了。如今咱們來看看,vue
中對於這塊具體是如何處理的
function patch (oldVnode, vnode) {
// 若是老節點不存在,則直接建立新節點
if (isUndef(oldVnode)) {
if (isDef(vnode)) createElm(vnode)
// 若是老節點存在,新節點卻不存在,則直接移除老節點
} else if (isUndef(vnode)) {
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
removeVnodes(parentElm, , 0, [oldVnode].length -1)
} else {
const isRealElement = isDef(oldVnode.nodeType)
// 若是新舊節點相同,則進行具體的 patch 操做
if (isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
// 不然建立新節點,移除老節點
createElm(vnode, parentElm, nodeOps.nextSibling(oldElm))
removeVnodes(parentElm, [oldVnode], 0, 0)
}
}
}
複製代碼
而後咱們再看 patchVnode
中間相關的邏輯,先看下,前面說起的 key
在這的用處
function patchVnode (oldVnode, vnode) {
// 新舊節點徹底同樣,則直接 return
if (oldVnode === vnode) {
return
}
// 若是新舊節點都被標註靜態節點,且節點的 key 相同。
// 則直接將老節點的 componentInstance 直接拿過來便OK了
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
}
複製代碼
接下來,咱們看看 vnode 上面的文本內容是如何進行對比的
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
if (isUndef(vnode.text)) {
// 若是 oldCh,ch 都存在且不相同,則執行 updateChildren 函數更新子節點
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch)
// 若是隻有 ch 存在
} else if (isDef(ch)) {
// 老節點爲文本節點,先將老節點的文本清空,而後將 ch 批量插入到節點 elm 下
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1)
// 若是隻有 oldCh 存在,則直接清空老節點
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 若是 oldCh,ch 都不存在,且老節點爲文本節點,則只將老節點文本清空
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
}
複製代碼
if (isDef(vnode.text) && oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
複製代碼
首先咱們先看下方法中對新舊節點起始和結束索引的定義
function updateChildren (parentElm, oldCh, newCh) {
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
}
複製代碼
直接畫張圖來理解下
緊接着就是一個 while
循環讓新舊節點起始和結束索引不斷往中間靠攏
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
複製代碼
若 oldStartVnode
或者 oldEndVnode
不存在,則往中間靠攏
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
複製代碼
接下來就是 oldStartVnode
,newStartVnode
,oldEndVnode
,newEndVnode
兩兩對比的四種狀況了
// oldStartVnode 和 newStartVnode 爲 sameVnode,進行 patchVnode
// oldStartIdx 和 newStartIdx 向後移動一位
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode 和 newEndVnode 爲 sameVnode,進行 patchVnode
// oldEndIdx 和 newEndIdx 向前移動一位
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode 和 newEndVnode 爲 sameVnode,進行 patchVnode
// 將 oldStartVnode.elm 插入到 oldEndVnode.elm 節點後面
// oldStartIdx 向後移動一位,newEndIdx 向前移動一位
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode)
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 同理,oldEndVnode 和 newStartVnode 爲 sameVnode,進行 patchVnode
// 將 oldEndVnode.elm 插入到 oldStartVnode.elm 前面
// oldEndIdx 向前移動一位,newStartIdx 向後移動一位
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode)
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
複製代碼
用張圖來總結上面的流程
當以上條件都不知足的狀況,則進行其餘操做。
在看其餘操做前,咱們先看一下函數 createKeyToOldIdx
,它的做用主要是 return
出 oldCh
中 key
和 index
惟一對應的 map
表,根據 key
,則可以很方便的找出相應 key
在數組中對應的索引
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
複製代碼
除此以外,這塊還有另一個輔助函數 findIdxInOld
,用來找出 newStartVnode
在 oldCh
數組中對應的索引
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
}
}
複製代碼
接下來咱們看下不知足上面條件的具體處理
else {
// 若是 oldKeyToIdx 不存在,則將 oldCh 轉換成 key 和 index 對應的 map 表
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 若是 idxInOld 不存在,即老節點中不存在與 newStartVnode 對應 key 的節點,直接建立一個新節點
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
// 在 oldCh 找到了對應 key 的節點,且該節點與 newStartVnode 爲 sameVnode,則進行 patchVnode
// 將 oldCh 該位置的節點清空掉,並在 parentElm 中將 vnodeToMove 插入到 oldStartVnode.elm 前面
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode)
oldCh[idxInOld] = undefined
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 找到了對應的節點,可是卻屬於不一樣的 element 元素,則建立一個新節點
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
// newStartIdx 向後移動一位
newStartVnode = newCh[++newStartIdx]
}
複製代碼
通過這一系列的操做,則完成了節點之間的 diff
和 patch
操做,即完成了 oldVnode
向 newVnode
轉換的操做。
文章到這裏也要告一段落了,看到這裏,相信你們已經對 vue
中的 vdom
這塊也必定有了本身的理解了。 那麼,咱們再回到文章開頭咱們拋出的問題,你們知道爲何會出現這個問題了麼?
emmm,若是想要繼續溝通此問題,歡迎你們加羣進行討論,前端大雜燴:731175396。小夥伴們記得加羣哦,哪怕一塊兒來水羣也是好的啊 ~ (注:羣裏單身漂亮妹紙真的不少哦,固然帥哥也不少,好比。。。me)
我的準備從新撿回本身的公衆號了,以後每週保證一篇高質量好文,感興趣的小夥伴能夠關注一波。