vue響應(yīng)式系統(tǒng)之observe、watcher、dep的源碼解析
Vue的響應(yīng)式系統(tǒng)
Vue 最獨(dú)特的特性之一,是其非侵入性的響應(yīng)式系統(tǒng)。數(shù)據(jù)模型僅僅是普通的JavaScript 對(duì)象,而當(dāng)你修改它們時(shí),視圖會(huì)進(jìn)行更新,這使得狀態(tài)管理非常簡(jiǎn)單直接,我們可以只關(guān)注數(shù)據(jù)本身,而不用手動(dòng)處理數(shù)據(jù)到視圖的渲染,避免了繁瑣的 DOM 操作,提高了開(kāi)發(fā)效率。
vue 的響應(yīng)式系統(tǒng)依賴于三個(gè)重要的類:Dep 類、Watcher 類、Observer 類,然后使用發(fā)布訂閱模式的思想將他們?nèi)嗪显谝黄穑ú涣私獍l(fā)布訂閱模式的可以看我之前的文章發(fā)布訂閱模式與觀察者模式)。

Observer
Observe扮演的角色是發(fā)布者,他的主要作用是調(diào)用defineReactive函數(shù),在defineReactive函數(shù)中使用Object.defineProperty 方法對(duì)對(duì)象的每一個(gè)子屬性進(jìn)行數(shù)據(jù)劫持/監(jiān)聽(tīng)。
部分代碼展示
defineReactive函數(shù),Observe的核心,劫持?jǐn)?shù)據(jù),在setter中向Dep(調(diào)度中心)添加觀察者,在getter中通知觀察者更新。
function defineReactive(obj, key, val, customSetter, shallow){
//監(jiān)聽(tīng)屬性key
//關(guān)鍵點(diǎn):在閉包中聲明一個(gè)Dep實(shí)例,用于保存watcher實(shí)例
var dep = new Dep();
var getter = property && property.get;
var setter = property && property.set;
if(!getter && arguments.length === 2) {
val = obj[key];
}
//執(zhí)行observe,監(jiān)聽(tīng)屬性key所代表的值val的子屬性
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
//獲取值
var value = getter ? getter.call(obj) : val;
//依賴收集:如果當(dāng)前有活動(dòng)的Dep.target(觀察者--watcher實(shí)例)
if(Dep.target) {
//將dep放進(jìn)當(dāng)前觀察者的deps中,同時(shí),將該觀察者放入dep中,等待變更通知
dep.depend();
if(childOb) {
//為子屬性進(jìn)行依賴收集
//其實(shí)就是將同一個(gè)watcher觀察者實(shí)例放進(jìn)了兩個(gè)dep中
//一個(gè)是正在本身閉包中的dep,另一個(gè)是子屬性的dep
childOb.dep.depend();
}
}
return value
},
set: function reactiveSetter(newVal) {
//獲取value
var value = getter ? getter.call(obj) : val;
if(newVal === value || (newVal !== newVal && value !== value)) {
return
}
if(setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
//新的值需要重新進(jìn)行observe,保證數(shù)據(jù)響應(yīng)式
childOb = observe(newVal);
//關(guān)鍵點(diǎn):遍歷dep.subs,通知所有的觀察者
dep.notify();
}
});
}
Dep
Dep 扮演的角色是調(diào)度中心/訂閱器,主要的作用就是收集觀察者Watcher和通知觀察者目標(biāo)更新。每個(gè)屬性擁有自己的消息訂閱器dep,用于存放所有訂閱了該屬性的觀察者對(duì)象,當(dāng)數(shù)據(jù)發(fā)生改變時(shí),會(huì)遍歷觀察者列表(dep.subs),通知所有的watch,讓訂閱者執(zhí)行自己的update邏輯。
部分代碼展示
Dep的設(shè)計(jì)比較簡(jiǎn)單,就是收集依賴,通知觀察者
//Dep構(gòu)造函數(shù)
var Dep = function Dep() {
this.id = uid++;
this.subs = [];
};
//向dep的觀察者列表subs添加觀察者
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(sub);
};
//從dep的觀察者列表subs移除觀察者
Dep.prototype.removeSub = function removeSub(sub) {
remove(this.subs, sub);
};
Dep.prototype.depend = function depend() {
//依賴收集:如果當(dāng)前有觀察者,將該dep放進(jìn)當(dāng)前觀察者的deps中
//同時(shí),將當(dāng)前觀察者放入觀察者列表subs中
if(Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify() {
// 循環(huán)處理,運(yùn)行每個(gè)觀察者的update接口
var subs = this.subs.slice();
for(var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
//Dep.target是觀察者,這是全局唯一的,因?yàn)樵谌魏螘r(shí)候只有一個(gè)觀察者被處理。
Dep.target = null;
//待處理的觀察者隊(duì)列
var targetStack = [];
function pushTarget(_target) {
//如果當(dāng)前有正在處理的觀察者,將他壓入待處理隊(duì)列
if(Dep.target) {
targetStack.push(Dep.target);
}
//將Dep.target指向需要處理的觀察者
Dep.target = _target;
}
function popTarget() {
//將Dep.target指向棧頂?shù)挠^察者,并將他移除隊(duì)列
Dep.target = targetStack.pop();
}
Watcher
Watcher扮演的角色是訂閱者/觀察者,他的主要作用是為觀察屬性提供回調(diào)函數(shù)以及收集依賴(如計(jì)算屬性computed,vue會(huì)把該屬性所依賴數(shù)據(jù)的dep添加到自身的deps中),當(dāng)被觀察的值發(fā)生變化時(shí),會(huì)接收到來(lái)自dep的通知,從而觸發(fā)回調(diào)函數(shù)。,
部分代碼展示
Watcher類的實(shí)現(xiàn)比較復(fù)雜,因?yàn)樗膶?shí)例分為渲染 watcher(render-watcher)、計(jì)算屬性 watcher(computed-watcher)、偵聽(tīng)器 watcher(normal-watcher)三種,
這三個(gè)實(shí)例分別是在三個(gè)函數(shù)中構(gòu)建的:mountComponent 、initComputed和Vue.prototype.$watch。
normal-watcher:我們?cè)诮M件鉤子函數(shù)watch 中定義的,都屬于這種類型,即只要監(jiān)聽(tīng)的屬性改變了,都會(huì)觸發(fā)定義好的回調(diào)函數(shù),這類watch的expression是我們寫(xiě)的回調(diào)函數(shù)的字符串形式。
computed-watcher:我們?cè)诮M件鉤子函數(shù)computed中定義的,都屬于這種類型,每一個(gè) computed 屬性,最后都會(huì)生成一個(gè)對(duì)應(yīng)的 watcher 對(duì)象,但是這類 watcher 有個(gè)特點(diǎn):當(dāng)計(jì)算屬性依賴于其他數(shù)據(jù)時(shí),屬性并不會(huì)立即重新計(jì)算,只有之后其他地方需要讀取屬性的時(shí)候,它才會(huì)真正計(jì)算,即具備 lazy(懶計(jì)算)特性。這類watch的expression是計(jì)算屬性中的屬性名。
render-watcher:每一個(gè)組件都會(huì)有一個(gè) render-watcher, 當(dāng) data/computed 中的屬性改變的時(shí)候,會(huì)調(diào)用該 render-watcher 來(lái)更新組件的視圖。這類watch的expression是 function () {vm._update(vm._render(), hydrating);}。
除了功能上的區(qū)別,這三種 watcher 也有固定的執(zhí)行順序,分別是:computed-render -> normal-watcher -> render-watcher。
這樣安排是有原因的,這樣就能盡可能的保證,在更新組件視圖的時(shí)候,computed 屬性已經(jīng)是最新值了,如果 render-watcher 排在 computed-render 前面,就會(huì)導(dǎo)致頁(yè)面更新的時(shí)候 computed 值為舊數(shù)據(jù)。
這里我們只看其中一部分代碼
function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
if(isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if(options) {
this.deep = !!options.deep; //是否啟用深度監(jiān)聽(tīng)
this.user = !!options.user; //主要用于錯(cuò)誤處理,偵聽(tīng)器 watcher的 user為true,其他基本為false
this.lazy = !!options.lazy; //惰性求職,當(dāng)屬于計(jì)算屬性watcher時(shí)為true
this.sync = !!options.sync; //標(biāo)記為同步計(jì)算,三大類型暫無(wú)
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
//初始化各種屬性和option
//觀察者的回調(diào)
//除了偵聽(tīng)器 watcher外,其他大多為空函數(shù)
this.cb = cb;
this.id = ++uid$1; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// 解析expOrFn,賦值給this.getter
// 當(dāng)是渲染watcher時(shí),expOrFn是updateComponent,即重新渲染執(zhí)行render(_update)
// 當(dāng)是計(jì)算watcher時(shí),expOrFn是計(jì)算屬性的計(jì)算方法
// 當(dāng)是偵聽(tīng)器watcher時(shí),expOrFn是watch屬性的名字,this.cb就是watch的handler屬性
//對(duì)于渲染watcher和計(jì)算watcher來(lái)說(shuō),expOrFn的值是一個(gè)函數(shù),可以直接設(shè)置getter
//對(duì)于偵聽(tīng)器watcher來(lái)說(shuō),expOrFn是watch屬性的名字,會(huì)使用parsePath函數(shù)解析路徑,獲取組件上該屬性的值(運(yùn)行g(shù)etter)
//依賴(訂閱目標(biāo))更新,執(zhí)行update,會(huì)進(jìn)行取值操作,運(yùn)行watcher.getter,也就是expOrFn函數(shù)
if(typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
this.value = this.lazy ? undefined : this.get();
};
//取值操作
Watcher.prototype.get = function get() {
//Dep.target設(shè)置為該觀察者
pushTarget(this);
var vm = this.vm;
//取值
var value = this.getter.call(vm, vm);
//移除該觀察者
popTarget();
return value
};
Watcher.prototype.addDep = function addDep(dep) {
var id = dep.id;
if(!this.newDepIds.has(id)) {
//為觀察者的deps添加依賴dep
this.newDepIds.add(id);
this.newDeps.push(dep);
if(!this.depIds.has(id)) {
//為dep添加該觀察者
dep.addSub(this);
}
}
};
//當(dāng)一個(gè)依賴改變的時(shí)候,通知它update
Watcher.prototype.update = function update() {
//三種watcher,只有計(jì)算屬性 watcher的lazy設(shè)置了true,表示啟用惰性求值
if(this.lazy) {
this.dirty = true;
} else if(this.sync) {
//標(biāo)記為同步計(jì)算的直接運(yùn)行run,三大類型暫無(wú),所以基本會(huì)走下面的queueWatcher
this.run();
} else {
//將watcher推入觀察者隊(duì)列中,下一個(gè)tick時(shí)調(diào)用。
//也就是數(shù)據(jù)變化不是立即就去更新的,而是異步批量去更新的
queueWatcher(this);
}
};
//update執(zhí)行后,運(yùn)行回調(diào)cb
Watcher.prototype.run = function run() {
if(this.active) {
var value = this.get();
if(
value !== this.value ||
isObject(value) ||
this.deep
) {
var oldValue = this.value;
this.value = value;
//運(yùn)行 cb 函數(shù),這個(gè)函數(shù)就是之前傳入的watch中的handler回調(diào)函數(shù)
if(this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch(e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
//對(duì)于計(jì)算屬性,當(dāng)取值計(jì)算屬性時(shí),發(fā)現(xiàn)計(jì)算屬性的watcher的dirty是true
//說(shuō)明數(shù)據(jù)不是最新的了,需要重新計(jì)算,這里就是重新計(jì)算計(jì)算屬性的值。
Watcher.prototype.evaluate = function evaluate() {
this.value = this.get();
this.dirty = false;
};
//收集依賴
Watcher.prototype.depend = function depend() {
var this$1 = this;
var i = this.deps.length;
while(i--) {
this$1.deps[i].depend();
}
};
總結(jié)
Observe是對(duì)數(shù)據(jù)進(jìn)行監(jiān)聽(tīng),Dep是一個(gè)訂閱器,每一個(gè)被監(jiān)聽(tīng)的數(shù)據(jù)都有一個(gè)Dep實(shí)例,Dep實(shí)例里面存放了N多個(gè)訂閱者(觀察者)對(duì)象watcher。
被監(jiān)聽(tīng)的數(shù)據(jù)進(jìn)行取值操作時(shí)(getter),如果存在Dep.target(某一個(gè)觀察者),則說(shuō)明這個(gè)觀察者是依賴該數(shù)據(jù)的(如計(jì)算屬性中,計(jì)算某一屬性會(huì)用到其他已經(jīng)被監(jiān)聽(tīng)的數(shù)據(jù),就說(shuō)該屬性依賴于其他屬性,會(huì)對(duì)其他屬性進(jìn)行取值),就會(huì)把這個(gè)觀察者添加到該數(shù)據(jù)的訂閱器subs里面,留待后面數(shù)據(jù)變更時(shí)通知(會(huì)先通過(guò)觀察者id判斷訂閱器中是否已經(jīng)存在該觀察者),同時(shí)該觀察者也會(huì)把該數(shù)據(jù)的訂閱器dep添加到自身deps中,方便其他地方使用。
被監(jiān)聽(tīng)的數(shù)據(jù)進(jìn)行賦值操作時(shí)(setter)時(shí),就會(huì)觸發(fā)dep.notify(),循環(huán)該數(shù)據(jù)訂閱器中的觀察者,進(jìn)行更新操作。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
手把手教你拿捏vue?cale()計(jì)算函數(shù)使用
這篇文章手把手教你拿捏vue?cale()計(jì)算函數(shù)使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
基于vue實(shí)現(xiàn)分頁(yè)/翻頁(yè)組件paginator示例
本篇文章主要介紹了基于vue實(shí)現(xiàn)分頁(yè)/翻頁(yè)組件paginator示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-03-03
vue.js 2.0實(shí)現(xiàn)簡(jiǎn)單分頁(yè)效果
這篇文章主要為大家詳細(xì)介紹了vue.js 2.0實(shí)現(xiàn)簡(jiǎn)單分頁(yè)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07
Vue實(shí)現(xiàn)搖一搖功能(兼容ios13.3以上)
這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)搖一搖功能,兼容ios13.3以上,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-01-01
Vue.delete()刪除對(duì)象的屬性說(shuō)明
這篇文章主要介紹了Vue.delete()刪除對(duì)象的屬性說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
axios發(fā)送post請(qǐng)求,提交圖片類型表單數(shù)據(jù)方法
下面小編就為大家分享一篇axios發(fā)送post請(qǐng)求,提交圖片類型表單數(shù)據(jù)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
詳解Vue.js使用Swiper.js在iOS<11時(shí)出現(xiàn)錯(cuò)誤
這篇文章主要介紹了詳解Vue.js使用Swiper.js在iOS<11時(shí)出現(xiàn)錯(cuò)誤,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-09-09
vue3.2?Composition?API項(xiàng)目依賴升級(jí)
這篇文章主要為大家介紹了vue3.2?Composition?API項(xiàng)目依賴升級(jí)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08

