vue響應(yīng)式原理

vue響應(yīng)式原理

vue框架 中最核心的就是 vue的響應(yīng)式 ,通過對vuedata數(shù)據(jù)的變更實現(xiàn)頁面效果的重新渲染。但在實際開發(fā)中經(jīng)常會有人發(fā)現(xiàn)明明更改了對應(yīng)數(shù)據(jù)的值,但是vue卻沒有重新渲染。明明說好了是響應(yīng)式的,但為什么有的數(shù)據(jù)可以通過響應(yīng)式實現(xiàn),而有的只能通過vue.set方法實現(xiàn)

官方文檔的流程圖

在此基礎(chǔ)上,我們根據(jù)源碼更細一步劃分出watcher和data之間的部分,即Depobserver。

總體架構(gòu)簡介

在Vue源碼內(nèi),Dep類作為依賴,Watcher類則用來收集依賴和通知依賴重新求值。對于在實例化時傳入的數(shù)據(jù),使用工廠函數(shù)defineReactive令其響應(yīng)式。而在實例后再通過Vue.set/vm.$set添加的響應(yīng)式數(shù)據(jù),則需要借助Observer類來使其成為響應(yīng)式數(shù)據(jù),最后也是通過defineReactive實現(xiàn)響應(yīng)式。

對于每個響應(yīng)式數(shù)據(jù),會有兩個Dep實例,第一個是在defineReactive中的閉包遍歷,用途顯而易見。而第二個Dep則在響應(yīng)式數(shù)組的__ob__屬性值中,這個值是Observer實例,其實例屬性dep是Dep實例,在執(zhí)行Vue.set/vm.$set添加響應(yīng)式數(shù)據(jù)后,會通知依賴更新。

在講defineReactive之前,先講一下這些輔助類的實現(xiàn)和用處。

Dep

我們都知道,Vue響應(yīng)式的實現(xiàn),會在getter中收集響應(yīng)式數(shù)據(jù)的依賴,在setter中通知依賴數(shù)據(jù)更新,重新計算數(shù)據(jù)然后來更新視圖。在Vue內(nèi)部,使用Dep實例表示依賴,讓我們看一下Dep類是怎么定義的。

Dep有兩個實例屬性,一個靜態(tài)屬性。靜態(tài)屬性targetWatcher實例,功能是重新求值和通知視圖更新,下文我們會講到。實例屬性id是Dep實例的唯一標識,無需多說;屬性subs是Watcher實例數(shù)組,用于收集Watcher實例,當依賴更新時,這些Watcher實例就會重新求值。

 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 () {
 // stabilize the subscriber list first
 const subs = this.subs.slice()
 for (let i = 0, l = subs.length; i < l; i++) {
 subs[i].update()
 }
 }
}```

方法`addSub`用于添加`Watcher`實例到`subs`中,方法`removeSub`用于從`subs`移除`Watcher`實例。

方法`depond`會在收集依賴的時候調(diào)用,實際上執(zhí)行了Watcher的實例方法`addDep`,在`addDep`內(nèi)除了調(diào)用dep實例的`addSup`方法外,還做了避免重復(fù)收集Watcher實例的工作。這個方法會在Vue為響應(yīng)式數(shù)據(jù)設(shè)置的自定義getter中執(zhí)行。

`notify`方法則遍歷`subs`,執(zhí)行Watcher實例方法update來重新求值。這個方法會在Vue為響應(yīng)式數(shù)據(jù)設(shè)置的自定義setter中執(zhí)行。

有人可能有疑問,`target`是靜態(tài)屬性,那不是每個實例的target都一樣的?實際上,重新求值的操作在Watcher實例方法`get`內(nèi)實現(xiàn)。在get方法內(nèi),會先調(diào)用`pushTarget`來更新`Dep.target`,使其指向當前Watcher實例,之前的``Dep.target`會被保存`targetStack`末尾(相當于入棧操作),完成操作后會執(zhí)行`popTarget`函數(shù),從`targetStack`取出最后一個元素來還原`Dep.target`(相當于出棧操作)。

```Dep.target = null
const targetStack = []
?
export function pushTarget (_target: ?Watcher) {
 if (Dep.target) targetStack.push(Dep.target)
 Dep.target = _target
}
?
export function popTarget () {
 Dep.target = targetStack.pop()
}```

## Watcher

當依賴更新時,Watcher類會重新求值,并可能觸發(fā)重渲染。

