vue中reactive和effect你能了解多少

211 阅读6分钟

张三面试记

张三是一名前端开发新人,经过几个月的努力学习,他终于鼓起勇气去面试了。今天,他穿着整洁的西装,信心满满地走进了面试公司。他觉得自己在面试前已经准备得很充分,尤其是在 Vue 相关知识上,几乎每个概念都了如指掌。

面试一开始,面试官便问了他一些常见的前端基础问题,张三回答得游刃有余。接着,面试官看了看他的简历,问道:“你在简历上提到你熟悉 Vue,能简要讲讲 Vue 的响应式原理吗?”

张三听到这个问题,心里一阵欣喜:“这个我最擅长了!”他深吸一口气,开始侃侃而谈:“Vue 3 的响应式系统主要是基于 Proxy 的,通过代理对象的 getter 和 setter 来追踪数据的变化。当我们访问某个属性时,Vue 会自动记录这个属性的依赖,属性发生变化时,就会触发相关的副作用函数重新执行,更新视图。使用 reactive 可以创建响应式对象,而 effect 则用于自动追踪依赖并执行副作用.”

607b3071b4a83oQp.gif

面试官听完后点了点头:“不错,你理解得很清楚。那你能告诉我,具体 Vue 是怎么通过 Proxy 实现这个过程的吗?”

张三的自信突然消失了一半,他顿了顿,开始结巴:“嗯,具体实现嘛……就是,Proxy 像一个……它会拦截对象的属性访问,然后……”他停顿了一下,脑海里似乎出现了一个大大的问号。尽管他能说出一些概念,但一时间竟然无法清晰地描述出具体的实现步骤。

607b3071b4a83oQp.gif

reactive、effect做了什么

  • reactive:将对象转换为响应式代理(基于 Proxy),支持嵌套对象的深度响应。
  • effect: 定义一个“副作用函数”,当它依赖的响应式数据变化时,自动重新执行该函数。 这是vue的响应式原理,后续的watch、computed、视图更新等相关操作都基于这两个api来实现
  • vue中核心中的核心,所有响应式的api都是基于reactiveeffect来实现的

如何实现

环境搭建

配置 pnpm 多包管理

pnpm(一个 JavaScript 包管理工具)中的一个功能,用来帮助管理和构建多包(monorepo)项目。多包项目通常包含多个相关的子包,它们共享一些依赖或有着紧密的关联。通过 pnpm workspace,你可以方便地在一个项目中同时管理多个包,并且有效地进行依赖的管理和构建。

pnpm init ## 初始化pnpm
创建文件基本结构
const fs = require("fs");
const path = require("path");

// 创建目录的函数
function makeDir(dirPath) {
  if (!fs.existsSync(dirPath)) {
    fs.mkdirSync(dirPath, { recursive: true });
    console.log(`创建目录: ${dirPath}`);
  }
}

// 创建文件的函数
function createFile(filePath, content = "") {
  if (!fs.existsSync(filePath)) {
    fs.writeFileSync(filePath, content, "utf8");
    console.log(`创建文件: ${filePath}`);
  }
}

// 主函数
function setup() {
  // 创建 pnpm-workspace.yaml
  const workspaceContent = `packages:
  - 'packages/*'`;
  createFile("./pnpm-workspace.yaml", workspaceContent);

  // 创建 tsconfig.json
  const tsconfigContent = `{
    "compilerOptions": {
      "outDir": "dist",
      "sourceMap": true,
      "target": "es2016",
      "module": "esnext",
      "moduleResolution": "node",
      "strict": false,
      "resolveJsonModule": true,
      "esModuleInterop": true,
      "jsx": "preserve",
      "lib": ["esnext", "dom"],
      "baseUrl": "./",
      "paths": {
        "@vue/*": ["packages/*/src"]
      }
    }
  }
  `;
  createFile("./tsconfig.json", tsconfigContent);

  // 创建 scripts 目录和文件
  makeDir("./scripts");
  createFile("./scripts/dev.js");

  // 创建 packages 目录
  makeDir("./packages");

  // 创建 reactivity 目录结构
  makeDir("./packages/reactivity/src");  // 创建 src 子目录
  createFile("./packages/reactivity/src/index.ts");  // 在 src 中创建 index.ts

  // 创建 shared 目录结构
  makeDir("./packages/shared/src");  // 创建 src 子目录
  createFile("./packages/shared/src/index.ts");  // 在 src 中创建 index.ts

  console.log("所有目录和文件创建完成!");
}

// 执行设置
setup();

初始化子包
cd ./packages/reactivity
pnpm init
cd ./packages/shared
pnpm init
安装esbuild
pnpm install esbuild -w
打包脚本 scripts/dev.js
const {build} = require("esbuild");
const { resolve } = require("path");
const target = "reactivity";
build({
  entryPoints: [resolve((__dirname, `./packages/${target}/src/index.ts`))],
  outfile: resolve((__dirname, `./packages/${target}/dist/${target}.js`)), 
  bundle: true,
  sourcemap: true,
  format: "esm",
  platform:"browser",
  
}).then(()=>{
  console.log("watch~~~~~");
})

配置package.json
 "dev": "node scripts/dev.js"

实现思路导图

whiteboard_exported_image.png

测试用例

  • 创建 index.html
  • 引入vue
  • 使用reactive定义对象
  • 使用effect更新页面
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module">
      import {
        reactive,
        effect,
      } from "https://unpkg.com/vue@3.5.13/dist/vue.esm-browser.js";
      const state = reactive({ name: "lexin", age: "18" });
      effect(()=>{
        app.innerHTML = `${state.age}:${state.name}`
      })
      setTimeout(()=>{
        state.age = 28
      },2000)
    </script>
  </body>
