首页 > 编程语言 >2. Vue3源码解析之 reactive

2. Vue3源码解析之 reactive

时间:2024-01-16 16:11:33浏览次数:27  
标签:const target effect value reactive 源码 key Vue3 函数

前言

我们知道 Vue3 中声明响应式是通过 reactiveref 这两个函数,下面我们通过案例先来看下 reactive 是如何实现的。

案例

首先引入 reactiveeffect 两个函数,之后声明 obj 响应式对象,接着又执行 effect 函数,该函数传入了一个匿名函数,最后两秒后又修改 obj.name 值。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { reactive, effect } = Vue;

      const obj = reactive({
        name: "jc",
        age: 18,
      });

      effect(() => {
        document.querySelector("#app").innerHTML = obj.name;
      });

      setTimeout(() => {
        obj.name = "cc";
      }, 2000);
    </script>
  </body>
</html>

reactive 实现

reactive 函数在 packages/reactivity/src/reactive.ts 文件下:

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target;
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  );
}

reactive 函数实际执行的是 createReactiveObject 方法,而 target 参数就是我们传进来的对象。接着我们再看下 createReactiveObject 函数,该函数也在 packages/reactivity/src/reactive.ts 文件下:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 省略
  // target already has corresponding Proxy
  // 缓存中读取 存在则直接返回
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  // 省略
  // 这里的 baseHandlers 参数 就是传进来的 mutableHandlers
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  );
  // 设置缓存
  proxyMap.set(target, proxy);
  // 返回 proxy 实例对象
  return proxy;
}

createReactiveObject 函数实际做了 proxyMap 缓存处理,最终返回一个 proxy 实例对象。这里我们主要关注 new Proxy 这段代码,第一个参数 target 为传进来的对象,即 { name: 'jc', age: 18 },第二个 baseHandlers 参数即传入的 mutableHandlers 对象,该对象定义在 packages/reactivity/src/baseHandlers.ts 中:

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys,
};

该对象定义了 getset 等方法,从而对传入的数据进行依赖收集和依赖触发,我们先看下结果,回头再对这块逻辑分析:

proxy.png

至此,reactive 函数执行完毕,obj 得到了一个 proxy 的实例对象。接着又执行 effect 方法,该方法定义在 packages/reactivity/src/effect.ts 文件中:

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn;
  }
  // 创建 ReactiveEffect 实例
  const _effect = new ReactiveEffect(fn);
  // 省略
  if (!options || !options.lazy) {
    // 执行 ReactiveEffect 中的 run 方法
    _effect.run();
  }
  // 省略
}

该函数先声明一个构造函数 ReactiveEffect 的实例对象 _effect,然后执行构造函数中的 run 方法。我们先看下 ReactiveEffect 构造函数:

export class ReactiveEffect<T = any> {
  active = true;
  deps: Dep[] = [];
  parent: ReactiveEffect | undefined = undefined;
  // 省略

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope);
  }

  run() {
    if (!this.active) {
      return this.fn();
    }
    let parent: ReactiveEffect | undefined = activeEffect;
    let lastShouldTrack = shouldTrack;
    while (parent) {
      if (parent === this) {
        return;
      }
      parent = parent.parent;
    }
    try {
      this.parent = activeEffect;
      activeEffect = this;
      shouldTrack = true;
      // 省略
      // 执行 fn 函数 即传入的匿名函数
      //  () => {
      //     document.querySelector('#app').innerHTML = obj.name
      //  }
      return this.fn();
    } finally {
      // 省略

      if (this.deferStop) {
        this.stop();
      }
    }
  }

  stop() {
    // stopped while running itself - defer the cleanup
    if (activeEffect === this) {
      this.deferStop = true;
    } else if (this.active) {
      cleanupEffect(this);
      if (this.onStop) {
        this.onStop();
      }
      this.active = false;
    }
  }
}

这里接收一个 fn 方法即传入的匿名函数,然后设置 activedeps 等属性:

_effect.png

之后执行 _effect.run(),即执行构造函数 ReactiveEffectrun 方法。我们需要关注 activeEffect = this,此时被赋值为:

activeEffect.png

然后执行 fn 函数,即执行传入的匿名函数,之后执行 document.querySelector('#app').innerHTML = obj.name 触发 objget 方法。

get 方法上述中被定义在 packages/reactivity/src/baseHandlers.ts 文件中:

const get = /*#__PURE__*/ createGetter();

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 省略

    const targetIsArray = isArray(target);

    // 省略
    // Reflect API
    // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
    // Reflect.get() 等同于 res = target[key]
    // Reflect 用来替代直接调用 Object 的方法
    const res = Reflect.get(target, key, receiver);

    // 省略

    if (!isReadonly) {
      // 核心,添加依赖收集
      track(target, TrackOpTypes.GET, key);
    }

    if (shallow) {
      return res;
    }

    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value;
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res);
    }

    return res;
  };
}

