全手打原創,轉載請標明出處:https://www.cnblogs.com/dreamsqin/p/15006455.html, 多謝,=。=~(如果對你有幫助的話請幫我點個贊啦)
作爲一個Web前端開發人員,使用Vue框架進行項目開發已經有一陣子,掐指一算,是時候認真探索一下Vue的底層了,以前的瞭解比較偏理論,這一次打算在弄清基本原理的前提下自己手寫Vue中的核心部分,也許這樣我纔敢說自己“深入理解”了Vue。上一篇完成發佈訂閱模式的編寫,實現
Dep
及Watcher
,但到目前爲止涉及視圖的部分都是預留的狀態,原因是我們還缺乏一個解析視圖代碼的功能,從本篇開始手擼編譯器~
編譯原理
爲什麼要進行編譯?因爲我們實際在書寫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-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;