原文地址:http://blog.csdn.net/pur_e/article/details/53066275
其實對JS我研究不是太深,用過很多次,但只是實現功能就算了。最近JS實在是太火,從前端到后端,應用越來越廣泛,各種框架層出不窮,忍不住也想趕一下潮流。 Vue是近年出的一個前端構建數據驅動的web界面的庫,主要的特色是響應式的數據綁定,區別于以往的命令式用法。也就是在var a=1;的過程中,攔截’=’的過程,從而實現更新數據,web視圖也自動同步更新的功能。而不需要顯式的使用數據更新視圖(命令式)。這種用法我最早是在VC MFC中見過的,控件綁定變量,修改變量的值,輸入框也同步改變。 Vue的官方文檔,網上的解析文章都很詳細,不過出于學習的目的,還是了解原理后,自己實現一下記憶深刻,同時也可以學習下Js的一些知識。搞這行的,一定要多WTFC(Write The Fucking Code)。
其實這里的思考是在看過幾篇文章、看過一些源碼后補上的,所以有的地方會有上帝視角的意思。但是這個過程是必須的,以后碰到問題就會有思考的方向。 先看看我們想要實現什么功能,以及現在所具有的條件: 效果圖如下:
使用Vue框架代碼如下:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>MVVM</title></head><body><script src="src/vue.js"></script><div id="msg"> {{b.c}}這是普通文本{{b.c+1+message}}這是普通文本 <p>{{message}}</p> <p><input type="text" v-model="message"/></p> <p>{{message}}</p> <p><button type="button" v-on:click="clickBtn(message)">click me</button></p></div><script> var vm = new Vue({ el:"#msg", data:{ b:{ c:1 }, message:"hello world" }, methods:{ clickBtn:function(message){ vm.message = "clicked"; } } });</script></body></html>12345678910111213141516171819202122232425262728293031323334351234567891011121314151617181920212223242526272829303132333435然后我們還知道一個條件,Vue的官方文檔所說的:
把一個普通對象傳給 Vue 實例作為它的 data 選項,Vue.js 將遍歷它的屬性,用 Object.definePRoperty 將它們轉為 getter/setter。這是 ES5 特性,不能打補丁實現,這便是為什么 Vue.js 不支持 IE8 及更低版本。
用這個特性實現這樣的功能,我們需要做什么呢?
首先,需要利用Object.defineProperty,將要觀察的對象,轉化成getter/setter,以便攔截對象賦值與取值操作,稱之為Observer;需要將DOM解析,提取其中的指令與占位符,并賦與不同的操作,稱之為Compiler;需要將Compile的解析結果,與Observer所觀察的對象連接起來,建立關系,在Observer觀察到對象數據變化時,接收通知,同時更新DOM,稱之為Watcher;最后,需要一個公共入口對象,接收配置,協調上述三者,稱為Vue;二、實現Observer
1.轉化getter/setter
本來以為實現起來很簡單,結果只是轉換為getter和setter就碰到了很多問題。原來對JS真得是只知道點皮毛啊……
開始Observer.js代碼如下:
/** Observer是將輸入的Plain Object進行處理,利用Object.defineProperty轉化為getter與setter,從而在賦值與取值時進行攔截 這是Vue響應式框架的基礎 */function isObject(obj){ return obj != null && typeof(obj) == 'object';}function isPlainObject(obj){ return Object.prototype.toString(obj) == '[object Object]';}function observer(data){ if(!isObject(data) || !isPlainObject(data)){ return; } return new Observer(data);}var Observer = function(data){ this.data = data; this.transform(data);};Observer.prototype.transform = function(data){ for(var key in data){ var value = data[key]; Object.defineProperty(data,key,{ enumerable:true, configurable:true, get:function(){ console.log("intercept get:"+key); return value; }, set:function(newVal){ console.log("intercept set:"+key); if(newVal == value){ return; } data[key] = newVal; } }); //遞歸處理 this.transform(value); }};12345678910111213141516171819202122232425262728293031323334353637383940414243444546471234567891011121314151617181920212223242526272829303132333435363738394041424344454647index.html:
<script src="src/Observer.js"></script><div id="msg"> <p>{{message}}</p> <p><input type="text" v-model="message"/></p> <p>{{message}}</p> <p><button type="button" v-on:click="clickBtn">click me</button></p></div><script> var a = { b:{c:1}, d:2 }; observer(a); a.d = 3;</script>1234567891011121314151612345678910111213141516瀏覽器執行直接死循環棧溢出了,問題出在set函數里,有兩個問題:
set:function(newVal){ console.log("intercept set:"+key); if(newVal == value){ return; } //這里,通過data[key]來賦值,因為我們對data對象進行了改造,set中又會調用set函數,就會遞歸調用,死循環 //而上面本來用來判斷相同賦值不進行處理的邏輯,也因為value的值沒有改變,沒有用到。很低級的錯誤! data[key] = newVal;}123456789123456789修改為value = newVal可以嗎?為什么可以這樣修改,因為JS作用域鏈的存在,value對于這個匿名對象來說,是如同全局變量的存在,在set中修改后,在get中也可正常返回修改后的值。
但是僅僅這樣是不夠的,因為一個很常見的錯誤,在循環中建立的匿名對象,使用外部變量用的是循環最終的值?。?!
還是作用域鏈的原因,匿名對象使用外部變量,不是保留這個變量的值,而是延長外部變量的生命周期,在該銷毀時也不銷毀(所以容易形成內存泄露),所以匿名對象被調用時,用的外部變量的值,是取決于變量在這個時刻的值(一般是循環執行完的最終值,因為循環結束后才有匿名函數調用)。
所以,打印a.d的值,將會是2
所以,最終通過新建函數的形式,Observer.js如下:
Observer.prototype.transform = function(data){ for(var key in data){ this.defineReactive(data,key,data[key]); }};Observer.prototype.defineReactive = function(data,key,value){ var dep = new Dep(); Object.defineProperty(data,key,{ enumerable:true, configurable:false, get:function(){ console.log("intercept get:"+key); if(Dep.target){ //JS的瀏覽器單線程特性,保證這個全局變量在同一時間內,只會有同一個監聽器使用 dep.addSub(Dep.target); } return value; }, set:function(newVal){ console.log("intercept set:"+key); if(newVal == value){ return; } //利用閉包的特性,修改value,get取值時也會變化 //不能使用data[key]=newVal //因為在set中繼續調用set賦值,引起遞歸調用 value = newVal; //監視新值 observer(newVal); dep.notify(newVal); } }); //遞歸處理 observer(value);};1234567891011121314151617181920212223242526272829303132333435363738391234567891011121314151617181920212223242526272829303132333435363738392.監聽隊列
現在我們已經可以攔截對象的getter/setter,也就是對象的賦值與取值時我們都會知道,知道后需要通知所有監聽這個對象的Watcher,數據發生了改變,需要進行更新DOM等操作,所以我們需要維護一個監聽隊列,所有對該對象有興趣的Watcher注冊進來,接收通知。這一部分之前看了Vue的實現,感覺也不會有更巧妙的實現方式了,所以直接說一下實現原理。
首先,我們攔截了getter;我們要為a.d添加Wacher監聽者tmpWatcher;將一個全局變量賦值target=tmpWatcher;取值a.d,也就調用到了a.d的getter;在a.d的getter中,將target添加到監聽隊列中;target = null;就是這么簡單,至于為什么可以這樣做,是因為JS在瀏覽器中是單線程執行的!!所以在執行這個監聽器的添加過程時,決不會有其他的監聽器去修改全局變量target!!所以這也算是因地制宜嗎0_0
詳細代碼可以去看github中源碼的實現,在Observer.js中。當然他還有比較復雜的依賴、剔重等邏輯,我這里只是簡單實現一個。
var Dep = function(){ this.subs = {};};Dep.prototype.addSub = function(target){ if(!this.subs[target.uid]) { //防止重復添加 this.subs[target.uid] = target; }};Dep.prototype.notify = function(newVal){ for(var uid in this.subs){ this.subs[uid].update(newVal); }};Dep.target = null;123456789101112131415123456789101112131415三.實現Compiler
這里,是在看過DMQ的源碼后,自己實現的一份代碼,因為對JS不太熟悉,犯了一些小錯誤。果然學習語言的最好方式就是去寫~_~,之后,對JS的理解又加深了不少。 又因為想要實現的深入一點,也就是不只是單純的變量占位符如{{a}},而是表達式如{{a+Math.PI+b+fn(a)}},想不出太好的辦法,又去翻閱了Vue的源碼實現,發現Vue的實現其實也不怎么優雅,但確實也沒有更好的辦法。有時候,不得不寫出這種代碼,如枚舉所有分支,是最簡單、最直接,也往往是最好的方法。
1.最簡單的實現
也就是純的變量占位,這個大家都想得到,用正則分析占位符,將這個變量添加監聽,與前面建立的setter/getter建立關系即可。
2.進階的實現——Vue
說一下Vue的實現方法:
原理:
將表達式{{a+Math.PI+b+fn(a)}},變成函數:function getter(scope) { return scope.a + Math.PI + scope.b + scope.fn(scope.a);}123123調用時,傳入Vue對象getter(vm),這樣,所有表達式中的變量、函數,變成vm作用域內的調用。Vue的實現
var body = exp.replace(saveRE, save).replace(wsRE, '');
* 利用了幾個正則,首先將所有的字符串提取出來,進行替換,因為后面要去除所有的空格; * 去除空格;body = (' ' + body).replace(identRE, rewrite).replace(restoreRE, restore);
* 將所有的變量前加scope(除了保留字如Math,Date,isNaN等,具體見代碼中的正則); * 將所有字符串替換回去 * 生成上面提到過的函數可以看出這個操作還是稍微有點耗時,所以Vue做了一些優化,加了一個緩存。
3.實現中碰到的問題
明白了一個概念,DOM中每一個文字塊,也是一個節點:文字節點,而且只要被其他節點分隔,就是不同的文字節點;JS中,可以使用childNodes與attributes等來枚舉子節點與屬性列表等;[].forEach.call,可以用來遍歷非Array對象如childNodes;[].slice會生成數組的一個淺復制,因為childNodes在修改DOM對象時,會實時變動,所以不能直接在遍歷中修改DOM,此時,可以生成淺復制數組,用來遍歷;具體代碼太長就不展示,可以直接看Git上的源碼。
四、實現Watcher
Watcher的實現,需要考慮幾個問題:
傳入的表達式如前面提到的{{a+Math.PI+b+fn(a)}},如何與每一個具體對象建立關系,添加監聽;添加后的關系如何維護,其中包括: 上一層對象被直接賦值,如表達式是{{a.b.c}},進行賦值a.b={c:4},此時,c的getter沒有被觸發,與c相關的Watcher如何被通知;還是上面的例子,新添加的c如何與老的c的Watcher建立關系;其實,上面說監聽隊列時,已經稍微提過,利用JS單線程的特性,在調用對象的getter前,將Dep.target這個全局變量修改為Watcher,然后getter中將其添加到監聽隊列中。所以,Watcher中,只需要取一次表達式的值,就會實現這個功能,而且,Watcher在初始化時,本來就需要調用一次取值來初始化DOM!
來看一下上面的問題:
首先,Watcher需要監聽的是一個表達式,所有表達式中的成員,都需要監聽,如{{a+Math.PI+b+fn(a)}}需要監聽a和b的變化,而取這個表達式值時,會調用a和b的getter,從而將自身添加到a和b的監聽隊列中!關于添加后關系的維護: 我們在取表達式值{{a.b.c}}時,a和b和c的getter都會被調用,從而都會將Watcher添加到自己的監聽隊列中,所以a.b={c:4}賦值時,Watcher同樣會被觸發!上面Watcher被觸發后,會重新獲取a.b.c的值,則新的c的getter會被調用,從而新的c會將Watcher添加到自己的監聽隊列中。可以發現,上面的問題都被圓滿解決,如果這是我自己想出來的方案,我會被自己感動哭的T_T 這才是優雅的解決方案!
五、實現Vue
這就是一個公共入口,整個框架從這里創建。需要實現的目標:
進行流程的串接,observe對象,compile Dom;對自己的對象data,函數methods等進行代理,從而可以直接使用vm.a,vm.init等進行調用,同樣通過Object.defineProperty進行對象定義;具體實現比較簡單,可以直接參考源碼
新聞熱點
疑難解答