原文链接:www.yuque.com/wuhaosky/vu…
一 前言
数据驱动视图是MVVM框架的显著特点,MVVM框架的出现将前端开发者从繁杂的、“鸟巢”般的dom操作中解放出来,开发体验比jQuery/underscore模板提升了不知道几个层次。
想要实现数据驱动视图,需要解决两个问题,一是框架要知道数据什么时候变更、二是框架如何把变更后的数据更新到视图。对于解决第一个问题,React是通过开发者手动执行this.setState方法实现的,Vue框架是通过自身数据响应式系统实现的;对于解决第二个问题,React和Vue都是通过patch函数和virtual dom diff算法实现的。
这篇文章里,我们只关注Vue的响应式系统是如何实现的。无论是Vue2还是Vue3,两者的响应式系统都是基于观察者模式(发布订阅模式)实现的,所以都涉及这么几个概念:目标对象(target)、依赖收集器(Dep)、观察者(Watcher)。依赖收集器收集目标对象的观察者,当目标对象的状态发生改变,所有的观察者都将得到通知。示意图如下:
二 Vue2响应式系统的实现
我们先整体看下Vue2响应式系统是怎么运作的,有个大体的概念,然后再拆分每一部分,看下每部分的实现。
2.1 先整体看下Vue2响应式系统的实现
目标对象经过observe函数,新增__ob__属性,这个属性是一个Observer实例,这个Observer实例含有dep属性,dep属性指向依赖收集者。然后,对目标对象的每一个属性执行defineReactive函数,将属性转换成访问器属性,这样我们就可以对属性的读写操作进行拦截。这个过程称之为“数据劫持”。
当执行观察者get方法时,会触发目标对象属性的getter方法,在getter方法里收集观察者,这个过程就是“收集观察者”。
当目标对象属性变更时,会触发目标对象的setter方法,在setter方法里执行观察者的update方法,这个过程就是“通知观察者”。
2.2 observe函数和defineReactive函数
observe函数和defineReactive函数的作用是把目标对象属性转换成访问器属性。
我们看下,这两个函数是怎么实现的。首先看下observe函数,observe函数创建一个Observer实例,在Observer构造函数里做了三件事:
1.首先new了一个依赖收集器,这个dep的作用是,当目标对象增删属性时,通知对目标对象“感兴趣”的观察者;
2.给目标对象添加不可枚举的__ob__属性,指向Observer实例;
3.最后遍历对象属性,并执行defineReactive函数。
export function observe (value: any): Observer | void {
let ob: Observer | void
ob = new Observer(value)
return ob
}
export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
// ...
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
js对象里目前存在的属性描述符有两种主要形式:数据描述符和访问器描述符。defineReactive函数的作用,就是把目标对象属性设置为访问器属性,这样可以在getter/setter方法中拦截属性的读写操作。如果属性是对象或数组,则递归执行observe函数,使目标对象深度可侦测。defineReactive函数里做了三件事:
1.创建了一个dep实例,这个dep 在访问器属性的 getter/setter 中被闭包引用,这个dep的作用是当目标对象属性发生写操作时,通知“感兴趣”的观察者;
2.如果属性是对象或者数组,则调用observe函数并把这个属性当做实参,目的是使目标对象深度可侦测;
3.使用Object.defineProperty函数把目标对象属性转成访问器属性,在getter方法里,通过执行dep.depend方法,收集对当前属性“感兴趣”的观察者;在setter方法里,执行observe(newVal),把新增加的属性值变成可侦测的,并执行dep.notify(),通知对此属性“感兴趣”的所有观察者。
export function defineReactive (
obj: Object,
key: string,
val: any
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
// cater for pre-defined getter/setters
const getter = property && property.get
let val;
if (!getter) {
val = obj[key]
}
const setter = property && property.set
let childOb = 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
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
observe(newVal)
dep.notify()
}
})
}
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
2.3 Dep依赖收集器
顾名思义,Dep依赖收集器的作用就是收集观察者的。
我们来看下Dep的实现,
1.Dep有个静态属性target,当观察者初始化时,会在观察者的构造方法里,执行观察者的get方法,在观察者的get方法里,观察者会把自己赋值给Dep.target,意味着当前的观察者是自己;
2.dep.addSub方法把当前的观察者收集,存储到subs属性中;
3.dep.notify方法会调用所有观察者的update方法。
let uid = 0
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 () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
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()
}
2.4 Watcher观察者
观察者的作用是监听目标对象的变化。观察者构造方法中的参数expOrFn,可以是表达式,如果是表达式的话,只接受键路径,例如"a.b.c";对于更复杂的表达式,可以使用一个函数替代。
我们来看下Watcher的实现,
1.Watcher的构造方法里执行get方法里,get方法里执行expOrFn,expOrFn中对目标对象进行求值,触发Dep收集观察者;
2.当目标对象更新时,会调用观察者的update方法,如果是同步更新则接着调用run方法,如果是异步更新则执行queueWatcher方法,但无论是同步更新还是异步更新,最终都会执行run方法;
3.在run方法里,执行get方法,重新求expOrFn的值,如果有cb参数,则调用cb函数,把新值和旧值当做实参传入。
let uid = 0
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
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)
if (options) {
this.deep = !!options.deep
} else {
this.deep = false
}
this.cb = cb
this.id = ++uid
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
throw e
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
}
return value
}
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
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)
}
}
}
}
2.5 对对象属性进行增删操作的拦截
Object.defineProperty并不能拦截对象增删属性,Vue是通过Vue.set和Vue.delete实现对象增删属性拦截的。set方法里,首先将新加的属性设置为访问器属性,使其变为响应式,然后调用target.__ob__.dep.notify方法,通知观察者。del方法里,首先将属性从对象里删除,然后调用target.__ob__.dep.notify方法,通知观察者。
export function set (target: Object, key: any, val: any): any {
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
export function del (target: Object, key: any) {
const ob = (target: any).__ob__
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
2.6 对数组操作的拦截
在defineReactive函数里,如果目标对象属性为数组,则对数组调用observe方法进行侦测。
let childOb = observe(val)
对数组的侦测,首先重写数组的原型为arrayMethods;然后遍历数组,对每一个元素调用observe函数。何为arrayMethods?首先设置arrayMethods的原型为Array.prototype;然后往arrayMethods上定义7个属性,这7个属性其实是重写的7个数组变异方法。有的数组变异方法是可以新增元素的,要把新增加的元素变成响应式的;在所有的变异方法里都会调用数组的__ob__.dep.notify方法通知观察者。示意图如下:
export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
value.__proto__ = arrayMethods
this.observeArray(value)
} else {
// ...
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (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)
ob.dep.notify()
return result
})
})
然后把目标对象属性设置为访问器属性,在访问器属性的get方法里,则执行childOb.dep.depend(),收集对此数组“感兴趣”的观察者;并调用dependArray,每个数组元素同样把对此数组“感兴趣”的观察者收集为依赖,这样保证每个数组元素变更时,会通知到对此数组“感兴趣”的观察者。
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
2.7 Vue2数据变更拦截的缺陷
2.7.1 Vue2可以拦截的数据变更:
对象属性的写操作;
非根级响应式对象的增删属性操作;
数组7个变异方法的拦截。
2.7.2 Vue2不能拦截的数据变更:
Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性;
使用array[index] = item方式给数组元素赋值;
使用array.length = newLength方式改变数组长度。
三 Vue3响应式系统的实现
我们先整体看下Vue3响应式系统是怎么运作的,有个大体的概念,然后再拆分每一部分,看下每部分的实现。
3.1 先整体看下Vue3响应式系统的实现
目标对象经过reactive函数,生成Proxy代理对象,可以对5种操作进行拦截。这个过程就是“数据劫持”。示意图:
Vue3的观察者不叫Watcher,而是叫effect,它是基于ReactiveEffect接口实现的。effect初始化时,执行它的入参fn,fn里执行proxy对象的值,触发get/has/ownKeys trap。在get/has/ownKeys trap 里执行track方法,将目标对象属性和观察者存储到依赖收集表。这个过程就是“收集观察者”。示意图:
当proxy对象的值发生改变,触发deleteProperty/set trap。在deleteProperty/set trap 里执行trigger方法,从依赖收集表中找出目标对象属性对应的观察者set集合,遍历所有的观察者,执行run方法,最终会执行effect的入参fn函数。这个过程就是“通知观察者”。示意图:
Vue3响应式系统整体工作过程(鉴于掘金不支持视频,而gif最大支持5M,所以我把视频传到了B站):
3.2 reactive函数
reactive函数的作用就是生成目标对象的proxy代理对象。mutableHandlers包含proxy 拦截方法。rawToReactive、reactiveToRaw存储目标对象和proxy对象的映射关系。
export function reactive(target: object) {
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject(
target: unknown,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
const handlers = baseHandlers
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}
3.3 mutableHandlers
Vue3使用Proxy拦截了5个方法,在get/has/ownKeys trap 里通过track方法收集依赖,在deleteProperty/set trap 里通过trigger方法触发通知。
createGetter函数中,只有在用到某个对象时,才执行reactive函数对其进行数据劫持,生成proxy对象。
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
function createGetter(isReadonly: boolean, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
let res = Reflect.get(target, key, receiver)
track(target, TrackOpTypes.GET, key)
return isObject(res)
? reactive(res)
: res
}
}
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
track(target, TrackOpTypes.HAS, key)
return result
}
function ownKeys(target: object): (string | number | symbol)[] {
track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.ownKeys(target)
}
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key)
}
}
return result
}
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key)
}
return result
}
3.4 effect
Vue3的观察者不叫Watcher,而是叫effect,它是基于ReactiveEffect接口实现的。effect初始化时,执行它的入参fn,fn里执行proxy对象的值,触发get/has/ownKeys trap。
export function effect<T = any>(
fn: () => T, // 需要监听的函数
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect() // 非懒计算,则立即执行effect函数,effect函数内部执行run方法
}
return effect
}
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
return run(effect, fn, args)
} as ReactiveEffect
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
if (!effectStack.includes(effect)) {
cleanup(effect) // 把当前观察者,从依赖收集表中删除,并把当前观察者的deps字段设置为空数组
try {
effectStack.push(effect) // 进栈
return fn(...args)
} finally {
effectStack.pop() // 出栈
}
}
}
3.5 track方法和trigger方法
track方法的作用是收集观察者到依赖收集表;trigger方法的作用是从依赖收集表中找到effect,并执行effect,最终会执行effect的实参,也就是fn函数。
export function track(target: object, type: TrackOpTypes, key: unknown) {
const effect = effectStack[effectStack.length - 1]
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
if (!dep.has(effect)) {
dep.add(effect) // 将观察者添加到依赖收集表的合适位置
effect.deps.push(dep) // 将依赖收集表的Dep添加到观察者的deps数组中
}
}
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
extraInfo?: DebuggerEventExtraInfo
) {
const depsMap = targetMap.get(target)
const effects = depsMap.get(key)
const run = (effect: ReactiveEffect) => {
effect()
}
effects.forEach(run)
}
四 Vue3和Vue2在响应式系统方面的对比
4.1 Vue3和vue2的响应式系统都采用观察者模式:
Vue3的响应式系统和Vue2一样,也是观察者模式(发布订阅者模式)。所以,Vue3的响应式系统同样包含三个阶段,1.数据劫持(变动侦测);2.收集依赖(观察者);3.通知依赖(观察者)。
4.2 Vue3相比Vue2在响应式系统方面的提升:
4.2.1 数据劫持的方式
Vue3的数据劫持是通过Proxy实现的,而Vue2是通过Object.defineProperty实现的;长远来看JS引擎会继续优化Proxy,但Object.defineProperty不会再有针对性的优化,所以Proxy性能上整体优于Object.defineProperty;
总结:Vue3比Vue2有更快的性能。
4.2.2 支持数据劫持的数据类型
Vue3支持Object、Array、Map、WeakMap、Set、WeakSet六种数据类型的数据劫持,而Vue2只支持Object、Array两种数据类型;并且Vue3可以劫持对象的属性增删和数组的索引操作。
总结:Vue3支持更多数据类型的数据劫持。
4.2.3 依赖收集的时机和触发通知的时机
Vue3在目标对象进行get/has/iterate三种操作时,进行依赖收集;而Vue2只在目标对象的属性进行get操作时,进行依赖收集;
Vue3在目标对象进行set/add/delete/clear四种操作时,触发通知依赖;而Vue2只在对目标对象的属性进行set操作时,触发通知依赖。
总结:Vue3支持更多的时机来进行依赖收集和触发通知。
4.2.4 目标对象嵌套对象的数据劫持时机
Vue2会把整个data进行递归数据劫持,而Vue3只有在用到某个对象时,才对其进行数据劫持,所以Vue3响应式系统更快并且占用内存更小。想象下,一个很庞大的对象,我们并不是需要对其所有属性进行变动侦测,Vue2的方式就会导致无用的内存消耗和性能消耗。
总结:数据劫持方面,Vue3做到了“精准数据”的数据劫持,Vue3比Vue2占用更小的内存。
4.2.5 依赖收集器的差异
Vue3通过一个WeakMap作为全局的依赖收集器,Vue3依赖收集器的结构是:
Vue2则是通过被闭包引用的dep和通过observer实例引用的dep来作为依赖收集器;
总结:Vue3的依赖收集器更容易维护,可以方便的找到或者移除目标对象的依赖。
五 总结
Vue3响应式系统显著优点是:有更快的性能、占用更小的内存、支持Vue根数据增删属性的拦截、支持数组的拦截。
需要技术交流可以加微信。