miniVue3的简单实现-reactive响应式实现

时间:2021-2-20 作者:admin

1.先列举对于reactive实现过程中重要的几个东西

  1. reactive, 此api可实现对数据转换为响应式数据,保证经过reative转换过的数据使用后,改变数据,界面能随之更新。
  2. proxy基本api的认识和使用,如果不太了解的话, 推荐阮一峰的ES6入门
  3. track依赖收集函数, 收集当前访问数据所需要的依赖, 在proxy的get中执行
  4. trigger依赖触发函数,通知执行收集的effect函数,在proxy的set中执行
  5. targetMap,一个WeakMap数据结构,存储数据依赖,相当于vue2中的dep
  6. effect函数,相当于vue2中的watcher,组件渲染、计算属性computed、watch函数、watchEffect函数实现都是依赖于effect函数
  7. activeEffect, 表示当前正在执行的effect,用于收集依赖
  8. effectStack,用于模拟栈结构存储effect的数组,解决effect执行过程中effect嵌套执行的场景,保证正确收集依赖函数
示例 
  const { reactive, effect } = vue;
  const effectDom = document.querySelector(".effect");

   const state = reactive([1, 2, 3]);

   effect(function () {
     effectDom.innerHTML = state
   })
   setTimeout(() => {
     state[2] = 2;
   }, 1000)
将数组初次渲染到界面中后,1秒后修改state数据, 界面自动更新

2.reactive函数

reactive函数实现
  // reactive函数
export function reactive (target) {
  return createReactiveObject(target, baseHandler)
}
// 定义createReactiveObject 创建proxy代理, 是因为代理模式有多种, 可能是只读的也可能是
// 即可读,也可设置,通过高阶函数灵活传入baseHandler, 创建不同功能的响应式数据如 readOnlyRactive和Reactive
  function createReactiveObject (target, baseHandler) {
  // 首先判断target是否是对象类型, 如果不是不许转换成响应式
  if (!isObject(target)) {
    return;
  }
  // 进行代理
  let proxy = new Proxy(target, baseHandler);
  return proxy;
}
// baseHandler
export const baseHandler = {
get (target, key, recevier) {
  // 获取key的值
  const res = Reflect.get(target, key);
  // 收集依赖

  track(target, key);
  return res;
},
set (target, key, val, recevier) {
  // /先获取旧的值
  const oldVal = Reflect.get(target, key);
   // 需要判断是新增属性值, 还是更改属性值
  // 需判断数组情况和对象情况如果是arr[10]这种形式更改的数组
  // 需要判断长度和key的大小, 其余的按对象属性走
  const hadKey = Array.isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key);
  // 设置新值
  const res = Reflect.set(target, key, val);

  if (!hadKey) {
    // 新增属性 通知依赖更新
    trigger(target, "ADD", key, val, oldVal);
  } else {
    // 修改属性 通知依赖更新
    trigger(target, "SET", key, val, oldVal);
  }
  return res;
}
}

1 rective函数中调用createReactiveObject函数创建响应式对象, 因为在源码中不光有有reactive数据, 还有readOnly数据,通过createReactiveObject参数的不同创建不同的数据类型, 我们这里只实现rective数据。

  1. createReactiveObject实现。
    2.1 createReactiveObject中第一步会先判断数据类型, 如果是基本的数据类型,不会进行响应式转换。
    2.2 用proxy对传入的数据进行代理,用baseHandler作为代理配置项,传入proxy第二个参数。
    2.3 返回proxy代理后的数据,提供给用户使用

  2. 当用户访问或设置proxy代理返回的数据时,会触发baseHandle 对象里的get和set。

  3. baseHandler中get实现逻辑。

    4.1 用Reflect.get获取值。

    4.2 用track收集数据所需的依赖(主要逻辑)。

    4.3 返回数据给用户。

  4. baseHandler中set实现逻辑。

    5.1 先使用 Reflect.get 获取旧值oldValue。

    5.2。判断当前设置的key是否存在数据target中, 如果是数组 并且key是字符串的数字, 就根据当前key的数值是否大于数组长度判断是否为新增数据还是修改数据, 如果是对象就用hasOwnProperty判断key是否存在于对象中,判断是新增还是修改。

    5.3 用Reflect.set设置新值

    5.4 触发trigger通知依赖进行更新,根据hadKey告知trigger是添加属性还是更改属性。将newValue和oldValue都传入函数中

3.track函数实现

