【Vue源码学习】依赖收集

3,156 阅读6分钟

前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些数据同样会出发setter导致重新渲染,所以vue在这里做了优化,通过收集依赖来判断哪些数据的变更需要触发视图更新。

前言

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~

我们先来考虑两个问题:

  • 1.我们如何知道哪里用了data里面的数据?
  • 2.数据变更了,如何通知render更新视图?

在视图渲染过程中,被使用的数据需要被记录下来,并且只针对这些数据的变化触发视图更新

这就需要做依赖收集,需要为属性创建 dep 用来收集渲染 watcher

我们可以来看下官方介绍图,这里的collect as Dependency就是源码中的dep.depend()依赖收集,Notify就是源码中的dep.notify()通知订阅者

响应式原理.png

依赖收集中的各个类

Vue源码中负责依赖收集的类有三个:

  • Observer:可观测类,将数组/对象转成可观测数据,每个Observer的实例成员中都有一个Dep的实例(上一篇文章实现过这个类)

  • Dep:观察目标类,每一个数据都会有一个Dep类实例,它内部有个subs队列,subs就是subscribers的意思,保存着依赖本数据的观察者,当本数据变更时,调用dep.notify()通知观察者

  • Watcher:观察者类,进行观察者函数的包装处理。如render()函数,会被进行包装成一个Watcher实例

依赖就是Watcher,只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个watcher收集到Dep中。Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的watcher都通知一遍,这里我自己画了一张更清晰的图:

vue响应式原理.png

Observer类

这个类我们上一期已经实现过了,这一期我们主要增加的是defineReactive在劫持数据gētter时进行依赖收集,劫持数据setter时进行通知依赖更新,这里就是Vue收集依赖的入口

class Observer {
     constructor(v){
         // 每一个Observer实例身上都有一个Dep实例
         this.dep = new Dep()
        // 如果数据层次过多,需要递归去解析对象中的属性,依次增加set和get方法
        def(v,'__ob__',this)  //给数据挂上__ob__属性,表明已观测
        if(Array.isArray(v)) {
            // 把重写的数组方法重新挂在数组原型上
            v.__proto__ = arrayMethods
            // 如果数组里放的是对象,再进行监测
            this.observerArray(v)
        }else{
            // 非数组就直接调用defineReactive将数据定义成响应式对象
            this.walk(v)
        }
        
     }
     observerArray(value) {
         for(let i=0; i<value.length;i++) {
             observe(value[i])
         }
     }
     walk(data) {
         let keys = Object.keys(data); //获取对象key
         keys.forEach(key => {
            defineReactive(data,key,data[key]) // 定义响应式对象
         })
     }
 }

 function  defineReactive(data,key,value){
     const dep = new Dep() //实例化dep,用于收集依赖,通知订阅者更新
     observe(value) // 递归实现深度监测,注意性能
     Object.defineProperty(data,key,{
         configurable:true,
         enumerable:true,
         get(){
             //获取值
             // 如果现在处于依赖的手机阶段
             if(Dep.target) {
                 dep.depend()
             }
            //  依赖收集
            return value
         },
         set(newV) {
             //设置值
            if(newV === value) return
            observe(newV) //继续劫持newV,用户有可能设置的新值还是一个对象
            value = newV
            console.log('值变化了:',value)
            // 发布订阅模式,通知
            dep.notify()
            // cb() //订阅者收到消息回调
         }
     })
 }

Observer类的实例挂在__ob__属性上,提供后期数据观察时使用,实例化Dep类实例,并且将对象/数组作为value属性保存下来 - 如果value是个对象,就执行walk()过程,遍历对象把每一项数据都变为可观测数据(调用defineReactive方法处理) - 如果value是个数组,就执行observeArray()过程,递归地对数组元素调用observe()

Dep类(订阅者)

Dep类的角色是一个订阅者,它主要作用是用来存放Watcher观察者对象,每一个数据都有一个Dep类实例,在一个项目中会有多个观察者,但由于JavaScript是单线程的,所以在同一时刻,只能有一个观察者在执行,此刻正在执行的那个观察者所对应的Watcher实例就会赋值给Dep.target这个变量,从而只要访问Dep.target就能知道当前的观察者是谁。

var uid = 0
export default class Dep {
    constructor() {
        this.id = uid++
        this.subs = [] // subscribes订阅者,存储订阅者,这里放的是Watcher的实例
    }

