博客 / 詳情

返回

Vue Virtual DOM基礎之Snabbdom解析(3-1-3)

前言:前面寫了1篇vue 觀察者模式響應式數據的解析,裏面關於dom操作部分是直接操作的真實dom,並沒有涉及到虛擬dom部分,然後這裏就做一個虛擬dom的實現解析。
首先,還是説什麼是虛擬dom,我之前一篇mini-react中説過虛擬dom的概念,這裏再重複一下。

  • Virtual DOM(虛擬 DOM),是由普通的 JS 對象來描述 DOM 對象,因為不是真實的 DOM 對象,所以叫 Virtual DOM
  • 可以使用 Virtual DOM 來描述真實 DOM,示例:
{
    sel:'div',
    data:{},
    children:undefined,
    text:'hello word',
    elm:undefined,
    key:undefined
}

這裏和我之前寫的 mini-react中説的虛擬dom結構不一樣,這裏是以Snabbdom生成的虛擬dom結構為例子,表述,因為vue2.0內部使用就是Snabbdom,只是在它的基礎上改造了一些新的功能,它也是最快的虛擬dom庫之一。

然後就是動機,這裏説下為啥要用虛擬dom:

  • 手動操作 DOM 比較麻煩,還需要考慮瀏覽器兼容性問題,雖然有 jQuery 等庫簡化 DOM 操作,但是隨着項目的複雜 DOM 操作複雜提升
  • 為了簡化 DOM 的複雜操作於是出現了各種 MVVM 框架,MVVM 框架解決了視圖和狀態的同步問題
  • 為了簡化視圖的操作我們可以使用模板引擎,但是模板引擎沒有解決跟蹤狀態變化的問題,於是Virtual DOM 出現了
  • Virtual DOM 的好處是當狀態改變時不需要立即更新 DOM,只需要創建一個虛擬樹來描述DOM, Virtual DOM 內部將弄清楚如何有效(diff)的更新 DOM

虛擬dom的作用:

  • 維護視圖和狀態的關係
  • 複雜視圖情況下提升渲染性能
  • 除了渲染 DOM 以外,還可以實現 SSR(Nuxt.js/Next.js)、原生應用(Weex/React Native)、小程序(mpvue/uni-app)等

虛擬dom是可以做(真實dom,ssr,原生應用,小程序)的轉換的

前面説了vue 的虛擬dom基礎就是Snabbdom,下面我們就來做下Snabbdom 的源碼分析,方便後續進一步瞭解vue的源碼

首先準備工作:和mini-react一樣打包工具使用parcel,這裏貼一下package.json的配置

{
  "name": "snabbdom-demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "parcel index.html --open",
    "build": "parcel build index.html"
  },
  "dependencies": {
    "parcel-bundler": "^1.12.4",
    "snabbdom": "^0.7.4"
  }
}

注意:這裏用的是snabbdom 0.7.4版本,版本不一致使用方式可能會略有變化,可以去查看官網,最新版是2.1.0,核心邏輯沒有什麼特別大的變化。
首先沒了解過它的,可以先看下它的官網中文翻譯:
snabbdom中文翻譯
基本使用可以查看官網,可以參考上面的中文翻譯

我們這裏使用例子用的是es6模塊導入,官網是commonjs規範,

import{init,h,thunk}from'snabbdom'
// 1. 導入模塊
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'

snabbdom核心提供三個函數,init,h,thunk。

  • init是一個高階函數,返回patch函數用來對比更新dom。
  • h函數用來返回虛擬節點vNode,vue中經常會看到.
  • thunk是一種優化策略,處理不可變數據時使用

模塊,Snabbdom 的核心庫並不能處理元素的屬性/樣式/事件等,如果需要處理的話,可以使用模塊,官方提供了6個模塊。

  • attributes

    • 設置 DOM 元素的屬性,使用setAttribute()
    • 處理布爾類型的屬性
  • props

    • 和attributes模塊相似,設置 DOM 元素的屬性element[attr]=value
    • 不處理布爾類型的屬性
  • class

    • 切換類樣式
    • 注意:給元素設置類樣式是通過sel選擇器
  • dataset

    • 設置data-*的自定義屬性
  • eventlisteners

    • 註冊和移除事件
  • style

    • 設置行內樣式,支持動畫
    • delayed/remove/destroy

這裏放一個使用模塊的例子:

import { init, h } from 'snabbdom'
// 1. 導入模塊
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'
// 2. 註冊模塊
let patch = init([
  style,
  eventlisteners
])
// 3. 使用 h() 函數的第二個參數傳入模塊需要的數據(對象)
let vnode = h('div', {
  style: {
    backgroundColor: 'red'
  },
  on: {
    click: eventHandler
  }
}, [
  h('h1', 'Hello Snabbdom'),
  h('p', '這是p標籤')
])