// 用于存储正在执行的effect, 再收集依赖的步骤需要用到
let activeEffect;
//依赖存储数据解构
const targetMap = new WeakMap();
// 用于收集依赖
export function track (target, key) {
 // 获取对象存储的deps对象
 let depsMap = targetMap.get(target);
 // 如果deps对象不存在说明是第一次此对象收集依赖需新建对应的数据结构
 if (!depsMap) {
   targetMap.set(target, (depsMap = new Map()));
 }
 // 获取对象中key存储的依赖dep数据
 let deps = depsMap.get(key);
 // deps不存在说明是第一次收集, 需要新建set数据结构收集依赖
 if (!deps) {
   depsMap.set(key, (deps = new Set()));
 }
 // 当前执行的activeEffect就是我们当前值需要收集的依赖函数
 // 先判断是否已经收集过当前的副作用函数, 不要重复收集
 if (!deps.has(activeEffect)) {
   // 加入到依赖中
   deps.add(activeEffect);
   // 副作用函数也需要收集deps, 形成一个双向记录的过程
   activeEffect.deps.push(deps);
 }
}
  1. 先根据target获取到依赖的map对象depsMap,如果depsMap不存在就先创建一个Map以target为key存入targetMap中。

2.根据获取数据的key值从depsMap中获取到当前key的所有依赖deps, 如果deps不存在, 就创建一个set数据结构以key为键值存入depsMap中。

  1. 最后一步是建立一个双向的数据存储,deps存储effect副作用函数,用于下次数据变更触发执行,effect也要存储它的deps,供后续取消数据响应。

4 trigger函数的实现

// 用于触发依赖
export function trigger (target, type, key, val, oldVal) {
  // 获取对象target的依赖数据map
  let depsMap = targetMap.get(target);
  // 没有收集过依赖不许执行
  if (!depsMap) { return }
  // 获取effect数组
  let deps = depsMap.get(key);
  let effects = new Set();
  const add = (effectsSet) => {
    if (effectsSet) {
      effectsSet.forEach(effect => {
        effects.add(effect);
      })
    }
  }

  if (key != void 0) {
    //处理特殊情况对数组
    /* 情况1:
      effect(function () {
        effectDom.innerHTML = state.length
      })
      setTimeout(() => {
        state.length = 2;
      }, 1000)
      当effect中获取length时 改变length会触发依赖更新,这是正常情况 
    */
    /*情况2:
      effect(function () {
        effectDom.innerHTML = state[2]
      })
      setTimeout(() => {
        state.length = 2;
      }, 1000) 
      当用下标获取值时, 修改length不会触发更新, 因为对length未收集依赖, 
      收集的是effect中下标的依赖键值为下标, 但因为此时 length长度变小, 导致数组变化
      所以需要更新界面
    */
    //  如果修改的是length 并且target是数组
    if (key === "length" && Array.isArray(target)) {
      // 循环target的所有key的依赖
      depsMap.forEach((dep, key) => {
        // 因为在用 state.length = 2修改值时 只有当设置的length的
        // 值(val)小于 数组依赖项的下标时,才更新界面
        // 或本身就有length收集的依赖时

        if (key === "length" || key >= val) {
          // 触发更新
          add(dep);
        }
      })
    } else {
      // 根据我们传入的type处理余下特殊情况
      switch (type) {
        case "ADD":
          // 如果是数组新增并且是用的下标, 直接获取length的依赖执行
          if (Array.isArray(target)) {
            if (isIntegerKey(key)) {
              add(depsMap.get("length"));
            }
          } else {
            depsMap.forEach((dep, key) => {
              add(dep);
            })
          }
          break;
        case "SET":
          add(deps);
          break;
      }
    }

  }

  const run = (effect) => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect);
    } else {
      effect();
    }
  }
  effects.forEach(run)
}
  1. 先获取当前target数据的depMap依赖集合, 如果depMap不存在, 说明当前数据没有依赖,改变这个数据,不需要更新界面, 直接return掉

  2. 从depMap中获取key值得依赖set集合, 定义一个effects的set数据结构存储将要执行的effect,并定义向effects中添加effect的add函数方法

  3. 通过数据的改变获取相应的需要更新的依赖函数,用add添加到effects中

    3.1 首先要处理数组操作length改变数组的特殊情况,

    1– 当修改length的数值比模板中使用的数组的数据的下标小, 说明大于现在数组长度的数据都不需要在页面中展示了, 所以需要循环depsMap找出所有大于length的下标依赖并执行更新界面
    2– 当模板中直接使用数组数据, 不是单个数组的某个值时, 模板渲染时会访问数组的length数组, 所以length属性会收集对应的数组依赖, 当我们后续改变length值导致数组数据改变时,可以获取length属性的依赖并添加到effects, 去执行依赖函数,更新界面
    3.2 处理完数组的特殊情况, 下面就根据set函数中hadKey判断的是添加属性ADD操作,还是修改属性Set操作

    1– ADD添加属性
    如果是数组, 就判断key是否是数值的字符串,如果是说明是通过下标修改的数组, 这里就获取length的依赖函数集合通过add添加到effects中
    如果是对象新家属性, 就获取所有对象数据的依赖,添加到effects
    2– set修改数据, 直接获取对应key值得set集合添加到effects中

  4. 循环effects数据结构,通过run函数执行effect副作用函数执行更新操作

