Vue源码篇:数据响应式

167 阅读5分钟

最近在看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
  })
})