Spiga

前端提升6:手撸vue3响应式实现

2025-04-05 18:21:43

Vue3响应式的实现跟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搭建一个本地环境

环境搭建

  1. 新建一个vue3的文件夹,然后安装rollup

    yarn add rollup --dev
    
  2. 目录搭建

    --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
    
  3. 基本配置

    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
    
  4. 每个包的配置

    /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"
         ]
      }
    }
    
  5. 打包脚本

    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']
    
  6. 运行

    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 直接使用
重新赋值 可以重新赋值整个对象 保持同一引用,只能修改属性
解构/展开 保持响应性 会失去响应性

使用建议:

  1. 对于基本类型数据,使用 ref
  2. 对于对象/数组,如果不需要重新赋值的场景,使用 reactive
  3. 需要重新赋值的对象,使用 ref
  4. 组合逻辑时,通常统一使用 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>