```constructor (
 vm: Component,
 expOrFn: string | Function,
 cb: Function,
 options?: ?Object,
 isRenderWatcher?: boolean
 ) {
 this.vm = vm
 // 與渲染相關(guān)的watcher
 if (isRenderWatcher) {
 vm._watcher = this
 }
 vm._watchers.push(this)
 // options
 if (options) {
 this.deep = !!options.deep
 this.user = !!options.user
 this.computed = !!options.computed
 this.sync = !!options.sync
 this.before = options.before
 } else {
 this.deep = this.user = this.computed = this.sync = false
 }
 this.cb = cb
 this.id = ++uid // uid for batching
 this.active = true
 this.dirty = this.computed // for computed watchers
 this.deps = []
 this.newDeps = []
 this.depIds = new Set()
 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)
 if (!this.getter) {
 this.getter = function () {}
 process.env.NODE_ENV !== 'production' && warn(
 `Failed watching path: "${expOrFn}" ` +
 'Watcher only accepts simple dot-delimited paths. ' +
 'For full control, use a function instead.',
 vm
 )
 }
 }
 if (this.computed) {
 this.value = undefined
 this.dep = new Dep()
 } else {
 this.value = this.get()
 }
 }```

構(gòu)造函數(shù)接受五個參數(shù),`vm`是掛載的Component實例;`expOrFn`是觀察的屬性,當是字符串時表示屬性名,是函數(shù)時會被當成屬性的get方法;`cb`是屬性更新后執(zhí)行的回調(diào)函數(shù);`options`是配置項;`isRenderWatcher`表示當前實例是否與渲染相關(guān)。

在構(gòu)造函數(shù)內(nèi),先將實例屬性`vm`指向傳入的Component實例vm,如果當前Watcher實例與渲染相關(guān),會將其保存在`vm._watcher`中。接著將當前實例添加到`vm._watchers`中,同時根據(jù)傳入的配置項`options`初始化實例屬性。實例屬性`getter`是監(jiān)聽屬性的getter函數(shù),如果`expOrFn`是函數(shù),直接賦值,否則會調(diào)用`parsePath`來獲取屬性的getter。

`parsePath`內(nèi)部會先使用正則來判斷屬性名,如果有除數(shù)字、字母、`.`和`$`以外的字符時視為非法屬性名,直接返回,所以屬性只能是以`.`分隔的屬性。如果屬性名合法,則`parsePath`返回一個閉包函數(shù),調(diào)用時會傳入`vm`,即`obj`是`vm`的引用,這個閉包函數(shù)最終的目的是從vm實例里獲取屬性。

```const bailRE = /[^\w.$]/
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
 }
}```

初始化完成之后,如果不是計算屬性相關(guān)的Watcher實例,會調(diào)用實例方法`get`求值。

### get方法

執(zhí)行g(shù)etter方法求值,完成依賴收集的過程。

方法開始時,執(zhí)行`pushTarget(this)`,將`Dep.target`指向當前Watcher實例。然后執(zhí)行`getter`收集依賴,最后將`Dep.target`復(fù)原,并執(zhí)行`cleanDeps`遍歷`deps`。在每次求值之后,都會調(diào)用`cleanupDeps`方法重置依賴,具體如何重置,稍后再講。

實際上,`Dep.target`指向的實例是即將要收集的目標。

`getter`的執(zhí)行,除了會獲取值外,還會觸發(fā)在`defineReactive`中為屬性設(shè)置的getter,完成依賴的收集。

