面試官問你Vue2的響應(yīng)式原理該如何回答?
前言
可能很多小伙伴之前都了解過 Vue2實現(xiàn)響應(yīng)式的核心是利用了ES5的Object.defineProperty
但是面對面試官時如果只知道一些模糊的概念,回答肯定是虎頭蛇尾的,只有深入底層了解響應(yīng)式的原理,才能在關(guān)鍵時刻對答如流,百毒不侵。
響應(yīng)式對象
Object.defineProperty
方法的官方解釋是可以直接在一個對象上定義一個新屬性或者修改一個對象的現(xiàn)有屬性
let object1 = {}; Object.defineProperty(object1, 'property1', { value: 42, }); console.log(object1); //{property1: 42}
經(jīng)過Object.defineProperty定義后,object1就有了一個property1屬性
并且通過這種方式能為屬性添加get
與set
方法,
當(dāng)一個對象的屬性都擁有g(shù)et和set方法時,就可以稱這個對象為響應(yīng)式對象
let object1 ={} Object.defineProperty(object1, "name", { get() { return 1; }, set(x) { console.log("數(shù)據(jù)變化了",1+x) } }); console.log(object1) 當(dāng)我們?yōu)閛bject1添加name屬性以及get和set方法時 //{ // name:1 // get name:function()... // set name:function()... //} console.log(object1.name) //1 object1.name = 1 //數(shù)據(jù)變化了 2
當(dāng)我們讀取object1的name值會觸發(fā)get
方法 這里會打印出1,
當(dāng)我們修改object1的name值會觸發(fā)set
方法 這里會打印出 ”數(shù)據(jù)變化了 2“
響應(yīng)式開始的地方
vue源碼中在初始化data的方法initData
有一句 observe(data)
這就是夢開始的地方,讓我們看一下observe具體實現(xiàn)
function observe(value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else{ ob = new Observer(value) } return ob }
首先對傳入的值做了一些類型校驗,如果不是引用類型或者是vNode實例就直接return返回
接下來判斷對象下是否有_ob_
屬性,如果有直接返回否則執(zhí)行new Observer(value)
那么這個_ob_
以及 Observer
是什么東西呢?我們接著往下看
Observer
class Observer { value: any; dep: Dep; vmCount: number; constructor(value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { this.observeArray(value) } else { this.walk(value) } } walk(obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray(items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
Observer
的邏輯也非常簡單,首先為傳入的這個引用類型new了一個dep
,這個dep主要是為了后續(xù)的$set方法使用暫且不看并將當(dāng)前的observer實例儲存在目標(biāo)的_ob_
里,上面說的observe
方法會根據(jù)這個_ob_
進行判斷,這樣是為了防止data里的屬性相互引用導(dǎo)致多次生成新實例接下來判斷如果是對象類型則對每個屬性執(zhí)行defineReactive
方法
如果是數(shù)組類型則遍歷數(shù)組對每個子項執(zhí)行observe
方法,observe
方法上面我們說過,它會根據(jù)值的類型進行判斷如果是數(shù)組或者對象就執(zhí)行new Observer
這一層的套娃實際上是對數(shù)組的層層解析,目的就是為了讓數(shù)組里的對象都執(zhí)行defineReactive方法
實現(xiàn)響應(yīng)式的defineReactive
vue2的源碼中是通過遞歸調(diào)用defineReactive
方法將所有對象變?yōu)轫憫?yīng)式對象接下來我們簡單看一下defineReactive
的主要邏輯
function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() ... let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
首先為每一個對象屬性都添加的get
與set
方法
并且為每個屬性都new
一個dep,這個dep接下來會介紹,值得一提的是這句let childOb = !shallow && observe(val)
observe我們上面說了它會對所有對象以及數(shù)組里嵌套的對象執(zhí)行defineReactive
這段邏輯就是在遞歸調(diào)用defineReactive
方法,這樣不管我們對象套了多少層,它都能實現(xiàn)響應(yīng)vue的響應(yīng)式實際上的經(jīng)典的觀察者模式,dep
在get方法里實現(xiàn)對觀察者watcher
進行收集,在set方法里通知每個觀察者watcher執(zhí)行 update
方法,想要了解過程,接下來我們重點看一下dep
與watcher
的定義
dep
dep.js export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; 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 () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
dep
充當(dāng)一個中間橋梁的作用,收集以及維護觀察者,在目標(biāo)屬性發(fā)生變化時調(diào)用自己的notify
方法,對每個觀察者都執(zhí)行update
方法通知觀察者需要更新
watcher
watcher.js export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; ... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (typeof expOrFn === 'function') { this.getter = expOrFn } if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } } } 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 (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) } } } 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) } } }
watcher
中的邏輯不適合單獨拆開解析,接下來我們結(jié)合流程分析,watcher
首次創(chuàng)建實例的場景,這是在第一次渲染頁面組件的時候,我們傳入的expOrFn
參數(shù)對應(yīng)的是updateComponent
,updateComponent
是vue定義的用于重新渲染頁面組件的函數(shù),在代碼中updateComponent
又被賦值給了this.getter
new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true )
接著往下看,由于當(dāng)前不是計算屬性所以this.computed
是false,執(zhí)行this.value = this.get()
在執(zhí)行g(shù)et()的時候會執(zhí)行到pushTarget
方法
Dep.target = null function pushTarget (_target: ?Watcher) { ... Dep.target = _target }
這個方法實際上是將當(dāng)前的watcher
賦值給了Dep.target
,Dep.target
是一個全局變量,為啥要這么干呢
因為會有組件嵌套的情況,所以可能會有多個渲染watcher
,但是通過這種方式就這樣保證了 Dep.target
指向的是最新創(chuàng)建的watcher
,接下來執(zhí)行了value = this.getter.call(vm, vm)
,上面說了這個this.getter就是傳入的updateComponent
,這個updateComponent
就是頁面組件重新渲染的方法,
流程分別是:生成vnode->根據(jù)vnode樹生成真實的dome樹->掛載到頁面上,在使用rander
生成vnode的時候就會讀取到模版語法中的值,當(dāng)訪問到值時就觸發(fā)了我們通過defineReactive
方法添加的get
方法,就觸發(fā)了依賴收集過程。
依賴收集
function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() ... let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } } return value }, ... }) }
剛剛我們說到Dep.target
就是當(dāng)前的watcher,在上面????看dep的定義中,dep.depend就是調(diào)用當(dāng)前watcher
的addDep
方法,將當(dāng)前的watcher觀察者收集到dep的subs
數(shù)組,這句childOb.dep.depend()
是為$set
量身定做的,事先將當(dāng)前的watcher收集到Observer
實例里的dep的subs數(shù)組中后續(xù)當(dāng)我們使用$set方法時,只需要調(diào)用引用對象保存在_ob_的Observer實例中的dep的notify
方法就能實現(xiàn)手動派發(fā)更新
watcher.js 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) } } }
this
在這里是指向當(dāng)前創(chuàng)建的watcher
實例,這樣這個watcher就被收集到該屬性的dep對象的subs數(shù)組中了,到這里一依賴收集的主干流程就完成了
派發(fā)更新
function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() ... let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, ... set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
觸發(fā)set
方法時就會進行派發(fā)更新,在set方法里我們首先對修改后的值和原來的值進行對比,如果相同就return
,如果不相同就繼續(xù)執(zhí)行下面的邏輯,將新值賦值給舊值,childOb = !shallow && observe(newVal)
這一段的邏輯是如果新值也是對象的話就對新值執(zhí)行defineReactive
將新值變?yōu)?strong>響應(yīng)式對象,接下來我們看一下dep.notify()
的定義
notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }
notify
方法會遍歷當(dāng)前收集的watcher
,調(diào)用每個watcher的update
方法
update () { /* istanbul ignore else */ if (this.computed) { ... } else if (this.sync) { ... } else { queueWatcher(this) } }
update方法分別判斷了是否是計算屬性以及是否是同步的watcher
,在當(dāng)前執(zhí)行的環(huán)境下都為false
,所以執(zhí)行了queueWatcher(this)
const queue: Array<Watcher> = [] const activatedChildren: Array<Component> = [] let has: { [key: number]: ?true } = {} let circular: { [key: number]: number } = {} let waiting = false let flushing = false function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } }
因為多個dep
有可能收集的是同一個watcher
,當(dāng)多個dep收集的同一個watcher時這個watcher
只需要更新一次,所以通過這種方式去除重復(fù)的watcher
,由于flushing
與waiting
初始值都為false
,所以會將經(jīng)過過濾的watcher
push進queue數(shù)組,然后會在下一個tick內(nèi)執(zhí)行flushSchedulerQueue
function flushSchedulerQueue () { flushing = true let watcher, id queue.sort((a, b) => a.id - b.id) for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() ... }
首先會根據(jù)id進行一次排序因為如果有嵌套組件的情況出現(xiàn)的話,是先更新父組件再更新子組件,所以進行了一次由小到大的排序,后續(xù)對每個watcher
都調(diào)用了run
方法
run () { if (this.active) { this.getAndInvoke(this.cb) } }
active的默認(rèn)值是true所以run方法里調(diào)用了getAndInvoke
getAndInvoke (cb: Function) { const value = this.get() .... }
繞了這么大一圈最后還是執(zhí)行的watcher中的get方法,get方法在上面講解watcher的時候就已經(jīng)說過,這個get會執(zhí)行this.getter
而這個this.getter是創(chuàng)建watcher
時傳入的updateComponent
,updateComponent的執(zhí)行會導(dǎo)致重新渲染頁面組件,也就是在屬性被修改時觸發(fā)了set
方法,而這個set方法會將依賴當(dāng)前屬性的頁面組件重新渲染,從而達到數(shù)據(jù)驅(qū)動的效果。
總結(jié)
vue用Object.defineProperty重寫屬性的get和set方法
- 在get中new了一個dep收集當(dāng)前的渲染watcher
- 在set方法中遍歷收集的渲染watcher執(zhí)行update方法
- vue在new渲染watcher的時候會將組件掛載更新的方法(updateComponent)傳入,存儲在渲染watcher中,觸發(fā)渲染watcher的update方法時實際上是觸發(fā)這個組件掛載更新方法 也就是在屬性被修改時觸發(fā)了set方法,而這個set方法會將依賴當(dāng)前屬性的頁面重新渲染,從而達到數(shù)據(jù)驅(qū)動的效果。
以上就是面試官問你Vue2的響應(yīng)式原理該如何回答?的詳細內(nèi)容,更多關(guān)于Vue2響應(yīng)式原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue父子組件數(shù)據(jù)雙向綁定(父傳子、子傳父)及ref、$refs、is、:is的使用與區(qū)別
這篇文章主要介紹了Vue父子組件數(shù)據(jù)雙向綁定(父傳子、子傳父)及ref、$refs、is、:is的使用與區(qū)別,需要的朋友可以參考下2022-12-12Vue3實現(xiàn)canvas畫布組件自定義畫板實例代碼
Vue?Canvas是一個基于Vue.js的輕量級畫板組件,旨在提供一個簡易的畫布功能,用戶可以在網(wǎng)頁上進行自由繪圖,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-09-09elementui源碼學(xué)習(xí)仿寫一個el-tooltip示例
本篇文章記錄仿寫一個el-tooltip組件細節(jié),從而有助于大家更好理解餓了么ui對應(yīng)組件具體工作細節(jié),本文是elementui源碼學(xué)習(xí)仿寫系列的又一篇文章,后續(xù)空閑了會不斷更新并仿寫其他組件2023-07-07echarts鼠標(biāo)覆蓋高亮顯示節(jié)點及關(guān)系名稱詳解
下面小編就為大家分享一篇echarts鼠標(biāo)覆蓋高亮顯示節(jié)點及關(guān)系名稱詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03Vue Elenent實現(xiàn)表格相同數(shù)據(jù)列合并
這篇文章主要為大家詳細介紹了Vue Elenent實現(xiàn)表格相同數(shù)據(jù)列合并,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-11-11