Vue 2.x 源码阅读记录(三):响应式系统
写在前面
文章为阅读笔记向,需 clone 下来 Vue 源码并加以调试服用~~
Vue 的响应式:
new Vue({
el: '#app',
data: {
msg: 'Hello'
},
methods: {
changeMsg() {
this.msg = 'World';
}
}
});
响应式对象
initState
在初始化_init
方法中调用了initState
方法,来初始化props
、data
等属性,使其变成 Vue 的响应式对象。initState
调用在合并配置之后。它内部集成了对props
、methods
、data
、computed
、watch
的初始化:
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)
}
}
initProps
它做了两件事情:一是遍历传入的props
,调用defineReactive
将其属性变成响应式,通过定义的vm._props
可以访问props
中的属性;二是通过proxy
代理方法将其访问路径代理到vm.props
中,也就是通常访问的this.props
:
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') {
// 将 key 转成小写
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 (!isRoot && !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)
}
initData
在开头将调用data
函数拿到data
对象,随后它也做两件事情:一是遍历key
调用proxy
代理到vm.data
;二是observe
订阅整个data
的变化:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
// data 为 function 时调用 data 返回对象
// return data.call(vm, vm)
? 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)) {
// 在 methods 中以及定义了该 data key 值
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
// props 中以及定义该 data 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)) {
// 非保留属性名,代理到 vm.data
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
在前面两个 init 中将props
和data
都变成了响应式对象,接下来看看其中接触到的一些函数。
Proxy
在前面的例子中,是通过this.msg = 'xxx'
来改变data
中的数据,而this.msg
实际上会被代理到this._data.msg
,这就是proxy
函数做的事,它使用Object.defineProperty
代理了实例属性的访问路径:
const sharedPropertyDefinition = {
enumerable: true, // 可枚举
configurable: true, // 可遍历
get: noop,
set: noop
}
// proxy(vm, `_props`, key)
// proxy(vm, `_data`, key)
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
// 将 this.key 的访问代理到 this._xxx.key
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
// set 同理
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
observe
它用来监听数据(data
)的变化, 给非 VNode 的对象类型数据添加一个Observer
,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个Observer
对象实例 :
// observe(vm._data = {}, true /* asRootData */)
// observe(data, true /* asRootData */)
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
}
Observer
它是一个类,在实例化时,它定义了一些为响应式服务的属性,实例化Dep
类,调用def
方法为value
(这里的 value
都为data
) 定义一个__ob__
属性,也就是打印中的可以看到的,如果value
是数组则为每个子元素调用observe
方法,如果是一个正常的data
对象则为每个属性调用defineReactive
方法:
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 给 value(data) 定义一个不可枚举的 __ob__ 属性,
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// value(data) 为数组,循环调用 observe 方法
this.observeArray(value)
} else {
// 为 value(data) 每个属性调用 defineReactive 方法使其变成响应式
this.walk(value)
}
}
/**
* Walk through all properties 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])
}
}
}
dep
函数使用Object.defineProperty
为对象添加一个属性:
/**
* 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
})
}
defineReactive
它在一开始实例化Dep
拿到实例对象,再拿到obj
(此处为 data 对象) 的属性描述符,然后递归为data
中每个属性调用observe
方法(当属性是一个对象时),这样就确保了每个属性都带有__ob__
属性,都变成了响应式的, 这样我们访问或修改obj
中一个嵌套较深的属性,也能触发 getter 和 setter。最后使用Object.defineProperty
给obj
的key
属性值添加getter
和setter
,它们做的事情就是依赖收集和派发更新:
// defineReactive(obj /* data */, keys[i] /* data key */)
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// 拿到 obj 的属性描述对象
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) {
// 在 Observer 中 data 只传递了两个参数
// data 中的每个属性的值
val = obj[key]
}
// 递归地为 data 中的每个属性添加 __ob__ 对象
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
可以看到,初始化props
和data
的过程中就是利用Object.defineProperty
为数据添加getter
和setter
来拦截对象的读写,并递归给与__ob__
对象用于追踪数据变化。
依赖收集
在给响应式对象添加完getter
和setter
后,对对象的读取就会执行getter
,getter
会进行一波依赖收集的过程,然后将访问的值返回:
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
},
具体流程
在mountComponent
执行时,会定义一个渲染Watcher
,会将updateComponent
函数作为Watcher
的getter
传入:
updateComponent = () => {
// _update() 将 vnode 生成为实际 DOM 元素
// _render() 生成 vnode
vm._update(vm._render(), hydrating /* false */)
}
new Watcher(vm, updateComponent, noop /* 空函数 */, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 组件已挂载并未销毁,执行 beforeUpdate 生命周期
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
在Watcher
类中,会定义一些依赖相关的数组,将getter
赋值为传入的updateComponent
,最后会执行get
方法,get
方法中执行了pushTarget
和getter
方法:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
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)
//...
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 = noop
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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
debugger
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)
}
// 这里是 getter 执行完之后执行的
popTarget()
this.cleanupDeps()
}
return value
}
//...
}
pushTarget
方法会将当前渲染Watcher
存储到Dep.target
并添加到targetStack
栈中:
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
// 当前渲染 watcher
Dep.target = target
}
然后Watch
会执行getter
方法,也就是外部传入的updateComponent
,然后结合前面可以知道,会先执行_render()
走到render
方法的执行,在render
执行时,也就访问了data
中的属性,触发了响应式对象getter
开始依赖收集过程:
vnode = render.call(vm._renderProxy, vm.$createElement)
在defineReactive
中定义了一个Dep
类的实例对象,首先会对这个Dep
实例进行依赖收集,执行了当前Dep
实例的depend
方法:
// 这个 dep 是 data 中 __ob__ 中的 dep
const dep = new Dep()
if (Dep.target) {
dep.depend()
if (childOb) {
// dep 在 Observer 中定义
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
depend
方法实际执行了当前渲染Watcher
的addDep
方法来收集依赖:
depend () {
if (Dep.target) {
// Dep.target 是当前渲染 Watcherdep
// this 是 data 中每个属性 __ob__ 中的 dep
Dep.target.addDep(this)
}
}
addDep
方法通过一些判断将当前dep
收集到依赖相关的数组中:
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
方法将当前渲染Watcher
订阅到dep
的subs
数组中, 这个目的是为后续数据变化时候能通知到哪些subs
做准备:
addSub (sub: Watcher) {
this.subs.push(sub)
}
到此,已经完成了一个依赖收集的过程。此时回到Watcher
的get
方法执行中,会接着执行finally
块:
finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
// 这里是 getter 执行完之后执行的
popTarget()
this.cleanupDeps()
}
先了解一下popTarget
,当前vm
的依赖收集完成后,它将当前的渲染Watcher
回归到上一个的状态,因为如果存在组件嵌套的关系,会递归地执行mountComponent
方法:
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
暂时跳过cleanupDeps
的总结
可以看到,在依赖收集的过程中当前的渲染Watcher
会收集当前dep
,而当前dep
会收集当前渲染Watcher
,这样,二者就建立了桥梁。换一种说法,Watcher
作为一个观察者,它会将当前的dep
收集起来,作为当前Watcher
的一个订阅者,而dep
则在它的subs
中订阅了当前Watcher
,这样,当dep
对应的数据改变时,会调用dep
的notify
通知订阅的Watcher
,执行Watcher
上的update
方法告诉Watcher
可以开始更新数据了。这是一个典型的观察者模式的实现。
派发更新
完成对数据的依赖收集之后,点击按钮进行赋值操作时会进入响应式对象的setter
,其中进行了派发更新的流程,对依赖的收集就是为了修改数据时对相关的依赖派发更新:
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// notify 之前,以及将 data[val] 更新了
dep.notify()
}
可以看到,setter
会先获取当前需要操作的旧值,对新旧值进行对比,然后将当前值变成新值,如果新值是一个对象,则再调用observe
方法将其变成响应式对象,最后调用当前dep
的notify
方法通知Watcher
开始派发更新过程。notify
方法遍历了之前dep.subs
订阅的Watcher
,调用了它们的update
方法:
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
在当前流程中,update
中执行了queueWatcher
方法,对dep.subs
下的Watcher
做了一个队列操作:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
在当前例子中只有一个渲染Watcher
,所以执行queue.push
分支,将当前Watcher
push 到队列中,然后调用nextTick
方法执行flushSchedulerQueue
, 这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher
的回调,而是把这些 watcher
先添加到一个队列里,然后在 nextTick
后执行 flushSchedulerQueue
,稍后手动添加watch
的时候再来验证这一点:
const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 保证同一个 Watcher 只 push 一次
if (has[id] == null) {
// 存储当前 watcher.id
has[id] = true
if (!flushing) {
// 向队列中插入当前 Watcher
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.
// 第二个 Watcher 进来的时候
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
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
// 异步执行 flushSchedulerQueue
debugger
nextTick(flushSchedulerQueue)
}
}
}
(计算属性之后过来的补充)当在
changeMsg
中有多个赋值语句时,它们会被合并。因为它们属于同一个渲染Watcher
。当第一个赋值语句执行时,已经进入了has[id] == null
的逻辑,第二次赋值时这个渲染Watcher
已经存在于has
中了,则不会进入。又因为在这些属性的setter
中,已经完成了新值的赋值,那么它们就只需要等待执行nextTick(flushSchedulerQueue)
就可以了:
this.msg = 'World'; this.msg2 = 'World2';
先看看flushSchedulerQueue
前面主要的逻辑,它会对队列中的Watcher
根据自增的id
进行从小到大排序,因为父组件的Watcher
先于子组件的Watcher
创建,手动写的watch
先于渲染Watcher
创建,所以要将先创建的Watcher
先执行,可以考虑单向数据流的概念来理解这一点:
queue.sort((a, b) => a.id - b.id)
然后将队列遍历,先执行watcher.before
方法,也就是在渲染Watcher
执行时定义的,它内部调用了beforeUpdate
生命周期函数,然后将has[id]
置空,并执行watcher.run()
:
// queue.length 没有缓存是因为可能在 watcher.run() 的过程中插入新的 Watcher
// 参考下面的 `边界情况`
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// mountComponent 中传入了 before
// before 方法执行了 beforeUpdate hook
watcher.before()
}
id = watcher.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
}
}
}
watcher.run
方法就是重新渲染 DOM 的方法,它内部重新调用了watcher.get
方法,再次执行了一遍watcher.getter
,也就是渲染Watcher
定义时的updateComponent
方法,走一遍getter
流程更新 DOM,最后拿到新旧值执行cb
回调,回调对应用户手写的watch
,在渲染Watcher
中,回调是一个noop
:
run () {
if (this.active) {
// 这里又调用了一次 getter
// set 中将 val 变成了新的值,这里再次调用会进入 updateComponent 的调用
// _render() 会重新读取改变后的 val,从而渲染页面
debugger
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
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
DOM 更新完成之后,再看看flushSchedulerQueue
收尾逻辑:
// 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')
}
先调用resetSchedulerState
重置上面的一些全局变量:
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
最后调用了两个 call hooks 函数会当前Watcher
的vm
实例执行activated
、updated
生命周期:
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
边界情况
如果在手动添加watch
中又对监听的值进行了赋值:
watch: {
msg() {
this.msg = Math.random();
}
},
那么它在queueWatcher
函数中 push 到队列时会进入flushing = true
的分支,这是因为在执行watcher.cb
回调函数时,又进入的响应式对象的setter
,并没有执行到watcher.run
之后的resetSchedulerState
重置变量的流程。此时就会向队列中当前的Watcher
后一位插入一个新的Watcher
,它可以看作用户手动写的Watcher
的副本:
if (!flushing) {
// 向队列中插入当前 Watcher
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)
}
由于每次run
的时候又会添加一个新的Watcher
,这是就会进入一个添加Watcher
的死循环,最后会在watch.run()
方法之后报出一个警告提示当前进入了无限更新循环:
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
}
}
注意
在循环执行subs[i].update()
方法时,会将循环的当前Watcher
push 到队列中,但遇到nextTick
函数时会返回循环,接着 push 下一个Watcher
,nextTick
就是派发更新操作中的关键步骤。
总结
1、更新新旧数据,调用notify
开始队列操作;
2、循环调用watcher.update
添加Watcher
到队列;
3、添加完队列后nextTick
循环队列中的Watcher
执行watcher.run
更新 DOM;
nextTick
nextTick
是一个setter
的一个核心实现,它会在 DOM 完成渲染之后执行。在 Vue 2.5 以上的版本中,在目前主流的 web 环境中nextTick
函数使用Promise
实现微任务(microTask)。它的整个实现都在一个next-tick.js
中。
首先它定义了一些全局变量,用于控制Promise
队列:
// 回调函数队列
const callbacks = []
// promise pending
let pending = false
// 微任务入口函数
let timerFunc
在当前环境支持Promise
的情况下,使用Promise.resove()
来获得一个微任务,并将timerFunc
赋值为一个入口函数:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
在微任务中,执行了fulshCallbacks
函数,它将遍历执行callbacks
队列:
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
微任务已经创建完毕了,接下来看看nextTick
函数如何运用微任务去执行。它接收一个cb
回调,将cb
回调的调用作为一个callback
插入到callbacks
回调队列中,然后开始执行timerFunc
函数。最后,在没有传入cb
的情况下,会返回一个Promise
,也就是nextTick
支持Promise
调用:
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
// 相当于执行了 `this.nextTick().then(() => {})`
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
timerFunc
会使用p.then(flushCallbacks)
将所有用户传入的回调都压到一个微任务中执行,等待当前宏任务执行完毕后,会按开始读取微任务中的结果:
timerFunc = () => {
p.then(flushCallbacks)
// ...
}
按照 JS 异步的概念来说,会先执行同步的代码,比如render
、patch
等过程,同步代码执行完成之后在读取异步代码执行的结果,也就是调用用户传入的回调。这也就是为什么在派发更新的过程中遇到nextTick(flushSchedulerQueue)
时会回到dep.subs[i].update
的循环中,flushSchedulerQueue
实际是进入了异步队列,在同步代码中,组件会完成patch
的过程,这就是为什么nextTick
能够拿到渲染之后的 DOM,而flushSchedulerQueue
又能在异步队列中按顺序执行(因为flushCallbacks
循环调用了它),这也保证了父组件会先于子组件更新完毕。Vue 在nextTick
中巧妙的运用了事件循环。
另外,在initGlobalAPI
和 Vue 初始化的的时候,向 Vue 挂载了nextTick
和$nextTick
函数,它实际就是调用了这里的nextTick
,它可以使用Vue.nextTick
、vm.$nextTick
调用:
// initGlobalAPI
Vue.nextTick = nextTick
// renderMixin
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
检测变化的注意事项
在data
中没有定义的属性是没有getter
和setter
的,所以访问不存在于一个对象中的值时它不会是响应式的。通常,能够使用Vue.set
或vm.$set
来将一个属性变成响应式的,set
可以给对象和数组添加响应式的属性:
var vm = new Vue({
data:{
a:1,
someObj: {}
}
})
// vm.b 与 vm.somObj.a 是非响应的
vm.b = 2
vm.someObj.a = 1
// 使其变成响应式
Vue.set(vm.someObj, 'a', 1);
set
与$set
分别定义在初始化实例的时候:
// initGlobalAPI
Vue.set = set
// stateMixin
Vue.prototype.$set = set
// set
export function set (target: Array<any> | Object, key: any, val: any): any {
debugger
// target 不能为 undefined, null, 普通类型值
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 数组情况
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
// key 存在 target 中,直接将 val 赋值给 target
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
对象属性
当target
中带有这个key
的时候,说明已经在初始化响应式对象或之前的set
中设置了getter
和setter
,则直接返回target[value]
的值:
if (key in target && !(key in Object.prototype)) {
// key 存在 target 中,直接将 val 赋值给 target
target[key] = val
return val
}
随后,它会调用defineReactive
给val
设置getter
和setter
,走一遍响应式对象的设置流程。之后,会手动调用ob.dep.notify
手动派发更新。当页面没有用到这个新值的时候,dep.subs
就没有订阅任何Watcher
,最后直接返回val
;当页面中用到了这个新值的时候,就会从notify
开始派发更新,最后执行watcher.run
方法来重新执行updateComponent
,就会成功将新值渲染到页面上去了:
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
数组元素
在数组的情况下,直接调用splice
方法就能将val
变成响应式的:
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
其实这里的splice
是 Vue 重写过的。在target
被实例Observer
的时候,如果遇到当前Observer
的值是数组时,会进行一些处理,在大部分现代浏览器环境中,它会走到protoAugment
方法,将target
的__proto__
指向arrayMethods
:
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 给 value(data) 定义一个不可枚举的 __ob__ 属性,
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// value(data) 为数组,循环调用 observe 方法
this.observeArray(value)
} else {
// 为 value(data) 每个属性调用 defineReactive 方法使其变成响应式
this.walk(value)
}
}
function protoAugment (target, src: Object, keys: any) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
arrayMethods
实际就是指向了Array.prototype
,然后在后面会对数组所有的原生方法进行遍历重写。这里的重写实际就是对原生方法的执行做一层拦截操作:
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
可以看到它会先执行原生的数组方法,然后将需要插入到数组中的值转为一个数组,调用observeArray
循环调用observe
方法使其变成响应式对象,最后调用dep.notify
通知Watcher
更新。
总结
set
就是将响应式对象/数组中不存在的属性以对象或数组的方式最终调用defineReactive
方法给它添加getter
和setter
。补充:响应式对象的流程:observe > Observer (Object, Array)> defineReactive
,整个流程就是一个闭环。
计算属性 computed
计算属性可用于对数据的复杂计算,它会缓存计算结果:
const vm = new Vue({
el: '#app',
data: {
firstName: 'Foo',
lastName: 'Bar',
},
methods:
changeName() {
this.firstName = 'Coven';
// this.firstName = 'Foo';
},
},
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
},
});
纠错
经过痛苦的调试与推理过程,我发现了一个之前定义响应式对象时的错误。在 defineReactive
为 data
中的属性设置 getter
和 setter
时,有一个 const dep = new Dep()
的操作,这个 dep
并不是我当时理解的是当前目标对象 __ob__
下的 dep
,而是当前目标对象下每个属性持有的 dep
。为每个属性添加一个 dep
,就能让每个属性都订阅当前的 Watcher
,在当前属性更新时通知 Watcher
更新:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 这个 dep 是为 data 中每个属性创建的 dep
const dep = new Dep()
//...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
/* 依赖收集 */
get: function reactiveGetter () {
if (Dep.target) {
// 为当前属性订阅 Watcher
dep.depend()
// ...
}
return value
},
set: function reactiveSetter (newVal) {
// 当前属性已经改变,通知 Watcher 开始更新
dep.notify()
}
})
}
初始化
首先看看初始化initComputed
的过程。首先遍历用户定义的computed
对象,检验定义的合法性,然后为每个计算属性方法定义一个computed watcher
,存储在vm._computedWatchers
中,然后判断计算属性名是否已经存在于vm
下,否则为每个计算属性方法执行defineComputed
:
function initComputed (vm: Component, computed: Object) {
// debugger
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
// computed 可以支持带有 set 和 get 的对象写法
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
// key 已经在当前 vm 实例 data/props 中定义了
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
defineComputed
的核心就是为计算属性函数添加getter
和setter
,开发中最常用的就是用计算属性进行复杂计算,所以这里重点看getter
,它是createComputedGetter
:
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering() // true
if (typeof userDef === 'function') {
// 给当前 userDes 函数添加 getter 和 setter
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
createComputedGetter
返回实际的getter
,它的核心就是根据计算属性的key
取出之前缓存的computed watcher
,然后执行Watcher
的getter
,也就是计算属性方法:
function createComputedGetter (key) {
return function computedGetter () {
debugger
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
访问
前面知道,在_render()
的过程中会访问到定义在模板中的属性,当访问到计算属性时,会触发计算属性的getter
。在计算属性初始化的时候,提到过为计算属性定义了一个computed watcher
:
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
// const computedWatcherOptions = { lazy: true }
computedWatcherOptions
)
这里抽取出一些computed watcher
定义时与渲染Watcher
的不同之处,computed watcher
的关键在于 this.lazy
与 this.dirty
:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
// options
if (options) {
// ...
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.dirty = this.lazy // for lazy watchers
// ...
// lazy 表示是一个计算属性,不同于的渲染 Watcher 的是,它不会在 new Watcher() 时进行求值
this.value = this.lazy
? undefined
: this.get()
}
可以看到在最后,不会立即执行get
函数求值,而是会在计算属性getter
时再进行求值。回到计算属性getter
,此时dirty
为true
,会执行watcher.evaluate
方法,evaluate
则执行了 watcher.get
:
if (watcher.dirty) {
watcher.evaluate()
}
在 get
执行之前,此时的 Dep.target
是渲染 Watcher
。evaluate
方法执行了 Watcher
的get
,也就是执行了计算属性函数,最终返回计算属性的值,在例子中是return ${this.firstName} ${this.lastName}
。开始执行 get
时,此时的 Dep.target
是当前的 computed watcher
,在执行计算属性函数时,对 this.firstName
和 this.lastName
进行了访问,触发了它们的 getter
,随后它们所属的 dep
对象对当前 computed watcher
进行依赖收集,订阅到 dep.subs
中:
get () {
// debugger
// computed watcher 接管当前流程
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)
}
// 这里是 getter 执行完之后执行的
// computed watcher 内会访问 this.xxx 属性,因此会触发 this.xxx 的 getter
// 最终 value 拿到 computed 中 userDef 的返回值
// 将当前 computed watcher 出栈,Dep.target 交还给上一个 Watcher
// targetStack 是后进先出顺序
popTarget()
this.cleanupDeps()
}
return value
}
到这里,已经拿到计算属性的计算结果了,当渲染流程完成时,结果会显示到页面上。接下来,看看对计算属性依赖的值进行更改,它又是如何进行计算的:
changeName() {
this.firstName = 'Coven';
},
它触发了 firstName
的 setter
,然后执行 dep.notify
,此时的 dep.subs
中订阅了渲染 Watcher
和 computed watcher
,然后将它们循环执行 update
。渲染 Watcher
会走正常的 queueWatcher
流程,computed watcher
则只会将 dirty
设为 true
:
update () {
/* istanbul ignore else */
if (this.lazy) {
// 当前为 computed watcher 时,先不进入派发更新流程
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
当 dep.notify
执行完成后,会开始执行更新,此时又会执行到 _render()
,在 _render()
的过程中,又会访问到计算属性,开始执行计算属性的 getter
:
function createComputedGetter (key) {
return function computedGetter () {
debugger
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
经过 update
的流程,此时的 dirty
为 true
,接下来就开始走计算属性访问的流程。在 firstName
的 setter
中,已经执行 val = newVal
将它的值更新了,所以这里 computed watcher
的 getter
会拿到最新的 firstName
值,最后渲染到页面上。
结合上面的流程调试到最后,我发现,只有当计算属性中依赖(dep
中订阅了 computed watcher
)的值发生变化时,并且 _render()
流程再次访问到计算属性时,才会进行重新计算。
侦听属性 Watch
在响应式系统中,用户可以手动定义一个 watch
来监听数据的变化,它是一个 user watcher
,接下来看看它的实现原理:
watch: {
msg() {
// this.msg = Math.random();
console.log('msg changed.');
}
},
初始化
在初始化的 initState
中,如果用户定义了 watch
,则会执行 initWatch
来初始化 user watcher
。它会遍历 watch
属性,拿到 watch
中每一个函数/对象,对函数或对象中的 handler
执行 createWatcher
方法:
function initWatch (vm: Component, watch: Object) {
debugger
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
createWatcher
对参数进行了一些规范处理,最终调用初始化 stateMixin
中定义的 $watch
方法,实际上用户定义在选项中的 watch
也是调用了 $watch
API:
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
$watch
中实例化了一个 Watcher
,注意 options.user = true
,表示这是一个用户定义的 watcher
,它先于渲染 Watcher
被订阅:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
那么这时在 Watcher
被实例化的时候,expOrFn
是用户传入的 watch
函数的函数名,cb
是 watch
函数本身,user
为 true
,注意这里的 expOrFn
是一个字符串,会调用 parsePath(expOrFn)
将其转换成函数赋值给 getter
,最后调用 get
方法求值:
get () {
// debugger
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)
}
// 这里是 getter 执行完之后执行的
// computed watcher 内会访问 this.xxx 属性,因此会触发 this.xxx 的 getter
// 最终 value 拿到 computed 中 userDef 的返回值
// 将当前 computed watcher 出栈,Dep.target 交还给上一个 Watcher
// targetStack 是后进先出顺序
popTarget()
this.cleanupDeps()
}
return value
}
实例化过程中,this.getter = parsePath(expOrFn)
是最重要的一步。parsePath
会先校验是不是一个合法的函数名,然后将函数名用 .
分割为一个字符串数组,因为用户可以采用 someObj.foo
这种方式定义 watch
的函数名,以监听对象内的某个属性,最后它返回一个函数,这个函数就是 Watcher
的 getter
。
在上面 getter
调用时传入的是当前的 vm
实例,就是下面的 obj
,然后循环 segments
,在循环中将 obj[segments[i]]
的值赋值给 obj
,适配了两种 watch
函数名的定义:一是 msg(n, o) {}
;二是 'someObj.msg'(n, o) {}
。这是一个很巧妙的方式:
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\d]`)
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
}
}
回到 get
函数,当执行 getter
的时候实际是对 data
中的数据做了访问,触发了数据的 getter
,然后数据的 dep
对象就会订阅当前的 user watcher
,当更新时就会通知它进行派发更新过程。
执行 watch
在看派发更新的时候,知道数据更新会触发数据的 setter
,进行派发更新过程。当 watch
监听了一个属性的时候,这个属性的 dep.subs
中就会在初始化 watch
时订阅这个 user watcher
,那么到这个 user watch
执行 run
的时候,它就会进入 this.user = true
的逻辑,执行 cb
,也就是用户定义的 watch
,返回新旧值。这一点在看派发更新时粗略地介绍过:
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
deep watch
在定义一个 watch
时候,可以设置一个 deep: true
属性来监听一个对象或数组中的所有属性变化:
watch: {
someObj: {
deep: true,
handler() {}
}
}
在 user watcher
初始化调用 getter
最后的 finally
块中,会执行这一段逻辑,也就是 deep: true
的实现:
if (this.deep) {
traverse(value)
}
traverse
实际调用了 _traverse
,首先判断当前 user watch
监听的不是对象或数组,不是则直接返回。它遍历 value
中所有的属性,并递归地调用自身,调用时实际又触发了属性的 getter
,给每个属性的 dep.subs
订阅当前的 user watcher
,这样,当 value
内部的属性发生改变时也能触发 watch
的回调。 这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id
记录到 seenObjects
,避免以后重复访问。
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
总结 computed 和 watch
computed
和 watch
实际上都是 Watcher
类的实例对象。目前为止,已经接触过三个 Watcher
的不同使用方式:渲染 Watcher
、computed watcher
和 user watcher
。它们的变化实质都是 _render()
方法触发数据的 getter
依赖收集,赋值操作触发数据的 setter
派发更新。 就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。