其實它核心主要分這幾步:

  1. 使用init設置模塊,創建patch函數
  2. 使用h函數創建js對象(vNode)描述真實dom
  3. patch函數對比新舊兩個vNode
  4. 把變化內容更新到真實dom樹上

我們來看下node_modules中snabbdom它的源碼src下的結構
image.png

我們主要分析的有:

  • h函數 src/h.ts
  • vnode src/vnode.ts
  • init src/snabbdom.ts
  • patch src/snabbdom.ts
  • createElm src/snabbdom.ts
  • patchVnode src/snabbdom.ts
  • updateChildren src/snabbdom.ts

這裏先説下對應函數的大概功能,最後我會把我做過註釋的源代碼都貼出來,有興趣的可以把package.json拿走,下載然後找到對應node_modules裏的snabbdom裏的代碼和我貼的代碼註釋對比看下。

h函數

  • Snabbdom 中的 h() 函數不是用來創建超文本,而是創建 VNode
  • 函數重載:

    • 參數個數或類型不同的函數
    • JavaScript 中沒有重載的概念
    • TypeScript 中有重載,不過重載的實現還是通過代碼調整參數

    h.ts中就使用了重載,源碼中最後一個導出是對重載的實現,也是根據傳入參數來判斷的。

vnode

  • 一個 VNode 就是一個虛擬節點用來描述一個 DOM 元素,如果這個 VNode 有 children 就是Virtual DOM

image.png

init

  • init(modules, domApi),返回 patch() 函數(高階函數)

    • 因為 patch() 函數再外部會調用多次,每次調用依賴一些參數,比如:modules/domApi/cbs
    • 通過高階函數讓 init() 內部形成閉包,返回的 patch() 可以訪問到 modules/domApi/cbs,而不需要重新創建
  • init() 在返回 patch() 之前,首先收集了所有模塊中的鈎子函數存儲到 cbs 對象中

patch

它的作用:

  • 打補丁,把新節點中變化的內容渲染到真實 DOM,最後返回新節點作為下一次處理的舊節點
  • 回新節點作為下一次處理的舊節點對比新舊 VNode 是否相同節點(節點的 key 和 sel 相同)
  • 如果不是相同節點,刪除之前的內容,重新渲染
  • 如果是相同節點,再判斷新的 VNode 是否有 text,如果有並且和 oldVnode 的 text 不同,直接更新文本內容
  • 如果新的 VNode 有 children,判斷子節點是否有變化,判斷子節點的過程使用的就是 diff 算法
  • diff 過程只進行同層級比較

功能:

  • 傳入新舊 VNode,對比差異,把差異渲染到 DOM
  • 返回新的 VNode,作為下一次 patch() 的 oldVnode

執行過程:

  • 首先執行模塊中的鈎子函數pre
  • 如果 oldVnode 是 DOM 元素

    • 把 DOM 元素轉換成 oldVnode
  • 如果 oldVnode 和 vnode 相同(key 和 sel 相同)

    • 調用 patchVnode(),找節點的差異並更新 DOM
  • 如果不相同

    • 調用 createElm() 把 vnode 轉換為真實 DOM,記錄到 vnode.elm
    • 把剛創建的 DOM 元素插入到 parent 中
    • 移除老節點
    • 觸發用户設置的insert鈎子函數
    • 觸發模塊post鈎子

createElm

功能:

  • createElm(vnode, insertedVnodeQueue),返回創建的 DOM 元素
  • 創建 vnode 對應的 DOM 元素,放到vnode的elm上

執行過程:

  • 首先觸發用户設置的init鈎子函數
  • 如果選擇器是!,創建註釋節點
  • 如果選擇器不為空

    • 解析選擇器,生成真實dom,設置標籤的 id 和 class 屬性
    • 執行模塊的create鈎子函數
    • 如果 vnode 有 children,遞歸調用createElm創建子 vnode 對應的 DOM,追加到 DOM 樹
    • 如果 vnode 的 text 值是 string/number,創建文本節點並追擊到 DOM 樹
    • 執行用户設置的create鈎子函數
    • 如果有用户設置的 insert 鈎子函數,把 vnode 添加到隊列中
  • 如果選擇器為空,創建文本節點

patchVnode

功能:

  • patchVnode(oldVnode, vnode, insertedVnodeQueue)
  • 對比 oldVnode 和 vnode 的差異,把差異渲染到 DOM

