张三面试记
张三是一名前端开发新人,经过几个月的努力学习,他终于鼓起勇气去面试了。今天,他穿着整洁的西装,信心满满地走进了面试公司。他觉得自己在面试前已经准备得很充分,尤其是在 Vue 相关知识上,几乎每个概念都了如指掌。
面试一开始,面试官便问了他一些常见的前端基础问题,张三回答得游刃有余。接着,面试官看了看他的简历,问道:“你在简历上提到你熟悉 Vue,能简要讲讲 Vue 的响应式原理吗?”
张三听到这个问题,心里一阵欣喜:“这个我最擅长了!”他深吸一口气,开始侃侃而谈:“Vue 3 的响应式系统主要是基于 Proxy 的,通过代理对象的 getter 和 setter 来追踪数据的变化。当我们访问某个属性时,Vue 会自动记录这个属性的依赖,属性发生变化时,就会触发相关的副作用函数重新执行,更新视图。使用 reactive 可以创建响应式对象,而 effect 则用于自动追踪依赖并执行副作用.”
张三的自信突然消失了一半,他顿了顿,开始结巴:“嗯,具体实现嘛……就是,Proxy 像一个……它会拦截对象的属性访问,然后……”他停顿了一下,脑海里似乎出现了一个大大的问号。尽管他能说出一些概念,但一时间竟然无法清晰地描述出具体的实现步骤。
reactive、effect做了什么
reactive:将对象转换为响应式代理(基于Proxy),支持嵌套对象的深度响应。effect: 定义一个“副作用函数”,当它依赖的响应式数据变化时,自动重新执行该函数。 这是vue的响应式原理,后续的watch、computed、视图更新等相关操作都基于这两个api来实现- vue中核心中的核心,所有响应式的api都是基于
reactive、effect来实现的
如何实现
环境搭建
配置 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"
实现思路导图
测试用例
- 创建 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
}
}
});
}
张三说: 这么多让我怎么学吗,连个大纲都没有!!!
作者:你说的不就是大纲吗,我就照的这么写的。哈哈哈哈哈哈