深入Vue3响应式:手写实现reactive与ref

前言

上篇文章介绍了Vue3响应式的两个核心API,知道了两者的用法于区别,本文将带您深入实现其核心机制。我们将重点实现响应式数据变化时的依赖收集与触发更新功能,暂不涉及虚拟DOM和diff算法部分,视图更新将直接使用DOM API实现。通过这个实现,将更透彻地理解:

  1. 如何建立数据与视图的响应式关联
  2. 依赖收集的核心原理
  3. 触发更新的执行机制

响应式实现方案

关于响应式方案,Vue目前一共出现过三种方案,分别是:

方案 版本 核心缺陷
defineProperty Vue2 无法拦截数组操作、对象属性增删
Proxy + Reflect Vue3 完美解决Vue2的响应式限制
getter/setter ref实现 支持基本数据类型的响应式

defineProperty是Vue2中使用的响应式方案,由于该API有挺多缺陷,Vue2底层对此做了许多处理,比如:

  • 对数组无法拦截
  • 对象属性的新增与删除无法拦截

对Vue2响应式原理感兴趣的,可以去查看之前的这篇文章:【Vue源码学习】响应式原理探秘

所以Vue3选择了使用Proxy这个核心API与对象的getter与setter,**响应式机制的主要功能就是,可以把普通的 JavaScript 对象封装成为响应式对象,拦截数据的获取和修改操作,实现依赖数据的自动化更新。**接下来我们尝试动手实现:

reactive

reactive是通过ES6中Proxy来实现属性拦截的,所以我们可以先来实现一下:

ts 复制代码
const reactive =  <T extends object>(target: T) => {
    // 限制reactive只能传递引用类型,如果传递的不是引用类型,则出警告并将原始值直接返回
    if (typeof target !== 'object' || target === null) {
        console.warn('Reactive can only be applied to objects');
        return target
    }

    // 返回原始值的代理对象
    return new Proxy(target, {
        get(target, key, receiver) {
            const value = Reflect.get(target, key, receiver);
            // 这里需要收集依赖(后面实现)
            track(target, key);
            // 如果值是对象,则递归调用reactive
            if (typeof value === 'object' && value !== null) {
                return reactive(value); 
            }
            
            return value;
        },
        set(target, key, value, receiver) {
            const result = Reflect.set(target, key, value, receiver);

            // 这里需要触发更新(后面实现)
            trigger(target, key)
            return result;
        },
    })
}

export default reactive;

Proxy有许多拦截方法,这里我们暂时只需要拦截getset的操作

  • get方法中除了需要返回最新的数据,还需要收集依赖
  • set方法中除了更新数据,还需要执行上面收集的依赖

核心架构:

track(依赖收集)

接着来实现一下track方法,该方法的主要作用就是收集依赖,这里可以使用Map去进行存储依赖关系,Map的key就是我们的代理对象,而value还是一个嵌套的map,存储代理对象的每个key以及对应的依赖函数数组,因为每个key都可以有多个依赖

结构如图:

