前言
Vue是一個非常出名的MVVM框架吉嚣,其設(shè)計理念主要核心是view-model。現(xiàn)在Vue3使用的是Proxy
阻肿,而過去則是Object.defineProperty
。這篇轉(zhuǎn)文主要參考的是后者相關(guān)資料以及設(shè)計理念丛塌,從實現(xiàn)一個簡單的Vue著手畜疾,很有閱讀價值。
MVVM雙向綁定的簡單流程
首先是思路上的明確啡捶。Vue中采用的是Object.defineProperty()
中的setter
,getter
來對每個屬性進行劫持彤敛,同時結(jié)合訂閱者-發(fā)布者模式的方式,實現(xiàn)數(shù)據(jù)模版的自動更新墨榄。
簡單的流程可以這樣來理解:
一、MVVM編譯前新建一個Observer
對象來攔截data
里面的每個數(shù)據(jù)阵翎,大概有以下幾個步驟:
- 使用閉包之剧,讓每個屬性有一個各自的
Dep
對象來存放自己的依賴隊列,里面是一系列的Watcher
- 在
getter
中把Watcher
添加到隊列上 - 在
setter
中讓Dep
對象觸發(fā)update
背稼,即依次執(zhí)行里面的Watcher
- 視圖更新的邏輯就放在
Watcher
中
這樣蟹肘,我們在每次更新數(shù)據(jù)的時候就是觸發(fā)setter
從而實現(xiàn)視圖更新。
但這里有兩個關(guān)鍵問題需要解決:
- 編譯是在
Observer
之后的疆前,如何實現(xiàn)在getter
中把Watcher
添加到隊列上? - 一個屬性就只有一個
getter
童太,但卻可以有多個Watcher
依賴胸完,如何確保準確地添加Watcher
到隊列上?
二爆惧、新建一個Compiler
對象進行模版編譯锨能,大概有以下幾個步驟:
- 對每個元素節(jié)點的指令進行掃描和解析,根據(jù)指令調(diào)用相應(yīng)的
handler
函數(shù)進行處理 - 對每個屬性依賴新建
Watcher
對象進行監(jiān)聽
三址遇、在MVVM實例初始化中整合流程一和流程二
一個簡單的MVVM實現(xiàn)流程大概就是這些步驟,下面大概介紹下具體實現(xiàn)思路:
Observer實現(xiàn)
對于第一個問題秃殉,解決也不難,我們在Watcher
的初始化中對要監(jiān)聽的數(shù)據(jù)進行訪問钾军,自然就會觸發(fā)到getter
。
巧妙的地方在第二點拗小,如何確保當前的Watcher
能夠被正確添加砸泛?在需要把自身添加到隊列時,我們可以在Dep
全局對象中設(shè)置Dep.target
為自身勾栗,在getter
中則進行判斷是否有Dep.target
這個屬性才決定是否進行添加隊列操作盏筐,看一下別人實現(xiàn)的一個簡單版的代碼,留意 get
方法里面的一段:
var uid = 0; //避免重復(fù)添加
function Watcher(vm, expOrFn, cb){
this.uid = uid++;
this.$vm = vm;
this.expOrFn = expOrFn;
this.value = null;
this.cb = cb;
this.update();//初始化時執(zhí)行一遍
}
Watcher.prototype = {
get: function(){
Dep.target = this; //把自身添加到target上
var value = computeExpression(this.$vm, this.expOrFn); //這里會觸發(fā)到getter
Dep.target = null; //執(zhí)行完記得設(shè)為null
return value;
},
....//省略
}
//精髓在于with與eval的應(yīng)用琢融,用with指定scope作用域漾抬,然后用eval執(zhí)行表達式
//其實用eval會有安全問題,而且性能上不太好纳令,更好的解決辦法是使用New Function()來動態(tài)構(gòu)建函數(shù),表達式置于函數(shù)體內(nèi)
function computeExpression(scope, exp){
try{
with(scope){
return eval(exp);
}
} catch(e){
console.error('ERROR', e);
}
}
然后是getter
里面圈匆,留意到判斷到有Dep.target
屬性才添加到依賴隊列中:
Observer.prototype = {
....//省略
defineReactive: function(data, key, val){
var dep = new Dep();
var self = this;
self.observe(val); //如果是對象則遞歸遍歷
Object.defineProperty(data, key, {
enumerable: true, //可枚舉遍歷
configureable: false, //不可再次配置
get: function(){
Dep.target && dep.addSub(Dep.target);
return val;
},
set: function(newVal){
if(val === newVal){ return; }
val = newVal;
self.observe(newVal); //對新值進行遍歷
dep.notify(); //執(zhí)行更新
}
})
}
}
Dep
里面的代碼比較簡單捏雌,無非就是維護一個存放Watcher
的對象:
function Dep(){
this.subs = {};
}
Dep.prototype = {
addSub: function(sub){
//防止重復(fù)添加Watcher
if(!this.subs[sub.uid]){
this.subs[sub.uid] = sub;
}
},
notify: function(){
for(var uid in this.subs){
this.subs[uid].update();
}
}
}
要知道js是單線程的性湿,所以可以確保每次只有一個Watcher
在調(diào)用,也就確保了它能準備地添加到它所監(jiān)聽變量的依賴隊列上肤频。這個做法可謂是十分巧妙,不得不佩服尤大大的厲害。
經(jīng)過這樣完整的一個結(jié)構(gòu),我們就已經(jīng)可以簡單地實現(xiàn)攔截變量和通知變化的功能了摔竿。
Compiler實現(xiàn)
要實現(xiàn)這個需要對原生的一些DOM屬性和節(jié)點操作辦法比較熟悉少孝,下面以一個最簡單的文本節(jié)點解析為例。
文本節(jié)點里面的表達式一般是 {{ a + 'b' }} + 某些文字 這樣稍走,對于這種字符串,我們就要轉(zhuǎn)換為scope.a + 'b' + '某些文字'這樣的表達式來執(zhí)行粱胜,可以回顧一下上面的computeExpression
函數(shù)狐树,下面繼續(xù)看一下別人的簡單實現(xiàn):
//先看下parseTextExp函數(shù),其實就是正則匹配加字符串拼接的過程
function parseTextExp(text) {
//匹配{{ }}里面的內(nèi)容
var regText = /\{\{(.+?)\}\}/g;
//存放其余的片段涯曲,類似'某些文字'這些
var pieces = text.split(regText);
var matches = text.match(regText);
var tokens = [];
pieces.forEach(function (piece) {
if (matches && matches.indexOf('{{' + piece + '}}') > -1) { // 注意排除無{{}}的情況
tokens.push(piece);
} else if (piece) {
tokens.push('`' + piece + '`');
}
});
//最后返回類似 scope.a + 'b' + '某些文字' 這樣的字符串表達式
return tokens.join('+');
}
function Compiler(el, vm){
this.$el = el;
this.$vm = vm;
if (this.$el) {
//轉(zhuǎn)換為節(jié)點片段在塔,提高執(zhí)行效率,同時用于去除一些注釋節(jié)點绰沥,空文本節(jié)點等
this.$fragment = nodeToFragment(this.$el);
this.compiler(this.$fragment);
this.$el.appendChild(this.$fragment);
}
}
Compiler.prototype = {
//分兩類城榛,元素節(jié)點和文本節(jié)點,同時進行遞歸
compiler: function(node, scope){
var childs = [].slice.call(node.childNodes);
childs.forEach(function(child){
if (child.nodeType === 1) {
this.compileElementNode(child, scope);
}else if(child.nodeType === 3){
this.compileTextNode(child, scope);
}
}.bind(this))
},
compileTextNode: function(textNode, scope){
var text = textNode.textContent.trim();
if(!text) return;
//將文本中的{{a + 'bbb'}} asdsd 轉(zhuǎn)換成 scope.a + 'bbb' + asdsd 的形式
var exp = parseTextExp(text);
scope = scope || this.$vm;
this.textHandler(textNode, exp, scope);
},
textHandler: function(textNode, exp, scope){
//增加一個Watcher依賴
new Watcher(scope, exp, function(newVal){
textNode.textContent = !newVal ? '' : newVal ;
})
},
....//省略
}
先轉(zhuǎn)換成fragment
疟位,然后對于文本節(jié)點喘垂,直接解析并轉(zhuǎn)換里面的內(nèi)容,然后增加一個Watcher
依賴得院。我們再看一下Watcher
里面的代碼
function Watcher(vm, expOrFn, cb){
this.uid = uid++;
this.$vm = vm;
this.expOrFn = expOrFn;
this.value = null;
this.cb = cb;
this.update(); //初始化時就先執(zhí)行一次cb函數(shù)
}
Watcher.prototype = {
get: function(){
Dep.target = this;
var value = computeExpression(this.$vm, this.expOrFn);
Dep.target = null;
return value;
},
update: function(){
//此處會調(diào)用getter章贞,將Watcher添加到dep里面
var newVal = this.get();
if(newVal !== this.value){
this.cb.call(this.$vm, newVal, this.value);
this.value = newVal;
}
}
}
可以看到,在Watcher
初始化時便自動添加到依賴隊列中蜕径,同時也得到了經(jīng)過首次解析后的表達式的值,并存放在this.value
中兜喻,而且還執(zhí)行了回調(diào),觸發(fā)了視圖更新帕识。
這是最簡單的文本節(jié)點的解析遂铡,對于像v-for
、v-if
忧便、v-model
等其他較為復(fù)雜的指令都有其相應(yīng)的處理辦法,并且有些指令的實現(xiàn)是十分有趣的超歌,詳情可以閱讀文章最后的Reference蒂教,我就不復(fù)制粘貼了。
MVVM實例化
有了前兩部分的實現(xiàn)凝垛,這里的實例化就顯得簡單很多了,繼續(xù)看代碼:
function MVVM(options){
this.$options = options;
//先提取根節(jié)點
this.$el = typeof options.el === 'string'
? document.querySelector(options.el)
: options.el || document.body;
var data = this._data = this.$options.data;
//Observer所有數(shù)據(jù)
var ob = new Observer(this._data);
if(!ob) return;
//對data里面的數(shù)據(jù)代理到實例上
Object.keys(data).forEach(function(key){
this._proxy(key);
}.bind(this))
//模版編譯
new Compiler(this.$el, this);
}
MVVM.prototype = {
_proxy: function(key){
var self = this;
Object.defineProperty(self, key, {
configureable: false,
enumerable: true,
get: function(){
return self._data[key];
},
set: function(val){
self._data[key] = val;
}
})
},
$watch: function(expOrFn, cb){
new Watcher(this, expOrFn, cb);
}
}
正如前面所說炭分,這里只需要把Observer
和Compiler
整合一下就可以了捧毛。需要注意的是這里實現(xiàn)了一個代理让网,因為它的數(shù)據(jù)是掛載在vm._data
上的,假如我們要改變數(shù)據(jù)的值溃睹,則要用vm._data.a = xxx
這樣的方式來改變,這樣顯示是不符合我們期望的泞辐,我們希望可以直接用vm.a = xxx
這樣的方式來改變數(shù)據(jù)的值。
所以我們增加了一個_proxy
函數(shù)铛碑,其實主要還是用Object.defineProperty()
這個方法來攔截類似vm.a
這樣的屬性,使它變成返回和設(shè)置vm._data.a
上的值。至此莉御,一個簡單版的MVVM變完全實現(xiàn)了。
總結(jié)
首先需要說明的是上面的代碼基本上都是各種博客或者源碼里面的牍颈,我這里主要是分析其實現(xiàn)思路琅关。當然我自己也照著這些代碼仿造了一個,但其實代碼內(nèi)容大同小異画机,就沒必要貼上來了新症。
寫這篇文章的目的主要是讓自己明白一個流行的輪子是大概是基于怎樣的思路造出來的,旨在提升一下擼碼水平徒爹。如有不妥的地方大家一起探討。
關(guān)于Vue2.0的源碼其實還有非常多值得學習的地方界阁,例如virtual dom
及其diff
算法實現(xiàn)胖喳,各種正則的巧妙運用,transition
過渡指令集的實現(xiàn)等禀晓。可惜本人水平有限重付,還不能完全參透其原理凫乖,看以后有沒機會解讀弓颈。最后删掀,感謝大家的閱讀!
本文轉(zhuǎn)載自Github_Bless-L披泪。