摘要:淺析響應(yīng)式原理一的特點之一是響應(yīng)式,視圖隨著數(shù)據(jù)的更新而更新,在視圖中修改數(shù)據(jù)后實例中的數(shù)據(jù)也會同步更新。對于每個響應(yīng)式數(shù)據(jù),會有兩個實例,第一個是在中的閉包遍歷,用途顯而易見。接收一個回調(diào)函數(shù),會在重新求值且值更新后執(zhí)行。
淺析Vue響應(yīng)式原理(一)
Vue的特點之一是響應(yīng)式,視圖隨著數(shù)據(jù)的更新而更新,在視圖中修改數(shù)據(jù)后Vue實例中的數(shù)據(jù)也會同步更新。內(nèi)部借助依賴(下文中的Dep類)來實現(xiàn),數(shù)據(jù)的獲?。磄et操作)會觸發(fā)收集依賴,而對數(shù)據(jù)賦值(即set操作)會通知依賴數(shù)據(jù)更新,重新渲染視圖。對數(shù)據(jù)的get/set操作的攔截借助的是ES5的Object.defineProperty。
總體架構(gòu)簡介在Vue源碼內(nèi),Dep類作為依賴,Watcher類則用來收集依賴和通知依賴重新求值。對于在實例化時傳入的數(shù)據(jù),使用工廠函數(shù)defineReactive令其響應(yīng)式。而在實例后再通過Vue.set/vm.$set添加的響應(yīng)式數(shù)據(jù),則需要借助Observer類來使其成為響應(yīng)式數(shù)據(jù),最后也是通過defineReactive實現(xiàn)響應(yīng)式。
對于每個響應(yīng)式數(shù)據(jù),會有兩個Dep實例,第一個是在defineReactive中的閉包遍歷,用途顯而易見。而第二個Dep則在響應(yīng)式數(shù)組的__ob__屬性值中,這個值是Observer實例,其實例屬性dep是Dep實例,在執(zhí)行Vue.set/vm.$set添加響應(yīng)式數(shù)據(jù)后,會通知依賴更新。
在講defineReactive之前,先講一下這些輔助類的實現(xiàn)和用處。
Dep我們都知道,Vue響應(yīng)式的實現(xiàn),會在getter中收集響應(yīng)式數(shù)據(jù)的依賴,在setter中通知依賴數(shù)據(jù)更新,重新計算數(shù)據(jù)然后來更新視圖。在Vue內(nèi)部,使用Dep實例表示依賴,讓我們看一下Dep類是怎么定義的。
Dep有兩個實例屬性,一個靜態(tài)屬性。靜態(tài)屬性target是Watcher實例,功能是重新求值和通知視圖更新,下文我們會講到。實例屬性id是Dep實例的唯一標(biāo)識,無需多說;屬性subs是Watcher實例數(shù)組,用于收集Watcher實例,當(dāng)依賴更新時,這些Watcher實例就會重新求值。
export default class Dep { static target: ?Watcher; id: number; subs: Array; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
方法addSub用于添加Watcher實例到subs中,方法removeSub用于從subs移除Watcher實例。
方法depond會在收集依賴的時候調(diào)用,實際上執(zhí)行了Watcher的實例方法addDep,在addDep內(nèi)除了調(diào)用dep實例的addSup方法外,還做了避免重復(fù)收集Watcher實例的工作。這個方法會在Vue為響應(yīng)式數(shù)據(jù)設(shè)置的自定義getter中執(zhí)行。
notify方法則遍歷subs,執(zhí)行Watcher實例方法update來重新求值。這個方法會在Vue為響應(yīng)式數(shù)據(jù)設(shè)置的自定義setter中執(zhí)行。
有人可能有疑問,target是靜態(tài)屬性,那不是每個實例的target都一樣的?實際上,重新求值的操作在Watcher實例方法get內(nèi)實現(xiàn)。在get方法內(nèi),會先調(diào)用pushTarget來更新Dep.target,使其指向當(dāng)前Watcher實例,之前的`Dep.target會被保存targetStack末尾(相當(dāng)于入棧操作),完成操作后會執(zhí)行popTarget函數(shù),從targetStack取出最后一個元素來還原Dep.target(相當(dāng)于出棧操作)。
Dep.target = null const targetStack = [] export function pushTarget (_target: ?Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { Dep.target = targetStack.pop() }Watcher
當(dāng)依賴更新時,Watcher類會重新求值,并可能觸發(fā)重渲染。
constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm // 與渲染相關(guān)的watcher if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.computed = !!options.computed this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.computed = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.computed // for computed watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== "production" ? expOrFn.toString() : "" // parse expression for getter if (typeof expOrFn === "function") { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} process.env.NODE_ENV !== "production" && warn( `Failed watching path: "${expOrFn}" ` + "Watcher only accepts simple dot-delimited paths. " + "For full control, use a function instead.", vm ) } } if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } }
構(gòu)造函數(shù)接受五個參數(shù),vm是掛載的Component實例;expOrFn是觀察的屬性,當(dāng)是字符串時表示屬性名,是函數(shù)時會被當(dāng)成屬性的get方法;cb是屬性更新后執(zhí)行的回調(diào)函數(shù);options是配置項;isRenderWatcher表示當(dāng)前實例是否與渲染相關(guān)。
在構(gòu)造函數(shù)內(nèi),先將實例屬性vm指向傳入的Component實例vm,如果當(dāng)前Watcher實例與渲染相關(guān),會將其保存在vm._watcher中。接著將當(dāng)前實例添加到vm._watchers中,同時根據(jù)傳入的配置項options初始化實例屬性。實例屬性getter是監(jiān)聽屬性的getter函數(shù),如果expOrFn是函數(shù),直接賦值,否則會調(diào)用parsePath來獲取屬性的getter。
parsePath內(nèi)部會先使用正則來判斷屬性名,如果有除數(shù)字、字母、.和$以外的字符時視為非法屬性名,直接返回,所以屬性只能是以.分隔的屬性。如果屬性名合法,則parsePath返回一個閉包函數(shù),調(diào)用時會傳入vm,即obj是vm的引用,這個閉包函數(shù)最終的目的是從vm實例里獲取屬性。
const bailRE = /[^w.$]/ export function parsePath (path: string): any { if (bailRE.test(path)) { return } const segments = path.split(".") return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }
初始化完成之后,如果不是計算屬性相關(guān)的Watcher實例,會調(diào)用實例方法get求值。
get方法執(zhí)行g(shù)etter方法求值,完成依賴收集的過程。
方法開始時,執(zhí)行pushTarget(this),將Dep.target指向當(dāng)前Watcher實例。然后執(zhí)行getter收集依賴,最后將Dep.target復(fù)原,并執(zhí)行cleanDeps遍歷deps。在每次求值之后,都會調(diào)用cleanupDeps方法重置依賴,具體如何重置,稍后再講。
實際上,Dep.target指向的實例是即將要收集的目標(biāo)。
getter的執(zhí)行,除了會獲取值外,還會觸發(fā)在defineReactive中為屬性設(shè)置的getter,完成依賴的收集。
get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }addDep
addDep的功能是將當(dāng)前Watcher實例添加到傳入的Dep實例屬性subs數(shù)組里去。
addDep接受一個Dep實例作為參數(shù),如果 dep.id 沒有在集合 newDepIds 之中,則添加。如果不在集合 depIds 中,則將當(dāng)前實例添加到 dep.subs 中。 簡單來說,這里的操作會避免重復(fù)收集依賴,這也是不直接調(diào)用dep.addSub(Dep.target)的原因。
addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } }
從這里可以看出來Dep實例和Watcher實例會相互引用。Dep實例將Watcher實例保存在實例屬性subs中,在響應(yīng)式屬性調(diào)用setter時,執(zhí)行notify方法,通知Watcher實例重新求值。
Watcher實例將Dep實例保存在集合newDeps,目的是避免重復(fù)收集依賴,同時會執(zhí)行Dep實例方法addDep,將當(dāng)前Watcher實例添加到Dep實例屬性subs中。
cleanupDeps對于Watcher來說,每次求值的依賴并不一定與上一次的相同,在每次執(zhí)行get之后,都會調(diào)用cleanupDeps來重置收集的依賴。Watcher有四個實例屬性用于記錄依賴,分別是newDeps/newDepIds與deps/depIds。newDeps與deps是保存依賴的數(shù)組,newDepIds與depIds是保存依賴Id的集合。記錄上一次求值依賴的屬性是deps/depIds,記錄下一次求值依賴的屬性是newDeps/newDepIds(執(zhí)行cleanupDeps時已經(jīng)調(diào)用過getter重新求值了,所以說是上一次求值,下一次指的是下一次調(diào)用get的時候)。
cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } // 交換depIds和newDepIds let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() // 交換deps和newDeps tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }
首先遍歷deps,如果此次求值的依賴在下一次求值中并不存在,則需要調(diào)用removeSub方法,從subs數(shù)組中移除當(dāng)前Watcher實例。
接著交換newDeps/newDepIds與deps/depIds,并清空交換后的newDeps/newDepIds。
updateDep類的notify方法用于通知觀察者重新求值,該方法內(nèi)部實際是遍歷subs數(shù)組,執(zhí)行Watcher的update方法。
update 方法定義如下。當(dāng)實例與計算屬性相關(guān)時,xxx。如果不是計算屬性相關(guān)時,判斷是否需要同步觸發(fā),同步觸發(fā)時調(diào)用run,否則執(zhí)行queueWatcher(this),交由調(diào)度模塊統(tǒng)一調(diào)度。
update () { if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true } else { this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } }teardown
銷毀當(dāng)前Watcher實例。$watch方法返回一個函數(shù),函數(shù)內(nèi)部就是Watcher實例調(diào)用teardown方法。
先判斷Watcher實例是否在活躍狀態(tài)。首先要從Vue實例的觀察者隊列_watchers中移除當(dāng)前實例,如果vm正在銷毀,因為性能的問題會跳過這一操作。接著遍歷deps,取消這些Dep實例對當(dāng)前Watcher實例的訂閱。最后令this.active = false,表示當(dāng)前Watcher實例已被銷毀。
teardown () { if (this.active) { // remove self from vm"s watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } }getAndInvoke
不論是同步或異步更新,或者是計算屬性相關(guān)的Wathcer實例,最終求值都是通過getAndInvoke方法。
getAndInvoke接收一個回調(diào)函數(shù),會在重新求值且值更新后執(zhí)行。
當(dāng)新值與當(dāng)前值不同時會被判定為值已更新。當(dāng)值是對象時且this.deep為真時也判定為值已更新,盡管引用不發(fā)生改變,但其屬性卻可能發(fā)生變化,為避免屬性發(fā)生改變而Watcher判斷未更新的情況出現(xiàn)。
getAndInvoke (cb: Function) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value this.dirty = false if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) } } }run
run方法內(nèi)部只是對getAndInvoke的封裝,傳入的回調(diào)函數(shù)是實例化時傳入的函數(shù)。執(zhí)行之前會先判斷Watcher實例是否已棄用。
run () { if (this.active) { this.getAndInvoke(this.cb) } }小結(jié)
由于篇幅的原因,本文只簡單分析了輔助類和工廠函數(shù)的源碼和功能。干巴巴地講了這么多,現(xiàn)在來稍微捋一下。
Watcher類會保存響應(yīng)式數(shù)據(jù)的getter函數(shù),這個getter函數(shù)可能是實例化參數(shù)expOrFn(當(dāng)其是函數(shù)類型時),也可能是執(zhí)行parsePath(expOrFn)獲取到的getter函數(shù)。實例方法update對外暴露,用于重新求值,實際上執(zhí)行真正求值操作的get方法。方法addDep接受一個Dep實例參數(shù),在執(zhí)行訂閱操作前還會執(zhí)行兩個if判斷,避免重復(fù)訂閱。
Dep類代表依賴,實例屬性subs是Watcher數(shù)組,代表訂閱了當(dāng)前Dep實例的觀察者實例,depond方法收集依賴,notify方法通知觀察者實例重新求值。訂閱列表中可能會有與渲染相關(guān)的觀察者,所以可能會觸發(fā)重渲染。
Observer類與Vue.set/vm.$set的聯(lián)系比較大,所以分析放在后面。
參考鏈接Vue技術(shù)內(nèi)幕|揭開數(shù)據(jù)響應(yīng)系統(tǒng)的面紗
Vue.js源碼
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/99405.html
摘要:響應(yīng)式原理之不論如何,最終響應(yīng)式數(shù)據(jù)都要通過來實現(xiàn),實際要借助新增的。在函數(shù)內(nèi),首先實例化一個實例,會在稍后添加為響應(yīng)式數(shù)據(jù)自定義的中發(fā)揮作用。只有數(shù)組和對象才可能是響應(yīng)式,才能返回實例。參考鏈接技術(shù)內(nèi)幕揭開數(shù)據(jù)響應(yīng)系統(tǒng)的面紗源碼 Vue響應(yīng)式原理之defineReactive defineReactive 不論如何,最終響應(yīng)式數(shù)據(jù)都要通過defineReactive來實現(xiàn),實際要借助...
摘要:響應(yīng)式原理為了探究這一切的原因,我再次點開了的官網(wǎng)。在官網(wǎng)很下面的位置,找到了關(guān)于響應(yīng)式原理的說明。因此,新添加到數(shù)組中的對象中的屬性,就成了非響應(yīng)式的屬性了,改變它自然不會讓組件重新渲染。響應(yīng)式屬性的對象,有這個對象就代表是響應(yīng)式的。 ??最近在用Vue開發(fā)一個后臺管理的demo,有一個非常常規(guī)的需求。然而這個常規(guī)的需求中,包含了大量的知識點。有一個產(chǎn)品表格,用來顯示不同產(chǎn)品的信息。...
摘要:響應(yīng)式原理之之前簡單介紹了和類的代碼和作用,現(xiàn)在來介紹一下類和。對于數(shù)組,響應(yīng)式的實現(xiàn)稍有不同。不存在時,說明不是響應(yīng)式數(shù)據(jù),直接更新。如果對象是響應(yīng)式的,確保刪除能觸發(fā)更新視圖。 Vue響應(yīng)式原理之Observer 之前簡單介紹了Dep和Watcher類的代碼和作用,現(xiàn)在來介紹一下Observer類和set/get。在Vue實例后再添加響應(yīng)式數(shù)據(jù)時需要借助Vue.set/vm.$se...
摘要:哪吒別人的看法都是狗屁,你是誰只有你自己說了才算,這是爹教我的道理。哪吒去他個鳥命我命由我,不由天是魔是仙,我自己決定哪吒白白搭上一條人命,你傻不傻敖丙不傻誰和你做朋友太乙真人人是否能夠改變命運,我不曉得。我只曉得,不認(rèn)命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出處 查看github最新的Vue...
摘要:淺析的特點之一就是響應(yīng)式,但數(shù)據(jù)更新時,并不會立即更新。盡管已經(jīng)更新,但新增的元素并不立即插入到中。實際在中,執(zhí)行了,這也是自動綁定到執(zhí)行上下文的原因。在內(nèi),使用數(shù)組保存回調(diào)函數(shù),表示當(dāng)前狀態(tài),使用函數(shù)來執(zhí)行回調(diào)隊列。 Vue.nextTick 淺析 Vue 的特點之一就是響應(yīng)式,但數(shù)據(jù)更新時,DOM 并不會立即更新。當(dāng)我們有一個業(yè)務(wù)場景,需要在 DOM 更新之后再執(zhí)行一段代碼時,可以...
閱讀 2769·2021-11-24 10:44
閱讀 2150·2021-11-22 13:53
閱讀 2184·2021-09-30 09:47
閱讀 3845·2021-09-22 16:00
閱讀 2546·2021-09-08 09:36
閱讀 2386·2019-08-30 15:53
閱讀 2950·2019-08-30 15:48
閱讀 1119·2019-08-30 15:44