Vue底層學習4——編譯器框架搭建
全手打原創,轉載請標明出處:https://www.cnblogs.com/dreamsqin/p/15006455.html, 多謝,=。=~(如果對你有幫助的話請幫我點個贊啦)
作為一個Web前端開發人員,使用Vue框架進行專案開發已經有一陣子,掐指一算,是時候認真探索一下Vue的底層了,以前的瞭解比較偏理論,這一次打算在弄清基本原理的前提下自己手寫Vue中的核心部分,也許這樣我才敢說自己“深入理解”了Vue。上一篇完成釋出訂閱模式的編寫,實現
Dep
及Watcher
,但到目前為止涉及檢視的部分都是預留的狀態,原因是我們還缺乏一個解析檢視程式碼的功能,從本篇開始手擼編譯器~
編譯原理
為什麼要進行編譯?因為我們實際在書寫Vue模板的時候加入了很多瀏覽器不認識的程式碼,所以需要進行額外的轉換與處理。compile
{{}}
格式的變數、每個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-text
、v-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.js
的constructor
中之前模擬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;