細談 vue 核心- vdom 篇

很早以前,我曾寫過一篇文章,分析並實現過一版簡易的 vdom。想看的能夠點擊 傳送門javascript

聊聊爲何又想着寫這麼一篇文章,實在是項目裏,無論本身仍是同事,都或多或少會遇到這塊的坑。因此這裏當給小夥伴們再作一次總結吧,但願大夥看完,能對 vue 中的 vdom 有一個更好的認知。好了,接下來直接開始吧html

1、拋出問題

在開始以前,我先拋出一個問題,你們能夠先思考,而後再接着閱讀後面的篇幅。先上下代碼前端

<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
}
複製代碼

2、class VNode

開篇前,先講一下 VNodevue 中的 vdom 其實就是一個 vnode 對象。express

vdom 稍做了解的同窗都應該知道,vdom 建立節點的核心首先就是建立一個對真實 dom 抽象的 js 對象樹,而後經過一系列操做(後面我再談具體什麼操做)。該章節咱們就只談 vnode 的實現數組

一、constructor

首先,咱們能夠先看看, 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 的惟一標識
}
複製代碼

二、for example

如今,咱們舉個例子,假如我須要解析下面文本

<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
      }
    ]
  }
  // 依次循環
]
複製代碼

三、something else

咱們看完了 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

3、render

一、createElement

在看 vue 中 createElement 的實現前,咱們先看看同文件下私有方法 _createElement 的實現。其中是對 tag 具體的一些邏輯斷定

  • tagName 綁定在 data 參數裏面
if (isDef(data) && isDef(data.is)) {
  tag = data.is
}
複製代碼
  • tagName 不存在時,返回一個空節點
if (!tag) {
  return createEmptyVNode()
}
複製代碼
  • tagName 是 string 類型的時候,直接返回對應 tag 的 vnode 對象
vnode = new VNode(
  tag, data, children,
  undefined, undefined, context
)
複製代碼
  • tagName 是非 string 類型的時候,則執行 createComponent() 建立一個 Component 對象
vnode = createComponent(tag, data, context, children)
複製代碼
  • 斷定 vnode 類型,進行對應的返回
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)
複製代碼

二、render functions

i. renderHelpers

這裏咱們先總體看下,掛載在 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。具體代碼此處忽略
  }
}
複製代碼

ii. AST 抽象語法樹

到目前爲止,咱們看到的 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 第二個參數
};
複製代碼

iii. generate 字符串轉換

  • 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 字符串數組
  }
}
複製代碼

三、render 栗子

看了上面這麼多,對 vue 不太瞭解的一些小夥伴可能會以爲有些暈,這裏直接舉一個 v-for 渲染的例子給你們來理解。

i. demo

<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>`
複製代碼

ii. 相關正則

拿到 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 = {
  '&lt;': '<',
  '&gt;': '>',
  '&quot;': '"',
  '&amp;': '&',
  '&#10;': '\n',
  '&#9;': '\t'
}
const encodedAttr = /&(?:lt|gt|quot|amp);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#10|#9);/g
複製代碼

iii. parseHTML

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 這塊的邏輯了。

4、diff and patch

哎呀,終於到 diff 和 patch 環節了,想一想仍是很雞凍呢。

一、一些 DOM 的 API 操做

看進行具體 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
}
複製代碼

二、一些 patch 中的 API 操做

提示:上面咱們列出來的 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 type
function 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

i. 相關流程圖

談到這,先挪(盜)用下我之前文章中相關的兩張圖

ii. diff、patch 操做合二爲一

看過我之前文章的小夥伴都應該知道,我以前文章中關於 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 上面的文本內容是如何進行對比的

  • 若 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, '')
  }
}
複製代碼
  • 若 vnode 爲文本節點,且新舊節點文本不一樣,則直接將設置爲 vnode 的文本內容
if (isDef(vnode.text) && oldVnode.text !== vnode.text) {
  nodeOps.setTextContent(elm, vnode.text)
}
複製代碼

iii. updateChildren

首先咱們先看下方法中對新舊節點起始和結束索引的定義

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]
}
複製代碼

接下來就是 oldStartVnodenewStartVnodeoldEndVnodenewEndVnode 兩兩對比的四種狀況了

// 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,它的做用主要是 returnoldChkeyindex 惟一對應的 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 ,用來找出 newStartVnodeoldCh 數組中對應的索引

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]
}
複製代碼

通過這一系列的操做,則完成了節點之間的 diffpatch 操做,即完成了 oldVnodenewVnode 轉換的操做。

文章到這裏也要告一段落了,看到這裏,相信你們已經對 vue 中的 vdom 這塊也必定有了本身的理解了。 那麼,咱們再回到文章開頭咱們拋出的問題,你們知道爲何會出現這個問題了麼?

emmm,若是想要繼續溝通此問題,歡迎你們加羣進行討論,前端大雜燴:731175396。小夥伴們記得加羣哦,哪怕一塊兒來水羣也是好的啊 ~ (注:羣裏單身漂亮妹紙真的不少哦,固然帥哥也不少,好比。。。me)

我的準備從新撿回本身的公衆號了,以後每週保證一篇高質量好文,感興趣的小夥伴能夠關注一波。

相關文章
相關標籤/搜索