Vue3系列文章 —(3)响应式原理

143 阅读9分钟

本文已参与[新人创作礼]活动,一起开展掘金创作之路。

我们想要实现响应式,需要这三步:

  1. 拦截对数据的访问与更改 (数据劫持)

  2. 知道哪里用到了数据 (依赖收集)

  3. 当数据发生改变时,怎么自动通知用到数据的地方,让它们自动更新 (发布订阅模式)

数据:指响应式数据

第一步 : 怎么拦截对数据的访问与更改?

Q : 为什么我们需要拦截对数据的访问与更改?

主要是有两个理由:

  1. 我们怎么知道哪里用到了数据?
  2. 当数据改变时,我们怎么通知用到这些数据的地方自动更新? (没错,就是第二步和第三步)

一般来说,我们定义的数据只会被用到它的地方访问 ,比如:页面,或者需要用这些数据经过某些计算得到的数据。

假如我们可以拦截对数据的访问,那么一旦有人访问了这些数据,就自动调用我们事先定义好的函数(getter)来处理一些事情(比如依赖收集) ,

同理,如果我们可以拦截对数据的更改,那么一旦有人修改了这些数据,就自动调用我们事先定义好的函数(setter)来通知用到这些数据的地方进行更新

Q : 我们具体怎么拦截对数据的访问与更改?

在JS中有两种办法可以拦截对数据的访问与更改

  • 方法一 : Object.defineProperty

  • 方法二 : Proxy

    • datareactiveref函数定义的对象内部是通过 Proxy 来拦截对数据的访问与更改

    • ref函数定义的基本数据是通过 Object.defineProperty来拦截对数据的访问与更改

    • 实际上就是利用Object.definePropertyProxy来对数据(基本类型)加上gettersetter

这里不计较ProxydefineProperty的区别,前者更优

1. defineProperty函数

  • 该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性。
  • 语法:Object.defineProperty(obj, prop, descriptor)
  • obj:要添加或修改属性的对象
  • prop:要添加或修改属性的名称
  • descriptor:一个配置对象
descriptor对象的属性
  • value:prop属性的值,可以是任何有效的 JavaScript 值 默认为 undefined

  • writable:控制prop是否可以被修改,  默认为  false,即不能修改

  • configurable:控制prop是否可以遍历,  默认为  false,即不能遍历

  • enumerable:控制prop是否可以被删除,  默认为  false,即不能被删除

  • get:值为一个函数,默认为undefined

  • getter什么时候被调用?:当prop属性被访问时调用并返回prop属性的值

getter是指get属性+其值(函数)

  • set:值为一个函数,默认为undefined,该函数接受一个参数,并将该参数的新值分配给该属性
  • setter什么时候被调用?:当prop属性被修改时调用,把prop属性被修改之后的值传递给set函数的参数

setterset属性+其值(函数)

<script>
   let person = {
       name:"小明",
       age:18
   }
  let sex = "男" 
  Object.defineProperty(person, "sex", {
       value:"男",
       writable:true,
       enumerable:true,
       configurable:true,
       get:function(){
           return sex
       },
       set:function(value){
           number = value
       }
   })
   
</script>

2. Proxy函数

  • 用于创建一个对象的代理对象,然后通过操作这个代理对象来操作目标对象
  • 语法:const p = new Proxy(target, handler)
  • target:要拦截的目标对象,
  • handler:是一个对象,用来定制拦截行为
handlerd对象的属性
  • target:目标对象
  • proKey:属性名
  • value:属性值
  • receiver:proxy 实例本身
  • get(target, propKey, receiver) :拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver) :拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。

一共用13中拦截行为:es6.ruanyifeng.com/#docs/proxy

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});

3. 方法一拦截对数据的访问与更改(模拟)

  1. 假设我们要对ref函数定义的数据进行数据劫持
const sex = ref(0)
  1. Vue利用方法一对ref函数定义的所有属性(这里指基本类型的属性)加上getter,setter

