最簡化VUE的響應式原理
前言
前端目前兩個當家花旦框架VUE React,它們能夠流行開來,響應式原理做出了巨大貢獻。畢竟,它通過數據的變更就能夠更新相應的視圖,極大的將我們從繁瑣的DOM操作中解放出來。
所以掌握它們的響應式原理,對掌握前端框架的精髓就很重要了。
本文用最簡單的方式來解釋VUE2 最重點的響應式原理,看不懂算我輸!
一、 響應式原理
什麼是響應式原理?
意思就是在改變數據的時候,視圖會跟著更新。這意味著你只需要進行數據的管理,給我們搬磚提供了很大的便利。 React也有這種特性,但是React的響應式方式跟VUE完全不同。
React是通過this.setState去改變數據,然後根據新的數據重新渲染出虛擬DOM,最後通過對比虛擬DOM找到需要更新的節點進行更新。
也就是說React是依靠著虛擬DOM以及DOM的diff算法做到這一點的。而關於React這方面的文章,我已經寫了很多了,還不了解的同學可以自行複習一下。
而VUE則是利用了Object.defineProperty的方法裡面的setter與getter方法的觀察者模式來實現。
所以在學習VUE的響應式原理之前,先學習兩個預備知識:
Object.defineProperty 與觀察者模式。
如果你已經掌握了,可以直接跳到第三part。
二、預備知識
2.1 Object.defineProperty
這個方法就是在一個對像上定義一個新的屬性,或者改變一個對象現有的屬性,並且返回這個對象。裡面有兩個字段set,get。顧名思義,set都是取設置屬性的值,而get就是獲取屬性的值。
舉個栗子:
//在對像中添加一個屬性與存取描述符的範例varbValue;varo={};Object.defineProperty(o,"b",{get:function(){console.log('監聽正在獲取b')returnbValue;},set:function(newValue){console.log('監聽正在設置b')bValue=newValue;},enumerable:true,configurable:true});o.b=38;console.log(o.b)
最終打印
监听正在设置b监听正在获取b38
從在上述栗子中,可以看到當我們對ob 賦值38的時候,就會調用set函數,這時候給bValue賦值,之後我們就可以通過ob來獲取這個值,這時候,get函數被調用。
掌握到這一步,我們已經可以實現一個極簡的VUE雙向綁定了。
<inputtype="text"id="txt"/><spanid="sp"></span><script>vartxt=document.getElementById('txt'),sp=document.getElementById('sp'),obj={}//給對象obj添加msg屬性,並設置setter訪問器Object.defineProperty(obj,'msg',{//設置obj.msg當obj.msg反生改變時set方法將會被調用set:function(newVal){//當obj.msg被賦值時同時設置給input/spantxt.value=newValsp.innerText=newVal}})//監聽文本框的改變當文本框輸入內容時改變obj.msgtxt.addEventListener('keyup',function(event){obj.msg=event.target.value})</script>
VUE給data裡所有的屬性加上set,get這個過程就叫做Reactive化。
2.2 觀察者模式
什麼是觀察者模式?它分為註冊環節跟發布環節。
比如我去買芝士蛋糕,但是店家還沒有做出來。這時候我又不想在店外面傻傻等,我就需要隔一段時間來回來問問蛋糕做好沒,對於我來說是很麻煩的事情,說不定我就懶得買了。
店家肯定想要做生意,不想流失我這個吃貨客戶。於是,在蛋糕沒有做好的這段時間,有客戶來,他們就讓客戶把自己的電話留下,這就是觀察者模式中的註冊環節。然後蛋糕做好之後,一次性通知所有記錄了的客戶,這就是觀察者的發布環節。
這裡來簡單實現一個觀察者模式的類
functionObserver(){this.dep=[];register(fn){this.dep.push(fn)}notify(){this.dep.forEach(item=>item())}}constwantCake=newOberver();//每來一個顧客就註冊一個想執行的函數wantCake.register(()=>{'console.log("call daisy")'})wantCake.register(()=>{'console.log("call anny")'})wantCake.register(()=>{'console.log("call sunny")'})//最後蛋糕做好之後,通知所有的客戶wantCake.notify()
三、原理解析
在學完了前面的鋪墊之後,我們終於可以開始講解VUE的響應式原理了。
官網用了一張圖來表示這個過程,但是剛開始看可能看不懂,等到文章的最後,我們再來看,應該就能看懂了。
總共分為三步驟:
1、init階段: VUE的data的屬性都會被reactive化,也就是加上setter/getter函數。
functiondefineReactive(obj:Object,key:string,...){constdep=newDep()Object.defineProperty(obj,key,{enumerable:true,configurable:true,get:functionreactiveGetter(){....dep.depend()returnvalue....},set:functionreactiveSetter(newVal){...val=newValdep.notify()...}})}classDep{statictarget:?Watcher;subs:Array<Watcher>;depend(){if(Dep.target){Dep.target.addDep(this)}}notify(){constsubs=this.subs.slice()for(leti=0,l=subs.length;i<l;i++){subs[i].update()}}
其中這裡的Dep就是一個觀察者類,每一個data的屬性都會有一個dep對象。當getter調用的時候,去dep裡註冊函數,
至於註冊了什麼函數,我們等會再說。
setter的時候,就是去通知執行剛剛註冊的函數。
2、mount階段:
mountComponent(vm:Component,el:?Element,...){vm.$el=el...updateComponent=()=>{vm._update(vm._render(),...)}newWatcher(vm,updateComponent,...)...}classWatcher{getter:Function;//代碼經過簡化constructor(vm:Component,expOrFn:string|Function,...){...this.getter=expOrFnDep.target=this//注意這裡將當前的Watcher賦值給了Dep.targetthis.value=this.getter.call(vm,vm)//調用組件的更新函數...}}
mount 階段的時候,會創建一個Watcher類的對象。這個Watcher實際上是連接Vue組件與Dep的橋樑。
每一個Watcher對應一個vue component。
這裡可以看出new Watcher的時候,constructor 裡的this.getter.call(vm, vm)函數會被執行。 getter就是updateComponent。這個函數會調用組件的render函數來更新重新渲染。
而render函數里,會訪問data的屬性,比如
render:function(createElement){returncreateElement('h1',this.blogTitle)}
此時會去調用這個屬性blogTitle的getter函數,即:
// getter函数
get:functionreactiveGetter(){....dep.depend()returnvalue....},// dep的depend函数
depend(){if(Dep.target){Dep.target.addDep(this)}}
在depend的函數里,Dep.target就是watcher本身(我們在class Watch裡講過,不記得可以往上第三段代碼),這裡做的事情就是給blogTitle註冊了Watcher這個對象。這樣每次render一個vue 組件的時候,如果這個組件用到了blogTitle,那麼這個組件相對應的Watcher對像都會被註冊到blogTitle的Dep中。
這個過程就叫做依賴收集。
收集完所有依賴blogTitle屬性的組件所對應的Watcher之後,當它發生改變的時候,就會去通知Watcher更新關聯的組件。
3、更新階段:
當blogTitle 發生改變的時候,就去調用Dep的notify函數,然後通知所有的Watcher調用update函數更新。
notify(){constsubs=this.subs.slice()for(leti=0,l=subs.length;i<l;i++){subs[i].update()}}
可以用一張圖來表示:
由此圖我們可以看出Watcher是連接VUE component 跟data屬性的橋樑。
總結
最後,我們通過解釋官方的圖來做個總結。
1、第一步:組件初始化的時候,先給每一個Data屬性都註冊getter,setter,也就是reactive化。然後再new 一個自己的Watcher對象,此時watcher會立即調用組件的render函數去生成虛擬DOM。在調用render的時候,就會需要用到data的屬性值,此時會觸發getter函數,將當前的Watcher函數註冊進sub裡。
2、第二步:當data屬性發生改變之後,就會遍歷sub裡所有的watcher對象,通知它們去重新渲染組件。
整個過程就是那麼簡單啦~
彩蛋
本來這篇文章應該已經結束了,但是我們既然已經學會了響應式原理,那當然要對目前vue的一些規則做點解釋啦。算是贈送的彩蛋~。
- 如果你想要屬性是響應式的,就一定要寫在data對象裡。因為VUE只對data裡的屬性做reactive化處理。
var vm = new Vue({ data:{ a:1 } }) // `vm.a` 是响应的vm.b = 2 // `vm.b` 是非响应的
或者使用Vue.set(vm.someObject, 'b', 2)動態添加。
參考文檔:
1、 https:// zhuanlan.zhihu.com/p/67 893936
2、 https://www. njleonzhang.com/2018/09 /26/vue-reactive.html
3、 https:// vuejs.bootcss.com/v2/gu ide/reactivity.html