【Vuejs】952- 一文帶你了解vue2之響應(yīng)式原理

在面試的過程中也會問到:請闡述vue2的響應(yīng)式原理?,凡是出現(xiàn)闡述或者理解,一般都是知無不言言無不盡,知道多少說多少。接下來,我來談?wù)勛约旱睦斫猓杏洸灰ケ常欢ㄒ斫庵螅米约旱恼Z言來描述出來。
那什么是響應(yīng)式呢?響應(yīng)式就是當(dāng)對象本身(對象的增刪值)或者對象屬性(重新賦值)發(fā)生變化時,將會運行一些函數(shù),最常見的就是render函數(shù)。
在具體實現(xiàn)上,vue用到了幾個核心部件,每一個部件都解決一個問題:
Observer Dep Watcher Scheduler
?? Observer
Observer要實現(xiàn)的目標非常簡單,就是把一個普通的對象轉(zhuǎn)換為響應(yīng)式的對象。
為了實現(xiàn)這一點,Observer把對象的每個屬性通過Object.defineProperty轉(zhuǎn)換為帶有getter和setter的屬性,這樣一來,我們訪問或設(shè)置屬性時,會分別調(diào)用getter和setter方法,vue就有機會做一些別的事情。

Observer是vue內(nèi)部的構(gòu)造器,我們可以通過Vue提供的靜態(tài)方法Vue.observable(object)間接的使用該功能。
「示例」:
<body>
<script src="./vue.min.js"></script>
<script>
let obj = {
name:"法醫(yī)",
age:100,
like:{
a:"琴",
b:"棋"
},
character:[{
c:"性格好"
},{
d:"真帥哦"
}]
}
Vue.observable(obj);
</script>
</body>
「運行結(jié)果」:
輸出結(jié)果中的...代表數(shù)據(jù)是響應(yīng)式的,Invoke property getter表示調(diào)用了屬性的getter方法。如果對象中還存在對象,那么它會深度遞歸遍歷,讓所有的數(shù)據(jù)都是響應(yīng)式的數(shù)據(jù)。

如果說在組件當(dāng)中,配置中的data也會返回一個響應(yīng)式數(shù)據(jù),這一過程在組件生命周期中發(fā)生在beforeCreate之后,created之后
Observer在具體實現(xiàn)上,它會遞歸遍歷對象的所有屬性,以完成數(shù)據(jù)響應(yīng)式的轉(zhuǎn)換。如果說一個屬性一開始并不存在于對象中,是后面添加上的,那么這種屬性是檢測不到的,所以像之前使用obj.e = 3新增一個e:3也是檢測不到的,因為之前對象中沒有。但是到了vue3,使用了proxy,那就可以檢測到了。因此在vue2中提供了$set和$delete兩個實例方法,我們可以通過這兩個實例方法對已有響應(yīng)式對象添加或刪除屬性。
「示例」:
通過對昵稱的刪除和年齡的添加,對比$set、$delete和delete、set
<body>
<div id="app">
<p>昵稱:{{obj.name}}</p>
<p>年齡:{{obj.age}}</p>
<!-- 首先通過 delete obj.name 方式進行昵稱的刪除-->
<button @click="delete obj.name">刪除昵稱</button>
<button>添加年齡</button>
</div>
<script src="./vue.min.js"></script>
<script>
let vm = new Vue({
el:"#app",
data(){
return{
obj:{
name:"法醫(yī)"
}
}
}
})
</script>
</body>
「運行結(jié)果」:

從運行結(jié)果來看,在沒有點擊刪除昵稱按鈕之前vm.obj輸出的name是響應(yīng)式數(shù)據(jù),點擊刪除昵稱按鈕之后再次打印vm.obj此時數(shù)據(jù)已經(jīng)被刪除,但是頁面上昵稱法醫(yī)并未刪除,vue收不到屬性被刪除的通知,因為delete obj.name是不會被檢測到的
接下來使用$delete進行昵稱的刪除操作:
<body>
<div id="app">
<p>昵稱:{{obj.name}}</p>
<p>年齡:{{obj.age}}</p>
<!-- 使用 $delete 刪除昵稱-->
<button @click="$delete(obj,'name')">刪除昵稱</button>
<button>添加年齡</button>
</div>
<script src="./vue.min.js"></script>
<script>
let vm = new Vue({
el:"#app",
data(){
return{
obj:{
name:"法醫(yī)"
}
}
}
})
</script>
</body>
「運行結(jié)果」:

