Vue底層學習4——編譯器框架搭建

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

作爲一個Web前端開發人員,使用Vue框架進行項目開發已經有一陣子,掐指一算,是時候認真探索一下Vue的底層了,以前的瞭解比較偏理論,這一次打算在弄清基本原理的前提下自己手寫Vue中的核心部分,也許這樣我纔敢說自己“深入理解”了Vue。上一篇完成發佈訂閱模式的編寫,實現DepWatcher,但到目前爲止涉及視圖的部分都是預留的狀態,原因是我們還缺乏一個解析視圖代碼的功能,從本篇開始手擼編譯器~

編譯原理

爲什麼要進行編譯?因爲我們實際在書寫Vue模板的時候加入了很多瀏覽器不認識的代碼,所以需要進行額外的轉換與處理。compile的核心邏輯是獲取DOM、遍歷DOM,遍歷時找到{{}}格式的變量、每個DOM的屬性,與此同時截獲v-@開頭的響應式指令。

爲了方便我們手擼編譯器,簡化流程後如下圖所示,後續編碼建議結合下圖看思路會更清晰哦~:

目標功能

老規矩,先上一個日常開發的例子,幫助我們搞清楚最終需要實現的目標,這裏我重新創建了一個demo2的html文件:

<!-- demo2.html -->
<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <meta charset="UTF-8">
  <title>demo2</title>
</head>
<body>
  <div id="app">
    <p>{{name}}</p>
    <p v-text="name"></p>
    <p>{{location}}</p>
    <p>
      {{locationAgain}}
    </p>
    <input type="text" v-model="name" />
    <button @click="changeName">改名兒</button>
    <div v-html="html"></div>
  </div>

  <script src="compile.js"></script>
  <script src="MVue.js"></script>

  <script>
    const app = new MVue({
      el: '#app',
      data: {
        name: 'dreamsyang',
        location: 'chongqing',
        html: '<button>這是一個按鈕</button>'
      },
      created() {
        console.log('開始啦');
        setTimeout(() => {
          this.name = '我是測試';
        }, 1500);
      },
      methods: {
        changeName() {
          this.name = 'hello, dreamsyang!';
          this.location = 'oh, chongqing!';
        }
      }
    })
  </script>
</body>
</html>

根據上面的例子彙總3個目標:

  • 目標一:插值綁定,也就是{{}}中的變量綁定,例如{{name}}{{location}}{{locationAgain}}
  • 目標二:指令解析,也就是v-開頭的Dom屬性,例如v-textv-model(涉及雙向綁定的實現)、v-html(涉及html內容解析);
  • 目標三:事件的處理,也就是@開頭的Dom屬性,例如@click

編譯器框架搭建

獲取Dom

首先創建一個文件compile.js,也就是目標例子中引入的編譯器,主要接收兩個參數:el:需要解析的Dom元素選擇器,vm:當前的Vue實例。

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

class Compile{
  constructor(el, vm) {
    // 需要遍歷的Dom節點
    this.$el = document.querySelector(el);
    // 數據緩存
    this.$vm = vm;
  }
}

遍歷子節點

  • 如果獲取的Dom節點存在就進行子節點內容提取
    通過document.createDocumentFragment將元素附加到文檔片段,因爲文檔片段存在於內存中,並不在Dom樹中,所以將子元素插入到文檔片段時不會引起頁面迴流(對元素位置和幾何上的計算),方便後續編譯,減少Dom操作,提高性能。
/*** compile.js ***/
// new Compile(el, vm)

class Compile{
  constructor(el, vm) {
    // 需要遍歷的Dom節點
    this.$el = document.querySelector(el);
    // 數據緩存
    this.$vm = vm;

    // 編譯
    if (this.$el) {
      // 提取指定節點中的內容,提高效率,減少Dom操作
      this.$fragment = this.node2Fragment(this.$el);
  }

  // 提取指定Dom節點中的代碼片段
  node2Fragment(el) {
    const fragment = document.createDocumentFragment();
    // 將el中的所有子元素移動至fragment中
    let child = null;
    while(child = el.firstChild) {
      fragment.appendChild(child);
    }
    return fragment;
  }
}
  • 遍歷並判斷子節點類型爲節點還是插值文本
    編譯前先遍歷子節點並配合節點的nodeType屬性判斷節點類型,然後針對不同類型進行對應的編譯處理。
/*** compile.js ***/
// new Compile(el, vm)

class Compile{
  constructor(el, vm) {
    // 需要遍歷的Dom節點
    this.$el = document.querySelector(el);
    // 數據緩存
    this.$vm = vm;

    // 編譯
    if (this.$el) {
      // 提取指定節點中的內容,提高效率,減少Dom操作
      this.$fragment = this.node2Fragment(this.$el);
      // 執行編譯
      this.compile(this.$fragment);
      // 將編譯完的html追加至$el
      this.$el.appendChild(this.$fragment);
    }
  }

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

  // 編譯過程
  compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
      // 類型判斷
      if (this.isElement(node)) {
        // 節點
        console.log('編譯節點' + node.nodeName);
      } else if(this.isInterpolation(node)) {
        // 插值文本
        console.log('編譯插值文本' + node.textContent);
      }
      
      // 遞歸子節點
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    })
  }

  isElement(node) {
    return node.nodeType === 1;
  }

  isInterpolation(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }
}
  • demo2中測試一下
    先去掉MVue.jsconstructor中之前模擬Watcher的部分,因爲後續屬性的getter激活會加入到編譯器中,接着初始化一個Compile實例,並將需要解析的Dom元素選擇器以及當前的Vue實例作爲參數傳遞進去。
/*** MVue.js ***/
// new MVue({ data: {...} })

class MVue {
  constructor(options) {
    // 數據緩存
    this.$options = options;
    this.$data = options.data;

    // 數據遍歷
    this.observe(this.$data);

    new Compile(options.el, this);
  }
}

運行結果如下,可以看到,我們想要根據不同的節點類型做區別編譯的分流已經實現,後續就是實打實的編譯操作,且聽下回分解:

參考資料

1、Document.createDocumentFragment()https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createDocumentFragment
2、Vue源碼:https://github.com/vuejs/vue

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