執行過程

  • 首先執行用户設置的prepatch鈎子函數
  • 執行先模塊update 鈎子函數再用户的update鈎子函數
  • 如果vnode.text未定義

    • 如果oldVnode.children和vnode.children都有值

      • 新舊的children不相等 調用updateChildren()
      • 使用 diff 算法對比子節點,更新子節點
    • 如果vnode.children有值,oldVnode.children無值

      • 清空oldVnode中文本 DOM 元素
      • 調用addVnodes(),批量添加子節點
    • 如果oldVnode.children有值,vnode.children無值

      • 調用removeVnodes(),批量移除子節點
    • 如果oldVnode.text有值,新舊的children都沒有

      • 清空 DOM 元素的內容
  • 如果設置了vnode.text並且和和oldVnode.text不等

    • 如果老節點有子節點,全部移除
    • 設置 DOM 元素的textContent為vnode.text
  • 最後執行用户設置的postpatch鈎子函數

updateChildren

功能:diff 算法的核心,對比新舊節點的 children,更新 DOM(市面上虛擬dom的庫,diff算法核心都是更新對比子集)

執行過程:

  • 要對比兩棵樹的差異,我們可以取第一棵樹的每一個節點依次和第二課樹的每一個節點比較,但是這樣的時間複雜度為 O(n^3)
  • 在DOM 操作的時候我們很少很少會把一個父節點移動/更新到某一個子節點
  • 因此只需要找同級別的子節點依次比較,然後再找下一級別的節點比較,這樣算法的時間複雜度為 O(n)

image.png

  • 在進行同級別節點比較的時候,首先會對新老節點數組的開始和結尾節點設置標記索引,遍歷的過程中移動索引
  • 在對開始和結束節點比較的時候,總共有四種情況

    • oldStartVnode / newStartVnode (舊開始節點 / 新開始節點)
    • oldEndVnode / newEndVnode (舊結束節點 / 新結束節點)
    • oldStartVnode / oldEndVnode (舊開始節點 / 新結束節點)
    • oldEndVnode / newStartVnode (舊結束節點 / 新開始節點)

image.png

下面就是具體執行順序的比較了,如下:

重點1:

  • 開始節點和結束節點比較,這兩種情況類似

    • oldStartVnode / newStartVnode (舊開始節點 / 新開始節點)
    • oldEndVnode / newEndVnode (舊結束節點 / 新結束節點)
  • 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同,同一個)

    • 調用 patchVnode() 對比和更新節點
    • 把舊開始和新開始索引往後移動 oldStartIdx++ / oldEndIdx++

    image.png
    這兩種情況,這會是不需要調整位置的,因為都是開始節點。

重點2

  • oldStartVnode / newEndVnode (舊開始節點 / 新結束節點) 相同

    • 調用 patchVnode() 對比和更新節點
    • 把 oldStartVnode 對應的 DOM 元素,移動到右邊

      • 更新索引

    image.png

這裏需要調整節點位置了,因為是要以新的children為主,對比的是新結束節點,先等也就是説(舊的開始節點現在=新的結束節點) 所以我們把舊開始節點移到最後,然後把++oldStartIdx,--newEndIdx,繼續下一次的比對,如果下次還還是進入了這個判斷,也就是説明 等於了新children倒數第二個,那就是要把oldStartIdx它插入到倒數第二的位置上,依次類推

重點3

  • oldEndVnode / newStartVnode (舊結束節點 / 新開始節點) 相同

    • 調用 patchVnode() 對比和更新節點
    • 把 oldEndVnode 對應的 DOM 元素,移動到左邊
    • 更新索引

    image.png

這裏和上面剛好相反,也是需要調整位置,因為是要以新的children為主,對比的是新開始傑點,先等也就是説(舊的結束節點現在=新的開始節點)所以我們把舊結束節點移到最開始,然後把--oldEndIdx,++newEndIdx,繼續下一次的比對,如果下次還還是進入了這個判斷,也就是説明 等於了新children正數第二個,那就是要把oldEndIdx它插入到倒數第二的位置上,再進入的話依次類推

重點4

  • 如果不是以上四種情況

    • 遍歷新節點,使用 newStartNode 的 key 在老節點數組中找相同節點
    • 如果沒有找到,説明 newStartNode 是新節點

      • 創建新節點對應的 DOM 元素,插入到 DOM 樹中,插入到oldStartVnode之前
    • 如果找到了

      • 判斷新節點和找到的老節點的 sel 選擇器是否相同
      • 如果不相同,説明節點被修改了

        • 重新創建對應的 DOM 元素,插入到 DOM 樹中,插到插到oldStartVnode之前
      • 如果相同

        • patchVnode更新節點
        • 對比過的老children對應位置置空
        • 把 elmToMove 對應的 DOM 元素,移動到左邊, 插入到 oldStartVnode之前 ,更新dom順序

