要了解 Vue2 響應式系統原理,我們要思考兩個問題:
- 當我們改變元件的狀態時,系統會發生什麼變化?
- 系統是如何知道哪些部分依賴於這個狀態?
實際上,元件的渲染、計算屬性、元件watch
物件和Vue.&watch()
方法,它們之所以能響應元件props
和data
的變化,都是圍繞著Watcher
類來實現的。
本文只擷取部分核心程式碼,重在講解響應式原理,儘量減少其它程式碼的干擾,但會註釋程式碼來源,結合原始碼觀看風味更佳。另外,本文原始碼版本:
"version": "2.7.14",
定義響應式屬性
首先,看看元件的props
和data
中的屬性是如何定義為響應式的:
// src/core/instance/init.ts
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
initState(vm) // 初始化狀態
}
// src/core/instance/state.ts
export function initState(vm: Component) {
const opts = vm.$options
initProps(vm, opts.props) // 初始化Props
initData(vm) // 初始化Data
initComputed(vm, opts.computed)
initWatch(vm, opts.watch)
}
function initProps(vm: Component, propsOptions: Object) {
const props = (vm._props = shallowReactive({}))
for (const key in propsOptions) {
defineReactive(props, key, value) // 定義響應式屬性
}
}
function initData(vm: Component) {
let data: any = vm.$options.data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
observe(data)
}
// src/core/observer/index.ts
export function observe(value: any, shallow?: boolean, ssrMockReactivity?: boolean) {
return new Observer(value, shallow, ssrMockReactivity)
}
export class Observer {
constructor(public value: any, public shallow = false, public mock = false) {
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// 定義響應式屬性
defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
}
}
}
從上面程式碼可以看出,在元件初始化階段,無論是props
還是data
屬性,最終都透過函式defineReactive
定義為響應式屬性。所以我們要重點關注這個方法:
// src/core/observer/index.ts
export function defineReactive(obj: object, key: string, val?: any, customSetter?: Function | null, shallow?: boolean, mock?: boolean) {
const dep = new Dep() // 建立一個dep例項
const property = Object.getOwnPropertyDescriptor(obj, key)
const getter = property && property.get
const setter = property && property.set
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
dep.depend() // 新增依賴關係Watcher
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
setter.call(obj, newVal)
dep.notify() // 賦值時,釋出通知
}
}
}
Object.defineProperty
重新定義了屬性的get
和set
。當讀取屬性時,會自動觸發get
,當設定屬性值時,會自動觸發set
,記住這一機制。從上面程式碼可以發現,每個屬性都有一個dep
例項,它的作用就是記錄依賴這個屬性watcher
列表,並在屬性賦值時,通知列表中的watcher
更新,這些更新包括:改變計算屬性值、執行元件watch物件中定義的方法、重新渲染等。
收集依賴關係
在進一步瞭解dep.depend()
是之前,先看一下Vue.$watch
如何方法建立watcher
,有利於後面的理解:
Vue.prototype.$watch = function (
expOrFn: string | (() => any), // 重點關注這個引數
cb: any,
options?: Record<string, any>
) {
const vm: Component = this
const watcher = new Watcher(vm, expOrFn, cb, options) // 建立watcher
}
expOrFn
型別是一個字串或函式,如果是字串,會轉化成函式,賦值給watcher.getter
。接下來看dep.depend()
是如何收集依賴的,重點關注Dep
和Watcher
兩個類:
// src/core/observer/dep.ts
export default class Dep {
static target?: DepTarget | null // Watcher正是DepTarget類的實現
subs: Array<DepTarget | null> // 依賴列表
addSub(sub: DepTarget) {
this.subs.push(sub)
}
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this) // 向watcher中新增dep例項
}
}
}
const targetStack: Array<DepTarget | null | undefined> = []
// 入棧watcher,並將target指向這個watcher
export function pushTarget(target?: DepTarget | null) {
targetStack.push(target)
Dep.target = target
}
// 出棧watcher,並將target指向最後的watcher
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
// src/core/observer/watcher.ts
export default class Watcher implements DepTarget {
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
this.cb = cb // 回撥函式
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) // 轉化為函式
}
this.value = this.get() // 獲取值
}
// 獲取值,並收集依賴關係
get() {
pushTarget(this) // 入棧,Dep.target指向當前watcher
let value
const vm = this.vm
value = this.getter.call(vm, vm) // 執行getter期間只要讀取了響應式屬性,會觸發屬性的get,然後呼叫dep.depend(),再呼叫Dep.target(當前watcher)的addDep方法,將watcher新增到dep.subs
popTarget() // 出棧
return value
}
addDep(dep: Dep) {
dep.addSub(this) // 將watcher新增到dep.subs
}
}
執行getter
期間只要讀取了響應式屬性,會觸發改屬性重寫的get
,然後呼叫dep.depend()
,再呼叫Dep.target
(當前watcher
)的addDep
方法,將watcher
新增到dep.subs
。於是,屬性的dep
就知道了哪些watcher
用到了這個屬性,它們都儲存在了dep.subs
列表中。
賦值響應式屬性
接著,看改變props或state後,會發生什麼情況:
- 改變響應式屬性值
- 觸發重寫的
set
,呼叫dep.notify()
dep.notify()
通知dep.subs
所有的watcher.update()
watcher.update()
中將watcher
自己加入更新佇列nextTick
後執行更新,呼叫佇列中所有watcher.run()
watcher.run()
中呼叫watcher.get()
獲得新值,並重新收集依賴- 呼叫回撥函式
watcher.cb
,傳入新舊值
// 1. 改變響應式屬性值 examples/composition/todomvc.html
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="state.allDone"/>
// 2. 觸發重寫的set,呼叫dep.notify() src/core/observer/index.ts
export function defineReactive() {
const dep = new Dep()
Object.defineProperty(obj, key, {
set: function reactiveSetter(newVal) {
dep.notify()
}
}
}
// 3. dep.notify()通知dep.subs所有的watcher.update() src/core/observer/dep.ts
notify(info?: DebuggerEventExtraInfo) {
const subs = this.subs.filter(s => s) as DepTarget[]
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
sub.update()
}
}
// 4. watcher.update()中將watcher自己加入佇列 src/core/observer/watcher.ts
update() {
queueWatcher(this)
}
// 5. nextTick後執行更新,呼叫佇列中所有watcher.run() src/core/observer/seheduler.ts
const queue: Array<Watcher> = []
export function queueWatcher(watcher: Watcher) {
queue.push(watcher)
nextTick(flushSchedulerQueue)
}
function flushSchedulerQueue() {
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
watcher.run()
}
}
// 6. watcher.run()中呼叫watcher.get()獲得新值,並重新收集依賴 src/core/observer/watcher.ts
run() {
const value = this.get()
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue) // 7. 呼叫回撥函式watcher.cb,傳入新舊值
}
渲染函式響應式
渲染函式_render
用於生成虛擬DOM,也就是VNode
。當元件的props
或data
發生變化時,會觸發_render
重新渲染元件:
// src/types/component.ts
class Component {
_render: () => VNode
}
觸發重繪機制也是透過watcher
來實現的,不過這個watcher
會比較特殊,它沒有回撥函式,建立於元件mount
階段:
// src/platforms/web/runtime/index.ts
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// src/core/instance/lifecycle.ts
export function mountComponent(vm: Component, el: Element | null | undefined, hydrating?: boolean) {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, watcherOptions, true /* isRenderWatcher */)
}
updateComponent
作為第二引數,也就成為了watcher.getter
。和普通的watcher
一樣,getter
執行時,也就是updateComponent
執行期間,或者說_update
和_render
執行期間,讀取響應式屬性時,會觸發它們的get
,將渲染watcher
新增到屬性對應的dep.subs
中。當響應式屬性發生變化時,觸發重新渲染,這個流程與之前略有不同:
- 改變響應式屬性值
- 觸發重寫的
set
,呼叫dep.notify()
dep.notify()
通知dep.subs
所有的watcher.update()
watcher.update()
中將watcher
自己加入更新佇列nextTick
後執行更新,呼叫佇列中所有watcher.run()
watcher.run()
中呼叫watcher.get()
獲得新值,並重新收集依賴watcher.get()
中會呼叫wacher.getter.call()
- 等於呼叫
updateComponent
,重新渲染元件(渲染watcher
回撥函式等於noop
,相當於不執行回撥)
以官方例子來看以上流程:
// 1. 改變響應式屬性值 examples/composition/todomvc.html
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="state.allDone"/>
// 2. 觸發重寫的set,呼叫dep.notify() src/core/observer/index.ts
export function defineReactive() {
const dep = new Dep()
Object.defineProperty(obj, key, {
set: function reactiveSetter(newVal) {
dep.notify()
}
}
}
// 3. dep.notify()通知dep.subs所有的watcher.update() src/core/observer/dep.ts
notify(info?: DebuggerEventExtraInfo) {
const subs = this.subs.filter(s => s) as DepTarget[]
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
sub.update()
}
}
// 4. watcher.update()中將watcher自己加入佇列 src/core/observer/watcher.ts
update() {
queueWatcher(this)
}
// 5. nextTick後執行更新,呼叫佇列中所有watcher.run() src/core/observer/seheduler.ts
const queue: Array<Watcher> = []
export function queueWatcher(watcher: Watcher) {
queue.push(watcher)
nextTick(flushSchedulerQueue)
}
function flushSchedulerQueue() {
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
watcher.run()
}
}
// 6. watcher.run()中呼叫watcher.get()獲得新值,並重新收集依賴 src/core/observer/watcher.ts
run() {
const value = this.get()
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
// 7. watcher.get()中會呼叫wacher.getter.call() src/core/observer/watcher.ts
get() {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm) // 等於updateComponent()
popTarget()
return value
}
// 8. 等於呼叫updateComponent,重新渲染元件(渲染watcher回撥函式等於noop,相當於不執行任何回撥)src/core/instance/lifecycle.ts
export function mountComponent(vm: Component, el: Element | null | undefined, hydrating?: boolean) {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, watcherOptions, true /* isRenderWatcher */)
}
計算屬性響應式
計算屬性同樣是透過watcher
實現的。在例項初始化階段initState
時,呼叫initComputed
為每個計算屬性建立一個watcher
,它同樣沒有回撥函式:
// src/core/instance/state.ts
export function initState(vm: Component) {
const opts = vm.$options
if (opts.computed) initComputed(vm, opts.computed)
}
const computedWatcherOptions = { lazy: true }
function initComputed(vm: Component, computed: Object) {
const watchers = (vm._computedWatchers = Object.create(null))
for (const key in computed) {
const userDef = computed[key]
const getter = isFunction(userDef) ? userDef : userDef.get
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
export function defineComputed(target: any, key: string, userDef: Record<string, any> | (() => any)) {
sharedPropertyDefinition.get = createComputedGetter(key) // 重寫屬性的get
sharedPropertyDefinition.set = noop // 不允許更改屬性值
Object.defineProperty(target, key, sharedPropertyDefinition) // 重新定義計算屬性的set和get
}
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
return watcher.value // 返回watcher.value值
}
}
以上程式碼可以看出,defineComputed
重新定義了計算屬性的set
和get
,get
永遠返回對應watcher.value
。計算屬性的值是使用者定義的函式,它也是watcher.getter
,原理同上。函式中的響應式屬性發生變化時:
- 改變響應式屬性值
- 觸發重寫的
set
,呼叫dep.notify()
dep.notify()
通知dep.subs
所有的watcher.update()
watcher.update()
中將watcher
自己加入更新佇列nextTick
後執行更新,呼叫佇列中所有watcher.run()
watcher.run()
中呼叫watcher.get()
獲得新值,並重新收集依賴- 讀取計算屬性時,觸發重寫的
get
方法,返回watcher.value
值
元件的watch物件
它透過Vue.$watch
來實現的,看程式碼即可,原理同上。
// src/core/instance/state.ts
function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
createWatcher(vm, key, handler)
}
}
function createWatcher(
vm: Component,
expOrFn: string | (() => any),
handler: any,
options?: Object
) {
return vm.$watch(expOrFn, handler, options)
}
非同步更新和 Watcher執行順序
nextTick
中的函式是非同步執行的,也就是說隨響應式屬性變化的watcher
會依次加入更新佇列中,直到這部分同步程式碼全部執行完畢,之後才會執行非同步程式碼,按順序呼叫佇列中watch.run
,執行回撥函式和重新渲染元件。
watcher.run
執行是講究順序的,為了滿足執行順序,必須在watcher.run
之前重新按watcher.id
大小排序,因為watcher.id
是自增的,所以後建立的wacher.id
要大於先建立的。排序能滿足以下要求:
- 元件更新必須從父元件到子元件。(父元件永遠先於子元件建立,因此父元件
watcher.id
小於子元件) - 使用者
wachers
必須在渲染watcher
之前執行。(使用者props
、data
和computed
的wacher
建立於元件初始化階段,watcher.id
一定小於mount
階段建立的渲染watcher
)
function flushSchedulerQueue() {
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort(sortCompareFn)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
watcher.run()
}
}
const sortCompareFn = (a: Watcher, b: Watcher): number => {
if (a.post) {
if (!b.post) return 1
} else if (b.post) {
return -1
}
return a.id - b.id
}