深入响应式原理
大家应该都知道响应式原理的核心API是Object.defineProperty。如果不了解请点击:
Object.defineProperty,去MDN了解下这个API的用途。
那这个API是如何做到响应式对象的呢?
接下来我们从源码的角度来进行分析:
我们知道在组件的data中定义的数据,最终都能响应式,看下Vue2.x的源码是如何初始化data的:
响应式对象
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)
}
}
function initData (vm: Component) {
let data = vm.$options.data
// 1.获取到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
// 2.验证是否与定义的methods与props冲突;
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)) {
// 3.proxy代理;
proxy(vm, `_data`, key)
}
}
// observe data 4.oberve实现data对象响应式;
observe(data, true /* asRootData */)
}
这里面主要分为如下几步:
- 获取到data中的返回对象;
- 验证是否与定义的methods与props冲突;
- proxy代理;
- oberve实现data对象响应式;
这里的第一步和第二部很容易理解,第四步是实现响应式对象的核心,第三步简单的分析下是为什么,贴上代码:
proxy
proxy(vm, `_data`, key)
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)
}
可以看到,这个地方其实也是利用了Object.defineProperty这个api,定义了存储描述符,让我们在vue内部data属性中定义的key通过代理的方式,可以使用this.key的方式去访问到。简化了我们的代码量。
当我们访问this.key的时候,其实内部返回的就是this._data.key的值。
在vue中我们也可以通过this._data.key的方式访问到我们定义的data,但不建议这么做,_data的意思就是内部属性data。
observe
observe
的功能就是用来监测数据的变化,它是vue2.x 实现响应式的核心,它定义在 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
}
可以看到,这里实例化了一个Observer类,并返回这个实例化后的值,接着看下Observer这个类的源码
/**
* 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])
}
}
}
这个构造函数首先实例化了Dep对象(这个类跟依赖收集相关,这里不做分析,以后的文章会讲清。)接着执行def函数,把自身的实例添加到数据对象value的__ob__属性上
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
接下来对这个value值做判断,数组的话会调用observeArray方法,否则调用walk方法。而observe最终也会调用observe方法,最终都会执行defineReactive。
/**
* 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对象,接着拿到属性描述符,然后对子对象递归调用observe,这样就实现了对象的深度响应式,保证无论访问多深的obj的属性,都使用Object.defineProperty给对应的属性添加了getter和setter。
这个作用是为了访问属性的时候触发getter,收集属性的依赖,改变属性的时候触发setter,派发对应的更新,从而通知页面做更新,接下来我们来聊聊这两个过程。
依赖收集
当访问响应式数据的时候,会触发getter方法,下面我们分析这个过程中,Vue的内部做了哪些事情:
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建Dep实例
const dep = new Dep()
// 获取obj描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
// 不可配置则直接返回,不做响应式处理
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 存储预先定义的get方法
const getter = property && property.get
......
// 深层次对象,则递归执行observe
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 计算值
const value = getter ? getter.call(obj) : val
// 如果Dep.target存在,进行依赖收集的操作
if (Dep.target) {
// 收集依赖
dep.depend()
// childOb存在
if (childOb) {
// child收集依赖
childOb.dep.depend()
// 如果是数组,数组中的值也需要收集依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
......
})
}
上面的代码中,有个Dep的类,我们继续来看:
Dep
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
* dep是一个可监听对象,可以有多个指令订阅它。
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
// 收集依赖的数组
this.subs = []
}
addSub (sub: Watcher) {
// 将订阅者Watcher推入依赖收集数组
this.subs.push(sub)
}
removeSub (sub: Watcher) {
// 在收集依赖数组移除订阅者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.target是Dep上的一个静态属性,起始时候是一个null值,
- pushTarget这个函数,是将当前的目标观察者Watcher推入targetStack栈数组中,并将传入的最新的Watcher,赋值给Dep.target
- popTarget这个函数,取出targetStack栈数组中的最后一个Watcher,赋值给Dep.target
那么这个Dep.target是什么时候赋值的呢?
我们在之前的文章中有过分析,页面渲染的时候会初始化一个updateComponent方法,随后实例化了一个render Watcher,实例化的过程中,执行了Watcher上的get函数(不清楚这段逻辑可以点击这里),这里我们继续以render Watcher来分析:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
// this指向当前的render Watcher(后续还会分析自定义Watcher与计算属性Watcher)
pushTarget(this)
let value
// vm指向当前实例
const vm = this.vm
try {
// 获取当前的值,在取值的过程中,就会触发响应式数据的get函数,而这时的Dep.target就是当前的render Watcher
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)
}
// 取值完毕,依赖收集完成,释放当前render Watcher
popTarget()
// 清除依赖,后面会分析这段逻辑
this.cleanupDeps()
}
return value
}
执行this.get的时候,会将Dep.target赋值为当前的render Watcher,而执行this.getter,就是执行updateComponent函数,会调用render方法,这个过程中会访问到渲染当前页面的数据,有响应式数据的话,就会进入get方法:
get: function reactiveGetter () {
......
// Dep.target为render Watcher进入逻辑
if (Dep.target) {
// 进入逻辑,收集依赖
dep.depend()
// 下面逻辑我们之后的文章分析
if (childOb) {
// child收集依赖
childOb.dep.depend()
// 数组中的值依次收集依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
接下来执行dep.depend()
// Dep类上的方法
depend () {
// 通知依赖更新,Dep.target为render Watcher
if (Dep.target) {
// 执行Watcher上的addDep方法
Dep.target.addDep(this)
}
}
// Watcher类上的方法
/**
* Add a dependency to this directive.
* 添加一个依赖到这个指令
*/
addDep (dep: Dep) {
// 这儿id的作用是防止重复订阅
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
// (上面的逻辑先忽略,之后的文章分析)这儿判断如果这个Watcher没有订阅过这个dep
if (!this.depIds.has(id)) {
// 将Watcher推入这个dep,也就是Watcher订阅这个dep了
dep.addSub(this)
}
}
}
// Dep类上的方法
addSub (sub: Watcher) {
// 将这个Watcher推入Dep的subs中
this.subs.push(sub)
}
通过以上的分析,我们清楚了依赖收集的整个过程了,首先是实例化render Watcher,将当前的Dep.target更改,执行取值方法,获取响应式数据的值,触发响应式数据中的get方法,这样当前的render Watcher就订阅了响应式数据中的dep实例。
派发更新
当改变响应式数据的值的时候,会触发setter方法,下面我们分析整个过程:
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建Dep实例
const dep = new Dep()
// 获取obj描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
// 不可配置则直接返回,不做响应式处理
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 存储预先定义的get方法
const setter = property && property.set
......
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
......
set: function reactiveSetter (newVal) {
// 计算value的值
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 新赋的值与之前的值作对比,如果值相等,直接返回
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
......
// setter有定义,调用setter
if (setter) {
setter.call(obj, newVal)
} else {
// 未定义setter,直接将新值赋给旧值
val = newVal
}
// 这儿的逻辑之后的文章去分析
childOb = !shallow && observe(newVal)
// 通知dep去更新
dep.notify()
}
})
}
可以看到,当改变响应式数据的值的时候,set方法会将新值与旧值坐下对比,相等的情况下直接返回,不做任何处理。不相等的情况下再对旧值赋值,同时调用dep的notify方法。在依赖收集的章节我们也分析了Dep类中的一些方法,不清楚可以点击这里,接下来我们去分析dep.notify函数,定义在dep类中:
/**
* 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 = []
}
...
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
// 遍历订阅这个dep的所有Watcher,依次执行Watcher.update函数
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
继续去看Watcher上的update函数:
Watcher.update()
// 定义在Watcher类中
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
// 判断是否是计算属性Watcher
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) {
// 自定义Watcher中定义了sync(同步)会进入这个逻辑
this.run()
} else {
// render Watcher进入这个逻辑,我们本次还是分析render Watcher
queueWatcher(this)
}
}
继续看queueWatcher函数:
queueWatcher
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let flushing = false
let waiting = 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.
* 将一个观察者推入观察者队列。具有重复id的作业将被跳过,除非它在队列刷新时被推送。
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 如果id没有缓存过,进入逻辑
if (has[id] == null) {
has[id] = true
if (!flushing) {
// flush为false的时候,向queue队列push Watcher
queue.push(watcher)
} else {
// 这段逻辑是在flushing过程中如果queue中出现watcher变更的情况会进入
// if already flushing, splice the watcher based on its id
// 如果已经刷新,则根据观察者的id拼接观察者
// if already past its id, it will be run next immediately.
// 如果已经超过了它的id,它将立即运行。
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) {
// wating置为true,保证nextTick执行一次
waiting = true
// 执行nextTick
nextTick(flushSchedulerQueue)
}
}
}
从上面的代码可以看出,queueWatcher是将Watcher推入queue数组中进行存储,并执行nextTick,关于nextTick,可以查看我的另一篇分析文章,这个函数就是将放在里面的函数放在下一个tick中去执行,等同步任务全部执行完再触发。
从这里我们也清楚了一个问题,当改变数据触发页面重新渲染,这是个异步的过程。
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
// flushing赋值为true
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.
// 1. 组件的更新是先父后子
// 2. 组件的自定义watcher应该在render watcher前面运行(因为自定义watcher创建在先)
// 3. 如果组件在父组件watcher运行期间被销毁,那么他应该被跳过
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
// queue的长度不要缓存,因为我们在运行现有的watchers的时候,可能会推入新的watchers
for (index = 0; index < queue.length; index++) {
// 依次拿到watcher
watcher = queue[index]
// 如果watcher有before属性,则执行
if (watcher.before) {
// render Watcher的时候,这儿是调用生命周期函数beforeUpdated
watcher.before()
}
id = watcher.id
// 将has[id]重置
has[id] = null
// 执行Watcher.run,下面我们看这个逻辑
watcher.run()
// in dev build, check and stop circular updates.
// 这个地方防止无限循环bug,有一种情况会进入这里
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()
// 重置队列状态,flushing,waiting等置为初始状态
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
// 调用updated生命周期函数
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
watcher.run()与getAndInvoke():
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
// this.active为true
if (this.active) {
// 执行getAndInvoke
this.getAndInvoke(this.cb)
}
}
getAndInvoke (cb: Function) {
// 调用Watcher.get函数计算value值
const value = this.get()
// 判断新值与旧值是否相同,执行传入的cb函数,render Watcher 这儿传入的是个空函数
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) {
// 自定义watcher进入这个逻辑
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
// computed Watcher 进入这个逻辑
cb.call(this.vm, value, oldValue)
}
}
}
到这里我们清楚了,当我们改变数据值的时候,会遍历数据中实例化的dep收集的依赖,通知Watcher做更新,将所有的Watcher推入一个队列,放在nextTick的时候去执行,所以页面的更新渲染是个异步的过程。
执行的过程中,会对队列中的watchers进行排序,排序完成后遍历queue队列,执行钩子函数beforeUpdated,再依次执行定义的Watcher.get函数,完成queue的遍历后,再执行钩子函数updated。