vue3响应式原理: reactive、computed、ref源码实现


简要描述了vue3的reactive响应式原理,包括effect、track和trigger函数在实现数据变化监听中的作用。此外,还给出了简易版的computed原理的实现。

1、首先我们要创建 effect.js 文件,在里面写上一个全局变量 effectSave 用来存储传入的函数,然后再写一个 effect 函数,里面用来接收传入的 fn 形参,再写一个闭包函数 _effect,在闭包函数内部自动执行传入的 fn,并且把闭包赋值给 effectSave 全局变量。

  实现:effect.js。注:方法里有些没提到的是为computed准备的
    // 生成全局变量
    let effectSave;
    // 创建 effect 函数,接收 fn 函数。
    const effect = (fn, options) => {
      const _effect = function () {
        effectSave = _effect;
        // 是为了 computed 的回调, 在 trigger 函数里,把 computed 里传入来的 options 里的方法调用更改脏值,已用来不用每次获取 computed 的参数都会去调用计算。
        effectSave.options = options
        let res = fn();
        return res;
      }
      _effect(); // 要自走一下,因为要把传入的 fn 函数保存在全局变量 effectSave 里;
      return _effect;
    }

2、在写一个 tarck 函数,用来精准的把 effectSave 全局变量根据 target 存储到相应的 WeakMap 里。

  实现:effect.js
    // 全局变量用来根据传入的 target key 存储 effectSave 方法
    let weakMap = new WeakMap();
    const tarck = (target, key) = {
      // 首先获取
      let depsMap = weakMap.get(target);
      // 如果没获取到,就是说没存储过这个 target 下面就来存储一下新值
      if(!depsMap) {
        depsMap = new Map();
        weakMap.set(target, depsMap);
      }
      // 获取 key 对应的响应函数集, 没存过下面就存储一下新值
      let deps = depsMap.get(key);
      if(!deps) {
               // 建立当前key 和依赖的关系,因为一个key 会有多个依赖
               // 为了防止重复依赖,使用set
        deps = new Set()
        depsMap.set(key, deps)
      }
      // 把 effectSave 全局变量添加到 Set 里。
      deps.add(effectSave)
    }

3、再写一个 trigger 函数,用来调用 前面储存的 effectSave 函数。

  实现:effect.js
    // 同样接收一个 target 和 key 用来去 weakMap 里寻找 effectSave 函数。
    const trigger = (target, key) => {
      let depMap = weakMap.get(target);
      let deps = depMap.get(key)
      deps.forEach(effect = > {
        // computed 用
        if(effect?.options?.scheduler) {
          effect?.options?.scheduler()
        } else {
          effect();
        }
      })
    }

4、现在 effect 文件已经创建好了,就开始写 reactive 文件,要使用 proxy 来实现数据劫持来完成某些操作。

  实现:reactive.js
    const reactive = (target) => {
      // 先判断一下类型
      if(typeOf target != 'object') {
        console.error('reactive 只能接收 object 类型的数据')
      }
      return new Proxt({
        // 每次视图渲染的时候都会触发这个 get
        get(target, key, r) {
          let res = Reflect.get(target, key, r)
          // 每次视图渲染的时候都会获取这个值,所以在这个时候把 target 存入对应的 target 的 weakMap 里; 需要调用 tarck 方法
          tarck(target, key);
          // 用于多层的 object
          if(target != null && typeOf target == 'object') {
            reactive(target)
          }
          // 把值返回出去
          return res
        },
        // 每次修改视图数据的时候,都会触发这个 set 
        set(target, key, value, r) {
          let res = Reflect.set(target, key, value, r); // set 需要返回布尔值这个 Reflect.set 的返回值就是布尔值
          // 修改就触发, 那么视图也得更新, 就得调用之前存储的 effectSave 方法, 调用 trigger 方法去找到并调用
          trigger(target, key); // 为什么不用传 value 是因为 Reflect.set 的时候已经把 target 的数据变为最新的数据了。
          return res
        }
      })
    }

5、reactive 文件已创建好,通过 html 文件验证

  实现:index.html
    <body>
      <div id="app"></div>
    </body>
    let user = reactive({
      name: '王搏',
      age: 22
    })
    // effect 来挂载方法,传入一个函数,effect 方法那边形参接收到自走一下,把传入的函数赋值给 effectSave
    effect(() => {
      document.querySelect("#app").innerHtml = `${user.name}-${user.age}`;
    })
       setTimeout(_ => {
           user.name = '王一搏'
       }, 1000)
  实现逻辑:
    ①首先是调用 reactive 传入一个对象, 在函数内部创建一个 Proxy 把传入的对象实行 Proxy 数据劫持, 当使用 user 的时候会触发 Proxy 的 get 属性, 当修改 user 数据的时候会触发 Proxy 的 set 属性。
    ②调用 effect 传入一个函数,传入的函数内部是挂载 dom 渲染数据到视图, effect 函数内部接收的 fn 形参函数自走(渲染视图)把它写在一个闭包里并且把闭包赋值给 effectSave 存储一下, 以便根据 target 存储到 WeekMap 里。
    ③effect 传入的函数里使用了 user.name 的数据,所以会触发 Proxy 的 get 属性, get 属性内部调用了 tarck 方法并传入了 target 和 key, target 就是调用 reactive 时传入的一个对象, key 就是当前 user.name 要获取的 key(key=name)。tarck 里做了处理, 就是创建一个全局变量 new WeakMap 通过 target 去保存全局变量 effectSave(调用effect时传入的函数) 以便后面调用更新视图, 具体是先通过 weakMap 的 get 去获取 target 的数据, 获取不到就创建一个 new Map() 调用 WeakMap 的 set 传入 target 和 new Map 值存入 WeakMap, 然后接收刚才 set 的返回, 用 newMap 的 set 去存入 key 和 new Set() , 最后根据 newMap.get 接收到的值去把 effectSave(调用effect时传入的函数) 通过 .add() 添加进去。
    ④更新:当 user.name = '王一搏' 修改的时候,会触发 Proxy 的 set 属性, set 属性内部修改好最新的 target 数据之后, 会调用 trigger 函数, 传入 target 和 key 去找到对应的 effectSave(调用effect时传入的函数) 调用。就实现了视图的更新。