get 方法实际触发的是 createGetter 函数,我们主要关注 track(target, TrackOpTypes.GET, key) 这段代码,它是对数据的依赖收集,也是 get 方法的核心。 track 函数被定义在 packages/reactivity/src/effect.ts 文件中:

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      // key: target 传入的对象 value: 创建 Map 对象
      targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
      // key: 'name' value: Set 对象
      depsMap.set(key, (dep = createDep()));
    }

    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined;
    // dep 为 Set 对象
    // eventInfo = { effect: activeEffect(之前已设置), target(传入的对象), type('get'), key('name') }
    trackEffects(dep, eventInfo);
  }
}

这里的 targetMapWeakMap 对象,该对象是一个弱引用类型,那什么是弱引用类型呢?举个例子:

let obj = {
  name: "jc",
};
const map = new WeakMap(); // new Map()
map.set(obj, "cc");

obj = null;

正常来说,对象为空,堆内存中数据没有指针指向就会被回收。但 map 数据依然存在,说明 Map 为强引用;设置 WeakMap,数据为空,则说明 WeakMap 为弱引用。准确地说,obj 不存在其他引用时, WeakMap 不会阻止垃圾回收,基于 obj 的引用将会被清除,这就证明 WeakMap 的弱引用特性。

回过来我们再看下 targetMap 对象,以传入的对象 { name: 'jc', age: 18} 作为 keyvalue 值为 Map 对象,之后设置 depsMapkey 当前为 namevalueSet 对象,具体可以看下 createDep 方法,最后执行 trackEffects(dep, eventInfo)

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // 省略

  if (shouldTrack) {
    dep.add(activeEffect!);
    activeEffect!.deps.push(dep);
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack({
        effect: activeEffect!,
        ...debuggerEventExtraInfo!,
      });
    }
  }
}

此时我们再看下 targetMap 对象数据:

targetMap.png

这样就完成了数据的依赖收集,之后就可以通过指定对象指定属性获取到对应的 fn 方法。而依赖收集本质上就是 targetMapReactiveEffect 之间的关联。

createGetter 执行完毕返回对应的值,当前为 jc

getter.png

两秒后执行 obj.name = 'cc',触发 set 方法,该方法定义在 packages/reactivity/src/baseHandlers.ts 中:

const set = /*#__PURE__*/ createSetter();

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key];
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false;
    }
    if (!shallow && !isReadonly(value)) {
      if (!isShallow(value)) {
        value = toRaw(value);
        oldValue = toRaw(oldValue);
      }
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value;
        return true;
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key);
    // Reflect.set() 等同于 obj[key] = value
    const result = Reflect.set(target, key, value, receiver);
    // don't trigger if target is something up in the prototype chain of original
    // 触发 getter
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value);
      } else if (hasChanged(value, oldValue)) {
        // 新值 旧值比较
        // 依赖触发 核心
        trigger(target, TriggerOpTypes.SET, key, value, oldValue);
      }
    }
    return result;
  };
}

我们只需关注 trigger(target, TriggerOpTypes.SET, key, value, oldValue) 这行代码,也是依赖触发的核心。trigger 方法被定义在 packages/reactivity/src/effect.ts 文件中:

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 根据传入的对象 获取 对应的 Map 对象
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    // never been tracked
    return;
  }

  let deps: (Dep | undefined)[] = [];
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()];
  } else if (key === "length" && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === "length" || key >= (newValue as number)) {
        deps.push(dep);
      }
    });
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      // 根据属性获取对应的 ReactiveEffect
      deps.push(depsMap.get(key));
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      // 省略
      // 当前为 set 类型
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY));
        }
        break;
    }
  }

  const eventInfo = __DEV__
    ? { target, type, key, newValue, oldValue, oldTarget }
    : undefined;

  if (deps.length === 1) {
    if (deps[0]) {
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo);
      } else {
        triggerEffects(deps[0]);
      }
    }
  } else {
    const effects: ReactiveEffect[] = [];
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep);
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo);
    } else {
      triggerEffects(createDep(effects));
    }
  }
}

根据指定对象获取到对应的 Map 对象,此时 depsMap 为:

depsMap.png

之后再根据指定属性获取对应的 ReactiveEffect,再添加到 deps 中,此时 deps 为:

deps.png

后面我们只需关注 triggerEffects(deps[0], eventInfo) 这行代码, triggerEffects 函数也在packages/reactivity/src/effect.ts 文件中:

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  // 获取到 ReactiveEffect 数组
  const effects = isArray(dep) ? dep : [...dep];
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo);
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      // 实际执行是每个 effect 的 run 方法
      triggerEffect(effect, debuggerEventExtraInfo);
    }
  }
}

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
    }
    if (effect.scheduler) {
      effect.scheduler();
    } else {
      // 实际执行的是传入的匿名函数 fn 方法
      // 再次触发 getter 方法 从而进行赋值
      effect.run();
    }
  }
}

可以看出 triggerEffects 函数实际先获取到 effect 数组,之后遍历数组执行每个 effect.run(),实际执行的是 fn 方法,该方法是最初依赖收集时传入的匿名函数,之后再次触发 getter 方法,从而进行赋值,至此整个依赖触发完成。

setter.png