    //收集观察者
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // 添加依赖
    depend() {
        // 自己指定的全局位置,全局唯一
      //自己指定的全局位置,全局唯一,实例化Watcher时会赋值Dep.target = Watcher实例
        if(Dep.target) {
            this.addSub(Dep.target)
        }
    }
    //通知观察者去更新
    notify() {
        console.log('通知观察者更新~')
        const subs = this.subs.slice() // 复制一份
        subs.forEach(w=>w.update())
    }
}

Dep实际上就是对Watcher的管理,Dep脱离Watcher单独存在是没有意义的。

  • Dep是一个发布者,可以订阅多个观察者,依赖收集之后Dep中会有一个subs存放一个或多个观察者,在数据变更的时候通知所有的watcher
  • DepObserver的关系就是Observer监听整个data,遍历data的每个属性给每个属性绑定defineReactive方法劫持gettersetter, 在getter的时候往Dep类里塞依赖(dep.depend),在setter的时候通知所有watcher进行update(dep.notify)

Watcher类(观察者)

Watcher类的角色是观察者,它关心的是数据,在数据变更之后获得通知,通过回调函数进行更新。

由上面的Dep可知,Watcher需要实现以下两个功能:

  • dep.depend()的时候往subs里面添加自己
  • dep.notify()的时候调用watcher.update(),进行更新视图

同时要注意的是,watcher有三种:render watcher、 computed watcher、user watcher(就是vue方法中的那个watch)

var uid = 0
import {parsePath} from "../util/index"
import Dep from "./dep"
export default class Watcher{
    constructor(vm,expr,cb,options){
        this.vm = vm // 组件实例
        this.expr = expr // 需要观察的表达式
        this.cb = cb // 当被观察的表达式发生变化时的回调函数
        this.id = uid++ // 观察者实例对象的唯一标识
        this.options = options // 观察者选项
        this.getter = parsePath(expr)
        this.value = this.get()
    }

    get(){
        // 依赖收集,把全局的Dep.target设置为Watcher本身
        Dep.target = this
        const obj = this.vm
        let val
        // 只要能找就一直找
        try{
            val = this.getter(obj)
        } finally{
            // 依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。
            Dep.target = null
        }
        return val
        
    }
    // 当依赖发生变化时,触发更新
    update() {
        this.run()
    }
    run() {
        this.getAndInvoke(this.cb)
    }
    getAndInvoke(cb) {
        let val = this.get()

        if(val !== this.value || typeof val == 'object') {
            const oldVal = this.value
            this.value = val
            cb.call(this.target,val, oldVal)
        }
    }
}

要注意的是,watcher中有个sync属性,绝大多数情况下,watcher并不是同步更新的,而是采用异步更新的方式,也就是调用queueWatcher(this)推送到观察者队列当中,待nextTick的时候进行调用。

这里的parsePath函数比较有意思,它是一个高阶函数,用于把表达式解析成getter,也就是取值,我们可以试着写写看:

export function parsePath (str) {
   const segments = str.split('.') // 先将表达式以.切割成一个数据
  // 它会返回一个函数
  	return obj = > {
      for(let i=0; i< segments.length; i++) {
        if(!obj) return
        // 遍历表达式取出最终值
        obj = obj[segments[i]]
      }
      return obj
    }
}

Dep与Watcher的关系

watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者, dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

总结

依赖收集

  1. initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集
  2. initState 时,对侦听属性初始化时,触发 user watcher 依赖收集(这里就是我们常写的那个watch)
  3. render()时,触发 render watcher 依赖收集
  4. re-render 时,render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。
observe->walk->defineReactive->get->dep.depend()->
watcher.addDep(new Dep()) -> 
watcher.newDeps.push(dep) -> 
dep.addSub(new Watcher()) -> 
dep.subs.push(watcher)

派发更新

  1. 组件中对响应的数据进行了修改,触发defineReactive中的 setter 的逻辑
  2. 然后调用 dep.notify()
  3. 最后遍历所有的 subs(Watcher 实例),调用每一个 watcherupdate 方法。
set -> 
dep.notify() -> 
subs[i].update() -> 
watcher.run() || queueWatcher(this) -> 
watcher.get() || watcher.cb -> 
watcher.getter() -> 
vm._update() -> 
vm.__patch__()

推荐阅读

原文首发地址点这里,欢迎大家关注公众号 「前端南玖」

我是南玖,我们下一期见!!!