一、深入響應式原理
-
之前都是
Vue
怎麼實現數據渲染和組件化的,主要是初始化的過程,把原始的數據最終映射到DOM
中,但並沒有涉及到數據變化到DOM
變化的部分。而Vue
的數據驅動除了數據渲染DOM
之外,還有一個很重要的體現就是數據的變更會觸發DOM
的變化。 -
其實前端開發最重要的兩個工作,一個是把數據渲染到頁面,另一個是處理用戶交互。
Vue
把數據渲染到頁面的能力我們已經通過源碼分析出其中的原理了,但是由於一些用戶交互或者是其它方面導致數據發生變化重新對頁面渲染的原理我們還未分析,考慮如下示例:
<div id="app" @click="changeMsg">
{{ message }}
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
methods: {
changeMsg() {
this.message = 'Hello World!'
}
}
})
當我們去修改
this.message
的時候,模板對應的插值也會渲染成新的數據,那麼這一切是怎麼做到的呢?
- 在分析前,我們先直觀的想一下,如果不用
Vue
的話,我們會通過最簡單的方法實現這個需求:監聽點擊事件,修改數據,手動操作DOM
重新渲染。這個過程和使用Vue
的最大區別就是多了一步“手動操作DOM
重新渲染”。這一步看上去並不多,但它背後又潛在的幾個要處理的問題:
- 我需要修改哪塊的
DOM
? - 我的修改效率和性能是不是最優的?
- 我需要對數據每一次的修改都去操作
DOM
嗎? - 我需要
case by case
去寫修改DOM
的邏輯嗎?
- 如果我們使用了
Vue
,那麼上面幾個問題Vue
內部就幫你做了,那麼Vue
是如何在我們對數據修改後自動做這些事情呢,接下來就是Vue
響應式系統。
二、響應式對象
-
Vue.js
實現響應式的核心是利用了ES5
的Object.defineProperty
,這也是爲什麼Vue.js
不能兼容IE8
及以下瀏覽器的原因,我們先來對它有個直觀的認識。 -
Object.defineProperty
,Object.defineProperty
方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回這個對象,先來看一下它的語法:
Object.defineProperty(obj, prop, descriptor)
obj
是要在其上定義屬性的對象;prop
是要定義或修改的屬性的名稱;descriptor
是將被定義或修改的屬性描述符。
-
這裏比較核心的是
descriptor
,它有很多可選鍵值,具體的可以去參閱它的文檔。這裏我們最關心的是get
和set
,get
是一個給屬性提供的getter
方法,當我們訪問了該屬性的時候會觸發getter
方法;set
是一個給屬性提供的setter
方法,當我們對該屬性做修改的時候會觸發setter
方法。一旦對象擁有了getter
和setter
,我們可以簡單地把這個對象稱爲響應式對象。那麼Vue.js
把哪些對象變成了響應式對象了呢,接下來我們從源碼層面分析。 -
initState
,在Vue
的初始化階段,_init
方法執行的時候,會執行initState(vm)
方法,它的定義在src/core/instance/state.js
中,如下所示:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initState
方法主要是對props
、methods
、data
、computed
和wathcer
等屬性做了初始化操作。這裏我們重點分析props
和data
,對於其它屬性的初始化我們之後再詳細分析。
initProps
,如下所示:
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (vm.$parent && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
props
的初始化主要過程,就是遍歷定義的props
配置。遍歷的過程主要做兩件事情:一個是調用defineReactive
方法把每個prop
對應的值變成響應式,可以通過vm._props.xxx
訪問到定義props
中對應的屬性。對於defineReactive
方法,我們稍後會介紹;另一個是通過proxy
把vm._props.xxx
的訪問代理到vm.xxx
上,我們稍後也會介紹。
initData
,如下所示:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
data
的初始化主要過程也是做兩件事,一個是對定義data
函數返回對象的遍歷,通過proxy
把每一個值vm._data.xxx
都代理到vm.xxx
上;另一個是調用observe
方法觀測整個data
的變化,把data
也變成響應式,可以通過vm._data.xxx
訪問到定義data
返回函數中對應的屬性,observe
後面會介紹。可以看到,無論是props
或是data
的初始化都是把它們變成響應式對象,這個過程我們接觸到幾個函數,接下來我們來詳細分析它們。
proxy
,首先介紹一下代理,代理的作用是把props
和data
上的屬性代理到vm
實例上,這也就是爲什麼比如我們定義瞭如下props
,卻可以通過vm
實例訪問到它,如下所示:
let comP = {
props: {
msg: 'hello'
},
methods: {
say() {
console.log(this.msg)
}
}
}
我們可以在
say
函數中通過this.msg
訪問到我們定義在props
中的msg
,這個過程發生在proxy
階段:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
-
proxy
方法的實現很簡單,通過Object.defineProperty
把target[sourceKey][key]
的讀寫變成了對target[key]
的讀寫。所以對於props
而言,對vm._props.xxx
的讀寫變成了vm.xxx
的讀寫,而對於vm._props.xxx
我們可以訪問到定義在props
中的屬性,所以我們就可以通過vm.xxx
訪問到定義在props
中的xxx
屬性了。同理,對於data
而言,對vm._data.xxxx
的讀寫變成了對vm.xxxx
的讀寫,而對於vm._data.xxxx
我們可以訪問到定義在data
函數返回對象中的屬性,所以我們就可以通過vm.xxxx
訪問到定義在data
函數返回對象中的xxxx
屬性了。 -
observe
,observe
的功能就是用來監測數據的變化,它的定義在src/core/observer/index.js
中:
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
export 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 if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
observe
方法的作用就是給非 VNode 的對象類型數據添加一個Observer
,如果已經添加過則直接返回,否則在滿足一定條件下去實例化一個Observer
對象實例。接下來我們來看一下Observer
的作用。
Observer
,Observer
是一個類,它的作用是給對象的屬性添加getter
和setter
,用於依賴收集和派發更新:
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Observer
的構造函數邏輯很簡單,首先實例化Dep
對象,這塊稍後會介紹,接着通過執行def
函數把自身實例添加到數據對象value
的__ob__
屬性上,def
的定義在src/core/util/lang.js
中:
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
def
函數是一個非常簡單的Object.defineProperty
的封裝,這就是爲什麼我在開發中輸出data
上對象類型的數據,會發現該對象多了一個__ob__
的屬性。
-
回到
Observer
的構造函數,接下來會對value
做判斷,對於數組會調用observeArray
方法,否則對純對象調用walk
方法。可以看到observeArray
是遍歷數組再次調用observe
方法,而walk
方法是遍歷對象的 key 調用defineReactive
方法,那麼我們來看一下這個方法是做什麼的。 -
defineReactive
,defineReactive
的功能就是定義一個響應式對象,給對象動態添加getter
和setter
,它的定義在src/core/observer/index.js
中:
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
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()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
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()
}
})
}
-
defineReactive
函數最開始初始化Dep
對象的實例,接着拿到obj
的屬性描述符,然後對子對象遞歸調用observe
方法,這樣就保證了無論obj
的結構多複雜,它的所有子屬性也能變成響應式的對象,這樣我們訪問或修改obj
中一個嵌套較深的屬性,也能觸發getter
和setter
。最後利用Object.defineProperty
去給obj
的屬性key
添加getter
和setter
。 -
總結:這裏介紹了響應式對象,核心就是利用
Object.defineProperty
給數據添加了getter
和setter
,目的就是爲了在我們訪問數據以及寫數據的時候能自動執行一些邏輯:getter
做的事情是依賴收集,setter
做的事情是派發更新。
三、依賴收集
- 瞭解
Vue
會把普通對象變成響應式對象,響應式對象getter
相關的邏輯就是做依賴收集,詳細分析這個過程,我們先來回顧一下getter
部分的邏輯:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
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()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
// ...
})
}
這段代碼我們只需要關注 2 個地方,一個是
const dep = new Dep()
實例化一個Dep
的實例,另一個是在get
函數中通過dep.depend
做依賴收集,這裏還有個對childOb
判斷的邏輯,我們之後會介紹它的作用。
Dep
,Dep
是整個getter
依賴收集的核心,它的定義在src/core/observer/dep.js
中:
import type Watcher from './watcher'
import { remove } from '../util/index'
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
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 () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
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()
}
-
Dep
是一個Class
,它定義了一些屬性和方法,這裏需要特別注意的是它有一個靜態屬性target
,這是一個全局唯一Watcher
,這是一個非常巧妙的設計,因爲在同一時間只能有一個全局的Watcher
被計算,另外它的自身屬性subs
也是Watcher
的數組。Dep
實際上就是對Watcher
的一種管理,Dep
脫離Watcher
單獨存在是沒有意義的,依賴收集過程,我們有必要看一下Watcher
的一些相關實現,它的定義在src/core/observer/watcher.js
中。 -
Watcher
,如下所示:
let uid = 0
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
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()
}
}
/**
* 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)
}
}
}
/**
* Clean up for dependency collection.
*/
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
}
// ...
}
Watcher
是一個Class
,在它的構造函數中,定義了一些和Dep
相關的屬性:
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
-
其中,
this.deps
和this.newDeps
表示Watcher
實例持有的Dep
實例的數組;而this.depIds
和this.newDepIds
分別代表this.deps
和this.newDeps
的id
Set(這個 Set 是 ES6 的數據結構,它的實現在src/core/util/env.js
中)。Watcher
還定義了一些原型的方法,和依賴收集相關的有get
、addDep
和cleanupDeps
方法。 -
過程分析,當對數據對象的訪問會觸發他們的
getter
方法,那麼這些對象什麼時候被訪問呢?在Vue
的mount
過程是通過mountComponent
函數,其中有一段比較重要的邏輯,大致如下:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
- 當我們去實例化一個渲染
watcher
的時候,首先進入watcher
的構造函數邏輯,然後會執行它的this.get()
方法,進入get
函數,首先會執行:
pushTarget(this)
pushTarget
的定義在src/core/observer/dep.js
中:
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
實際上就是把
Dep.target
賦值爲當前的渲染watcher
並壓棧(爲了恢復用)。接着又執行了:
value = this.getter.call(vm, vm)
this.getter
對應就是updateComponent
函數,這實際上就是在執行:
vm._update(vm._render(), hydrating)
- 它會先執行
vm._render()
方法,因爲之前分析過這個方法會生成 渲染VNode
,並且在這個過程中會對vm
上的數據訪問,這個時候就觸發了數據對象的getter
。那麼每個對象值的getter
都持有一個dep
,在觸發getter
的時候會調用dep.depend()
方法,也就會執行Dep.target.addDep(this)
。剛纔我們提到這個時候Dep.target
已經被賦值爲渲染watcher
,那麼就執行到addDep
方法:
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.addSub(this)
,那麼就會執行this.subs.push(sub)
,也就是說把當前的watcher
訂閱到這個數據持有的dep
的subs
中,這個目的是爲後續數據變化時候能通知到哪些subs
做準備。所以在vm._render()
過程中,會觸發所有數據的getter
,這樣實際上已經完成了一個依賴收集的過程。那麼到這裏就結束了麼,其實並沒有,在完成依賴收集後,還有幾個邏輯要執行,首先是:
if (this.deep) {
traverse(value)
}
這個是要遞歸去訪問
value
,觸發它所有子項的getter
,這個之後會詳細講。接下來執行:
popTarget()
popTarget
的定義在src/core/observer/dep.js
中:
Dep.target = targetStack.pop()
實際上就是把
Dep.target
恢復成上一個狀態,因爲當前 vm 的數據依賴收集已經完成,那麼對應的渲染Dep.target
也需要改變。最後執行:
this.cleanupDeps()
- 瞭解到
Vue
有依賴收集的過程,但分析依賴清空的過程,其實這是會忽視的一點,也是Vue
考慮特別細的一點,如下所示:
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
}
-
考慮到
Vue
是數據驅動的,所以每次數據變化都會重新render
,那麼vm._render()
方法又會再次執行,並再次觸發數據的getters
,所以Wathcer
在構造函數中會初始化兩個Dep
實例數組,newDeps
表示新添加的Dep
實例數組,而deps
表示上一次添加的Dep
實例數組。在執行cleanupDeps
函數的時候,會首先遍歷deps
,移除對dep.subs
數組中Wathcer
的訂閱,然後把newDepIds
和depIds
交換,newDeps
和deps
交換,並把newDepIds
和newDeps
清空。 -
那麼爲什麼需要做
deps
訂閱的移除呢,在添加deps
的訂閱過程,已經能通過id
去重避免重複訂閱了。考慮到一種場景,我們的模板會根據v-if
去渲染不同子模板a
和b
,當我們滿足某種條件的時候渲染a
的時候,會訪問到a
中的數據,這時候我們對a
使用的數據添加了getter
,做了依賴收集,那麼當我們去修改a
的數據的時候,理應通知到這些訂閱者。那麼如果我們一旦改變了條件渲染了b
模板,又會對b
使用的數據添加了getter
,如果我們沒有依賴移除的過程,那麼這時候我去修改a
模板的數據,會通知a
數據的訂閱的回調,這顯然是有浪費的。因此Vue
設計了在每次添加完新的訂閱,會移除掉舊的訂閱,這樣就保證了在我們剛纔的場景中,如果渲染b
模板的時候去修改a
模板的數據,a
數據訂閱回調已經被移除了,所以不會有任何浪費。 -
總結:我們對
Vue
數據的依賴收集過程已經有了認識,並且對這其中的一些細節做了分析。收集依賴的目的是爲了當這些響應式數據發生變化,觸發它們的setter
的時候,能知道應該通知哪些訂閱者去做相應的邏輯處理,我們把這個過程叫派發更新,其實Watcher
和Dep
就是一個非常經典的觀察者設計模式的實現。
四、派發更新
- 響應式數據依賴收集過程,收集的目的就是爲了當我們修改數據的時候,可以對相關的依賴派發更新,那麼詳細分析這個過程,回顧一下
setter
部分的邏輯:
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
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()
}
})
}
setter 的邏輯有 2 個關鍵的點,一個是
childOb = !shallow && observe(newVal)
,如果shallow
爲 false 的情況,會對新設置的值變成一個響應式對象;另一個是dep.notify()
,通知所有的訂閱者,接下來會完整的分析整個派發更新的過程。
- 過程分析,當我們在組件中對響應的數據做了修改,就會觸發
setter
的邏輯,最後調用dep.notify()
方法,
它是Dep
的一個實例方法,定義在src/core/observer/dep.js
中:
class Dep {
// ...
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
這裏的邏輯非常簡單,遍歷所有的
subs
,也就是Watcher
的實例數組,然後調用每一個watcher
的update
方法,它的定義在src/core/observer/watcher.js
中:
class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
這裏對於
Watcher
的不同狀態,會執行不同的邏輯,computed
和sync
等狀態的分析,在一般組件數據更新的場景,會走到最後一個queueWatcher(this)
的邏輯,queueWatcher
的定義在src/core/observer/scheduler.js
中:
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
-
這裏引入了一個隊列的概念,這也是
Vue
在做派發更新的時候的一個優化的點,它並不會每次數據改變都觸發watcher
的回調,而是把這些watcher
先添加到一個隊列裏,然後在nextTick
後執行flushSchedulerQueue
。這裏有幾個細節要注意一下,首先用has
對象保證同一個Watcher
只添加一次;接着對flushing
的判斷;最後通過waiting
保證對nextTick(flushSchedulerQueue)
的調用邏輯只有一次,另外nextTick
的實現後面會分析,目前就可以理解它是在下一個tick
,也就是異步的去執行flushSchedulerQueue
。 -
接下來我們來看
flushSchedulerQueue
的實現,它的定義在src/core/observer/scheduler.js
中,如下所示:
let flushing = false
let index = 0
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
flushing = true
let watcher, id
// 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((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
這裏有幾個重要的邏輯要梳理一下,對於一些分支邏輯如
keep-alive
組件相關和之前提到過的updated
鉤子函數的執行會略過。
- 隊列排序,
queue.sort((a, b) => a.id - b.id)
對隊列做了從小到大的排序,這麼做主要有以下要確保以下幾點:
- 組件的更新由父到子;因爲父組件的創建過程是先於子的,所以
watcher
的創建也是先父後子,執行順序也應該保持先父後子。 - 用戶的自定義
watcher
要優先於渲染watcher
執行;因爲用戶自定義watcher
是在渲染watcher
之前創建的。 - 如果一個組件在父組件的
watcher
執行期間被銷燬,那麼它對應的watcher
執行都可以被跳過,所以父組件的watcher
應該先執行。
- 隊列遍歷,在對
queue
排序後,接着就是要對它做遍歷,拿到對應的watcher
,執行watcher.run()
。這裏需要注意一個細節,在遍歷的時候每次都會對queue.length
求值,因爲在watcher.run()
的時候,很可能用戶會再次添加新的watcher
,這樣會再次執行到queueWatcher
,如下:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// ...
}
}
可以看到,這時候
flushing
爲 true,就會執行到 else 的邏輯,然後就會從後往前找,找到第一個待插入watcher
的 id 比當前隊列中watcher
的 id 大的位置。把watcher
按照id
的插入到隊列中,因此queue
的長度發生了變化。
- 狀態恢復,這個過程就是執行
resetSchedulerState
函數,它的定義在src/core/observer/scheduler.js
中,如下所示:
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
/**
* Reset the scheduler's state.
*/
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
邏輯非常簡單,就是把這些控制流程狀態的一些變量恢復到初始值,把
watcher
隊列清空。
- 接下來我們繼續分析
watcher.run()
的邏輯,它的定義在src/core/observer/watcher.js
中,如下所示:
class Watcher {
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}
getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
}
-
run
函數實際上就是執行this.getAndInvoke
方法,並傳入watcher
的回調函數。getAndInvoke
函數邏輯也很簡單,先通過this.get()
得到它當前的值,然後做判斷,如果滿足新舊值不等、新值是對象類型、deep
模式任何一個條件,則執行watcher
的回調,注意回調函數執行的時候會把第一個和第二個參數傳入新值value
和舊值oldValue
,這就是當我們添加自定義watcher
的時候能在回調函數的參數中拿到新舊值的原因。 -
那麼對於渲染
watcher
而言,它在執行this.get()
方法求值的時候,會執行getter
方法:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
所以這就是當我們去修改組件相關的響應式數據的時候,會觸發組件重新渲染的原因,接着就會重新執行
patch
的過程,但它和首次渲染有所不同。
- 總結:對
Vue
數據修改派發更新的過程也有了認識,實際上就是當數據發生變化的時候,觸發setter
邏輯,把在依賴過程中訂閱的的所有觀察者,也就是watcher
,都觸發它們的update
過程,這個過程又利用了隊列做了進一步優化,在nextTick
後執行所有watcher
的run
,最後執行它們的回調函數。nextTick
是Vue
一個比較核心的實現了。