Vue3的reactive、computed等api的简单实现

前言

Vue3已经出来好几年,之前了解过之后又忘记,今天来简单的了解一下reactive、effect、ref、toRef、toRefs、computed 使用和实现。

Vue3的reactive、effect、ref、toRef、toRefs、computed 实现

reactive实现

基本使用

 const { reactive, effect }  = VueReactive;
// 数据响应式
const state = reactive({name:"张三"});

// 在方法中获取值
effect(()=>{
  app.innerHTML = state.name;
})

// 赋值,effect再次执行,页面动态改变
state.name = '李四';

响应式实现思路

  • proxy实现数据的响应式,get中执行track收集,set中触发trigger更新
  • effect方法传入函数,函数被立即执行,里面访问数据时,触发get中的track,将数据和effect关联
  • 该数据数据被设置时,触发set中的trigger,取出数据中关联的effect,再次执行
proxy代理
const isObject = t => t && typeof t === 'object';
function reactive(target) {
  return new Proxy(target, {
      get(target,key,receiver) {
          // 获取值
          const res = Reflect.get(target, key, receiver);
          
          // 收集effect
          track(target, key);
          
          // 如果是对象,递归执行
          if(isObject(res)) {
            return reactive(res);
          }
          
          return res;
      },
      set(target,key,value,receiver) {
          const oldValue = target[key];
          const result = Reflect.set(target, key, value, receiver);
          if(value !== oldValue) {
            // 触发effect执行
            trigger(target,key)
          }
          return result;
      }
  })
}

effect方法
  • effect方法,传入的函数,在执行中触发get
function effect(fn) {
  const _effect = createEffect(fn);
  _effect()
  return _effect
}

let activeEffect;
// 栈结构存储,防止嵌套的effect
const effectStack = [];
function createEffect(fn) {
  const effect = function() {
    try {
      effectStack.push(effect);
      activeEffect = effect;
      return fn()
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
    }
  }
  return effect;
}
track方法
  • track方法,在proxy的get中执行,收集effect,让它与被访问的数据产生关联
const targetMap = new WeakMap();
function track(target, key) {
  let depMaps = targetMap.get(target);
  if(!depMaps) {
    targetMap.set(target, (depMaps = new Map())
  }
  let deps = depMaps.get(key);
  if(!deps) {
    depMaps.set(key, (deps = new Set()))
  }
  if(!deps.has(activeEffect)) deps.add(activeEffect);
}
trigger方法
  • trigger方法,在proxy的set中执行,查看该属性是否存有effect方法,存有方法说明属性之前被get访问过,再次执行effect方法
function trigger(target, key) {
  const depMaps = targetMap.get(target);
  if(!depMaps) return
  
  const effectSet = new Set();
  const add = (effects) => {
    if(effects) {
      effects.forEach(e => {
        effectSet.add(e);
      });
    }
  }
  add(depMaps.get(key))

  effectSet.forEach(e => e())
}

ref实现

  • 原因:因为proxy只能传对象,所以对于初始值的响应式来说,提供了ref的api
  • 思路:通过class的get和set中收集和触发effect方法,实现使用的vue2的劫持

使用

 const { ref, effect }  = VueReactive;
// 数据响应式
const age = ref(12);

// 在方法中获取值
effect(()=>{
  app.innerHTML = age.value;
})

// 赋值,effect再次执行,页面动态改变
age.value = 30;

实现

function ref(rawValue) {
  return new RefImp(rawValue);
}

class RefImp {
  constructor(rawValue) {
    this._value = rawValue
    this.rawValue = rawValue
  }

  get value() {
    // 收集effect
    track(this, 'value')
    return this._value;
  }
  set value(newValue) {
    if (this._value !== newValue) {
      this._value = newValue
      this.rawValue = newValue
      // 触发effect执行
      trigger(this, 'value', newValue)
    }
  }
}

toRef和toRefs

toRef和toRefs作用是为了减少state中的访问,将数据解构出来,直接赋值和访问,响应式还是依赖的reactive

使用

 const { reactive, effect, toRef, toRefs }  = VueReactive;
// 数据响应式
const state = reactive({name:"张三", age: 20});

// name通过toRef处理,可以直接使用
const name = toRef(state, 'name');

// toRef处理整个state
// const { name, age } = toRefs(state);

// 在方法中获取值
effect(()=>{
  app.innerHTML = name.value;
})

// 赋值,effect再次执行,页面动态改变
name.value = '李四';

实现

  • 通过拦截set和get实现
class ObjectRefIml {
  constructor(target, key) {
    this.target = target
    this.key = key
  }

  get value() {
    return this.target[this.key]
  }

  set value(newValue) {
    this.target[this.key] = newValue
  }
}

function toRef(target, key) {
  return new ObjectRefIml(target, key);
}

function toRefs(target) {
  let ret = Array.isArray(target) ? new Array(target.length) : {};
  for (let key in target) {
    ret[key] = toRef(target, key)
  }
  return ret;
}

计算属性

使用

const { ref, computed }  = VueReactive;
// 数据响应式
const age = ref(10);

// 创建计算属性,不会立马执行
const myAge = computed(()=>{
    return age.value + 8
})

// 读取时,执行computed传入的函数
console.log(myAge.value)

// 重复读取,去缓存里取值
console.log(myAge.value)

setTimeout(()=>{
    age.value = 20;
    // 访问时,重新执行获取值
    console.log(myAge.value)
})

实现

  • 将传入的getter函数做为effect,访问变量时才执行effect,同时收集依赖(getter函数中使用到的响应式数据)
  • 数据变化时,不执行effect方法,但是执行传入sch方法,将_dirty值改为true,下次访问计算属性,重新执行getter取新值
function computed(getterOrOptions) {
  // 取出参数中的getter函数
  let getter, setter;
  if (typeof getterOrOptions === 'function') {
    getter = getterOrOptions;
    setter = () => {
      console.warn('computed value must be readonly')
    }
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
  // 创建计算属性的实例
  return new ComputedRefImp(getter, setter);
}

class ComputedRefImp {
  constructor(getter, setter) {
    this._dirty = true;
    this._value = undefined;
    this.setter = setter
    // 将getter传入effect中,lazy为true时,不会立马执行effect
    this.effect = effect(getter, { lazy: true, sch:()=>{
      // 在触发trigger时,不会执行effect方法,执行sch方法
      if(!this._dirty) this._dirty = true;
    }})
  }

  get value() {
    // 第一次执行effect,取到新值,后面不再重复执行
    if(this._dirty) {
      this._value = this.effect();
      this._dirty = false;
    }
    return this._value
  }

  set value(newValue) {
    this.setter(newValue)
  }
}

// 数据变化时,trigger方法执行时,不执行effect方法,执行传入的sch
function trigger(target, key) {
    // ...
    effectSet.forEach(e => {
    // 存在sch方法,说明是计算属性的effect,只需将sch执行
    if(e.options.sch) {
      e.options.sch(e);
    } else {
      e();
    }
  })
}

最后

这里只是简单的介绍Vue3响应式相关的几个api,很多特殊场景都未考虑进去,想要深入了解原理,还是需要自己深入源码。总体来说Vue3使用了新的proxy属性,简化了整个响应式流程;相比于Vue2没有一开始对数据做响应式处理,访问时才会代理,减少内存消耗。

原文链接:https://juejin.cn/post/7258233838371061821 作者:竹业

(0)
上一篇 2023年7月23日 上午11:00
下一篇 2023年7月23日 上午11:10

相关推荐

发表回复

登录后才能评论