重點5

  • 循環結束

    • 當老節點的所有子節點先遍歷完 (oldStartIdx > oldEndIdx),循環結束
    • 新節點的所有子節點先遍歷完 (newStartIdx > newEndIdx),循環結束
  • 如果老節點的數組先遍歷完(oldStartIdx > oldEndIdx),説明新節點有剩餘,把剩餘節點批量插入到右邊,批量插入newEndIdx之後,因為經過比對結束之後首先順序是沒有問題的,我們把沒有出現的依次插入oldEndIdx之後就可以了。
    插入一段源代碼

    //查看老的結束的下標是不是最後一個,是最後一個的話給null,不是最後一個的話獲取最後一個的下一個的elm 真實dom
    before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
    // 把newStartIdx, newEndIdx之間的vnode 進行插入,然後根據before 看插入到最後,還是 newCh[newEndIdx+1].elm之前
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);

    這裏可以看出,如果是null證明newEndIdx沒有移動過下一個值是null,所我們全部插入在最後就可以,如果不是null,證明移動過,就獲取它的下一個dom,因為--newEndIdx過,插入它之前,因為我們知道它的順序是沒問題,newEndIdx倒數第幾個的位置也是沒問題,這張圖片可能有有點描述不對,主要看代碼。

image.png

  • 如果新節點的數組先遍歷完(newStartIdx > newEndIdx),説明老節點有剩餘,把剩餘節點從oldStartIdx, 到oldEndIdx之間的vnode批量刪除
    image.png

上面寫了幾個函數的具體實現思路,這裏把函數在那裏被調用,用圖去描述一下,圖裏面只是簡單描述了調用,具體方法的實現,跳轉文章對應位置查看就可以了,vnode和h函數就不在圖裏描述了,一個是虛擬dom的結構,一個是用來生成虛擬dom的
init.png

微信截圖_20201217161651.png

最後我把我註釋的代碼貼上

snabbdom.ts

/* global module, document, Node */
import {Module} from './modules/module';
import {Hooks} from './hooks';
import vnode, {VNode, VNodeData, Key} from './vnode';
import * as is from './is';
import htmlDomApi, {DOMAPI} from './htmldomapi';

function isUndef(s: any): boolean { return s === undefined; }
function isDef(s: any): boolean { return s !== undefined; }

type VNodeQueue = Array<VNode>;

const emptyNode = vnode('', {}, [], undefined, undefined);

function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

function isVnode(vnode: any): vnode is VNode {
  return vnode.sel !== undefined;
}

type KeyToIndexMap = {[key: string]: number};

type ArraysOf<T> = {
  [K in keyof T]: (T[K])[];
}

type ModuleHooks = ArraysOf<Module>;

function createKeyToOldIdx(children: Array<VNode>, beginIdx: number, endIdx: number): KeyToIndexMap {
  let i: number, map: KeyToIndexMap = {}, key: Key | undefined, ch;
  for (i = beginIdx; i <= endIdx; ++i) {
    ch = children[i];
    if (ch != null) {
      key = ch.key;
      if (key !== undefined) map[key] = i;
    }
  }
  return map;
}

const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];