當(dāng)使用$delete的時候vue就會收到通知了,進行昵稱刪除操作,頁面也會及時響應(yīng)。
同理,我們來看看$set和set
<body>
<div id="app">
<p>昵稱:{{obj.name}}</p>
<p>年齡:{{obj.age}}</p>
<!-- -->
<button @click="$delete(obj,'name')">刪除昵稱</button>
<!-- 使用 obj.age=100 添加年齡-->
<button @click="obj.age=100">添加年齡</button>
</div>
<script src="./vue.min.js"></script>
<script>
let vm = new Vue({
el:"#app",
data(){
return{
obj:{
name:"法醫(yī)"
}
}
}
})
</script>
</body>
「運行結(jié)果」:

當(dāng)使用傳統(tǒng)方式obj.age=100向?qū)ο筇砑訉傩缘臅r候,其實可以添加成功的,只是數(shù)據(jù)并不是響應(yīng)式的,頁面上沒有顯示年齡。
接下來就使用$set添加屬性:
<body>
<div id="app">
<p>昵稱:{{obj.name}}</p>
<p>年齡:{{obj.age}}</p>
<!-- 使用 $set 添加年齡 -->
<button @click="$delete(obj,'name')">刪除昵稱</button>
<button @click="$set(obj,'age','100')">添加年齡</button>
</div>
<script src="./vue.min.js"></script>
<script>
let vm = new Vue({
el:"#app",
data(){
return{
obj:{
name:"法醫(yī)"
}
}
}
})
</script>
</body>
「運行結(jié)果」:

一目了然,使用$set添加的屬性是響應(yīng)式的,age:(...)三個點很明顯。
以上就是針對對象的檢測,那么數(shù)組呢?數(shù)組又是怎樣檢測的呢?Object和Array的變化檢測處理方式是不同的。
對于數(shù)組,vue會更改它的隱式原型,之所以這樣做,是因為vue需要監(jiān)聽那些可能改變數(shù)組內(nèi)容的 方法。

總之,Observer的目標,就是要讓一個對象,它的屬性的讀取、賦值,內(nèi)部數(shù)組的變化都要能夠被vue檢測到,這樣才能讓數(shù)據(jù)轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)。
?? Dep
現(xiàn)在有兩個問題沒有解決,就是讀取屬性時要做什么事情?屬性變化時要做什么事情?這個問題就需要Dep來解決。
Dep的全稱是Dependency,表示依賴的意思,
Vue會為響應(yīng)式對象中的每個屬性、對象本身、數(shù)組本身創(chuàng)建一個Dep實例,每個Dep實例都有能力做以下兩件事:
記錄依賴:是誰在用我 派發(fā)更新:我變了,我要通知那些用到我的人
當(dāng)讀取響應(yīng)式對象的某個屬性時,它會進行依賴收集:有人用到了我
當(dāng)改變某個屬性時,它會派發(fā)更新:那些用我的人聽好了,我變了

Watcher
現(xiàn)在又有一個問題,就是Dep如何知道是誰在用我?
要解決這個問題,需要依靠另一個東西,就是Watcher
當(dāng)某個函數(shù)執(zhí)行的過程中,用到了響應(yīng)式數(shù)據(jù),響應(yīng)式數(shù)據(jù)是無法知道是哪個函數(shù)在用自己,因此,vue通過一種巧妙的辦法來解決這個問題:
我們不要直接執(zhí)行函數(shù),而是把函數(shù)交給一個叫做watcher的東西去執(zhí)行,watcher是一個對象,每個這樣的函數(shù)執(zhí)行時都應(yīng)該創(chuàng)建一個watcher,通過watcher去執(zhí)行。watcher會創(chuàng)建一個全局變量,讓全局變量記錄當(dāng)前負責(zé)執(zhí)行的watcher等于自己,然后再去執(zhí)行函數(shù),在函數(shù)執(zhí)行的過程中,如果發(fā)生了依賴記錄dep.depend(),那么Dep就會把這個全局變量記錄下來,表示:有一個watcher用到了我這個屬性
當(dāng)Dep進行派發(fā)更新時,它會通知之前記錄的所有watcher:我變了