effect函数实现

  // 副作用函数, 相当于vue2中的watcher
 export function effect (fn) {
   // 因为副作用函数执行时, 我们需要将当前执行的副作用函数储存,获取值时使用
   // 并且需要增加一些属性, 所以用高阶函数创建effect
   let effect = createReactiveEffect(fn);
   if (!effect.options.lazy) {
     // 如果不是懒函数(即computed), 用于第一次收集依赖
     effect();
   }
 }
 // 用于给每个副作用函数增加标记, 标记唯一
   let uid = 0;

   // 真正创建副作用函数, 并做一些属性定义
 function createReactiveEffect (fn, options = {}) {
   const effect = function () {
     // 副作用函数执行完需要将activeEffect,重置为空, 防止不在effect函数中的取值收集依赖
     // !effectStack.includes(effect)判断用于处理死循序如下写法
     // effect(function () {
     //   state.a++;
     // })
     // 当effect中是state.a++时, 因为a每次增加, 所以会通知依赖更新频繁触发,
     // 导致死循环, 所以增加判断,当前effect 执行时, 因为effect在effectStack栈中
     // 而且我们已经在执行副作用函数了, 所以不需要在触发了
     if (!effectStack.includes(effect)) {
       try {
         // 先将effect存入栈中解决effect嵌套问题
         effectStack.push(effect);
         activeEffect = effect;
         fn()
       } finally {
         // 执行完后将activeEffect设置成栈中前一个effect
         effectStack.pop();
         activeEffect = effectStack[effectStack.length - 1];
       }
     }
   }
   effect.active = true;
   // 唯一标记
   effect.uid = uid++;
   // 用于存储被哪个值依赖
   effect.deps = [];
   // effect的一些其他属性, 如lazy、shelduler
   effect.options = options;
   return effect
 }
  1. effect函数内部逻辑, 先通过高阶函数createReactiveEffect创建effect,然后判断effect.options.lazy, 如果lazy是true说明是计算属性computed或watch函数, 不需要创建就执行,如果不是true就需要立即执行effect函数

2.createReactiveEffect函数实现

2.1 在createReactiveEffect中创建effect,并为effect增加一些属性如deps用于储存依赖dep、options配置项、uid标识effect的唯一性、active标识是否是活动的effect。

2.2 effect内部其实最根本就是执行传入的fn函数, fn可能是用户通过计算属性或watch、watchEffect定义的, 也可能是渲染函数。

2.3 effect函数执行时需要将effect先存入effectStack中,再将赋值给activeEffect(activeEffect= effect)然后执行fn, fn中可能访问了用proxy代理的数据,访问数据触发proxy 中的baseHandler的get方法,get中的track就会把activeEffect存入访问数据的Set结构中, 这样就完成了依赖的收集
2.4 effect执行最后需要将当前的effect函数从effectStack栈中删除, 并将栈中的最后一项赋值给activeEffect即 activeEffect = effectStack[effectStack.length – 1];

这里effectStack栈结构的作用

  1. 防止死循环

      effect(function () {
        state.a++;
      })
    

    此写法当effect执行时又改变了 state.a的值,导致触发了trigger方法,又通知effect执行,依次往复就会无线死循环
    增加!effectStack.includes(effect)判断当前effect是否在栈中来判断是否正在运行,如果在栈中,就不再执行,就会避免死循环

  2. 嵌套effect函数的情况

        const counter = reactive({ 
          num: 0, 
          num2: 0 
        }) 
        function logCount() { 
         console.log('num2:', counter.num2) 
        } 
        effect(() => { 
          effect(logCount) 
          console.log('num:', counter.num) 
        } ) 

当执行外部effect时,activeEffect被设置为外层的effect, 当内部effect执行时被设置为activeEffect

  1. 如果不使用effectStack栈结构,我们需要在effect函数执行完时将activeEffect设置为null, 所以这个例子中会导致内部effect执行完activeEffect是null当执行console.log(‘num:’, counter.num) 时,num收集不到依赖函数

  2. 当使用栈effectStack,activeEffect被设置为外层的effect,并将effect推入数组,然后执行内部effect,内部effect被推入数组,activeEffect为内层effect, 然后执行console.log(‘num2:’, counter.num2) ,num2成功收集内部effect函数为依赖, 收集完成effectStack删除内部effect,并将数组的最后一项赋值给activeEffect, 此时activeEffect是外层的effect, 然后执行console.log(‘num:’, counter.num) ,num成功收集外部effect函数作为依赖,从effectStack删除外层effect函数,因为现在数组为空,所以activeEffect被设置为null

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。