Vue底層學習6——節點編譯與屬性遍歷

全手打原創,轉載請標明出處:https://www.cnblogs.com/dreamsqin/p/15175740.html, 多謝,=。=~(如果對你有幫助的話請幫我點個贊啦)

作爲一個Web前端開發人員,使用Vue框架進行項目開發已經有一陣子,掐指一算,是時候認真探索一下Vue的底層了,以前的瞭解比較偏理論,這一次打算在弄清基本原理的前提下自己手寫Vue中的核心部分,也許這樣我纔敢說自己“深入理解”了Vue。上篇完成了編譯器的第一項目標:插值文本編譯和依賴收集,編譯後template中的插值變量會替換爲實際值展示,當用戶修改data屬性中的值時視圖也會同步更新,接下來是另外一塊核心部分,也就是分流處理的另一分支,節點編譯,本篇涉及節點屬性遍歷、指令解析和事件處理~

先上一張我們在《Vue底層學習4——編譯器框架搭建》中拋出的編譯器流程圖,本篇會嚴格按照這個流程進行編譯器的功能編碼,=。=我確實比較喜歡看圖說話:

屬性遍歷

之前在compile的編譯函數中,對Dom子節點遍歷時如果子節點類型是Element,我們預留了一個編譯節點的分支,接下來的重點就是在該分支中進行節點屬性的遍歷,找到v-@開頭的屬性並實現各自的處理流程。

/*** compile.js ***/
// new Compile(el, vm)

class Compile{
  constructor(el, vm) {...}

  // 提取指定Dom節點中的代碼片段
  node2Fragment(el) {...}

  // 編譯過程
  compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
      // 類型判斷
      if (this.isElement(node)) {
        // 節點屬性遍歷
        const nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach(attr => {
          // 屬性名
          const attrName = attr.name;
          // 屬性值
          const exp = attr.value;
          if(this.isDirective(attrName)) {
            // 如果是v-開頭的指令
          }
          if (this.isEvent(attrName)) {
            // 如果是@開頭的事件
          }
        })
      } else if(this.isInterpolation(node)) {
        // 編譯插值文本
        this.compileText(node);
      }

      // 遞歸子節點
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    })
  }

  // 是否是節點
  isElement(node) {...}
  // 是否是插值文本
  isInterpolation(node) {...}

  // 更新函數
  update(node, vm, exp, dir) {...}

  // 插值文本更新
  textUpdater(node, value) {...}

  // 插值文本編譯
  compileText(node) {...}
}

通過isDirectiveisEvent方法分別判斷是否是v-@開頭的屬性:

  // 是否是指令
  isDirective(name) {
    return name.indexOf('v-') === 0;
  }

  // 是否是事件
  isEvent(name) {
    return name.indexOf('@') === 0;
  }

指令解析

對於v-開頭的指令我們根據指令名稱標識抽象出需要調用的解析方法:

if(this.isDirective(attrName)) {
  // 如果是v-開頭的指令
  const dir = attrName.substring(2);
  this[dir] && this[dir](node, this.$vm, exp);
}

針對不同類型的指令調用不同的方法,實現對應的功能:

  • v-text
text(node, vm, exp) {
  this.update(node, vm, exp, 'text');
}

// 與插值文本更新調用的是同一個函數
textUpdater(node, value) {
  node.textContent = value;
}

運行結果如下,可以看到通過v-text指令實現了name插值,視圖中新增了一行name的展示,與此同時,created中對name的修改也同樣生效:

  • v-html
  html(node, vm, exp) {
    this.update(node, vm, exp, 'html');
  }
  
    htmlUpdater(node, value) {
    node.innerHTML = value;
  }

運行結果如下,可以看到html屬性的button被插入到了v-html所在到Dom節點中:

  • v-model
    該指令是Vue雙向綁定的關鍵體現,可以理解爲它是:value@input的結合體:
  // 指令v-model,雙向綁定
  model(node, vm, exp) {
    // 指定input的value屬性(值對視圖的影響——MV)
    this.update(node, vm, exp, 'model');

    // input事件監聽並修改數據模型中的值(視圖對值對影響——VM)
    node.addEventListener('input', e => {
      vm[exp] = e.target.value;
    })
  }
  
  modelUpdater(node, value) {
    node.value = value;
  }

運行結果如下,name屬性的值會以input當前的value值進行展示,與此同時,inputvalue的修改都會同步到一切使用name屬性的視圖中,這就實現了雙向綁定:

事件處理

對於@開頭的事件我們根據事件名稱抽象出需要調用的處理方法:

if (this.isEvent(attrName)) {
  // 如果是@開頭的事件
  const dir = attrName.substring(1);
  this.eventHandler(node, this.$vm, exp, dir);
}

具體的事件處理放到eventHandler中實現,實際就是給指定的節點綁定對應的事件監聽,並且將回調函數的this指向當前的Vue實例,原因跟之前一樣,這樣就可以在回調函數中通過this輕鬆的獲取到掛載到Vue實例上的屬性:

  // 事件處理
  eventHandler(node, vm, exp, dir) {
    // 事件觸發後需要執行的函數
    const fn = vm.$options.methods && vm.$options.methods[exp];
    if(dir && fn) {
      node.addEventListener(dir, fn.bind(vm));
    }
  }

運行結果如下,點擊“改名兒”按鈕時,namelocation的值被修改,視圖也同步更新:

總結

手擼Vue核心代碼就這樣悄悄的結束了,心情舒暢,好像打通了任督二脈~最後做個小小的總結,整體下來我覺得Vue核心代碼中最重要的部分就是編譯,編譯可以實現依賴收集,這樣可以建立視圖和數據模型之間的聯繫,當數據模型變更時,可以通知依賴對應數據的視圖進行更新,也就是我們常說的數據驅動視圖。而Vue中著名的雙向綁定其實是通過v-model指令實現,在編譯時解析該指令,爲所屬節點添加事件監聽,事件觸發時就可以即時修改綁定的值,綁定值的修改又會觸發所有依賴的視圖更新。

參考資料

1、Vue源碼:https://github.com/vuejs/vue

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章