每一個vue組件實例,都至少對應(yīng)一個watcher,該watcher中記錄了該組件的render函數(shù)。
watcher首先會把render函數(shù)運行一次以收集依賴,于是那些在render中用到的響應(yīng)式數(shù)據(jù)就會記錄這個watcher。
當(dāng)數(shù)據(jù)變化時,dep就會通知該watcher,而watcher將重新運行render函數(shù),從而讓界面重新渲染,同時重新記錄當(dāng)前的依賴。
?? Scheduler
現(xiàn)在還剩最后一個問題,就是Dep通知watcher之后,響應(yīng)數(shù)據(jù)又多次改變,造成watcher執(zhí)行重復(fù)運行對應(yīng)函數(shù),就有可能導(dǎo)致函數(shù)頻繁運行,從而導(dǎo)致效率低下
試想,如果一個交給watcher的函數(shù),它里面用到了屬性a、b、c、d,那么a、b、c、d屬性都會記錄依賴,于是下面的代碼將會觸發(fā)4次更新:
state.a = "new data";
state.b = "new data";
state.c = "new data";
state.d = "new data";
這樣肯定是不行的,因此,watcher收到派發(fā)更新的通知后,它不會立即執(zhí)行對應(yīng)render函數(shù),當(dāng)然不僅僅是render函數(shù),還有可能是其它的函數(shù),而是把自己交給一個叫調(diào)度器的東西,在調(diào)度器里面有個隊列,可以認為是一個數(shù)組,這個隊列數(shù)組中記錄了當(dāng)前要運行哪些watcher,調(diào)度器維護一個執(zhí)行隊列,在隊列中同一個watcher只會存在一次,隊列中的watcher不是立即執(zhí)行,它會通過一個叫做nextTick的工具方法,把這些需要執(zhí)行的watcher放入到事件循環(huán)的微隊列中,nextTick的具體做法是通過Promise完成的,nextTick其實就是一個函數(shù)
nextTick((fn)=>{
Promise.resolve().then(fn);//通過這種方式就跑到微隊列中去了
})
也就是說,當(dāng)響應(yīng)式數(shù)據(jù)變化時,render函數(shù)的執(zhí)行是異步的,并且在微隊列中
?? 總體流程圖

我們簡單過一遍這個流程圖:
原始對象通過 Observer將轉(zhuǎn)換成一個響應(yīng)式的對象,具有getter和setter方法,然后就靜靜等待著。突然有一天,雷雨交加,有一個render函數(shù)要執(zhí)行,但不是直接就執(zhí)行了,而是交給watcher來執(zhí)行,watcher通過設(shè)置全局變量的方式讀取數(shù)據(jù),因為讀取了數(shù)據(jù),所以會觸發(fā)響應(yīng)式對象的getter,隨后getter會從全局變量的位置讀取到當(dāng)前正在讀取的watcher并把watcher收集到Dep中。 通過以上步驟頁面就會被渲染出來了。 又是突然的一天哈,風(fēng)和日麗,我觸發(fā)了一個按鈕或者事件,不管干了什么,反正是數(shù)據(jù)改變了,進行新的步驟—— 派發(fā)更新,隨后通知watcher,我變了哦,你給我馬上搞定這件事情,但是watcher并不是立即就執(zhí)行的,因為數(shù)據(jù)變動有時候不是一個,而是很多,立即執(zhí)行的話會重復(fù)執(zhí)行很多render函數(shù)或者其它數(shù)據(jù)變動的函數(shù),執(zhí)行效率會變低。然而watcher把自己交給調(diào)度器Scheduler調(diào)度器會把watcher添加到隊列中,當(dāng)然在隊列中也不會執(zhí)行的,而是將隊列交給 nextTick隊列,nextTick里面的函數(shù)全是在微隊列的,等同步代碼執(zhí)行完成后,會異步地執(zhí)行函數(shù)fn1、fn2、watcher等等,這一步相當(dāng)于重新執(zhí)行了watcher,然后又重新執(zhí)行了render函數(shù),就這樣地循環(huán)往復(fù)。
?? 好了, 以上就是我今天的分享,大家對于vue2響應(yīng)式原理還有什么問題可以在評論區(qū)討論鴨~
記得點贊 ?? 支持一下哦~ ??

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點擊“閱讀原文”查看 120+ 篇原創(chuàng)文章