``` 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

`addDep`的功能是將當前Watcher實例添加到傳入的Dep實例屬性subs數(shù)組里去。

`addDep`接受一個Dep實例作為參數(shù),如果 `dep.id` 沒有在集合 `newDepIds` 之中,則添加。如果不在集合 `depIds` 中,則將當前實例添加到 `dep.subs` 中。 簡單來說,這里的操作會避免重復(fù)收集依賴,這也是不直接調(diào)用`dep.addSub(Dep.target)`的原因。

``` 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)
 }
 }
 }```

從這里可以看出來Dep實例和Watcher實例會相互引用。Dep實例將Watcher實例保存在實例屬性`subs`中,在響應(yīng)式屬性調(diào)用`setter`時,執(zhí)行`notify`方法,通知Watcher實例重新求值。

Watcher實例將Dep實例保存在集合`newDeps`,目的是避免重復(fù)收集依賴,同時會執(zhí)行Dep實例方法`addDep`,將當前Watcher實例添加到Dep實例屬性`subs`中。

### cleanupDeps

對于Watcher來說,每次求值的依賴并不一定與上一次的相同,在每次執(zhí)行`get`之后,都會調(diào)用`cleanupDeps`來重置收集的依賴。Watcher有四個實例屬性用于記錄依賴,分別是`newDeps/newDepIds`與`deps/depIds`。`newDeps`與`deps`是保存依賴的數(shù)組,`newDepIds`與`depIds`是保存依賴Id的集合。記錄上一次求值依賴的屬性是`deps/depIds`,記錄下一次求值依賴的屬性是`newDeps/newDepIds`(執(zhí)行`cleanupDeps`時已經(jīng)調(diào)用過`getter`重新求值了,所以說是上一次求值,下一次指的是下一次調(diào)用`get`的時候)。

``` cleanupDeps () {
 let i = this.deps.length
 while (i--) {
 const dep = this.deps[i]
 if (!this.newDepIds.has(dep.id)) {
 dep.removeSub(this)
 }
 }
 // 交換depIds和newDepIds
 let tmp = this.depIds
 this.depIds = this.newDepIds
 this.newDepIds = tmp
 this.newDepIds.clear()
 // 交換deps和newDeps
 tmp = this.deps
 this.deps = this.newDeps
 this.newDeps = tmp
 this.newDeps.length = 0
 }```

首先遍歷`deps`,如果此次求值的依賴在下一次求值中并不存在,則需要調(diào)用`removeSub`方法,從`subs`數(shù)組中移除當前Watcher實例。

接著交換`newDeps/newDepIds`與`deps/depIds`,并清空交換后的`newDeps/newDepIds`。

### update

Dep類的`notify`方法用于通知觀察者重新求值,該方法內(nèi)部實際是遍歷`subs`數(shù)組,執(zhí)行Watcher的`update`方法。

update 方法定義如下。當實例與計算屬性相關(guān)時,xxx。如果不是計算屬性相關(guān)時,判斷是否需要同步觸發(fā),同步觸發(fā)時調(diào)用`run`,否則執(zhí)行`queueWatcher(this)`,交由調(diào)度模塊統(tǒng)一調(diào)度。

``` 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)
 }
 }```
總的來說,vue的數(shù)據(jù)響應(yīng)式實現(xiàn)主要分成2個部分:

1.  把數(shù)據(jù)轉(zhuǎn)化為getter和setter

2.  建立watcher并收集依賴

第一部分是上圖中`data`、`observer`、`dep`之間聯(lián)系的建立過程,第二部分是`watcher`、`dep`的關(guān)系建立

### 響應(yīng)式原理整合

在生成vue實例時,為對傳入的data進行遍歷,使用`Object.defineProperty`把這些屬性轉(zhuǎn)為`getter/setter`.

`Object.defineProperty` 是 ES5 中一個無法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的原因。

每個vue實例都有一個watcher實例,它會在實例渲染時記錄這些屬性,并在setter觸發(fā)時重新渲染。

`Vue 無法檢測到對象屬性的添加或刪除`

`Vue 不允許動態(tài)添加根級別的響應(yīng)式屬性。但是,可以使用 Vue.set(object, propertyName, value)方法向嵌套對象添加響應(yīng)式屬性。`

### 聲明響應(yīng)式屬性

由于 Vue 不允許動態(tài)添加根級響應(yīng)式屬性,所以你必須在初始化實例前聲明所有根級響應(yīng)式屬性,哪怕只是一個空值。

如果你未在 data 選項中聲明 message,Vue 將警告你渲染函數(shù)正在試圖訪問不存在的屬性。

### 異步更新隊列

vue更新dom時是異步執(zhí)行的

數(shù)據(jù)變化、更新是在主線程中同步執(zhí)行的;在偵聽到數(shù)據(jù)變化時,watcher將數(shù)據(jù)變更存儲到異步隊列中,當本次數(shù)據(jù)變化,即主線成任務(wù)執(zhí)行完畢,異步隊列中的任務(wù)才會被執(zhí)行(已去重)。

如果你在js中更新數(shù)據(jù)后立即去操作DOM,這時候DOM還未更新;vue提供了nextTick接口來處理這樣的情況,它的參數(shù)是一個回調(diào)函數(shù),會在本次DOM更新完成后被調(diào)用。

使用方法:

*   1.在組件內(nèi)使用 vm.$nextTick() 實例方法特別方便,因為它不需要全局 Vue,并且回調(diào)函數(shù)中的 this 將自動綁定到當前的 Vue 實例上:

```Vue.component('example', {
 template: '<span>{{ message }}</span>',
 data: function () {
 return {
 message: '未更新'
 }
 },
 methods: {
 updateMessage: function () {
 this.message = '已更新'
 console.log(this.$el.textContent) // => '未更新'
 this.$nextTick(function () {
 console.log(this.$el.textContent) // => '已更新'
 })
 }
 }
})```

*   2.因為 `$nextTick()` 返回一個 `Promise` 對象,所以你可以使用新的 [ES2016 async/await](https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FJavaScript%2FReference%2FStatements%2Fasync_function) 語法完成相同的事情:

```methods: {
 updateMessage: async function () {
 this.message = '已更新'
 console.log(this.$el.textContent) // => '未更新'
 await this.$nextTick()
 console.log(this.$el.textContent) // => '已更新'
 }
}```
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容