export {h} from './h';
export {thunk} from './thunk';
//高階函數,接收使用的模塊列表,比如説h函數第二個參數需要使用style,對應init初始化時就需要傳入對應的模塊,
//第二個參數時dom操作的列表,不傳入的話就是默認的dom操作
//高階函數時因為 返回的patch要多次調用,我們需要閉包存儲一些每次調用都使用的參數,而不需要重新創建
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number, j: number, cbs = ({} as ModuleHooks);

  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
  //cbs把所有的生命週期初始化成數組,然後把對應模塊的生命週期函數放到對應數組裏,後期再合適的時機統一執行
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = [];
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]];
      if (hook !== undefined) {
        (cbs[hooks[i]] as Array<any>).push(hook);
      }
    }
  }
  //下面是一些輔助函數,我們先直接跳到最後的返回的函數也就是patch
  function emptyNodeAt(elm: Element) {
    const id = elm.id ? '#' + elm.id : '';
    const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
  }

  function createRmCb(childElm: Node, listeners: number) {
    return function rmCb() {
      if (--listeners === 0) {
        const parent = api.parentNode(childElm);
        api.removeChild(parent, childElm);
      }
    };
  }

  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any, data = vnode.data; //獲取data {style樣式,hook鈎子,on事件等}  除了hook其他的需要對應模塊支持
    if (data !== undefined) {
      if (isDef(i = data.hook) && isDef(i = i.init)) { //獲取傳入init鈎子
        i(vnode); //開始執行,傳入vNode
        data = vnode.data;//這裏重新給data賦值下,因為鈎子函數執行過程可能會更改他
      }
    }
    let children = vnode.children, sel = vnode.sel;//獲取它的子節點  和 選擇器
    if (sel === '!') { //查看是否是註釋節點
      if (isUndef(vnode.text)) { //是註釋節點,查看vNode文本是否有
        vnode.text = ''; //清空
      }
      vnode.elm = api.createComment(vnode.text as string); //創建註釋節點 賦值給vNode.elm 給它真實dom引用
    } else if (sel !== undefined) { //如果選擇器存在
      // Parse selector
      const hashIdx = sel.indexOf('#'); //id開始位置
      const dotIdx = sel.indexOf('.', hashIdx); //class開始位置
      const hash = hashIdx > 0 ? hashIdx : sel.length; //查看有沒有id
      const dot = dotIdx > 0 ? dotIdx : sel.length;//查看有沒有class
      const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
      //獲取標籤 div span
      const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag)
                                                                               : api.createElement(tag);
      //根據tag創建元素標籤,並且賦給 vnode.elm 給它真實dom的引用
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));//設置id
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));//設置class
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); //執行模塊中cbs中的 create的鈎子函數
      //emptyNode空vNode  後面vNode是當前的
      if (is.array(children)) {//如果vNode子節點是數字,遍歷 只要
        for (i = 0; i < children.length; ++i) { 
          const ch = children[i];
          if (ch != null) {
            //如果當前子節點項存在,繼續createElm轉換為真實dom返回插入到,父dom elm中
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
          }
        }
      } else if (is.primitive(vnode.text)) {//如果沒有子節點,有text文本,證明內部是文本內容
        api.appendChild(elm, api.createTextNode(vnode.text)); //創建文本節點放到elm dom中
      }
      i = (vnode.data as VNodeData).hook; // Reuse variable
      if (isDef(i)) {
        if (i.create) i.create(emptyNode, vnode); //取出data中的create鈎子  執行
        if (i.insert) insertedVnodeQueue.push(vnode); 
        // 如果有insert鈎子的話,把vNode放insertedVnodeQueue隊列後面會執行insert
      }
    } else {
      //vNode選擇器為空也不是註釋節點,那就是一個文本  生成文本節點dom,賦值elm  真實dom引用
      vnode.elm = api.createTextNode(vnode.text as string);
    }
    //返回生成的dom結構 需要返回這個結構,遞歸中會用到
    return vnode.elm;
  }

  function addVnodes(parentElm: Node,
                     before: Node | null,
                     vnodes: Array<VNode>,
                     startIdx: number,
                     endIdx: number,
                     insertedVnodeQueue: VNodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx];
      if (ch != null) {
        //取出當前傳入的 children 子節點生成html ,插入到父節點中
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
      }
    }
  }

  function invokeDestroyHook(vnode: VNode) {
    let i: any, j: number, data = vnode.data;
    if (data !== undefined) {
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
      if (vnode.children !== undefined) {
        for (j = 0; j < vnode.children.length; ++j) {
          i = vnode.children[j];
          if (i != null && typeof i !== "string") {
            invokeDestroyHook(i);
          }
        }
      }
    }
  }

  function removeVnodes(parentElm: Node,
                        vnodes: Array<VNode>,
                        startIdx: number,
                        endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx];
      if (ch != null) {
        if (isDef(ch.sel)) {
          invokeDestroyHook(ch);
          listeners = cbs.remove.length + 1;
          rm = createRmCb(ch.elm as Node, listeners);
          for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
          if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
            i(ch, rm);
          } else {
            rm();
          }
        } else { // Text node
          api.removeChild(parentElm, ch.elm as Node);
        }
      }
    }
  }
  //diff核心,對比新舊節點的children 更新dom
  //在DOM 操作的時候我們很少很少會把一個父節點移動/更新到某一個子節點
  //所以是同級比對,再找下一屆級別比對,算法複雜度 O(n)
  //幾種對比方式會在 文章裏具體説明
  function updateChildren(parentElm: Node,
                          oldCh: Array<VNode>,
                          newCh: Array<VNode>,
                          insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0, newStartIdx = 0; //老的開始座標  新的開始座標
    let oldEndIdx = oldCh.length - 1; //老的結束座標 
    let oldStartVnode = oldCh[0]; //老的開始vnode
    let oldEndVnode = oldCh[oldEndIdx]; //老的結束vnode
    let newEndIdx = newCh.length - 1; //新的結束座標
    let newStartVnode = newCh[0]; //新的開始vnode
    let newEndVnode = newCh[newEndIdx]; //新的結束vnode
    let oldKeyToIdx: any;  
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;
    //開始比對 首先兩個的開始結束都沒有相交,都沒有循環完畢
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { 
      if (oldStartVnode == null) { //老開始節點不存在
        oldStartVnode = oldCh[++oldStartIdx]; //++開始下標,獲取下一個做開始老vnode, 下標加防止死循環
      } else if (oldEndVnode == null) { // 結束不存在
        oldEndVnode = oldCh[--oldEndIdx]; // ++結束下標 獲取前一個 結束 老vnode ,下標加防止死循環
      } else if (newStartVnode == null) { //同上,不存在,這裏獲取的是新開始位置vnode  
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {//同上,不存在,這裏是獲取新結束vnode
        newEndVnode = newCh[--newEndIdx];
        //比較開始和結束的四種情況
      } else if (sameVnode(oldStartVnode, newStartVnode)) {//開始的vnode 是一個
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); // 對比不同進行更新dom 插入insert隊列  後期執行鈎子
        oldStartVnode = oldCh[++oldStartIdx]; //老開始vnode 更新 老開始下標更新
        newStartVnode = newCh[++newStartIdx];//新開始 vnode更新  新開始下標更新  第一種情況,進行下輪循環
      } else if (sameVnode(oldEndVnode, newEndVnode)) { // 結束的vnode 是一個
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); //對比不同更新 dom
        oldEndVnode = oldCh[--oldEndIdx]; //老結束vnode更新 老結束下標
        newEndVnode = newCh[--newEndIdx];//新結束 vnode更新 新結束下標
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // 老開始vnode 和 新 結束vnode  是否同一個 
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); //更新節點dom
        api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));
        //dom更新完成之後,更改位置,因為是已新的children順序為主,
        //所以把 這個老開始vnode放到 倒數第一個,newEndVnode已經對比過了,在對比的話還進來這裏就是放到倒數第二個(因為等於了新結束vnode)以此類推
        oldStartVnode = oldCh[++oldStartIdx]; //老開始對比完畢更新 下標更新
        newEndVnode = newCh[--newEndIdx]; //新結束 對比完畢 更新vnode  下標更新
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // 老結束vnode和新開始vnode 是同一個
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); //對比更新節點 dom
        api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
        //dom更新完了之後,同上更新位置,已新的children的順序為主,把老的結束vnodedom放到 老的開始前面
        //第一位 ,newStartVnode對比過了,再進來就是第二位 以此類推
        oldEndVnode = oldCh[--oldEndIdx]; //更新老結束的位置及vnode
        newStartVnode = newCh[++newStartIdx];//更新新開始位置及vnode
      } else {
        //四種情況結束,從 根據現在的oldStartIdx, oldEndIdx在老的children裏找和新開始對應的vnode
        if (oldKeyToIdx === undefined) {
          //根據oldStartIdx, oldEndIdx從老的children裏把還沒有比對過的 vnode拿出來,放到一個{}裏,key就是對應vnode的key
          //值就是他們的位置座標  方便後期直接查找
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        idxInOld = oldKeyToIdx[newStartVnode.key as string]; //根據新的開始vnode的key 通過哪個{}找到對應老的children裏的vnode 
        //idxInOld有值的話就是找到對應的key相同的項了
        if (isUndef(idxInOld)) { // New element
          //如果不存在 證明  newStartVnode是個新的vnode,生成真實dom 插入 oldStartVnode 之前 根據順序老的開始的位置
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          newStartVnode = newCh[++newStartIdx]; //這個新的開始節點比對完畢之後 獲取下個新的開始節點vnode 進行繼續比對
        } else {
          //在老的剩餘沒比對的idxInOld 對象裏vnode  找到了
          elmToMove = oldCh[idxInOld];
          if (elmToMove.sel !== newStartVnode.sel) { //如果選擇器標籤不一樣了,元素替換了 
            //證明不一樣了,直接用新的newStartVnode 生成dom  插到oldStartVnode.elm dom之前
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          } else {
            //證明key sel選擇器都相同
             
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            //比對不同點更新 newStartVnode更新到elmToMove elm  真實dom
            oldCh[idxInOld] = undefined as any;  //比對過的置成空
            api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
            //把更新完畢的dom 插入到 oldStartVnode之前 ,更新dom順序
          }
          newStartVnode = newCh[++newStartIdx]; //最後一種情況新開始vnode對比過了,繼續下一個新開始vnode對比
        }
      }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      //老的children或者新的children 有任意一個對比完了
      if (oldStartIdx > oldEndIdx) { //老的先完成了,證明新的比老的多,新的為主,把新的補上去
        //查看老的結束的下標是不是最後一個,是最後一個的話給null,不是最後一個的話獲取最後一個的下一個的elm 真實dom
        before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
        // 把newStartIdx, newEndIdx之間的vnode 進行插入,然後根據before 看插入到最後,還是 newCh[newEndIdx+1].elm之前
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      } else {  
        //新的先循環完成,老的比新的多
        //把老的children 從oldStartIdx, 到oldEndIdx之間的vnode  全部刪除
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }

  function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    //同一個vnode sel  key 都相同對比 oldVnode 和 vnode 的差異,把差異渲染到 DOM
    let i: any, hook: any;
    if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
      i(oldVnode, vnode);
      //首先調用用户prepatch鈎子 一個元素即將被修補(patched)  對應鈎子可以去看官網啥意思
      //新舊節點傳入
    }
    const elm = vnode.elm = (oldVnode.elm as Node); //如果上一個oldVnode的elm 有的話賦值給vnode.elm 因為是一個,然後賦值給elm
    let oldCh = oldVnode.children; //獲取上一個oldvnode的子節點
    let ch = vnode.children; //當前 vnode 的子節點
    if (oldVnode === vnode) return; //如果 新老vnode相同沒變化返回
    if (vnode.data !== undefined) { //模塊鈎子
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode
        );
      i = vnode.data.hook;
      if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
      //取出 data傳入的update鈎子執行
    }
    if (isUndef(vnode.text)) {//如果新vnode的text沒有,開始對比 新舊的子節點 children和text互斥
      if (isDef(oldCh) && isDef(ch)) { //如果新舊 都有子節點
        //新舊兩個子節點不相同 有變化 開始對比 diff對比不同更新到elm  dom上
        if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
      } else if (isDef(ch)) {
        //新vnode 子節點children不為空 老vnode children為空 
        if (isDef(oldVnode.text)) api.setTextContent(elm, ''); 
        //如果老vnode的文本有的話dom中給置空,刪除老vnode dom內容
        addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
        //添加 新的vnode 的children 子節點vnode生成dom 到 插入到elm中 
      } else if (isDef(oldCh)) {
         //新vnode 子節點children為空 老vnode children不為空 
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
        //刪除老vnode的children 子節點  執行對應模塊 以及hook鈎子
      } else if (isDef(oldVnode.text)) {
        //新vnode節點啥都沒有,老vnode節點有text,從dom刪除 
        api.setTextContent(elm, '');
      }
    } else if (oldVnode.text !== vnode.text) {
      //如果vnode.text有值,兩個還不一致
      if (isDef(oldCh)) {
        //老的vnode有children  ,從dom上清空
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
      }
      //把新的vnode的text渲染到dom中
      api.setTextContent(elm, vnode.text as string);
    }
    if (isDef(hook) && isDef(i = hook.postpatch)) {//執行最後的用户傳入hook模塊中的postpatch鈎子一個元素已經被修補完成(patched)
      i(oldVnode, vnode);
    }
  }
  //patch打補丁,把新節點的變化內容渲染到真實dom,返回新節點作為下一次處理的舊節點
  return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = []; //保存新插入節點的隊列,為了觸發鈎子函數
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); //模塊pre ptach過程開始鈎子

    if (!isVnode(oldVnode)) { //isVnode vnode.sel 判斷是否是虛擬dom,如果上一次的節點不是虛擬dom,也就是説是真實dom
      oldVnode = emptyNodeAt(oldVnode); //取出真實dom的標籤名 id class 來創建一個空的vNode  這個帶有elm儲存它的真實dom引用
    }

    if (sameVnode(oldVnode, vnode)) { //通過key和sel選擇器 對比老的新的兩個 vNode是否是同一個
      patchVnode(oldVnode, vnode, insertedVnodeQueue); //是同一個的話對比兩個差異來進行 更新dom,把新的更新上
    } else {
      //新舊節點不一致,  vNode創建對應的dom
      elm = oldVnode.elm as Node;//獲取舊節點的真實dom引用
      parent = api.parentNode(elm); //獲取它的父級 

      createElm(vnode, insertedVnodeQueue); //根據vNode創建真實dom 並且 elm存入真實dom引用

      if (parent !== null) {//父級存在
        api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm)); //把vNode.elm插入到父級中,elm舊的 之後 
        removeVnodes(parent, [oldVnode], 0, 0); //刪除舊的 
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) { //所有的vnode補丁完了,執行用户設置節點的的insert鈎子
      (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); //最後執行模塊的 post生命鈎子
    return vnode; //最後把更新完vnode返回出去,下次patch它就是舊的了,下一次調用時再和新的對比
  };
}