ts 复制代码
const targetMap = new WeakMap()
export const track = (target: object, key: PropertyKey) => {
    
    // 先找到target对应的依赖
    let depsMap = targetMap.get(target)

    if(!depsMap) {
        // 如果没找到,则说明是第一次收集,需要初始化
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
    // 接着需要对代理对象的属性进行依赖收集
    let deps = depsMap.get(key)
    if(!deps) {
        deps = new Set()
    }
    if (!deps.has(activeEffect) && activeEffect) { 
        // 防止重复注册 
        deps.add(activeEffect) 
        
    }
    depsMap.set(key, deps)
    console.log(`Tracking ${String(key)} on`, target);
};

trigger(更新触发)

实现完track 方法后,我们再来实现一下trigger ,该方法的主要作用就是从 targetMap 中,根据 target 和 key 找到对应的依赖函数集合 deps,然后遍历 deps 执行依赖函数

ts 复制代码
export const trigger = (target: object, key: PropertyKey) => {
    // 先找到target对应的依赖map
    // console.log('----',targetMap)
    const depsMap = targetMap.get(target)
    if(!depsMap) return
    // 再找到对应属性的依赖
    const deps = depsMap.get(key)
    // 如果没有依赖可执行,则返回
    if(!deps) return
    // 最后遍历整个依赖set分别执行
    console.log('--deps', deps)
    deps.forEach(effect => {
        effect?.()
    })
};

effect(副作用管理)

最后我们再来实现effect副作用函数,该副作用函数主要是在依赖更新的时候调用,它接受一个函数,在被调用的时候执行这个函数

在 effectFn 函数内部,把函数赋值给全局变量 activeEffect;然后执行 fn() 的时候,就会触发响应式对象的 get 函数,get 函数内部就会把 activeEffect 存储到依赖地图中,完成依赖的收集

js 复制代码
let activeEffect
export const effect = (fn: () => void) => {
    const effectFn = () => {
        activeEffect = effectFn
        fn()
    }

    effectFn()
}

关键流程:当effect执行时,内部函数会访问响应式数据,触发getter→track→将当前effect存入依赖集合

验证

响应式底层的几个核心方法都实现了,现在需要来验证是否可行,比如:通过reactive处理的数据,在数据更新时对应页面内容也需要更新。

由于没有写虚拟DOM与diff算法的逻辑,所以更新的操作我们直接使用DOM API来代替,主要是验证依赖收集与触发更新的逻辑是否符合预期

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>
    <div id="app">
        <div id="content"></div>
        <button id="countBtn">count++</button>
    </div>
    <script type="module">
        // ts 部分先编译成js
        import reactive from './reactive/reactive.js';
        import { effect } from './reactive/effect.js'
        // 通过自定义reactive创建响应式数据
        const state = reactive({
            count: 0,
            name: '南玖'
        });
        // 注册副作用函数,更新视图
        effect(() => {
            document.querySelector('#content').innerText = `name: ${state.name} --- car数量: ${state.count}`
        })
        // 按钮点击操作
        document.querySelector('#countBtn').addEventListener('click', () => {
            // 数据更新
            state.count += 1
        })
        console.log(state); // 0
    </script>
</body>
</html>

到这里reactive的响应式原理就基本实现了,我们继续来实现一下ref的响应式逻辑

ref

相比reactiveref的实现原理更简单一些,由于ref即可以传递基本数据类型也可以传递引用数据类型,而Proxy只能只能接受引用数据类型。所以ref采用的是面向对象的 getter 和 setter 拦截了 value 属性的读写,这也是为什么我们 ref 数据的 需要通过.value访问的原因

ts 复制代码
import { track, trigger } from './effect'
import  reactive  from './reactive'


const ref = (v) => {
    return new RefImpl(v)
}

class RefImpl {
    _value
    constructor(v) {
        this._value = convert(v)
    }

    get value() {
        track(this, 'value')
        return this._value
    }

    set value(val) {
        if(val === this._value) return
        this._value = convert(val)
        console.log('触发更新')
        trigger(this, 'value')
    }
}


const convert = (v) => {
    return isObject(v) ? reactive(v) : v
}

const isObject = (v) => {
    return typeof v === 'object' && v !== null
}

export default ref

对于引用类型的数据,ref底层会去调用reactive进行处理

总结

  1. 响应式核心三角
  1. reactive核心

    • 基于Proxy的深度代理
    • 嵌套对象自动响应化
    • 使用WeakMap存储依赖关系
  2. ref核心

    • getter/setter拦截value访问
    • 基本类型与引用类型统一处理
    • 对象类型自动转为reactive
  3. 性能优化点

    • 相同值不触发更新
    • WeakMap避免内存泄漏
    • 依赖函数精确收集
相关推荐
小小小小宇2 小时前
TS泛型笔记
前端
小小小小宇2 小时前
前端canvas手动实现复杂动画示例
前端
codingandsleeping2 小时前
重读《你不知道的JavaScript》(上)- 作用域和闭包
前端·javascript
小小小小宇2 小时前
前端PerformanceObserver使用
前端
zhangxingchao3 小时前
Flutter中的页面跳转
前端
前端风云志4 小时前
TypeScript实用类型之Omit
javascript
烛阴4 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝5 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇5 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军5 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js