全手打原創,轉載請標明出處:https://www.cnblogs.com/dreamsqin/p/15044033.html, 多謝,=。=~(如果對你有幫助的話請幫我點個贊啦)
作爲一個Web前端開發人員,使用Vue框架進行項目開發已經有一陣子,掐指一算,是時候認真探索一下Vue的底層了,以前的瞭解比較偏理論,這一次打算在弄清基本原理的前提下自己手寫Vue中的核心部分,也許這樣我纔敢說自己“深入理解”了Vue。上篇簡述了整個編譯器原理並擬定了三項編譯目標,完成編譯器框架搭建,在遍歷Dom子節點時實現分流處理,本篇主要實現第一個目標插值文本編譯和依賴收集~
插值文本編譯
由上一篇提供的demo2
可以得到如下的運行結果:
但實際上我們想要展示的是各個變量對應的值,而不是變量名,所以需要編譯Dom中的插值變量,並將其替換爲對應的值,這裏新建一個compileText
方法實現:
/*** 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) {
const fragment = document.createDocumentFragment();
// 將el中的所有子元素移動至fragment中
let child = null;
while(child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
// 編譯過程
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
// 類型判斷
if (this.isElement(node)) {
// 節點
console.log('編譯節點' + node.nodeName);
} else if(this.isInterpolation(node)) {
// 編譯插值文本
this.compileText(node);
}
// 遞歸子節點
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);
}
// 插值文本編譯
compileText(node) {
node.textContent = this.$vm.$data[RegExp.$1];
}
}
需要特別注意的是RegExp.$1
的巧用,在做子節點分流時我們通過正則表達式對插值文本進行了匹配分組,所以在執行compileText
方法時我們可以通過RegExp.$1
獲取到分組中的內容,也就是插值括號{{}}
中的變量,例如name
、location
、locationAgain
,然後通過傳遞的Vue實例this.$vm
獲取到$data
中的屬性變量值,再對節點內容進行替換操作,最終運行結果如下:
可以看到頁面中的變量成功被替換,但這種方式只會初始化一次,當變量值發生改變時,頁面中展示的內容是不會同步變更的,可以利用demo2
(源碼可參見《Vue底層學習4——編譯器框架搭建》)中created
方法的延遲賦值操作測試一下,我們在MVue
的構造函數中執行一下created
方法:
/*** 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);
// created執行
if (options.created) {
options.created.call(this);
}
}
}
調用時使用call
綁定this
指向是爲了方便在Vue實例的created
方法中輕鬆使用this
訪問當前的Vue實例對象,例如我們日常用this.data
去訪問實例的數據屬性。created
執行後結果如下:
開始啦
成功打印,但name
的重新賦值並沒有同步更新至頁面,與上面的猜想一致。其主要原因是沒有做依賴收集,也就是之前MVue.js
的 constructor
中模擬Watcher
激活getter
的部分,除此之外,我們編譯器中還需要一個更新函數,之前Watcher
中update
方法都是通過console
實現視圖更新的預留,這些事還是得編譯器來完成。
更新函數
觸發更新的操作有很多,視圖中不僅僅只有插值文本,還有一系列的v-
指令或者事件,所以我們需要抽象出一個更新函數供所有的觸發調用,在編譯器中定義一個更新函數update
,它接收4個參數,分別表示需要更新的節點、當前的Vue實例、屬性標識、觸發更新的指令標識。
/*** compile.js ***/
// new Compile(el, vm)
class Compile{
// 更新函數
update(node, vm, exp, dir) {
const updateFn = this[dir + 'Updater'];
// 如果存在就執行,實現初始化
updateFn && updateFn(node, vm.$data[exp]);
}
}
updateFn
的執行只能達到初始化的作用,跟上述compileText
函數實現的效果一致,但當數據變更時想要同步更新,就需要做依賴收集,跟之前模擬的一樣,我們需要創建一個Watcher
實例,接收3個參數,分別表示當前的Vue實例、屬性標識、當屬性變更時執行的更新回調函數:
/*** compile.js ***/
// new Compile(el, vm)
class Compile{
// 更新函數
update(node, vm, exp, dir) {
const updateFn = this[dir + 'Updater'];
// 如果存在就執行,實現初始化
updateFn && updateFn(node, vm.$data[exp]);
// 依賴收集
new Watcher(vm, exp, function(value) {
updateFn && updateFn(node, value);
});
}
}
那麼對於插值文本的更新我們就需要創建一個對應的更新函數textUpdater
,並且之前用於插值文本編譯的compileText
函數就需要做對應的變更:
/*** compile.js ***/
// new Compile(el, vm)
class Compile{
// 更新函數
update(node, vm, exp, dir) {
const updateFn = this[dir + 'Updater'];
// 如果存在就執行,實現初始化
updateFn && updateFn(node, vm.$data[exp]);
// 依賴收集
new Watcher(vm, exp, function(value) {
updateFn && updateFn(node, value);
});
}
// 插值文本更新
textUpdater(node, value) {
node.textContent = value;
}
// 插值文本編譯
compileText(node) {
this.update(node, this.$vm, RegExp.$1, 'text');
}
}
可以看到以前我們在模擬依賴收集時,實例化Watcher
時是不會傳參的,但是現在接收了3個參數,所以需要同步修改MVue
中的Watcher
類,並通過Watcher
拿到的Vue實例及屬性標識激活getter
實現依賴收集:
/*** MVue.js ***/
class Watcher {
constructor(vm, exp, cb) {
// 數據緩存
this.$vm = vm;
this.$key = exp;
this.$cb = cb;
// 將當前Watcher的實例指定到Dep靜態屬性target
Dep.target = this;
// 激活屬性的getter,添加依賴
this.$vm.$data[this.$key];
// 置空,防止重複添加
Dep.target = null;
}
update() {
// 預留視圖更新
console.log('數據更新了,需要我們更新視圖');
}
}
那麼現在預留的視圖更新就可以直接執行傳入的cb
回調了,並綁定其中的this
指向爲當前的Vue實例,同時將修改後的值作爲參數傳遞進去:
/*** MVue.js ***/
class Watcher {
constructor(vm, exp, cb) {
// 數據緩存
this.$vm = vm;
this.$key = exp;
this.$cb = cb;
// 將當前Watcher的實例指定到Dep靜態屬性target
Dep.target = this;
// 激活屬性的getter,添加依賴
this.$vm.$data[this.$key];
// 置空,防止重複添加
Dep.target = null;
}
update() {
// 視圖更新
this.$cb.call(this.$vm, this.$vm.$data[this.$key]);
}
}
爲了方便我們獲取和設置data
中的屬性,我們可以做一層代理,將data
屬性掛載到Vue的實例上,實現通過Vue實例就可以直接訪問或設置data
屬性:
/*** MVue.js ***/
// new MVue({ data: {...} })
class MVue {
constructor(options) {...}
observe(data) {
// 確定data存在並且爲對象
if (!data || typeof data !== 'object') {
return;
}
// 遍歷data對象
Object.keys(data).forEach(key => {
// 重寫對象屬性的getter和setter,實現數據的響應化
this.defineReactive(data, key, data[key]);
// 代理data中的屬性到Vue實例上
this.proxyData(key);
})
}
defineReactive(obj, key, val) {...}
proxyData(key) {
Object.defineProperty(this, key, {
get: function() {
return this.$data[key];
},
set: function(newVal) {
this.$data[key] = newVal;
}
})
}
}
接下來就可以把代碼中通過this.$vm.$data
訪問或設置data
中屬性的操作修改爲this.$vm
直接進行訪問和設置,修改後的代碼就不貼出來了,全局搜索一下~
下面就是見證奇蹟的時刻,再次運行一下demo2
,效果如下,1.5s左右後視圖被同步更新了:
參考資料
1、Vue源碼:https://github.com/vuejs/vue;