h.ts

import {vnode, VNode, VNodeData} from './vnode';
export type VNodes = Array<VNode>;
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>
import * as is from './is';

function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
  data.ns = 'http://www.w3.org/2000/svg';
  if (sel !== 'foreignObject' && children !== undefined) {
    for (let i = 0; i < children.length; ++i) {
      let childData = children[i].data;
      if (childData !== undefined) {
        addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
      }
    }
  }
}

export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
export function h(sel: any, b?: any, c?: any): VNode {//主要作用生產 虛擬dom
  
  var data: VNodeData = {}, children: any, text: any, i: number;
  //處理參數,實現重載
  if (c !== undefined) {
    //三個參數時
    //1sel:div#id.calss, 2data: {style,on事件hook鈎子等},
    //3children/text 子節點數組或者是文本 互斥   
    data = b;
    if (is.array(c)) { children = c; }//數組,有子集 children儲存子集
    else if (is.primitive(c)) { text = c; } //c是字符串或數字,沒有子集,內容是文本
    else if (c && c.sel) { children = [c]; }//c是vnode,h(...)函數最後返回轉換出來的vnode,
    //第三個參數就是子集,不是文本的話,轉換為數組統一處理
  } else if (b !== undefined) {
    //二個參數
    if (is.array(b)) { children = b; } // 第二個參數是數組子集,放到children
    else if (is.primitive(b)) { text = b; } //是文本和children互斥
    else if (b && b.sel) { children = [b]; }//是vnode 轉換為數組統一處理 儲存
    else { data = b; } //都不是,那可能是對象data,on事件之類的 直接儲存
  }
  //如果有子集
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      //循環子集,如果當前這項是string或者number, 返回一個只有 text有值的vnode 虛擬dom  賦給當前項
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
    }
  }
  if ( //是svg的情況  添加命名空間
    sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
    (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  ) {
    addNS(data, children, sel);
  }
  // 生成虛擬dom的結構 {sel 選擇器, data 樣式生命週期等, children 子節點數組, text 子節點文本, elm 真實dom引用, key data.key}
  return vnode(sel, data, children, text, undefined);
};
export default h;

