function defineReactive (data, key, val) { observer() let dep = new Dep() // 新增:这样每个属性就能对应一个Dep实例了 Object.defineProperty(data, key, { configurable: true, enumerable: true, get () { dep.addSub() // 新增:get触发时会触发addSub来收集当前的Dep.target,即watcher return val }, set (newVal) { if (newVal === val) { return } else { data[key] = newVal observer(newVal) dep.notify() // 新增:通知对应的依赖 } } }) }
至此observer、Dep、Watch三者就形成了一个整体,分工明确。但还有一些地方需要处理,比如我们直接对被劫持过的对象添加新的属性是监测不到的,修改数组的元素值也是如此。这里就顺便提一下Vue源码中是如何解决这个问题的:
对于对象:Vue中提供了Vue.set和vm.$set这两个方法供我们添加新的属性,其原理就是先判断该属性是否为响应式的,如果不是,则通过defineReactive方法将其转为响应式。
对于数组:直接使用下标修改值还是无效的,Vue只hack了数组中的七个方法:pop','push','shift','unshift','splice','sort','reverse',使得我们用起来依旧是响应式的。其原理是:在我们调用数组的这七个方法时,Vue会改造这些方法,它内部同样也会执行这些方法原有的逻辑,只是增加了一些逻辑:取到所增加的值,然后将其变成响应式,然后再手动出发dep.notify()
三、以Proxy实现响应系统
Proxy是在目标前架设一层"拦截",外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,我们可以这样认为,Proxy是Object.defineProperty的全方位加强版。
依旧是三大件:Observer、Dep、Watch,我们在之前的基础再完善这三大件。
1. Dep
let uid = 0 // 新增:定义一个id class Dep { constructor () { this.id = uid++ // 新增:给dep添加id,避免Watch重复订阅 this.subs = [] } depend() { // 新增:源码中在触发get时是先触发depend方法再进行依赖收集的,这样能将dep传给Watch Dep.target.addDep(this); } addSub () { this.subs.push(Dep.target) } notify () { for (let i = 1; i < this.subs.length; i++) { this.subs[i].cb() } } }
2. Watch
class Watch { constructor (exp, cb) { this.depIds = {} // 新增:储存订阅者的id,避免重复订阅 this.exp = exp this.cb = cb Dep.target = this data[exp] // 新增:判断是否订阅过该dep,没有则存储该id并调用dep.addSub收集当前watcher addDep (dep) { if (!this.depIds.hasOwnProperty(dep.id)) { dep.addSub(this) this.depIds[dep.id] = dep } } // 新增:将订阅者放入待更新队列等待批量更新 update () { pushQueue(this) } // 新增:触发真正的更新操作 run () { this.cb() } } }
3. Observer
与Object.defineProperty监听属性不同,Proxy可以监听(实际是代理)整个对象,因此就不需要遍历对象的属性依次监听了,但是如果对象的属性依然是个对象,那么Proxy也无法监听,所以依旧使用递归套路即可。
function Observer (data) { let dep = new Dep() return new Proxy(data, { get () { // 如果订阅者存在,进去depend方法 if (Dep.target) { dep.depend() } // Reflect.get了解一下 return Reflect.get(data, key) }, set (data, key, newVal) { // 如果值未变,则直接返回,不触发后续操作 if (Reflect.get(data, key) === newVal) { return } else { // 设置新值的同时对新值判断是否要递归监听 Reflect.set(target, key, observer(newVal)) // 当值被触发更改的时候,触发Dep的通知方法 dep.notify(key) } } }) } // 递归监听 function observer (data) { // 如果不是对象则直接返回 if (Object.prototype.toString.call(data) !== '[object, Object]') { return data } // 为对象时则递归判断属性值 Object.keys(data).forEach(key => { data[key] = observer(data[key]) }) return Observer(data) } // 监听obj Observer(data)
至此就基本完成了三大件了,同时其不需要hack也能对数组进行监听。
四、触发依赖收集与批量异步更新
完成了响应式系统,也顺便提一下Vue源码中是如何触发依赖收集与批量异步更新的。
1. 触发依赖收集
在Vue源码中的$mount方法调用时会间接触发了一段代码:
vm._watcher = new Watcher(vm, () => { vm._update(vm._render(), hydrating) }, noop)
这使得new Watcher()会先对其传入的参数进行求值,也就间接触发了vm._render(),这其实就会触发了对数据的访问,进而触发属性的get方法而达到依赖的收集。
2. 批量异步更新