一文帶你了解vue2之響應(yīng)式原理

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

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>
「運(yùn)行結(jié)果」:
輸出結(jié)果中的...代表數(shù)據(jù)是響應(yīng)式的,Invoke property getter表示調(diào)用了屬性的getter方法。如果對(duì)象中還存在對(duì)象,那么它會(huì)深度遞歸遍歷,讓所有的數(shù)據(jù)都是響應(yīng)式的數(shù)據(jù)。

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

從運(yùn)行結(jié)果來看,在沒有點(diǎn)擊刪除昵稱按鈕之前vm.obj輸出的name是響應(yīng)式數(shù)據(jù),點(diǎn)擊刪除昵稱按鈕之后再次打印vm.obj此時(shí)數(shù)據(jù)已經(jīng)被刪除,但是頁面上昵稱法醫(yī)并未刪除,vue收不到屬性被刪除的通知,因?yàn)?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(145, 109, 213);font-weight: bolder;background-image: none;background-position: initial;background-size: initial;background-repeat: initial;background-attachment: initial;background-origin: initial;background-clip: initial;">delete obj.name是不會(huì)被檢測(cè)到的
接下來使用$delete進(jìn)行昵稱的刪除操作:
<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>
「運(yùn)行結(jié)果」:

當(dāng)使用$delete的時(shí)候vue就會(huì)收到通知了,進(jìn)行昵稱刪除操作,頁面也會(huì)及時(shí)響應(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>
「運(yùn)行結(jié)果」:

當(dāng)使用傳統(tǒng)方式obj.age=100向?qū)ο筇砑訉傩缘臅r(shí)候,其實(shí)可以添加成功的,只是數(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>
「運(yùn)行結(jié)果」:

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

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

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

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

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