詳解Vue源碼學(xué)習(xí)之雙向綁定
原理
當(dāng)你把一個(gè)普通的 JavaScript 對(duì)象傳給 Vue 實(shí)例的 data 選項(xiàng),Vue 將遍歷此對(duì)象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉(zhuǎn)為 getter/setter。Object.defineProperty 是 ES5 中一個(gè)無(wú)法 shim 的特性,這也就是為什么 Vue 不支持 IE8 以及更低版本瀏覽器。
上面那段話(huà)是Vue官方文檔中截取的,可以看到是使用Object.defineProperty實(shí)現(xiàn)對(duì)數(shù)據(jù)改變的監(jiān)聽(tīng)。Vue主要使用了觀察者模式來(lái)實(shí)現(xiàn)數(shù)據(jù)與視圖的雙向綁定。
function initData(vm) { //將data上數(shù)據(jù)復(fù)制到_data并遍歷所有屬性添加代理 vm._data = vm.$options.data; const keys = Object.keys(vm._data); let i = keys.length; while(i--) { const key = keys[i]; proxy(vm, `_data`, key); } observe(data, true /* asRootData */) //對(duì)data進(jìn)行監(jiān)聽(tīng) }
在第一篇數(shù)據(jù)初始化中,執(zhí)行new Vue()操作后會(huì)執(zhí)行initData()去初始化用戶(hù)傳入的data,最后一步操作就是為data添加響應(yīng)式。
實(shí)現(xiàn)
在Vue內(nèi)部存在三個(gè)對(duì)象:Observer、Dep、Watcher,這也是實(shí)現(xiàn)響應(yīng)式的核心。
Observer
Observer對(duì)象將data中所有的屬性轉(zhuǎn)為getter/setter形式,以下是簡(jiǎn)化版代碼,詳細(xì)代碼請(qǐng)看這里。
export function observe (value) { //遞歸子屬性時(shí)的判斷 if (!isObject(value) || value instanceof VNode) { return } ... ob = new Observer(value) } export class Observer { constructor (value) { ... //此處省略對(duì)數(shù)組的處理 this.walk(value) } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) //為每個(gè)屬性創(chuàng)建setter/getter } } ... } //設(shè)置set/get export function defineReactive ( obj: Object, key: string, val: any ) { //利用閉包存儲(chǔ)每個(gè)屬性關(guān)聯(lián)的watcher隊(duì)列,當(dāng)setter觸發(fā)時(shí)依然能訪問(wèn)到 const dep = new Dep() ... //如果屬性為對(duì)象也創(chuàng)建相應(yīng)observer let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { if (Dep.target) { dep.depend() //將當(dāng)前dep傳到對(duì)應(yīng)watcher中再執(zhí)行watcher.addDep將watcher添加到當(dāng)前dep.subs中 if (childOb) { //如果屬性是對(duì)象則繼續(xù)收集依賴(lài) childOb.dep.depend() ... } } return value }, set: function reactiveSetter (newVal) { ... childOb = observe(newVal) //如果設(shè)置的新值是對(duì)象,則為其創(chuàng)建observe dep.notify() //通知隊(duì)列中的watcher進(jìn)行更新 } }) }
創(chuàng)建Observer對(duì)象時(shí),為data的每個(gè)屬性都執(zhí)行了一遍defineReactive方法,如果當(dāng)前屬性為對(duì)象,則通過(guò)遞歸進(jìn)行深度遍歷。該方法中創(chuàng)建了一個(gè)Dep實(shí)例,每一個(gè)屬性都有一個(gè)與之對(duì)應(yīng)的dep,存儲(chǔ)所有的依賴(lài)。然后為屬性設(shè)置setter/getter,在getter時(shí)收集依賴(lài),setter時(shí)派發(fā)更新。這里收集依賴(lài)不直接使用addSub是為了能讓W(xué)atcher創(chuàng)建時(shí)自動(dòng)將自己添加到dep.subs中,這樣只有當(dāng)數(shù)據(jù)被訪問(wèn)時(shí)才會(huì)進(jìn)行依賴(lài)收集,可以避免一些不必要的依賴(lài)收集。
Dep
Dep就是一個(gè)發(fā)布者,負(fù)責(zé)收集依賴(lài),當(dāng)數(shù)據(jù)更新是去通知訂閱者(watcher)。源碼地址
export default class Dep { static target: ?Watcher; //指向當(dāng)前watcher constructor () { this.subs = [] } //添加watcher addSub (sub: Watcher) { this.subs.push(sub) } //移除watcher removeSub (sub: Watcher) { remove(this.subs, sub) } //通過(guò)watcher將自身添加到dep中 depend () { if (Dep.target) { Dep.target.addDep(this) } } //派發(fā)更新信息 notify () { ... for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
Watcher
//解析表達(dá)式(a.b),返回一個(gè)函數(shù) 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]] //遍歷得到表達(dá)式所代表的屬性 } return obj } } export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } //對(duì)創(chuàng)建的watcher進(jìn)行收集,destroy時(shí)對(duì)這些watcher進(jìn)行銷(xiāo)毀 vm._watchers.push(this) // options if (options) { ... this.before = options.before } ... //上一輪收集的依賴(lài)集合Dep以及對(duì)應(yīng)的id this.deps = [] this.depIds = new Set() //新收集的依賴(lài)集合Dep以及對(duì)應(yīng)的id this.newDeps = [] 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) ... } ... this.value = this.get() } /** * Evaluate the getter, and re-collect dependencies. */ 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() //清空上一輪的依賴(lài) } return value } /** * Add a dependency to this directive. */ addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { //同一個(gè)數(shù)據(jù)只收集一次 this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } //每輪收集結(jié)束后去除掉上輪收集中不需要跟蹤的依賴(lài) cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }, update () { ... //經(jīng)過(guò)一些優(yōu)化處理后,最終執(zhí)行this.get this.get(); } // ... }
依賴(lài)收集的觸發(fā)是在執(zhí)行render之前,會(huì)創(chuàng)建一個(gè)渲染W(wǎng)atcher:
updateComponent = () => { vm._update(vm._render(), hydrating) //執(zhí)行render生成VNode并更新dom } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
在渲染W(wǎng)atcher創(chuàng)建時(shí)會(huì)將Dep.target指向自身并觸發(fā)updateComponent也就是執(zhí)行_render生成VNode并執(zhí)行_update將VNode渲染成真實(shí)DOM,在render過(guò)程中會(huì)對(duì)模板進(jìn)行編譯,此時(shí)就會(huì)對(duì)data進(jìn)行訪問(wèn)從而觸發(fā)getter,由于此時(shí)Dep.target已經(jīng)指向了渲染W(wǎng)atcher,接著渲染W(wǎng)atcher會(huì)執(zhí)行自身的addDep,做一些去重判斷然后執(zhí)行dep.addSub(this)將自身push到屬性對(duì)應(yīng)的dep.subs中,同一個(gè)屬性只會(huì)被添加一次,表示數(shù)據(jù)在當(dāng)前Watcher中被引用。
當(dāng)_render結(jié)束后,會(huì)執(zhí)行popTarget(),將當(dāng)前Dep.target回退到上一輪的指,最終又回到了null,也就是所有收集已完畢。之后執(zhí)行cleanupDeps()將上一輪不需要的依賴(lài)清除。當(dāng)數(shù)據(jù)變化是,觸發(fā)setter,執(zhí)行對(duì)應(yīng)Watcher的update屬性,去執(zhí)行g(shù)et方法又重新將Dep.target指向當(dāng)前執(zhí)行的Watcher觸發(fā)該Watcher的更新。
這里可以看到有deps,newDeps兩個(gè)依賴(lài)表,也就是上一輪的依賴(lài)和最新的依賴(lài),這兩個(gè)依賴(lài)表主要是用來(lái)做依賴(lài)清除的。但在addDep中可以看到if (!this.newDepIds.has(id))已經(jīng)對(duì)收集的依賴(lài)進(jìn)行了唯一性判斷,不收集重復(fù)的數(shù)據(jù)依賴(lài)。為何又要在cleanupDeps中再作一次判斷呢?
while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0
在cleanupDeps中主要清除上一輪中的依賴(lài)在新一輪中沒(méi)有重新收集的,也就是數(shù)據(jù)刷新后某些數(shù)據(jù)不再被渲染出來(lái)了,例如:
<body> <div id="app"> <div v-if='flag'> </div> <div v-else> </div> <button @click="msg1 += '1'">change</button> <button @click="flag = !flag">toggle</button> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { flag: true, msg1: 'msg1', msg2: 'msg2' } }) </script> </body>
每次點(diǎn)擊change,msg1都會(huì)拼接一個(gè)1,此時(shí)就會(huì)觸發(fā)重新渲染。當(dāng)我們點(diǎn)擊toggle時(shí),由于flag改變,msg1不再被渲染,但當(dāng)我們點(diǎn)擊change時(shí),msg1發(fā)生了變化,但卻沒(méi)有觸發(fā)重新渲染,這就是cleanupDeps起的作用。如果去除掉cleanupDeps這個(gè)步驟,只是能防止添加相同的依賴(lài),但是數(shù)據(jù)每次更新都會(huì)觸發(fā)重新渲染,又去重新收集依賴(lài)。這個(gè)例子中,toggle后,重新收集的依賴(lài)中并沒(méi)有msg1,因?yàn)樗恍枰伙@示,但是由于設(shè)置了setter,此時(shí)去改變msg1依然會(huì)觸發(fā)setter,如果沒(méi)有執(zhí)行cleanupDeps,那么msg1的依賴(lài)依然存在依賴(lài)表里,又會(huì)去觸發(fā)重新渲染,這是不合理的,所以需要每次依賴(lài)收集完畢后清除掉一些不需要的依賴(lài)。
總結(jié)
依賴(lài)收集其實(shí)就是收集每個(gè)數(shù)據(jù)被哪些Watcher(渲染W(wǎng)atcher、computedWatcher等)所引用,當(dāng)這些數(shù)據(jù)更新時(shí),就去通知依賴(lài)它的Watcher去更新。
以上所述是小編給大家介紹的Vue源碼學(xué)習(xí)之雙向綁定詳解整合,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
- Vue實(shí)現(xiàn)雙向綁定的方法
- 深入理解vue.js雙向綁定的實(shí)現(xiàn)原理
- Vue2實(shí)現(xiàn)組件props雙向綁定
- Vuejs第一篇之入門(mén)教程詳解(單向綁定、雙向綁定、列表渲染、響應(yīng)函數(shù))
- Vue.js每天必學(xué)之?dāng)?shù)據(jù)雙向綁定
- Vue 實(shí)現(xiàn)雙向綁定的四種方法
- 淺談vue中數(shù)據(jù)雙向綁定的實(shí)現(xiàn)原理
- vue數(shù)據(jù)雙向綁定的注意點(diǎn)
- Vue2.0利用 v-model 實(shí)現(xiàn)組件props雙向綁定的優(yōu)美解決方案
- Vue父子組件雙向綁定傳值的實(shí)現(xiàn)方法
相關(guān)文章
關(guān)于Vue實(shí)例創(chuàng)建的整體流程
這篇文章主要介紹了關(guān)于Vue實(shí)例創(chuàng)建的整體流程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06前端開(kāi)發(fā)指南之vue-grid-layout的使用實(shí)例
vue-grid-layout是一個(gè)vue柵格拖動(dòng)布局的組件,下面這篇文章主要給大家介紹了關(guān)于前端開(kāi)發(fā)指南之vue-grid-layout使用的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09基于 Vue 實(shí)現(xiàn)一個(gè)酷炫的 menu插件
本文給大家介紹基于 Vue 實(shí)現(xiàn)一個(gè)酷炫的 menu插件,此篇教程需要大家具備一定的css和vue基礎(chǔ)知識(shí),本文分步驟給大家介紹的非常詳細(xì),需要的朋友參考下吧2017-11-11Vue 父子組件的數(shù)據(jù)傳遞、修改和更新方法
下面小編就為大家分享一篇Vue 父子組件的數(shù)據(jù)傳遞、修改和更新方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03vue3 + vite + ts 中使用less文件全局變量的操作方法
這篇文章主要介紹了vue3 + vite + ts 中使用less文件全局變量的操作方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-03-03vue如何使用formData傳遞文件類(lèi)型的數(shù)據(jù)
這篇文章主要介紹了vue如何使用formData傳遞文件類(lèi)型的數(shù)據(jù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05vuex在vite&vue3中的簡(jiǎn)單使用說(shuō)明
這篇文章主要介紹了vuex在vite&vue3中的簡(jiǎn)單使用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06vue element-ui el-cascader級(jí)聯(lián)選擇器數(shù)據(jù)回顯的兩種實(shí)現(xiàn)方法
這篇文章主要介紹了vue element-ui el-cascader級(jí)聯(lián)選擇器數(shù)據(jù)回顯的兩種實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。2023-07-07