• Vue3源码【一】—— ref&reactive响应式原理及简单实现


    响应式 ref、reactive


    源码地址:https://github.com/vuejs/core

    首先还是从最开始学的ref的源码看起,他的路径在packages/reactivity/src/ref.ts,这里看源码分析就直接将源码执行的步骤给他粘贴出来了哈。首先我们看一下ref是怎么创建的

    1、创建Ref

    // 第一步,我们还是直接到ref关键字,可以看到这个,这个就是我们使用的ref()用来创建响应式对象的关键。他会去调用createRef,并且第二个指定了false
    export function ref(value?: unknown) {
      return createRef(value, false);
    }
    
    // 第二步,顺着上面往下执行,会调用createRef,这个时候我们知道第二个参数false是指什么了,也就是shallow,这个时候可能会想到shallowRef?
    // shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。那默认创建的这个ref指定了false,那要是shallowRef调用createRef创建Ref是不是就是指定的true呢?这个我们后面再看
    // 在这里先判断isRef,这个很好理解,就是看入参的是不是一个ref了,
    function createRef(rawValue: unknown, shallow: boolean) {
      if (isRef(rawValue)) {
        return rawValue;
      }
      return new RefImpl(rawValue, shallow);
    }
    
    // 第三步,直接看RefImpl,这个就是将一个变量给包装成Ref(响应式对象)
    // 这里看构造函数,先都会判断一下shallow是真还是假,响应式对象入的是false,他数据包装会变成toRaw和toReactive
    // 在这里我们知道reactive是用来包对象类型的,这里ref创建本质上也是对调reactive的方法,同时我们也知道了为什么使用ref包的对象要加一个.value取取值赋值
    // 看一下shallowRef的构造,果然就是return createRef(value, true),这样也解释了shallowRef为什么处理的是基本数据类型
    // 看一下isRef方法,return !!(r && r.__v_isRef === true) r就是RefImpl实例对象,用来判断的也就是这个r.__v_isRef === true 是否为ref
    class RefImpl<T> {
      private _value: T;
      private _rawValue: T;
    
      public dep?: Dep = undefined;
      public readonly;
      __v_isRef = true;
    
      constructor(
        value: T,
        public readonly __v_isShallow: boolean
      ) {
        this._rawValue = __v_isShallow ? value : toRaw(value);
        this._value = __v_isShallow ? value : toReactive(value);
      }
    
      get value() {
        trackRefValue(this);
        return this._value;
      }
    
      set value(newVal) {
        const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
        newVal = useDirectValue ? newVal : toRaw(newVal);
        if (hasChanged(newVal, this._rawValue)) {
          this._rawValue = newVal;
          this._value = useDirectValue ? newVal : toReactive(newVal);
          triggerRefValue(this, DirtyLevels.Dirty, newVal);
        }
      }
    }
    

    2、依赖收集

    我们先看一个他的get
    value是这么拿到值的,也就是trackRefValue方法。首先他在处理的时候先通过toRaw转成原始对象,从这里往下的源码就做了一些删减,有一些对数据进行异常判断处理的这里就都不展示了,主要看执行逻辑

    export function trackRefValue(ref: RefBase<any>) {
      // true && undefined
      if (shouldTrack && activeEffect) {
        // 先转成原始对象
        ref = toRaw(ref)
        trackEffect(
          activeEffect,
          // ref.dep 不存在就调用createDep赋值给ref.dep  本质上是一个Map
          (ref.dep ??= createDep(
            () => (ref.dep = undefined),
            ref instanceof ComputedRefImpl ? ref : undefined,
          )),
          void 0,
        )
      }
    }
    

    重要的还是这个trackEffect方法。简单来说就是将里面的所有属性都给收集到一个map当中,通过这个map来做统一的依赖控制。后面取值也会从Map当中取值。

    export function trackEffect(
      effect: ReactiveEffect,
      dep: Dep,
      debuggerEventExtraInfo?: DebuggerEventExtraInfo
    ) {
      // 默认值 eff._trackId = 0
      if (dep.get(effect) !== effect._trackId) {
        dep.set(effect, effect._trackId);
        // 默认值 effect._depsLength = 0
        const oldDep = effect.deps[effect._depsLength];
        if (oldDep !== dep) {
          if (oldDep) {
            cleanupDepEffect(oldDep, effect);
          }
          // effect.deps 本质上还是一个Map对象,在这里将所有的依赖收集起来
          effect.deps[effect._depsLength++] = dep;
        } else {
          effect._depsLength++;
        }
      }
    }
    

    3、依赖触发

    这里从set重新设置值的时候开始执行,核心方法在triggerEffects。进来线通过遍历所有的依赖,找到我们需要修改的依赖的值,然后重新赋值,执行effect.trigger()
    ,到这里完成了依赖的触发。同时可以看一下这个effect是个什么,在这里面会接收一个匿名函数fn,并且在这里他是回去走run方法的。他的本质还是ReactiveEffect类的实例,他的run方法就是实例的run。最后回去执行那个匿名函数fn,也就是更改视图的方法 document.querySelector('#xxx').innerHTML = xxx
    。到这里就完成了依赖值的更改以及视图的实时响应

    export function triggerEffects(
      dep: Dep,
      dirtyLevel: DirtyLevels,
      debuggerEventExtraInfo?: DebuggerEventExtraInfo
    ) {
      // pauseScheduleStack++
      pauseScheduling();
      // 遍历所有收集的依赖
      for (const effect of dep.keys()) {
        // dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
        let tracking: boolean | undefined;
        // 依赖的_dirtyLevel < 4 && dep.get(effect) === effect._trackId)
        if (
          effect._dirtyLevel < dirtyLevel &&
          (tracking ??= dep.get(effect) === effect._trackId)
        ) {
          // (effect._dirtyLevel)默认为4 === 0 则执行 effect._shouldSchedule
          effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty;
          effect._dirtyLevel = dirtyLevel; // 重新赋值为4
        }
    
        effect.trigger();
    
        effect._shouldSchedule = false;
        if (effect.scheduler) {
          queueEffectSchedulers.push(effect.scheduler);
        }
      }
      resetScheduling();
    }
    
    export function effect<T = any>(
      fn: () => T,
      options?: ReactiveEffectOptions
    ): ReactiveEffectRunner {
      if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
        fn = (fn as ReactiveEffectRunner).effect.fn;
      }
    
      const _effect = new ReactiveEffect(fn, NOOP, () => {
        if (_effect.dirty) {
          _effect.run();
        }
      });
      if (options) {
        extend(_effect, options);
        if (options.scope) recordEffectScope(_effect, options.scope);
      }
      if (!options || !options.lazy) {
        _effect.run();
      }
      const runner = _effect.run.bind(_effect) as ReactiveEffectRunner;
      runner.effect = _effect;
      return runner;
    }
    

    4、响应式扩展

    4.1、isRef、shallowRef、toRaw、markRaw

    这个的源码在1.1创建Ref的里面大概说了,就跳过了。补充一下toRaw和markRaw

    • isRef:是否为一个Ref

    • shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。

    • toRaw:将响应式对象转换成原始对象

    • markRaw:标记一个对象,使其永远不会再成为响应式对象

    // 通过observed尝试获取其原始对象,如果可以找到再递归对象的键的值再进行转换,把代理对象下的每一个值都转成原始对象
    export function toRaw<T>(observed: T): T {
      // const raw = observed && (observed as Target)['__v_raw']
      const raw = observed && (observed as Target)[ReactiveFlags.RAW];
      return raw ? toRaw(raw) : observed;
    }
    
    // 在markRaw实现是给对象添加了__v_skip属性,从这保证不会触发依赖收集和触发依赖
    export function markRaw<T extends object>(value: T): Raw<T> {
      if (Object.isExtensible(value)) {
        def(value, ReactiveFlags.SKIP, true);
      }
      return value;
    }
    
    export const def = (
      obj: object,
      key: string | symbol,
      value: any,
      writable = false
    ) => {
      Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: false,
        writable,
        value
      });
    };
    

    4.2、triggerRef

    使用triggerRef可以强制更新页面DOM。这是因为我们创建了这个triggerRef,他会去调用triggerEffects
    ,那也就是1.3的依赖触发,依赖出发后会执行回调去更新页面。同样的因为shallowRef他没有去转成toReactive()
    ,那么他也就不会去做依赖收集和依赖触发的操作

    export function triggerRefValue(
      ref: RefBase<any>,
      dirtyLevel: DirtyLevels = DirtyLevels.Dirty,  // 4
      newVal?: any,
    ) {
      ref = toRaw(ref)
      const dep = ref.dep
      if (dep) {
        triggerEffects(
          dep,
          dirtyLevel,
          __DEV__
            ? {
              target: ref,
              type: TriggerOpTypes.SET,
              key: 'value',
              newValue: newVal,
            }
            : void 0,
        )
      }
    }
    

    4.3、customRef

    自定义ref,它是个工厂函数要求我们返回一个对象 并且实现 get 和 set
    适合去做防抖之类的。这个的源码实现和Ref的区别就在于这里没有实现get和set的依赖收集和触发,需要手动实现。

    export type CustomRefFactory<T> = (
      track: () => void,
      trigger: () => void
    ) => {
      get: () => T
      set: (value: T) => void
    }
    
    class CustomRefImpl<T> {
      public dep?: Dep = undefined;
    
      private readonly _get: ReturnType<CustomRefFactory<T>>['get'];
      private readonly _set: ReturnType<CustomRefFactory<T>>['set'];
    
      public readonly __v_isRef = true;
    
      constructor(factory: CustomRefFactory<T>) {
        const {get, set} = factory(
          () => trackRefValue(this),
          () => triggerRefValue(this)
        );
        this._get = get;
        this._set = set;
      }
    
      get value() {
        return this._get();
      }
    
      set value(newVal) {
        this._set(newVal);
      }
    }
    

    4.4、toRef

    创建一个ref对象,其value值指向另一个对象中的某个属性。先看一下它怎么用的

    const obj = {
      a: 1,
      b: 2
    };
    
    // 把obj对象当中的a拿出来重新创建一个ref对象 ==>  const a = ref(1)
    const a = toRef(obj, 'a');
    
    setTimeout(() => {
      obj.a = 2;
      obj.b = 3;
      // 在这里修改了obj.a的值,a.value的值也会跟踪发生变化,但是DOM元素不会发生变化
      // 原因在于obj对象不是响应式的,那么a也不会更新视图。反之如果obj是响应式的,那么a.value的值也会更新视图
      console.log(' =====', obj, a);
    }, 2000);
    

    源码实现(简化一下):

    • 先找到toRef的实现,这里面有3种情况,第一种是source是ref,第二种是source是函数,第三种是source是对象,并且key存在,那么就返回propertyToRef
    • propertyToRef:判断对象的key的值是不是ref,如果是就返回,不是就返回propertyToRef
    • propertyToRef:把对象key的值做一个依赖收集
    export function toRef(source, key, defaultValue): Ref {
      // 如果source是一个Ref直接返回...这个我就删了,
      // 如果是一个函数,就拿函数返回值
      if (isFunction(source)) {
        return new GetterRefImpl(source) as any;
      } else if (isObject(source) && arguments.length > 1) {
        // 主要还是在这,入参是一个对象,并且有key
        return propertyToRef(source, key!, defaultValue);
      } else {
        return ref(source);
      }
    }
    
    // 看对象值是不是Ref直接返回,不然再实例化一个ObjectRefImpl
    function propertyToRef(source, key, defaultValue) {
      const val = source[key];
      return isRef(val)
        ? val
        : (new ObjectRefImpl(source, key, defaultValue) as any);
    }
    
    // 在这里没有对get、set方法做依赖收集和触发,所以toRef包装后的对象的响应式跟_object是否是一个响应式对象相关
    class ObjectRefImpl<T extends object, K extends keyof T> {
      public readonly __v_isRef = true;
    
      // 构造接收传递过来的对象,key,默认值
      // constructor(_object,_key,_defaultValue)
    
      get value() {
        return val === undefined ? this._defaultValue! : this._object[this._key];
      }
    
      set value(newVal) {
        this._object[this._key] = newVal;
      }
    }
    

    4.5、toRefs

    toRefs的实现:接收入参的一个object,遍历这个object,然后将每个值都用toRef包一层

    export function toRefs<T extends object>(object: T): ToRefs<T> {
      const ret: any = isArray(object) ? new Array(object.length) : {}
      for (const key in object) {
        ret[key] = propertyToRef(object, key)
      }
      return ret
    }
    

    4.6、Reactive&shallowReactive&readonly

    • 对于reactive来说他的响应式其实就是上面ref针对于对象那一块的响应式实现。
    • 而shallowReactive就是在创建reactive的时候传递的是mutableHandlers还是shallowReactiveHandlers,
      就是在创建MutableReactiveHandler实例的时候是否将_isShallow指定为了true,默认值为false
    • readonly在创建reactive时指定了_readonly,当值为true时会直接返回当前对象

    5、简单实现reactive响应式

    这里还是直接在vue的模版当中写了哈,就不用vue当中的ref、reactive什么的,从头实现一遍。

    5.1、创建myReactive

    首先我们直接创建一个myReactive变量指向一个回调函数,直接返回一个代理对象,首先看get方法,获取里面的key对应的值,我们把所有的key都放到track方法当中,也就是依赖收集,这个方法我们下一步再实现,同时我们判断对象的属性值是不是还是一个对象,如果还是我们就再用myReactive包一层,这样也就实现了深层监听。

    然后是set方法。这里主要就是实现trigger依赖触发方法

    const isObject = (target: any) => target !== null && typeof target === 'object';
    const myReactive: any = <T extends object>(target: T) => {
      return new Proxy(target, {
        get(target, key, receiver) {
          const result = Reflect.get(target, key, receiver) as object;
          track(target, key);
          if (isObject(result)) {
            return myReactive(result);
          }
          return result;
        },
        set(target, key, value, receiver) {
          const result = Reflect.set(target, key, value, receiver);
          trigger(target, key);
          return result;
        }
      });
    };
    

    5.2、effect副作用函数

    先实现一个effect,也就是一个对象去绑定一个对应的更新DOM的方法,当数据改变之后调用这个传递给effect的匿名函数去更新DOM

    let activeEffect: () => void;
    const effect = (fn: Function) => {
      const _effect = () => {
        activeEffect = _effect;
        fn();
      };
    
      _effect();
    
      console.log(' =====', activeEffect);
    };
    

    5.3、get方法:依赖收集,

    • 创建一个名为targetMapWeakMap实例。(WeakMap是一种特殊的Map,它的键名所指向的对象,不计入垃圾回收机制)
    • 方法入参:target(目标对象)和key(属性名)。这个函数用于追踪目标对象的属性与副作用函数(effect)之间的关系
    • 首先尝试从targetMap中获取depsMap(依赖映射),如果没有找到,则创建一个新的Map实例并将其与目标对象关联
    • 尝试从depsMap中获取deps(依赖集合),如果没有找到,则创建一个新的Set实例并将其与属性名关联
    • 最后,将当前的副作用函数(activeEffect)添加到deps集合中。这样,当目标对象的属性发生变化时,可以触发与之相关的副作用函数
    const targetMap = new WeakMap();
    const track = (target: object, key: any) => {
      let depsMap = targetMap.get(target);
      if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap);
      }
    
      let deps = depsMap.get(key);
      if (!deps) {
        deps = new Set();
        depsMap.set(key, deps);
      }
    
      deps.add(activeEffect);
    };
    

    5.4、set方法:依赖触发

    这里就简单说明一下,在前面get方法已经做好了依赖收集操作,所以当对对象属性重新赋值的时候会触发trigger,会先从targetMap找当前对象,并且发现该对象存在targetMap当中(也就是说这个是一个响应式对象),再会去depsMap寻找key(重新赋值的key)对应副作用函数,然后通过副作用函数更新DOM就完成了响应式。

    const trigger = (target: object, key: any) => {
      const depsMap = targetMap.get(target);
      if (!depsMap) return;
    
      const deps = depsMap.get(key);
      if (!deps) return;
    
      deps.forEach((effect: Function) => {
        effect();
      });
    };
    

    5.5、测试

    直接放到vue的模版里面测试,其中myReactive、effect、track、trigger方法从上面拿下来即可,在这里测试只需要定义好effect函数,并且指定匿名函数去修改DOM元素,然后其他的使用myReactive和reactive是一样的。

    
    
    
    
    
  • 相关阅读:
    Eth - Trunk链路聚合
    关于content-type的理解
    【智能优化算法】多目标于分解的多目标进化算法MOEA/D算法(Matlab代码实现)
    使用nsenter在容器内部执行宿主机的命令
    myArm 全新七轴桌面型机械臂
    熟悉Redis6
    Oracle设置某个表字段递增
    IT运维面试必须具备的排障知识题库
    The Missing Semester - 第五讲 学习笔记(二)
    该设备正在使用中。请关闭可能使用该设备的所有程序或窗口,然后重试。
  • 原文地址:https://blog.csdn.net/qq_44973159/article/details/139674262