6、实现 computed, 创建 computed.js 文件

  实现: computed.js
    const computed = (target) => {
      let _value = effect(target, {
        scheduler: () => _dirty = true;
      });
      let catchValue; // 存储值
      let _dirty = true; // 为了避免每次获取都会去调用 _value 计算。
      class ComputedRefImpl {
        // 每次获取这个值的时候都会执行 get 方法
        get() {
          // 首次进来 _dirty 是 true, 如果下次数据有修改就需要把 _dirty 改为 true 所以 effect 里,还要接收一个函数用来回调把 _dirty 的值改为 true, 重新渲染视图的时候, 就能重新计算获取了。
          if(_dirty) {
            catchValue = _value();
            // 计算过后就把 _dirty 改为 false;
            _dirty = false;
          }
          return catchValue;
        }
      }
      return ComputedRefImpl
    }

7、computed 文件已创建成功, 通过 html 文件验证

  实现:index.html
    <body>
      <div id="app"></div>
    </body>
    let user = reactive({
      name: '王搏',
      age: 22
    })
    let userInfo = computed(() => {
      return `用户信息:姓名:${user.name}。年龄:${user.age}`
    })
    // effect 来挂载方法,传入一个函数,effect 方法那边形参接收到自走一下,把传入的函数赋值给 effectSave
    effect(() => {
      document.querySelect("#app").innerHtml = `
        用户信息:${userInfo}
      `;
    })
  实现逻辑:
    ①调用 computed 传入的方法首先会自走一次,然后调用 effect 方法, 把调用 computed 里写的方法和在 computed 函数里定义的一个对象 scheduler 方法(options),传入赋值给 effectSave 里。这个 options 的作用是修改脏值,防止每次获取 computed 值的时候,都会重新计算。
    ②首次自走的时候会访问 user, 然后 user 的 proxy get 触发去把当前访问的 target 存入 weakMap 里。 存入 weakMap 之后,再把 weakMap 对应的 target 的值里添加一个 new Map 根据当前传入的 key 去创建 new Set 再把这个 key 和 new Set 加入到通过 target 找到的 weakMap 里。再把 computed 函数里 effect 传入的方法赋值的 effectSave 加入到这个 Set 里。
    ③每当 user 数据发生变化,就会调用 user 的 proxy set 方法,然后去 trigger 里通过当前修改的 target 找 weakMap 值, 找到后,根据当前修改的 key 去获取那个 Set 里存的方法(里面有 dom 挂载传入的方法,还有 computed 传入的方法)因为是 computed 创建在前所以先存的是 computed 里更改脏值 _dirty 的方法 scheduler 所以循环 Set 的时候就把脏值 _dirty 改为了 true。 然后才会调用挂载传入的 dom 函数, 当 dom 函数里的 userInfo 被调用了, 所以又会去调用 computed 方法,然后触发 computed 函数里的 ComputedRefImpl 类 get 属性,因为前面 scheduler 把 _dirty 脏值改为了 true, 所以获取的时候就会重新去调用 computed 传入的方法去更新最新的数据到页面。

8.ref 简述一下

ref 是由类创建的(扩展是 reactive),ref 需要 value 修改值的原因是类里面的 get value 和 set value 做了数据劫持,劫持到就会去更新视图。如果 ref 传的是 object 类的 constructor 会判断如果传的是 object 会去调用 reactive 来做响应式。
const isObject = (val) => val !== null && typeof val === 'object'
const toReactive = (value) => return isObject(value) ? reactive(value) : value
class RefImpl {
      // 源码是接受俩参数,第二个主要是判断 shallowRef
    constructor(value) {
        this._rawValue = value;
        this._value = toReactive(value); // 里面判断了是否为对象,如果是对象的话就去调用reactive不是就直接原样返回
    }
    get value() {
        return this._value
    }
    set value(newValue) {
        if(this.__rawValue != this._value) {
            console.log('需要更新视图')
        }
    }
}

   

208

声明:Web前端小站 - 前端博客 - 王搏的个人博客|版权所有,违者必究|如未注明,均为原创

转载:转载请注明原文链接 - vue3响应式原理: reactive、computed、ref源码实现

孙瑞杰生日