通過(guò)圖帶你深入了解vue的響應(yīng)式原理
前言
如果自己去實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)的模式,如何解決一下幾個(gè)問(wèn)題:
- 通過(guò)什么手段去知道我的數(shù)據(jù)變了?
- 通過(guò)什么東西去同步更新視圖?
數(shù)據(jù)劫持——obvserver
我們需要知道數(shù)據(jù)的獲取和改變,數(shù)據(jù)劫持是最基礎(chǔ)的手段。在Obeserver中,我們可以看到代碼如下:
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // ... }, set: function reactiveSetter (newVal) { // ... } })
通過(guò)Object.defineProperty這個(gè)方法,我們可以在數(shù)據(jù)發(fā)生改變或者獲取的時(shí)候,插入一些自定義操作。同理,vue也是在這個(gè)方法中做依賴(lài)收集和派發(fā)更新的。
綁定和更新視圖——watcher
從初始化開(kāi)始,我們渲染視圖的時(shí)候,便會(huì)生成一個(gè)watcher,他是監(jiān)視視圖中參數(shù)變化以及更新視圖的。代碼如下:
// 在mount的生命鉤子中 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
當(dāng)然,我們可以保留疑問(wèn):
- watcher是怎么去更新視圖的
- 數(shù)據(jù)又是怎么和watcher聯(lián)動(dòng)起來(lái)的
具體的綁定和更新的流程,我們到后續(xù)的依賴(lài)收集中講解。
我們先來(lái)講講響應(yīng)式系統(tǒng)中涉及到的設(shè)計(jì)模式。
發(fā)布訂閱模式
在發(fā)布訂閱模式中,發(fā)布者和訂閱者之間多了一個(gè)發(fā)布通道;一方面從發(fā)布者接收事件,另一方面向訂閱者發(fā)布事件;訂閱者需要從事件通道訂閱事件
以此避免發(fā)布者和訂閱者之間產(chǎn)生依賴(lài)關(guān)系
vue的響應(yīng)式流程
vue的響應(yīng)式系統(tǒng)借鑒了數(shù)據(jù)劫持和發(fā)布訂閱模式。
Vue用Dep作為一個(gè)中間者,解藕了Observer和Watcher之間的關(guān)系,使得兩者的職能更加明確。
那具體是如何來(lái)完成依賴(lài)收集和訂閱更新的呢?
依賴(lài)收集過(guò)程
依賴(lài)收集的流程
舉個(gè)例子
<div id="app"> {{ message }} {{ message1 }} <input type="text" v-model="message"> <div @click="changeMessage">改變message</div> </div>
var app = new Vue({ el: '#app', data: { message: '1', message1: '2', }, methods: { changeMessage() { this.message = '2' } }, watch: { message: function(val) { this.message1 = val } } })
依賴(lài)收集流程圖:
如何看懂這個(gè)依賴(lài)收集流程?關(guān)鍵在watcher代碼中:
get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { // 省略 } finally { if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }
調(diào)用的這個(gè)this.getter有兩種,一種是key值的getter方法,還有一種是expOrFn,比如mounted中傳入的updateComponent。
如何防止重復(fù)收集
我們不妨想想什么才算是重復(fù)收集了?
筆者想到一種情況:就是dep數(shù)組中,出現(xiàn)了多個(gè)一樣的watcher。
比如renderWatch就容易被重復(fù)收集,因?yàn)槲覀冊(cè)趆tml模版中,會(huì)重復(fù)使用data中的某個(gè)變量。那他是如何去重的呢?
1、只有watch在執(zhí)行g(shù)et時(shí),觸發(fā)的取數(shù)操作,才會(huì)被收集
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() // ... } return value }, set: function reactiveSetter (newVal) { // ... dep.notify() } })
當(dāng)只有Dep.target這個(gè)存在的時(shí)候才進(jìn)行依賴(lài)收集。Dep.target這個(gè)值只有在watcher執(zhí)行g(shù)et方法的時(shí)候才會(huì)存在。
2、在dep.depend的時(shí)候會(huì)判斷watch的id
depend () { if (Dep.target) { Dep.target.addDep(this) } } 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) } } }
我們會(huì)發(fā)現(xiàn),在depend過(guò)程中,會(huì)有一個(gè)newDepIds去記錄已經(jīng)存入的dep的id,當(dāng)一個(gè)watcher已經(jīng)被該dep存過(guò)時(shí),便不再會(huì)進(jìn)行依賴(lài)收集操作。
派發(fā)更新過(guò)程
收集流程講完了,不妨在聽(tīng)聽(tīng)更新流程。
訂閱更新的流程
老例子
<div id="app"> {{ message }} {{ message1 }} <input type="text" v-model="message"> <div @click="changeMessage">改變message</div> </div>
var app = new Vue({ el: '#app', data: { message: '1', message1: '2', }, methods: { changeMessage() { this.message = '3' } }, watch: { message: function(val) { this.message1 = val } } })
依賴(lài)收集的最終結(jié)果:
當(dāng)觸發(fā)click事件的時(shí)候,便會(huì)觸發(fā)訂閱更新流程。
訂閱更新流程圖:
當(dāng)renderWatch執(zhí)行更新的時(shí)候,回去調(diào)用beforeUpdate生命鉤子,然后執(zhí)行patch方法,進(jìn)行視圖的變更。
如何防止重復(fù)更新
如何去防止重復(fù)更新呢?renderWatch會(huì)被很多dep進(jìn)行收集,如果視圖多次渲染,會(huì)造成性能問(wèn)題。
其實(shí)問(wèn)題的關(guān)在在于——queueWatcher
在queueWatcher中有兩個(gè)操作:去重和異步更新。
function queueWatcher (watcher) { const id = watcher.id if (has[id] == null) { has[id] = true queue.push(watcher) // ... if (!waiting) { waiting = true // ... nextTick(flushSchedulerQueue) } } }
其實(shí)queueWatcher很簡(jiǎn)單,將所有watch收集到一個(gè)數(shù)組當(dāng)中,然后去重。
這樣至少可以避免renderWatch頻繁更新。
比如上述例子中的,message和message1都有一個(gè)renderWatch,但是只會(huì)執(zhí)行一次。
異步更新也可以保證當(dāng)一個(gè)事件結(jié)束之后,才會(huì)觸發(fā)視圖層的更新,也能防止renderWatch重復(fù)更新
結(jié)尾
文章講述了響應(yīng)式流程的原因,代碼細(xì)節(jié)并未深入,
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Vue數(shù)據(jù)綁定簡(jiǎn)析小結(jié)
這篇文章主要介紹了Vue數(shù)據(jù)綁定簡(jiǎn)析小結(jié),本文將從源碼的角度來(lái)對(duì)Vue響應(yīng)式數(shù)據(jù)中的觀(guān)察者模式進(jìn)行簡(jiǎn)析。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-05-05解決vue項(xiàng)目中出現(xiàn)Invalid Host header的問(wèn)題
這篇文章主要介紹了解決vue項(xiàng)目中出現(xiàn)"Invalid Host header"的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11關(guān)于前端報(bào)“應(yīng)為聲明或語(yǔ)句。ts(1128)“的原因及解決方案
最近在學(xué)習(xí)中遇到了個(gè)不常見(jiàn)的報(bào)錯(cuò),這里給大家總結(jié)下解決的辦法,這篇文章主要給大家介紹了關(guān)于前端報(bào)“應(yīng)為聲明或語(yǔ)句,ts(1128)“的原因及解決方案,需要的朋友可以參考下2024-08-08vue踩坑記錄之?dāng)?shù)組定義和賦值問(wèn)題
這篇文章主要給大家介紹了關(guān)于vue踩坑記錄之?dāng)?shù)組定義和賦值問(wèn)題的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用vue具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03ElementUI實(shí)現(xiàn)el-table列寬自適應(yīng)的代碼詳解
這篇文章給大家介紹了ElementUI實(shí)現(xiàn)el-table列寬自適應(yīng)的詳細(xì)步驟,文中通過(guò)代碼示例給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-01-01在vue中實(shí)現(xiàn)嵌套頁(yè)面(iframe)
這篇文章主要介紹了在vue中實(shí)現(xiàn)嵌套頁(yè)面(iframe),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-07-07