这里的person假设代表存放所有响应式数据的对象

 Object.defineProperty(person, "sex", {
       value:"man",
       writable:true,
       enumerable:true,
       configurable:true,
       get:function(){
           return sex
       },
       set:function(value){
           number = value
       }
   })

4. 方法二拦截对数据的访问与更改

  1. 假设我们要对reactive函数定义的响应式数据做数据劫持
ren = {
    name: '哈哈', 
    age: 18,
    person: {
        sex:"hello"
    }
}
  1. 利用方法二对reactive函数返回的对象中的所有基本类型的属性加上gettersetter
  • 如果属性是对象或者数组,那么递归调用方法二来对这个对象的所有基本类型属性加上gettersetter

  • data函数返回的对象实际上是通过reactive函数来弄的

reactive(ren)
reactive函数
function reactive(target) {
  // 通过proxy将对象变为响应式
  const observed = new Proxy(target, baseHandler);
  //  返回proxy代理后的对象
  return observed;
}

const baseHandler = {
 get(target, key) {
   // Reflect.get
   const res = Reflect.get(target, key);
   // @todo 依赖收集
   // 尝试获取值obj.age,触发getter
   track(target, key);
   return typeof res === "object" ? reactive(res) : res;
 },
 set(target, key, val) {
   const info = { oldValue: target[key], newValue: val };
   // Reflect.set
   // target[key] = val;
   const res = Reflect.set(target, key, val);
   // @todo 响应式去通知变化 触发执行,effect函数是响应式对象修改触发的
   trigger(target, key, info);
 },
};

第二步:依赖收集

依赖收集的核心思想就是:观察者模式

在观察者模式中

    • 观察目标:用到的响应式数据
    • 观察者:视图、计算属性、侦听器这些用到响应式数据的东西

依赖收集的时机

  • 依赖是指观察者Watcher
  • 只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个watcher收集到Dep中

依赖收集的原理

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

  • Dep : 扮演观察目标的角色
  • Watcher: 扮演观察者的角色
  • Observer: 扮演辅助的角色
三者的关系

Dep实际上就是对Watcher的管理

  • Dep是一个发布者,可以被多个观察者订阅,Dep中有一个subs存放依赖收集的观察者,以便在数据变更的时候通知所有的watcher
  • DepObserver的关系就是Observer监听整个data,遍历data的每个属性给每个属性加上gettersetter, 当getter被调用的时候往Dep类里塞依赖(dep.depend),当setter被调用的时候通知所有watcher进行update(dep.notify)
  • DepWatcher的关系就是watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者, dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新

data: 包括所有声明的响应式数据

Observer

defineReactivegētter调用时进行依赖收集,这里就是Vue收集依赖的入口

Observer类的实例挂在__ob__属性上,提供后期数据观察时使用,实例化Dep类实例,并且将对象/数组作为value属性保存下来

如果value是个对象,就执行walk()过程,遍历对象把每一项数据都变为可观测数据(调用defineReactive方法处理)

如果value是个数组,就执行observeArray()过程,递归地对数组元素调用observe()。

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() //订阅者收到消息回调
         }
     })
 }
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())
    }
}
Watcher

关心在数据变更之后能得获得通知,通过回调函数进行更新

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

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

note:watcher有三种:render watcher(模板渲染版观察者)、 computed watcher(计算属性版观察者)、user watcher(监视属性版观察者)

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的时候进行调用

总结

依赖收集
  1. computed 属性初始化时,触发 computed watcher 依赖收集
  2. watch属性初始化时,触发 user watcher 依赖收集
  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. 在组件中对响应式数据进行了修改,触发了对应数据的 setter
  2. 然后调用 dep.notify()
  3. 最后遍历 subs数组(Watcher 实例),调用每一个 watcher update 方法

核心就是订阅者模式,Watcher 订阅 Dep,当数据发生变化时,Dep实例会遍历subs数组通知每个订阅的Watcher

代码逻辑

set -> 
dep.notify() -> 
subs[i].update() -> 
watcher.run() || queueWatcher(this) -> 
watcher.get() || watcher.cb -> 
watcher.getter() -> 
vm._update() -> 
vm.__patch__()