vNode.ts

import {Hooks} from './hooks';
import {AttachData} from './helpers/attachto'
import {VNodeStyle} from './modules/style'
import {On} from './modules/eventlisteners'
import {Attrs} from './modules/attributes'
import {Classes} from './modules/class'
import {Props} from './modules/props'
import {Dataset} from './modules/dataset'
import {Hero} from './modules/hero'

export type Key = string | number;

export interface VNode {
  sel: string | undefined;
  data: VNodeData | undefined;
  children: Array<VNode | string> | undefined;
  elm: Node | undefined;
  text: string | undefined;
  key: Key | undefined;
}

export interface VNodeData {
  props?: Props;
  attrs?: Attrs;
  class?: Classes;
  style?: VNodeStyle;
  dataset?: Dataset;
  on?: On;
  hero?: Hero;
  attachData?: AttachData;
  hook?: Hooks;
  key?: Key;
  ns?: string; // for SVGs
  fn?: () => VNode; // for thunks
  args?: Array<any>; // for thunks
  [key: string]: any; // for any other 3rd party module
}

export function vnode(sel: string | undefined,
                      data: any | undefined,
                      children: Array<VNode | string> | undefined,
                      text: string | undefined,
                      elm: Element | Text | undefined): VNode {
  let key = data === undefined ? undefined : data.key;
  //sel:div#id.class選擇器 
  //data:節點數據{style樣式,hook鈎子,on事件等} 
  //children 子節點數組  和text互斥
  //text 節點內容 與 children互斥
  //elm 記錄vnode 對應的真實dom
  //key data中獲取  優化渲染使用 
  return {sel, data, children, text, elm, key};
}

export default vnode;

接下來就該正式進入vue的源碼學習了。

user avatar lantianhaijiao 頭像 kevin_5d8582b6a85cd 頭像 smile1213 頭像 yunuo_5f87fbee283af 頭像 jyeontu 頭像 3yya 頭像 _bleach 頭像 ch5nftr 頭像 zhuoooo 頭像 nxmin 頭像 shijuepaipie 頭像 eraitianshi 頭像
17 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.