总结

  1. reactive 函数实际执行了 createReactiveObject 方法。
  2. createReactiveObject 方法主要创建了一个 proxy 实例对象,给代理对象添加 gettersetter 行为,getset 方法主要在 mutableHandlers 对象中。
  3. get 方法实际执行了 createGetter 方法,该方法中 track 函数来进行依赖收集,而 set 方法实际执行了 createSetter 方法,该方法中 trigger 进行依赖触发。
  4. effect 函数实际创建了一个 ReactiveEffect 实例,该构造函数接收一个 fn 函数,等于传进来的匿名函数,该回调函数必须暴露 getter 行为。
  5. 另外该构造函数还做了两件事,第一是在 run 函数中给 avtiveEffect 赋值,第二是执行 fn 函数。
  6. 一旦 getter 触发,就会激活 track 方法,构建 WeakMaptargetMap 对象,从而完成指定对象指定属性到 effect 的依赖收集的工作。
  7. 此时已经完成了一个依赖收集,之后进行依赖触发 setter
  8. set 方法实际执行了 createSetter 方法,然后触发 trigger 函数进行依赖触发。
  9. trigger 函数中首先或从之前 targetMap 依赖收集的对象中获取,根据 key 获取到 effect,然后执行 fn 函数,从而完成一个依赖触发的过程。
  10. reactive 缺陷:一是解构后不支持响应性,二是不支持基本类型,只能是对象。

标签:const,target,effect,value,reactive,源码,key,Vue3,函数
From: https://www.cnblogs.com/wp-leonard/p/17967902

相关文章

  • 3. Vue3源码解析之 ref
    前言我们知道Vue3中声明响应式是通过reactive和ref这两个函数,上篇我们分析了reactive的实现原理,接下来我们再来看下ref是如何实现的。案例首先引入ref和effect两个函数,之后声明name响应式数据,接着又执行effect函数,该函数传入了一个匿名函数,最后两秒后又修改......
  • 初始化一个vite+vue3的前端项目要做的额外的事儿
    添加.editorconfig文件#http://editorconfig.orgroot=true[*]charset=utf-8indent_style=spaceindent_size=4end_of_line=lfinsert_final_newline=truetrim_trailing_whitespace=true[*.md]insert_final_newline=falsetrim_trailing_whitespace......
  • YOLOv8原理与源码解析(视频教程)
    课程链接:https://edu.51cto.com/course/35522.html【为什么要学习这门课】Linux创始人LinusTorvalds有一句名言:Talkischeap.Showmethecode.冗谈不够,放码过来!代码阅读是从基础到提高的必由之路。YOLOv8基于先前YOLO版本的成功,引入了新功能和改进,进一步提升性能和灵活性。......
  • JMeter 源码解读 - HashTree
    背景:在JMeter中,HashTree是一种用于组织和管理测试计划元素的数据结构。它是一个基于LinkedHashMap的特殊实现,提供了一种层次结构的方式来存储和表示测试计划的各个组件。HashTree的特点如下:层次结构:HashTree使用树状结构来组织测试计划元素。每个节点都可以包含子节点......
  • HashMap源码随笔
    源码第一块:概述:Map接口的基于哈希表的实现。此实现提供所有可选的映射操作,并允许null值和null键。(HashMap类大致等同于Hashtable,只不过它是不同步的,并且允许null。此类不保证地图的顺序;特别是,它不保证订单会随着时间的推移保持不变。此实现为基本操作(get和put)提供恒......
  • PriorityQueue源码阅读
    目录简介模型代码分析成员变量方法总结参考链接本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,......
  • 学习spring源码(一)
    学习文档来自小傅哥,详情可以去原文章了解,这边只是简单记录一下学习体会《Spring手撸专栏》第3章:初显身手,运用设计模式,实现Bean的定义、注册、获取工程结构:类似是这样,我这边稍微有点区别,仅做参考small-spring-step-02└──src├──main│└──java......
  • vue3使用 vant ui 3 如何获取组件 popup dom的高度?
    我目前使用的是vant-ui 3.1.2popup弹出层组件,我想要获取弹出层的高度来计算一些东西,但是使用常规定义refdom的方式总是无法获取,最终找到方案如下:vant-ui官方文档:https://vant-contrib.gitee.io/vant/v3/#/zh-CN/popup<template><van-popupv-model:show="show......
  • PDF.js实现按需分片加载pdf文件-包含前后端开发源码和详细开发教程
    PDF.js实现按需分片加载pdf文件-包含前后端开发源码和详细开发教程:https://blog.csdn.net/qq_29864051/article/details/130742657?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170529842016800186594900%2522%252C%2522scm%2522%253A%252220140713.130102334..%252......
  • 【Vue2+3入门到实战】(21)认识Vue3、使用create-vue搭建Vue3项目、熟悉项目和关键文件
    目录一、认识Vue31.Vue2选项式APIvsVue3组合式API2.Vue3的优势二、使用create-vue搭建Vue3项目1.认识create-vue2.使用create-vue创建项目三、熟悉项目和关键文件四、总结一、认识Vue31.Vue2选项式APIvsVue3组合式API<script>exportdefault{data(){r......