国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 網站 > WEB開發 > 正文

理解vue實現原理,實現一個簡單的Vue框架

2024-04-27 15:09:53
字體:
來源:轉載
供稿:網友

原文地址: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); }};12345678910111213141516171819202122232425262728293031323334353637383940414243444546471234567891011121314151617181920212223242526272829303132333435363738394041424344454647

index.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);};123456789101112131415161718192021222324252627282930313233343536373839123456789101112131415161718192021222324252627282930313233343536373839

2.監聽隊列

       現在我們已經可以攔截對象的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進行對象定義;

       具體實現比較簡單,可以直接參考源碼


上一篇:JavaScript arguments對象

下一篇:css選擇器

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 繁昌县| 华蓥市| 丹江口市| 荔浦县| 同江市| 华安县| 永康市| 漯河市| 丹江口市| 吴江市| 耿马| 河东区| 东港市| 祥云县| 诸暨市| 越西县| 盘锦市| 宁国市| 五峰| 铅山县| 巴中市| 全州县| 肇东市| 迭部县| 河东区| 保靖县| 五大连池市| 项城市| 新巴尔虎右旗| 淅川县| 易门县| 赤峰市| 镇宁| 崇明县| 呼伦贝尔市| 辽阳县| 德江县| 三门县| 通道| 潼南县| 中方县|