</html>

目录结构

packages/
    ├── reactivity/
    │   └── src/
    │       |── index.ts
    |       |── reactive
    |       |── handlers
    |       └── effect
   

对象代理

reactive
const reactiveMap = new WeakMap();
export enum ReactiveFlags {
  IS_REACTIVE = "__v_isReactive",
}

export const reactive = (target) => {
  
  // 只代理对象
  if (isObject(!target)) return target;
  
  // 设置弱引用关系
  const existProxy = reactiveMap.get(target);

  // 代理过直接返回
  if (existProxy) return existProxy;

  //是reactive 对象直接返回
  if (target[ReactiveFlags.IS_REACTIVE]) return target;

  // mutableHandlers get set 拦截回调 =>handlers 文件中详细讲解
  const proxy = new Proxy(target, mutableHandlers);

  // 新对象被代理直接存入
  reactiveMap.set(target, proxy);

  return proxy;
};

handlers收集依赖,触发更新
import { reactive, ReactiveFlags } from "./reactive";
import { track, trigger } from "./effect";
import { activeEffect } from "./effect";
import { isObject } from "@vue/shared";
export const mutableHandlers = {
  get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) return true;
    
    // 默认深度代理,嵌套对象多层代理
    if(isObject(target[key])){
      return reactive(target[key])
    }

    // 属性对应多个 effect关系,一个effect对应多个属性
    // 做依赖收集

    // 搭配Reflect 来解决this问题,
    const res = Reflect.get(target, key, receiver);

    // 收集依赖,只要对象的属性被取值,就会发生依赖收集
    track(target, key);
    return res;
  },
  set(target, key, value, receiver) {
    
    let oldValue = target[key];
    const r = Reflect.set(target, key, value, receiver);
    // 触发更新
    if (oldValue !== value) {
      trigger(target, key, value, oldValue);
    }

    return r;
  },
};

副作用函数

effect
// 这是一个很关键的时候 因为js是单线程,所以取值只能一个一个取值,并且对应的副作用函数只有一个!!!!
export let activeEffect = undefined;

// 移除没有必要的集合
function cleanUpEffect(effect) {
  // 在收集的列表中删除自己
  const { deps } = effect; // 这个deps指的是谁  思考题??

  // 找到存储自己的Set,然后将自己删除
  for (let i = 0; i < deps.length; i++) {
    const dep = deps[i];
    dep.delete(effect);
  }
  // 当前effect没有被任何人依赖,所以清除deps
  effect.deps.length = 0;
}

class ReactiveEffect {
  // 处理 effect嵌套调用问题
  parent = undefined;
  // 处理effect 被停止副作用问题
  active = true;
  // 记录当前effect是被谁收集了 即 Set
  deps = []; // 记录依赖了哪些列表
  // fn 是effect默认回调函数, scheduler 是处理用户自定义回调函数 例如watch传入的函数等
  constructor(private fn, public scheduler) {}

  // 默认执行fn
  run() {
    // 失活状态 不会发生依赖收集 只会执行依次fn
    if (!this.active) {
      return this.fn();
    }
    try {
      // 将上一个effect收集起来 解决effect嵌套问题
      this.parent = activeEffect;
      // 设置当前effect
      activeEffect = this;
      // 下面要发生依赖收集,所以对应当前effect的依赖全部清空
      cleanUpEffect(this);
      // 执行fn的时候,被reactive代理的对象,会发生取值行为。会走get方法,时候会触发依赖收集
      return this.fn();
    } finally {
      // 执行完成 父级effect回退
      activeEffect = this.parent;
      this.parent = undefined;
    }
  }
  stop() {
    // 停止依赖收集,并且清空数据变更时候需要执行的effect(即当前effect)
    if (this.active) {
      this.active = false;
      cleanUpEffect(this);
    }
  }
}
export const effect = (fn, options:any = {}) => {
  const _effect = new ReactiveEffect(fn, options.scheduler);
  _effect.run();
  // 重新代理对象
  const runner = _effect.run.bind(_effect);
  runner.effect = _effect;
  return runner;
};
// 依赖集合map
const targetMap = new WeakMap();

// weakMap:map:set
//{name:'lx',age:''} => 'name'=>set[effect,effect]  每个effect都是一个副作用函数比如name发生变更需要执行什么
//                   => 'age'=>set[effect,effect,...]

// 依赖收集方法
export function track(target, key) {
  // 让这个对象上面的属性记录当前activeEffect
  if (activeEffect) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, (dep = new Set()));
    }
    const shouldTrack = !dep.has(activeEffect);
    if (shouldTrack) {
      // 属性记录effect
      dep.add(activeEffect);
      // 当前 effect 记录自己被哪个属性依赖了,方便后续如果再次被调用,直接找到属性,删除自己
      activeEffect.deps.push(dep);
    }
  }
}

// 数据变更  执行副作用函数,比如更新页面,watch监听的函数,computed的函数
export function trigger(target, key, value, oldValue) {
  // 通过对象找到对应的属性,让这个属性副作用函数集合[effect,effect]
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (!dep) return;
  // 重新解构赋值 避免死循环
  const effects = [...dep];
  effects.forEach((effect) => {
    // 避免死循环
    if (effect !== activeEffect) {
      if (effect?.scheduler) {
        effect.scheduler(); // 用户传递调度函数则使用这个   watch/computed
      } else {
        effect.run(); // 重新运行run
      }
    }
  });
}

张三说: 这么多让我怎么学吗,连个大纲都没有!!!

作者:你说的不就是大纲吗,我就照的这么写的。哈哈哈哈哈哈