最簡化VUE的響應式原理

blank

最簡化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的響應式原理了。

官網用了一張圖來表示這個過程,但是剛開始看可能看不懂,等到文章的最後,我們再來看,應該就能看懂了。

blank

總共分為三步驟:
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()}}

可以用一張圖來表示:

blank

由此圖我們可以看出Watcher是連接VUE component 跟data屬性的橋樑。

總結

最後,我們通過解釋官方的圖來做個總結。

blank

1、第一步:組件初始化的時候,先給每一個Data屬性都註冊getter,setter,也就是reactive化。然後再new 一個自己的Watcher對象,此時watcher會立即調用組件的render函數去生成虛擬DOM。在調用render的時候,就會需要用到data的屬性值,此時會觸發getter函數,將當前的Watcher函數註冊進sub裡。

blank

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、 zhuanlan.zhihu.com/p/67
2、 njleonzhang.com/2018/09
3、 vuejs.bootcss.com/v2/gu

What do you think?

Written by marketer

blank

【D2 快報】關於前端與機器學習的疑惑,聽TensorFlow.js 負責人一一解答

blank

圖佈局VS 人類是如何利用能源的?