您好,登錄后才能下訂單哦!
這篇文章主要介紹Vue如何實現雙向綁定,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
原理
當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是為什么 Vue 不支持 IE8 以及更低版本瀏覽器。
上面那段話是Vue官方文檔中截取的,可以看到是使用Object.defineProperty實現對數據改變的監聽。Vue主要使用了觀察者模式來實現數據與視圖的雙向綁定。
function initData(vm) { //將data上數據復制到_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 */) //對data進行監聽 }
在第一篇數據初始化中,執行new Vue()操作后會執行initData()去初始化用戶傳入的data,最后一步操作就是為data添加響應式。
實現
在Vue內部存在三個對象:Observer、Dep、Watcher,這也是實現響應式的核心。
Observer
Observer對象將data中所有的屬性轉為getter/setter形式,以下是簡化版代碼,詳細代碼請看這里。
export function observe (value) { //遞歸子屬性時的判斷 if (!isObject(value) || value instanceof VNode) { return } ... ob = new Observer(value) } export class Observer { constructor (value) { ... //此處省略對數組的處理 this.walk(value) } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) //為每個屬性創建setter/getter } } ... } //設置set/get export function defineReactive ( obj: Object, key: string, val: any ) { //利用閉包存儲每個屬性關聯的watcher隊列,當setter觸發時依然能訪問到 const dep = new Dep() ... //如果屬性為對象也創建相應observer let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { if (Dep.target) { dep.depend() //將當前dep傳到對應watcher中再執行watcher.addDep將watcher添加到當前dep.subs中 if (childOb) { //如果屬性是對象則繼續收集依賴 childOb.dep.depend() ... } } return value }, set: function reactiveSetter (newVal) { ... childOb = observe(newVal) //如果設置的新值是對象,則為其創建observe dep.notify() //通知隊列中的watcher進行更新 } }) }
創建Observer對象時,為data的每個屬性都執行了一遍defineReactive方法,如果當前屬性為對象,則通過遞歸進行深度遍歷。該方法中創建了一個Dep實例,每一個屬性都有一個與之對應的dep,存儲所有的依賴。然后為屬性設置setter/getter,在getter時收集依賴,setter時派發更新。這里收集依賴不直接使用addSub是為了能讓Watcher創建時自動將自己添加到dep.subs中,這樣只有當數據被訪問時才會進行依賴收集,可以避免一些不必要的依賴收集。
Dep
Dep就是一個發布者,負責收集依賴,當數據更新是去通知訂閱者(watcher)。源碼地址
export default class Dep { static target: ?Watcher; //指向當前watcher constructor () { this.subs = [] } //添加watcher addSub (sub: Watcher) { this.subs.push(sub) } //移除watcher removeSub (sub: Watcher) { remove(this.subs, sub) } //通過watcher將自身添加到dep中 depend () { if (Dep.target) { Dep.target.addDep(this) } } //派發更新信息 notify () { ... for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
Watcher
源碼地址
//解析表達式(a.b),返回一個函數 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 } } export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } //對創建的watcher進行收集,destroy時對這些watcher進行銷毀 vm._watchers.push(this) // options if (options) { ... this.before = options.before } ... //上一輪收集的依賴集合Dep以及對應的id this.deps = [] this.depIds = new Set() //新收集的依賴集合Dep以及對應的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() //清空上一輪的依賴 } return value } /** * Add a dependency to this directive. */ 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) } } } //每輪收集結束后去除掉上輪收集中不需要跟蹤的依賴 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 () { ... //經過一些優化處理后,最終執行this.get this.get(); } // ... }
依賴收集的觸發是在執行render之前,會創建一個渲染Watcher:
updateComponent = () => { vm._update(vm._render(), hydrating) //執行render生成VNode并更新dom } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
在渲染Watcher創建時會將Dep.target指向自身并觸發updateComponent也就是執行_render生成VNode并執行_update將VNode渲染成真實DOM,在render過程中會對模板進行編譯,此時就會對data進行訪問從而觸發getter,由于此時Dep.target已經指向了渲染Watcher,接著渲染Watcher會執行自身的addDep,做一些去重判斷然后執行dep.addSub(this)將自身push到屬性對應的dep.subs中,同一個屬性只會被添加一次,表示數據在當前Watcher中被引用。
當_render結束后,會執行popTarget(),將當前Dep.target回退到上一輪的指,最終又回到了null,也就是所有收集已完畢。之后執行cleanupDeps()將上一輪不需要的依賴清除。當數據變化是,觸發setter,執行對應Watcher的update屬性,去執行get方法又重新將Dep.target指向當前執行的Watcher觸發該Watcher的更新。
這里可以看到有deps,newDeps兩個依賴表,也就是上一輪的依賴和最新的依賴,這兩個依賴表主要是用來做依賴清除的。但在addDep中可以看到if (!this.newDepIds.has(id))已經對收集的依賴進行了唯一性判斷,不收集重復的數據依賴。為何又要在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中主要清除上一輪中的依賴在新一輪中沒有重新收集的,也就是數據刷新后某些數據不再被渲染出來了,例如:
<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>
每次點擊change,msg1都會拼接一個1,此時就會觸發重新渲染。當我們點擊toggle時,由于flag改變,msg1不再被渲染,但當我們點擊change時,msg1發生了變化,但卻沒有觸發重新渲染,這就是cleanupDeps起的作用。如果去除掉cleanupDeps這個步驟,只是能防止添加相同的依賴,但是數據每次更新都會觸發重新渲染,又去重新收集依賴。這個例子中,toggle后,重新收集的依賴中并沒有msg1,因為它不需要被顯示,但是由于設置了setter,此時去改變msg1依然會觸發setter,如果沒有執行cleanupDeps,那么msg1的依賴依然存在依賴表里,又會去觸發重新渲染,這是不合理的,所以需要每次依賴收集完畢后清除掉一些不需要的依賴。
以上是“Vue如何實現雙向綁定”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。