最近在看vue数据响应式的实现,仅以此篇文章整理思路。主要是各个场景下的数据监听和收集依赖的实现。目前只是2.0相关的点,后面会继续vue3.0。
vue2.0中通过Object.definedProperty来监听数据状态,数组和对象的实现方式有所区分。
对象
通过Object.definedProperty来监听数据,但是我们监听数据的目的是为了通知视图去更新,所以就需要将监听到的数据状态收集起来,然后在数据更新的时候通知视图去更新,这就是依赖收集和触发依赖。我们只需要在getter中收集依赖,在setter中触发依赖。
Vue中函数defineReactive对Object.definedProperty进行封装,数组dep用来存储被收集的依赖,get收集,set触发。将依赖收集和触发依赖的代码封装为Dep类。
Dep
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub)
}
//收集依赖
depend() {
if(window.target) {
this.addSub(window.target)
}
}
//更新
notify() {
const subs = this.subs.slice() //拷贝一份数据,不污染原始数据
for( let i = 0, l = subs.length; i < l; i++ ) {
sub[i].update() //update方法会调用 watcher 的cb方法
}
}
}
对象的数据监听
function defineReactive(data, key, val) {
if(typeof val === 'object') { //val为对象时递归调用监听所有的key
new Observer(val)
}
Object.defineProperty(data, key, {
enumerable: true,
configable: true,
get: function() {
return val
},
set: function(newVal) {
if(val!==newVal) {
val = newVal
if(typeof val === 'object') { //newVal为对象时递归调用监听所有的key
new Observer(val)
}
}
}
})
}
对象的依赖收集及触发
function defineReactive(data, key, val) {
let dep = new Dep() //存储依赖
if(typeof val === 'object') { //val为对象时递归调用监听所有的key
new Observer(val)
}
Object.defineProperty(data, key, {
enumerable: true,
configable: true,
get: function() {
dep.depend() //收集依赖
return val
},
set: function(newVal) {
if(val!==newVal) {
val = newVal
if(typeof val === 'object') { //newVal为对象时递归调用监听所有的key
new Observer(val)
}
dep.notify() //通知更新
}
}
})
}
收集完之后,我们如何通知视图去更新呢? 通知的时候,可能使用到该数据的地方很多,可能类型还不一样,所以就抽象出Watcher来统一处理,数据更新的时候,我们只需要通知到Watcher,由Watcher通知其他需要更新的地方。Watcher主要做两件事,初始化时触发getter来收集依赖,setter时通知数据更新。
Watcher
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn) //调用后触发getter,读取data.a.c
this.cb = cb
this.value = this.get() //初始化调用
}
get() {
window.target = this //保存当前watcher实例
let value = this.getter(this.vm, this.vm) //依赖收集
window.target = undefined //收集完之后立刻清空
return value
}
update() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
数组
数组的数据监听
重写数组push、pop、shift、unshift、splice、sort、reverse的7个方法,去覆盖Array.prototype
const arrayProto = Array.prototype //arrayProto继承Array.prototype
export const arrayMethods = Object.create(arrayProto)
['push'、'pop'、'shift'、'unshift'、'splice'、'sort'、'reverse'].forEach(function (method) {
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
value: function mutator (...arg) {
return original(this, ...arg)
},
enumerable: false,
writable: true,
configurable: true
})
})
//在Observer中通过value.__proto__ = arrayMethods来覆盖数组原型的方法
对于不支持__proto__的浏览器,vue采用了简单粗暴的方法,将数组原型的这些方法直接遍历挂载到当前数组的身上。
const hasProto = '__proto__' in {} // 判断__proto__是否可用
const arrayKeys = Object.getOwnPropertyNames(arrayMethods) // 获取arrayMethods自身所有可枚举以及不可枚举的属性名组成的数组
class Observer {
constructor (value) {
this.value = value
if(Array.isArray(value)) {
//数组的数据监听
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value) //游走遍历obj的key,对象的数据监听
}
}
....省略部分代码....
}
function protoAugment(target, src, keys) {
target.__proto__ = src
}
function copyAugment(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
//工具函数def
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
数组的依赖收集
当我们读取数组时,也是通过属性去访问。所以数组的也是在defineReactive中的get中收集依赖,在我们调用数组相应方法时触发依赖,也就是在arrayMethods函数中触发依赖。对象的依赖列表存储在defineReactive函数中,而数组的依赖列表需要在getter和arrayMethods函数中都可以被访问到,所以数组的依赖存储在Observer中。
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep() //存储依赖列表
if(Array.isArray(value)) {
//数组的数据监听
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value) //游走遍历obj的key,对象的数据监听
}
}
....省略部分代码....
}
我们只需要在defineReactive函数中实例化一个Observer,在getter中就可以访问到dep。
function defineReactive(data, key, val) {
const childOb = observe(val)//如果val是对象或者数组时,此时得到Observer实例childOb,然后通过childOb.dep.depend()来收集依赖
let dep = new Dep() //存储依赖
if(typeof val === 'object') { //val为对象时递归调用监听所有的key
new Observer(val)
}
Object.defineProperty(data, key, {
enumerable: true,
configable: true,
get: function() {
dep.depend() //收集依赖
//为数组以及未被监听对象的子属性收集依赖
if(childOb) {
childOb.dep.depend()
}
return val
},
set: function(newVal) {
if(val!==newVal) {
val = newVal
if(typeof val === 'object') { //newVal为对象时递归调用监听所有的key
new Observer(val)
}
dep.notify() //通知更新
}
}
})
}
function observe(value, asRootData) {
if(!isObject(value)) {
return
}
//只有value是对象或者数组时,才会走这里
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else {
ob = new Observe(value)
}
return ob
}
数组的触发依赖
前面说过,数组的依赖收集在getter里面完成,依赖触发在调用方法时处理。触发依赖,我们需要访问到dep,那么如何在arrayMethods函数中访问到dep呢?我们在Observer类中new Dep就是为了此时的访问,所以我们只要能访问到Observer也就能访问到dep了。我们可以在Observer中将当前数据与Observer的实例绑定,这样我们在arrayMethods函数中通过访问自身的属性从而访问到Observer,也就是访问到dep。
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep() //存储依赖列表
def(value, '_ob_', this)
if(Array.isArray(value)) {
//数组的数据监听
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value) //游走遍历obj的key,对象的数据监听
}
}
....省略部分代码....
}
const arrayProto = Array.prototype //arrayProto继承Array.prototype
export const arrayMethods = Object.create(arrayProto)
['push'、'pop'、'shift'、'unshift'、'splice'、'sort'、'reverse'].forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this._ob_ //当前数组的_ob_
ob.dep.notify() //触发依赖
return result
})
})
监听数组中所有数据的子集
//对Observer进行改写,
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep() //存储依赖列表
def(value, '_ob_', this)
if(Array.isArray(value)) {
//数组的数据监听
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value) //游走遍历obj的key,对象的数据监听
}
}
....省略部分代码....
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
监听数组新增元素
对method进行判断,如果是push、unshift、splice这种可以新增元素的方法就直接获取新增元素,存在inserted中,然后使用ob.observeArray来监听元素的变化。
['push'、'pop'、'shift'、'unshift'、'splice'、'sort'、'reverse'].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
})
})