前端提升6:手撸vue3响应式实现
2025-04-05 18:21:43Vue3响应式的实现跟Vue2最大的不同点是Vue3中的响应式是个独立模块,可以单独拿出来使用。
不仅仅响应式是模块化的,Vue3的设计理念也是基于模块化来设计的。而要使用Vue2的响应式,就必须加载整个Vue2的库。
一、rollup环境搭建
我们使用Vue3搭建上面的时候,经常会用到Vite,而Vite实际上是基于rollup实现的。
Rollup 是一个 JavaScript 模块打包工具,它可以将多个 JavaScript 文件(模块)打包成一个或多个优化后的文件。与 Webpack 类似,但 Rollup 更专注于 ES6 模块的打包,通常用于库/框架的开发。
除了rollup外,esbuild也是一个很不错的前端构建工具。
- rollup
- 类库打包工具, 专注于模块的tree-shaking, 尽量减少模块打包的大小。
- 拥有庞大的插件生态系统,如代码拆分,语法转换等
- 常用于构建library, 特别是专注于es6模块
- 前端工程打包,一般使用webpack
- esbuild
- 主要特点速度很快,用go编写,可以并行处理。
- 对ast语法树操作能力不太强, 老项目升级用esbuild
- 开发使用esbuild构建编译,生产打包使用成熟的rollup
接下来我们使用先使用rollup搭建一个本地环境
环境搭建
新建一个vue3的文件夹,然后安装rollup
yarn add rollup --dev
目录搭建
--packages # 因为Vue3是基于模块化来设计的,我们设计包文件的时候也分开构建,所以packages目录下都有会自己的项目文件 --reactivity # 响应式实现包 --dist --node_modules --src index.ts package.json pnpm-lock.yaml --shared # 公共库 --dist --src index.ts package.json --scripts build.js dev.js .npmrc package.json pnpm-lock.yaml rollup.config.js tsconfig.json
基本配置
rollup.config.js
import path from 'node:path' import {fileURLToPath} from 'node:url' import {createRequire} from 'node:module' import json from '@rollup/plugin-json' import ts from 'rollup-plugin-typescript2' import resolvePlugin from '@rollup/plugin-node-resolve' // 解析第三方插件 import { defineConfig } from 'rollup' // rollup配置文件调用,是执行rollup命令打包时,指定配置文件时调用 // es6 commonjs 加载函数 const require = createRequire(import.meta.url); // __dirname node全局变量 当前目录 const __dirname = path.dirname(fileURLToPath(import.meta.url)); // 从当前目录下 找packages const packagesDir = path.resolve(__dirname, "packages"); // 获取需要打包的文件 const packageDir = path.resolve(packagesDir, process.env.TARGET); console.log("rollup.config.js", packageDir, packagesDir, process.env.TARGET) // 从packageDir目录下解析文件p 得到文件绝对路径 const resolve = p => path.resolve(packageDir, p); // 获取package.json文件 const pkg = require(resolve('package.json')) // 获取打包模块名 const name = path.basename(packageDir); //模块目录名 // 创建一个映射表 模块格式----模块文件路径 cjs: {file: dist/reactivity.cjs.js, format: 'cjs'} const outputConfigs = { 'esm-bundler': { file: resolve(`dist/${name}.esm-bundler.js`), // 需要绝对路径 es6-module format: 'esm' }, 'cjs': { file: resolve(`dist/${name}.cjs.js`), // commonjs format: 'cjs' }, 'global': { file: resolve(`dist/${name}.global.js`), // 适用于web格式 自执行函数 format: 'iife' } } // 创建打包配置函数 output是输出配置对象 function createConfig(output) { output.name = packageOptions.name; // 打包模块名 output.sourcemap = false; // 是否需要源码映射 // 生成rollup配置 const config = { input: resolve('src/index.ts'), // 打包的入口文件 output, // 输出文件 plugins: [ json(), // 处理json插件 ts({ tsconfig: path.resolve(__dirname, 'tsconfig.json') }), resolvePlugin() ] } return config; } // 获取package.json中buildOption中的formats,遍历打包格式进行单独打包 const packageOptions = pkg.buildOptions || {} // 打包格式 const packagesFormats = packageOptions.formats; // 遍历打包格式 根据打包格式从 outputConfigs 取对应的打包配置 const packageConfigs = packagesFormats.map(format => createConfig(outputConfigs[format])); export default defineConfig(packageConfigs)
tsconfig.json
{ "compilerOptions": { "outDir": "dist", // 输出目录 "sourceMap": true, // 采用sourceMap "target": "es2016", // 目标语法 "module": "ESNext", // 模块格式 "moduleResolution": "Node", // 模块解析方式 "strict": false, // 严格模式关闭,可用any "resolveJsonModule": true, // 解析json模块 "esModuleInterop": true, // 允许通过es6语法引用commonjs模块 "jsx": "preserve", // jsx不转义 "lib": ["ESNext", "DOM"], // 支持的类库 "baseUrl": ".", // 当前目录 "paths": { "@vue/*": ["packages/*/src"] // 模块名以 @vue/ 前缀 } } }
package.json
{ "name": "02", "version": "1.0.0", "description": "类库打包工具, 专注于模块的tree-shaking, 尽量减少模块打包的大小。\r 拥有庞大的插件生态系统,如代码拆分,语法转换等\r 常用于构建library, 特别是专注于es6模块", "main": "index.js", "type": "module", "scripts": { "dev": "node scripts/dev.js", "dev2": "node scripts/dev.js runtime-dom -f global", "build": "node scripts/build.js" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.3", "execa": "^8.0.1", "minimist": "^1.2.8", "rollup": "^4.17.2", "rollup-plugin-typescript2": "^0.36.0", "typescript": "^5.4.5", "vue": "^3.4.27" } }
.npmrc
# 将子依赖包也放到node_modules根目录 shamefully-hoist = true
每个包的配置
/packages/reactivity/dist/package.json
{ "name": "@vue/reactivity", "version": "1.0.0", "description": "", "main": "dist/reactivity.cjs.js", "module": "dist/reactivity.esm-bundler.js", "buildOptions": { "name": "VueReactivity", "formats": [ "esm-bundler", "global", "cjs" ] }, "dependencies": { "@vue/shared": "workspace:^" } }
/packages/reactivity/dist/pnpm-lock.yaml
lockfileVersion: '6.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false dependencies: '@vue/shared': specifier: workspace:^ version: link:../shared
/packages/shared/dist/package.json
{ "name": "@vue/shared", "version": "1.0.0", "description": "tools", "main": "dist/shared.cjs.js", "module": "dist/shared.esm-bundler.js", "buildOptions": { "formats": [ "esm-bundler", "cjs" ] } }
打包脚本
build.js
// 构建所有的模块 import { readdirSync, statSync} from 'fs' import { execa } from 'execa' // 思路:找到packages目录下的所有的模块,遍历循环打包 // 1. 获取打包目录 const dirs = readdirSync('packages') .filter(dir => statSync(`packages/${dir}`).isDirectory()); // 2. 打包函数 async function build(target) { await execa('rollup', ['-c', '--environment', `TARGET:${target}`], {stdio: 'inherit'}) } // 3. 遍历打包,等所有的打包完成后,再提示:已完成, Promise.all function runParaller(dirs) { let result = []; for (const dir of dirs) { result.push(build(dir)); // 将打包结果promise存入result. } return Promise.all(result); // 并行 } runParaller(dirs).then(()=>{ console.log("所有模块编译完成!") })
dev.js
import minimist from "minimist"; // 获取打包命令的参数 import { execa } from 'execa'; // 执行打包命令 // 获取打包的参数 const args = minimist(process.argv.slice(2)); // 截取命令中第三个参数及后面 console.log("args:", args); // 获取打包模块的参数 const target = args._.length ? args._[0] : 'reactivity'; // 打包模块的格式 umd, cjs, es, iife===(function(){})() const formats = args.f || 'global'; // 默认打web包 const sourceMap = args.s || false; // 源码映射,线上发布false, 开发true console.log("参数:", target, formats, sourceMap) // 编译函数 async function build(target) { await execa('rollup', [ '-wc', '--environment', [ `TARGET:${target}`, formats ? `FORMATS: ${formats}` : '', sourceMap? `SOURCE_MAP: true` : '' ].filter(Boolean).join(',') ], {stdio: 'inherit'}) // 用于配置父进程和子进程之间建立的管道 // 默认子进程的stdin,stdout,stderr被重定向到相应的subprocess.stdin, subprocess.stdout } // 编译 build(target).then(()=>{ console.log('success'); }) // 打包命令 //rollup -wc --environment TARGET: [target], FORMATS: cjs // -w 监视文件变化 -c 使用配置文件打包 默认rollup.config.js // 打包参数 // [reactivity, 'cjs', ''] [reactivity, '', ''] [reactivity, 'cjs', 'true']
运行
npm run build
二、Vue3响应式实现过程分析
在手撸实现之前,我们先看一下Vue3响应式实现的过程。
vue3的响应式实现比vue2要容易,vue3使用proxy来实现,可以直接代理整个对象。不过vue3的diff算法比vue2有争强,实现要复杂一些。
现在我们可以撸起袖子开始干了!
三、数据劫持
1. 添加公共方法
/packages/shared/src/index.ts
import {ReactiveFlags} from 'packages/reactivity/src/reactive'
/**
* 判断是否为对象
* @param value
* @returns
*/
export const isObject = (value) => {
return typeof value === 'object' && value !== undefined
}import {ReactiveFlags} from 'packages/reactivity/src/reactive'
export function isFunction(value): boolean {
return typeof value === 'function'
}
// 判断是否是一个响应式对象
export function isReactive(value): boolean {
return !!value[ReactiveFlags.IS_REACTIVE];
}
// 判断对象是否是ref对象
export function isRef(r): boolean {
return Boolean(r && r[ReactiveFlags.IS_REF]); // 将ref的值转为boolean
}
// 判断是不是一个字符串
export function isString(value): boolean {
return typeof value === 'string'
}
目前isReactive和isRef还没实现,可以先注释掉,以后实现了再放开
2. 创建响应式对象
/packages/reactivity/src/reactive.ts
import { isObject } from "@vue/shared";
// 判断代理对象标识
const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive'
}
const mutableHandlers = {
get(target, key, receiver) {
debugger
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
const res = Reflect.get(target, key, receiver);
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
return res;
}
}
const reactiveMap = new WeakMap();
// 创建响应式对象
function createReactiveObject(target: object) {
// 判断是否已经是代理对象
if ((target as any)[ReactiveFlags.IS_REACTIVE]) {
return target;
}
// 只能是对象才能响应式处理
if(!isObject(target)) {
return target;
}
// 从缓存中取代理对象
const existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, mutableHandlers)
return proxy;
}
export function reactive(target: object) {
return createReactiveObject(target);
}
3. 导出reactive
/packages/reactivity/src/index.ts
export {reactive} from './reactive'
四、依赖收集
根据Vue3的设计,依赖收集采用副作用函数实现,我们来实现一下
1. 实现effect函数
let activeEffect; // 当前活动的effect
// 判断是否存在活动的effect
export function isTracking() {
return activeEffect !== undefined;
}
// 依赖收集存储 弱引用
const targetMap = new WeakMap();
// 依赖收集
export function track(target, key) {
if (!isTracking()) {
// 如果这个属性不依赖于effect直接跳出
return;
}
// 获取响应式对象所有属性的依赖
let depsMap = targetMap.get(target);
if(!depsMap) {
// 初始化 {obj: map{}}
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
// 初始化effect {obj:map{prop: set[effect,effect]}}
depsMap.set(key, (dep = new Set()));
}
trackEffects(dep);
}
// 真正收集依赖
export function trackEffects(dep) {
let shouldTrack = !dep.has(activeEffect);
if (shouldTrack) {
// 不存在effect则收集
// {obj: map{prop: set[effect]}}
dep.add(activeEffect);
// effect也要引用属性
activeEffect.deps.push(dep);
debugger
}
}
2. 依赖清洗
// 每次执行effect时双向清理一次依赖,再重新收集
function cleanupEffect(effect) {
// deps 里面装 price 对应的effect,quantity对应的effect
const {deps} = effect;
for (let i =0; i<deps.length; i++) {
// 清除effect,重新依赖收集
deps[i].delete(effect);
}
// 双向删除
effect.deps.length = 0;
}
export class ReactiveEffect {
active = true; // 判断是否是活动的
deps = []; // 让effect记录他依赖了哪些属性
parent = null; // 表示在实例上新增parent属性, 记录父级effect
// public fn 等价于 this.fn = fn;
constructor(public fn, public scheduler?) {
}
run(){
try {
// effectStack.push(activeEffect = this);
// 记录父级effect
this.parent = activeEffect;
activeEffect = this;
// 清理依赖
// cleanupEffect(this);
return this.fn();
}catch(e){
console.error(e)
}finally{
// 将栈最上面的effect抛掉
// effectStack.pop();
// 调整activeEfffect
// activeEffect = effectStack[effectStack.length - 1];
// 还原父级effect
activeEffect = this.parent;
}
}
stop(){// 手动执行清理依赖
// 停止依赖收集
if (this.active) {
this.active = false;
cleanupEffect(this);
}
}
}
3. 任务调度
export function triggerEffects(dep: any){
for (const effect of dep) {
debugger
// 避免死循环,要判断是否是同一个effect
if (effect !== activeEffect) {
// 判断是否有调度函数
if (effect.scheduler) {
return effect.scheduler();
}
effect.run();
}
}
}
// 副作用函数--渲染页面
export function effect(fn, options={} as any) {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run(); // 默认会执行一次fn函数
// 要更改
const runner = _effect.run.bind(_effect); // this作用域绑定到_effect
runner.effect = _effect; // 可以让runner上调用effect方法
return runner;
}
完整代码
/packages/reactivity/src/effect.ts
// 嵌套effect 就会出现这种情况
// effect1(()=>{
// state.name; 收集 name: effect1
// effect2(()=>{
// state.age; 收集 age: effect2
// })
// state.address; 收集 address: effect2
// })
// 每次执行effect时双向清理一次依赖,再重新收集
function cleanupEffect(effect) {
// deps 里面装 price 对应的effect,quantity对应的effect
const {deps} = effect;
for (let i =0; i<deps.length; i++) {
// 清除effect,重新依赖收集
deps[i].delete(effect);
}
// 双向删除
effect.deps.length = 0;
}
// 数据结构 栈
let effectStack = []; // 为了保证effect执行时,维持正确的关系
let activeEffect; // 当前活动的effect
export class ReactiveEffect {
active = true; // 判断是否是活动的
deps = []; // 让effect记录他依赖了哪些属性
parent = null; // 表示在实例上新增parent属性, 记录父级effect
// public fn 等价于 this.fn = fn;
constructor(public fn, public scheduler?) {
}
run(){
try {
// effectStack.push(activeEffect = this);
// 记录父级effect
this.parent = activeEffect;
activeEffect = this;
// 清理依赖
cleanupEffect(this);
return this.fn();
}catch(e){
console.error(e)
}finally{
// 将栈最上面的effect抛掉
// effectStack.pop();
// 调整activeEfffect
// activeEffect = effectStack[effectStack.length - 1];
// 还原父级effect
activeEffect = this.parent;
}
}
stop(){// 手动执行清理依赖
// 停止依赖收集
if (this.active) {
this.active = false;
cleanupEffect(this);
}
}
}
// effect(()=>{ // activeEffect= e1
// state.name // name ==> e1
// effect(()=>{ // activeEffect = e2
// state.age // age ==> e2
// })
// // 还原activeEffect ==> e1
// state.address // address ==> e1
// })
// 判断是否存在活动的effect
export function isTracking() {
return activeEffect !== undefined;
}
// 依赖收集存储 弱引用
const targetMap = new WeakMap();
// 依赖收集targetMap depsMap depSet
export function track(target, key) {
if (!isTracking()) {
// 如果这个属性不依赖于effect直接跳出
return;
}
// 获取响应式对象所有属性的依赖
let depsMap = targetMap.get(target);
if(!depsMap) {
// 初始化 {obj: map{}}
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
// 初始化effect {obj:map{prop: set[effect,effect]}}
depsMap.set(key, (dep = new Set()));
}
trackEffects(dep);
}
// 真正收集依赖
export function trackEffects(dep) {
let shouldTrack = !dep.has(activeEffect);
if (shouldTrack) {
// 不存在effect则收集
// {obj: map{prop: set[effect]}}
dep.add(activeEffect);
// effect也要引用属性
activeEffect.deps.push(dep);// effect引用对应的属性
// debugger
}
}
// 触发effect执行
export function trigger(target, key) {
// 找到属性key对应的deps 去执行里面的effect
let depsMap = targetMap.get(target);
if (!depsMap) return;
let deps = []; // 用于存储所有的effect
if (key !== undefined) {
deps.push(depsMap.get(key));
}
let effects = [];
for (const dep of deps) {
effects.push(...dep);
}
triggerEffects(effects);
}
export function triggerEffects(dep: any){
dep = new Set(dep); // 处理死循环
for (const effect of dep) {
// debugger
// 避免死循环,要判断是否是同一个effect
if (effect !== activeEffect) {
// 判断是否有调度函数
if (effect.scheduler) {
return effect.scheduler();
}
effect.run();
}
}
}
// 副作用函数--渲染页面
export function effect(fn, options={} as any) {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run(); // 默认会执行一次fn函数
// 要更改
const runner = _effect.run.bind(_effect); // this作用域绑定到_effect
runner.effect = _effect; // 可以让runner上调用effect方法
return runner;
}
3. reactive中使用
/packages/reactivity/src/reactive.ts
import { isObject } from "@vue/shared";
import { track, trigger } from "./effect";
// 判断代理对象标识
const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive'
}
const mutableHandlers = {
get(target, key, receiver) {
debugger
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
// 依赖收集 收集effect
track(target, key);
const res = Reflect.get(target, key, receiver);
return res;
},
set(target, key, value, receiver) {
let oldValue = (target as any)[key];
const res = Reflect.set(target, key, value, receiver);
// 值变化后才会执行effect
if (oldValue !== value) {
// 触发effect渲染页面
trigger(target, key);
}
return res;
}
}
const reactiveMap = new WeakMap();
// 创建响应式对象
function createReactiveObject(target: object) {
// 判断是否已经是代理对象
if ((target as any)[ReactiveFlags.IS_REACTIVE]) {
return target;
}
// 只能是对象才能响应式处理
if(!isObject(target)) {
return target;
}
// 从缓存中取代理对象
const existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, mutableHandlers)
return proxy;
}
export function reactive(target: object) {
return createReactiveObject(target);
}
4. 导出effect
/packages/reactivity/src/index.ts
export {reactive} from './reactive'
export {effect, ReactiveEffect } from './effect'
5. 调试测试
编译后,dist中添加一个index.html,然后可以访问调试一下
/packages/reactivity/dist/index.html
<!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>
<!-- <script src="../../../node_modules/vue/dist/vue.global.js"></script> -->
<!-- <script src="../../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script> -->
<script src="./reactivity.global.js"></script>
<div id="total"></div>
<div id="lowerPrice"></div>
<div id="lowerQuantity"></div>
<script>
const { reactive, effect } = VueReactivity;
// 响应式
let obj = {price: 100, quantity: 10}
const state = reactive(obj);
effect(()=>{// effect1
// 总价格
total.innerHTML = state.price * state.quantity;
})
effect(()=>{// effect2
// 折后价
lowerPrice.innerHTML = state.price * 0.8;
})
effect(()=>{// effect3
// 总数量
lowerQuantity.innerHTML = state.quantity * 0.9;
})
</script>
</body>
</html>
五、计算属性
/packages/reactivity/src/effect.ts
// 计算属性
import { isFunction } from "@vue/shared";
import { ReactiveEffect, isTracking, triggerEffects, trackEffects } from "./effect";
export function computed(gettetOrOptions) {
const onlyGetter = isFunction(gettetOrOptions);
let getter;
let setter;
if (onlyGetter) {
getter = gettetOrOptions;
setter = ()=>{}
} else {
getter = gettetOrOptions.get;
setter = gettetOrOptions.set;
}
return new ComputedRefImpl(getter, setter);
}
class ComputedRefImpl{
public dep; // 依赖收集
public _dirty = true; // 默认是脏,表示需要计算 不脏则不需要计算
public effect; // 计算属性依赖于effect
public _value;
constructor(getter, public setter) {
this.effect = new ReactiveEffect(getter, ()=>{// 计算effect
// scheduler
if (!this._dirty) {
this._dirty = true;
console.log(this.dep);
// 计算属性值变化了,需要通过它依赖的dep的effect执行
triggerEffects(this.dep);// 计算属性的dep依赖的是渲染effect
}
})
}
get value(){
debugger
if (isTracking()) {// 收集计算属性依赖effect
trackEffects(this.dep || (this.dep = new Set()));
}
if (this._dirty) {
// 将计算结果缓存
this._value = this.effect.run(); // 执行计算属性
this._dirty = false;
}
return this._value;
}
set value(newValue){
this.setter(newValue); // 修改计算属怀的值
}
}
导出
/packages/reactivity/src/index.ts
export {reactive} from './reactive'
export {effect, ReactiveEffect } from './effect'
export { computed } from './computed'
六、实现ref
1. ref与reactivity的区别
特性 | ref | reactive |
---|---|---|
数据类型 | 适用于基本类型和对象 | 仅适用于对象 |
访问方式 | 需要通过 .value 访问 | 直接访问属性 |
模板中使用 | 自动解包,无需 .value | 直接使用 |
重新赋值 | 可以重新赋值整个对象 | 保持同一引用,只能修改属性 |
解构/展开 | 保持响应性 | 会失去响应性 |
使用建议:
- 对于基本类型数据,使用 ref
- 对于对象/数组,如果不需要重新赋值的场景,使用 reactive
- 需要重新赋值的对象,使用 ref
- 组合逻辑时,通常统一使用 ref 更一致
ref 和 reactive 在性能上没有显著差异,选择主要基于使用场景和个人偏好。Vue 官方推荐在逻辑组合时更倾向于使用 ref,因为它在各种场景下表现更一致。
2. 代码实现
/packages/reactivity/src/ref.ts
import { isTracking, trackEffects, triggerEffects } from "./effect";
import { toReactive } from "./reactive";
class RefImpl {
public dep; // 依赖收集
public __v_isRef=true; // 标识属性
public _value;
// public _rawValue 等价于 this._rawValue = _rawValue;
constructor(public _rawValue) {
// _rawValue 如果传入的值 是一个对象 需要将对象转化为响应式
this._value = toReactive(_rawValue);
}
get value() {
if (isTracking()) {
trackEffects(this.dep || (this.dep = new Set()));
}
return this._value;
}
set value(newValue) {
if (newValue !== this._rawValue) {
this._rawValue = newValue;
this._value = toReactive(newValue);
// 触发更新
triggerEffects(this.dep);
}
}
}
function createRef(value) {
return new RefImpl(value);
}
export function ref(value) {
return createRef(value);
}
// ref shallow 浅引用 深引用 shallowRef
// let person = {
// name: 'xxx',
// address: [
// {name: '武汉东湖'},
// {name: '北京西单'}
// ]
// }
// 浅引用--只 person.name = 'yyyy' 数据改变会触发更新, person.address[0].name='xxx' 不会更新
导出
/packages/reactivity/src/index.ts
export {reactive} from './reactive'
export {effect, ReactiveEffect } from './effect'
export { computed } from './computed'
export {ref} from './ref'
七、Watch监听
1. 代码实现
/packages/reactivity/src/watch.ts
// watch: ref, function: ()=>x.value+y.value, reactiveObj
import { isFunction, isObject, isReactive, isRef } from "@vue/shared";
import { ReactiveEffect } from "./effect";
import { reactive } from "./reactive";
// let obj = {
// key: {
// value: {
// key: {
// }
// }
// }
// }
// 递归循环读取对象的数据
// 避免死循环,每次遍历将key存入set中,下次遍历比较是否存在,不存在则添加并遍历,否则忽略
function traversal(value, set = new Set()) {
// 递归终止条件,不是对象就不递归了
if (!isObject(value)) {
return value;
}
// 如何避免死循环 双向引用
if (set.has(value)) {
return value;
}
set.add(value);
for (let key in value) {
traversal(value[key], set);
}
return value;
}
// immediate 立即执行一次, 默认是当属性变化后才触发监听函数执行
export function watch(source, callback, {immediate} = {} as any) {
debugger
let getter; // 监听effect的函数
if (isReactive(source)) {
getter = ()=>traversal(source);
} else if (isRef(source)) {
getter = ()=>source.value;
} else if (isFunction(source)) {
getter = source;
} else {
return; // 忽略
}
// 暂存用户的cleanup函数
let cleanup;
const onCleanup = fn => {
cleanup = fn;
}
let oldValue;
const scheduler = ()=>{
if (cleanup) {
// 第一次cleanup为空
// 第二次就不为空了,实际上执行的第一次传的的cleanup
cleanup();
}
debugger
const newValue = effect.run(); // 执行监听effect===getter
callback(newValue, oldValue, onCleanup);// 回调
// 更新旧值
oldValue = newValue;
}
// 在effect中访问属性就会依赖收集
const effect = new ReactiveEffect(getter, scheduler);
// 立即执行
if (immediate) {
scheduler();
}
// 运行getter, 让getter中每一个响应式变量都收集这个effect
oldValue = effect.run();
// 重新处理oldValue
oldValue = reactive(JSON.parse(JSON.stringify(oldValue)))
}
响应式实现完整导入
/packages/reactivity/src/index.ts
export {reactive} from './reactive'
export {effect, ReactiveEffect } from './effect'
export { computed } from './computed'
export {ref} from './ref'
export {watch} from './watch'
至此基于Vue3的响应式实现就完成了,接下来我们调试一下
八、调试测试
<!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>
<!-- <script src="../../../node_modules/vue/dist/vue.global.js"></script> -->
<!-- <script src="../../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script> -->
<script src="./reactivity.global.js"></script>
<div id="total"></div>
<div id="lowerPrice"></div>
<div id="lowerQuantity"></div>
<script>
const { reactive, effect, computed, ref, watch } = VueReactivity;
const state = ref(1);
const state2 = ref(2);
const person = reactive({
name: "test",
age: 22,
address: {
province: "广东省",
city: '广州市'
}
})
// let timer = 3000;
// function getData(timer) {
// return new Promise((resolve, reject)=>{
// setTimeout(()=>{
// resolve(timer);
// }, timer);
// })
// }
// oncleanup: 执行上一次的回调
// 宏任务 微任务
// watch(
// ()=> person.age,
// async (newValue, oldValue, onCleanup)=>{
// let clear = false;
// onCleanup(()=>{
// clear = true;
// })
// timer -= 1000;
// let res = await getData(timer); // 读取异步数据
// debugger
// if (!clear) {
// total.innerHTML = res;
// }
// }
// )
// person.age = 23;
// setTimeout(()=>{
// person.age = 33;
// })
// 只对根级对象做proxy, 子级对象没有proxy
watch(person.address,
(newValue, oldValue)=>{
console.log(`person :${JSON.stringify(newValue)}, ${JSON.stringify(oldValue)}`)
total.innerHTML = JSON.stringify(newValue);
},{immediate: false})
// const state = ref(1);
// effect(()=>{
// total.innerHTML = state.value;
// })
setTimeout(()=>{
person.address.city = '深圳'
}, 1000)
// 响应式
// let obj = {price: 100, quantity: 10, flag: true}
// const state = reactive(obj);
// let result = computed(()=>{
// console.log('computed');
// return state.price + state.quantity;
// })
// effect(()=>{// 渲染effect
// lowerPrice.innerHTML = result.value;
// })
// setTimeout(()=>{
// state.price++;
// })
// effect(()=>{// effect1
// lowerPrice.innerHTML = state.price * 0.8;
// effect(()=>{ // effect2
// lowerQuantity.innerHTML = state2.quantity * 0.9;
// })
// })
// 依赖收集时机====读取属性
// effect(()=>{// effect1
// // 总价格
// total.innerHTML = state.price * state.quantity;
// })
// effect(()=>{// effect2
// 折后价
// activeEffect->fn->set->trigger->activeEffect
// state.price = Math.random();
// lowerPrice.innerHTML = state.price * 0.8;
// lowerPrice.innerHTML = state.flag ? state.price : state.quantity;
// })
// 调度函数,可以停止依赖收集,可以手动渲染
// let waiting = false;
// let runner = effect(()=>{// effect3
// // 总数量
// lowerQuantity.innerHTML = state.quantity * 0.9;
// }, {
// scheduler(){// 调度函数
// if (!waiting) {
// console.log("==========", arguments);
// waiting = true;
// Promise.resolve().then(()=>{
// debugger
// runner();// 手动渲染
// waiting = false;
// })
// }
// }
// })
// runner.effect.stop(); // 手动清理依赖
// flag=true price--->effect
// flag=false quantity--->effect 再修改price会导致再次渲染?
// setTimeout(()=>{
// state.flag = false;
// }, 1000)
// setTimeout(()=>{
// state.quantity++; // 不会导致页面渲染 quantity没有effect关联
// state.quantity++;
// state.quantity++;
// // runner();// 手动渲染
// }, 2000)
</script>
</body>
</html>