1. 程式人生 > 其它 >Vue底層學習4——編譯器框架搭建

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