面試官問(wèn)你Vue2的響應(yīng)式原理該如何回答?
前言
可能很多小伙伴之前都了解過(guò) Vue2實(shí)現(xiàn)響應(yīng)式的核心是利用了ES5的Object.defineProperty 但是面對(duì)面試官時(shí)如果只知道一些模糊的概念,回答肯定是虎頭蛇尾的,只有深入底層了解響應(yīng)式的原理,才能在關(guān)鍵時(shí)刻對(duì)答如流,百毒不侵。
響應(yīng)式對(duì)象
Object.defineProperty方法的官方解釋是可以直接在一個(gè)對(duì)象上定義一個(gè)新屬性或者修改一個(gè)對(duì)象的現(xiàn)有屬性
let object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
});
console.log(object1);
//{property1: 42}
經(jīng)過(guò)Object.defineProperty定義后,object1就有了一個(gè)property1屬性
并且通過(guò)這種方式能為屬性添加get與set方法,
當(dāng)一個(gè)對(duì)象的屬性都擁有g(shù)et和set方法時(shí),就可以稱這個(gè)對(duì)象為響應(yīng)式對(duì)象
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方法時(shí)
//{
// name:1
// get name:function()...
// set name:function()...
//}
console.log(object1.name)
//1
object1.name = 1
//數(shù)據(jù)變化了 2
當(dāng)我們讀取object1的name值會(huì)觸發(fā)get方法 這里會(huì)打印出1,
當(dāng)我們修改object1的name值會(huì)觸發(fā)set方法 這里會(huì)打印出 ”數(shù)據(jù)變化了 2“
響應(yīng)式開始的地方
vue源碼中在初始化data的方法initData有一句 observe(data)這就是夢(mèng)開始的地方,讓我們看一下observe具體實(shí)現(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
}首先對(duì)傳入的值做了一些類型校驗(yàn),如果不是引用類型或者是vNode實(shí)例就直接return返回
接下來(lái)判斷對(duì)象下是否有_ob_屬性,如果有直接返回否則執(zhí)行new Observer(value)那么這個(gè)_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的邏輯也非常簡(jiǎn)單,首先為傳入的這個(gè)引用類型new了一個(gè)dep,這個(gè)dep主要是為了后續(xù)的$set方法使用暫且不看并將當(dāng)前的observer實(shí)例儲(chǔ)存在目標(biāo)的_ob_里,上面說(shuō)的observe方法會(huì)根據(jù)這個(gè)_ob_進(jìn)行判斷,這樣是為了防止data里的屬性相互引用導(dǎo)致多次生成新實(shí)例接下來(lái)判斷如果是對(duì)象類型則對(duì)每個(gè)屬性執(zhí)行defineReactive方法
如果是數(shù)組類型則遍歷數(shù)組對(duì)每個(gè)子項(xiàng)執(zhí)行observe方法,observe方法上面我們說(shuō)過(guò),它會(huì)根據(jù)值的類型進(jìn)行判斷如果是數(shù)組或者對(duì)象就執(zhí)行new Observer這一層的套娃實(shí)際上是對(duì)數(shù)組的層層解析,目的就是為了讓數(shù)組里的對(duì)象都執(zhí)行defineReactive方法
實(shí)現(xiàn)響應(yīng)式的defineReactive
vue2的源碼中是通過(guò)遞歸調(diào)用defineReactive方法將所有對(duì)象變?yōu)轫憫?yīng)式對(duì)象接下來(lái)我們簡(jiǎn)單看一下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()
}
})
}首先為每一個(gè)對(duì)象屬性都添加的get與set方法
并且為每個(gè)屬性都new一個(gè)dep,這個(gè)dep接下來(lái)會(huì)介紹,值得一提的是這句let childOb = !shallow && observe(val)observe我們上面說(shuō)了它會(huì)對(duì)所有對(duì)象以及數(shù)組里嵌套的對(duì)象執(zhí)行defineReactive這段邏輯就是在遞歸調(diào)用defineReactive方法,這樣不管我們對(duì)象套了多少層,它都能實(shí)現(xiàn)響應(yīng)vue的響應(yīng)式實(shí)際上的經(jīng)典的觀察者模式,dep在get方法里實(shí)現(xiàn)對(duì)觀察者watcher進(jìn)行收集,在set方法里通知每個(gè)觀察者watcher執(zhí)行 update 方法,想要了解過(guò)程,接下來(lái)我們重點(diǎn)看一下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)一個(gè)中間橋梁的作用,收集以及維護(hù)觀察者,在目標(biāo)屬性發(fā)生變化時(shí)調(diào)用自己的notify方法,對(duì)每個(gè)觀察者都執(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中的邏輯不適合單獨(dú)拆開解析,接下來(lái)我們結(jié)合流程分析,watcher首次創(chuàng)建實(shí)例的場(chǎng)景,這是在第一次渲染頁(yè)面組件的時(shí)候,我們傳入的expOrFn參數(shù)對(duì)應(yīng)的是updateComponent,updateComponent是vue定義的用于重新渲染頁(yè)面組件的函數(shù),在代碼中updateComponent又被賦值給了this.getter
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true )
接著往下看,由于當(dāng)前不是計(jì)算屬性所以this.computed是false,執(zhí)行this.value = this.get()在執(zhí)行g(shù)et()的時(shí)候會(huì)執(zhí)行到pushTarget方法
Dep.target = null
function pushTarget (_target: ?Watcher) {
...
Dep.target = _target
}這個(gè)方法實(shí)際上是將當(dāng)前的watcher賦值給了Dep.target,Dep.target是一個(gè)全局變量,為啥要這么干呢
因?yàn)闀?huì)有組件嵌套的情況,所以可能會(huì)有多個(gè)渲染watcher,但是通過(guò)這種方式就這樣保證了 Dep.target指向的是最新創(chuàng)建的watcher,接下來(lái)執(zhí)行了value = this.getter.call(vm, vm),上面說(shuō)了這個(gè)this.getter就是傳入的updateComponent,這個(gè)updateComponent就是頁(yè)面組件重新渲染的方法,
流程分別是:生成vnode->根據(jù)vnode樹生成真實(shí)的dome樹->掛載到頁(yè)面上,在使用rander生成vnode的時(shí)候就會(huì)讀取到模版語(yǔ)法中的值,當(dāng)訪問(wèn)到值時(shí)就觸發(fā)了我們通過(guò)defineReactive方法添加的get方法,就觸發(fā)了依賴收集過(guò)程。
依賴收集
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
},
...
})
}剛剛我們說(shuō)到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實(shí)例里的dep的subs數(shù)組中后續(xù)當(dāng)我們使用$set方法時(shí),只需要調(diào)用引用對(duì)象保存在_ob_的Observer實(shí)例中的dep的notify方法就能實(shí)現(xiàn)手動(dòng)派發(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實(shí)例,這樣這個(gè)watcher就被收集到該屬性的dep對(duì)象的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方法時(shí)就會(huì)進(jìn)行派發(fā)更新,在set方法里我們首先對(duì)修改后的值和原來(lái)的值進(jìn)行對(duì)比,如果相同就return,如果不相同就繼續(xù)執(zhí)行下面的邏輯,將新值賦值給舊值,childOb = !shallow && observe(newVal)這一段的邏輯是如果新值也是對(duì)象的話就對(duì)新值執(zhí)行defineReactive將新值變?yōu)?strong>響應(yīng)式對(duì)象,接下來(lái)我們看一下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方法會(huì)遍歷當(dāng)前收集的watcher,調(diào)用每個(gè)watcher的update方法
update () {
/* istanbul ignore else */
if (this.computed) {
...
} else if (this.sync) {
...
} else {
queueWatcher(this)
}
}update方法分別判斷了是否是計(jì)算屬性以及是否是同步的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)
}
}
}因?yàn)槎鄠€(gè)dep有可能收集的是同一個(gè)watcher,當(dāng)多個(gè)dep收集的同一個(gè)watcher時(shí)這個(gè)watcher只需要更新一次,所以通過(guò)這種方式去除重復(fù)的watcher,由于flushing與waiting初始值都為false,所以會(huì)將經(jīng)過(guò)過(guò)濾的watcherpush進(jìn)queue數(shù)組,然后會(huì)在下一個(gè)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()
...
}首先會(huì)根據(jù)id進(jìn)行一次排序因?yàn)槿绻?strong>嵌套組件的情況出現(xiàn)的話,是先更新父組件再更新子組件,所以進(jìn)行了一次由小到大的排序,后續(xù)對(duì)每個(gè)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的時(shí)候就已經(jīng)說(shuō)過(guò),這個(gè)get會(huì)執(zhí)行this.getter而這個(gè)this.getter是創(chuàng)建watcher時(shí)傳入的updateComponent,updateComponent的執(zhí)行會(huì)導(dǎo)致重新渲染頁(yè)面組件,也就是在屬性被修改時(shí)觸發(fā)了set方法,而這個(gè)set方法會(huì)將依賴當(dāng)前屬性的頁(yè)面組件重新渲染,從而達(dá)到數(shù)據(jù)驅(qū)動(dòng)的效果。
總結(jié)
vue用Object.defineProperty重寫屬性的get和set方法
- 在get中new了一個(gè)dep收集當(dāng)前的渲染watcher
- 在set方法中遍歷收集的渲染watcher執(zhí)行update方法
- vue在new渲染watcher的時(shí)候會(huì)將組件掛載更新的方法(updateComponent)傳入,存儲(chǔ)在渲染watcher中,觸發(fā)渲染watcher的update方法時(shí)實(shí)際上是觸發(fā)這個(gè)組件掛載更新方法 也就是在屬性被修改時(shí)觸發(fā)了set方法,而這個(gè)set方法會(huì)將依賴當(dāng)前屬性的頁(yè)面重新渲染,從而達(dá)到數(shù)據(jù)驅(qū)動(dòng)的效果。
以上就是面試官問(wèn)你Vue2的響應(yīng)式原理該如何回答?的詳細(xì)內(nèi)容,更多關(guān)于Vue2響應(yīng)式原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue?navbar?tabbar導(dǎo)航條根據(jù)位置移動(dòng)實(shí)現(xiàn)定位、顏色過(guò)渡動(dòng)畫效果的代碼
這篇文章主要介紹了vue?navbar?tabbar導(dǎo)航條根據(jù)位置移動(dòng)實(shí)現(xiàn)定位、顏色過(guò)渡動(dòng)畫效果,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08
vue3實(shí)現(xiàn)alert自定義的plugins方式
這篇文章主要介紹了vue3實(shí)現(xiàn)alert自定義的plugins方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
vue實(shí)現(xiàn)樹形結(jié)構(gòu)樣式和功能的實(shí)例代碼
這篇文章主要介紹了vue樹形結(jié)構(gòu)樣式和功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10
Vue動(dòng)態(tài)創(chuàng)建注冊(cè)component的實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于Vue動(dòng)態(tài)創(chuàng)建注冊(cè)component的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Vue具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06

