好记性不如烂笔头!通俗易懂,带你从零实现vue3响应式核心原理

尝试跟着本文来动手实现一个简单的响应式系统,并最终用其来完成一个小案例(effect、reactive、track、trigger)

一、阶段目标

响应式系统简单的理解为当我们的响应式数据发生改变时候,其依赖的副作用函数会重新被执行。本阶段的目标是实现下面一个的一个小案例。

index.html:

html 复制代码
<body>
  <button id="button">点击</button>
  <div id="app"></div>
  <script src="./main.js"></script>
</body>

main.js:

js 复制代码
import { reactive, effect } from './dist/vue.esm.js'

const button = document.querySelector("#button");
const app = document.querySelector("#app");
const a = reactive({ count: 0 })

effect(() => {
  app.innerHTML = a.count
})

button?.addEventListener('click', () => {
  a.count++
})

effect是副作用函数,会影响到其它函数的执行,其内部存在对被 reactive 函数声明的响应式对象 a.count 的访问,因此,当我们的 a.count 数据发生变化的时候,effect 的回调函数会被再次执行。

目标:

  1. effect 函数首次执行,app内显示文本。
  2. 点击button按钮,a.count属性改变,app内文本重新渲染。

现在只有一个空荡荡的按钮,数据也没被渲染出来,最终完成的结果可以先直接查看第四章。

一、reactive

其实说到Vue3响应式原理,我们大多数人都知道其是依赖于 Proxy 代理来实现的,但是对其内部逻辑是如何实现的任然是一知半解,现在就让我们来挑战这一重要函数。

ts 复制代码
export function reactive(target) {
  // 创建代理对象
  const _proxy = new Proxy(target, {
    // 拦截对象属性访问
    get(target, key) {
      const value = Reflect.get(target, key);
      // TODO: track函数收集依赖
      return value;
    },

    // 拦截对象属性修改
    set(target, key, newValue) {
      Reflect.set(target, key, newValue);
      // TODO:trigger函数触发依赖
      return true
    },
  });

  return _proxy
}

通过上面的几行简单代码,我们就完成了 reactive 的基本架构搭建,创建并返回了一个代理对象。

接下来,我们只需要在 get 方法内去收集依赖,在 set 方法中触发收集到的依赖。

二、effect

在前文我们就早早提到了 effect 函数,作为副作用函数,它将会被我们的响应式数据进行收集以及触发。

ts 复制代码
let currentActiveEffect = null

export function effect(fn) {
  const effectFn = () => {
    currentActiveEffect = effectFn
    fn()
  }

  effectFn()
}

当我们执行 effect 函数的时候,currentActiveEffect 会暂存该回调函数,如果其内部存在对响应式数据的访问,那么将等待被收集,接下来就到了我们的收集依赖和触发依赖的阶段。

三、track、trigger

如果说 reactive 是响应式系统的核心模块,那么可以说 tracktrigger 就是 reactive 的实现核心。

我们首先要考虑的是要采用何种结构来对副作用函数进行存储,并将属性和其对应的依赖进行一一对应,具体可见下图:

我们可以简单的梳理一下上面的结构图。

  1. 创建一个 WeakMap 结构,键为target对象,值为一个Map结构。
  2. Map结构的键为对应的属性,值为一个Set结构,内部存储的是我们最终要操作的effect函数。

简单的理解:

  • track 依赖收集的过程就是把依赖函数放入对应的Set结构的过程。
  • trigger依赖触发的过程就是把对应Set结构的依赖函数获取到并依次执行的过程。
ts 复制代码
const bucket = new WeakMap()

// 依赖收集函数
export function track(target, key) {
  let depsMap = bucket.get(target);
  if (!depsMap) {
    depsMap = new Map();
    bucket.set(target, depsMap);
  }
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  currentActiveEffect && deps.add(currentActiveEffect);
}

// 依赖触发函数
export function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (!depsMap) {
    depsMap = new Map();
    bucket.set(target, depsMap);
  }
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  // 依次执行收集到的依赖函数
  deps.forEach(dep => {
    dep()
  })
}

实现这两个函数后,我们再将其放入对应的执行位置当中:

typescript 复制代码
export function reactive(target) {
  const _proxy = new Proxy(target, {
    get(target, key) {
      const value = Reflect.get(target, key);
      // 新增
      track(target, key);
        
      return value;
    },
    set(target, key, newValue) {
      Reflect.set(target, key, newValue);
      // 新增
      trigger(target, key)
        
      return true
    },
  });

  return _proxy
}

四、最终

至此,我们已经可以采用我们自己的响应式系统来实现第一章的小案例,通过点击按钮,成功触发视图的重新渲染,如下图:

相关推荐
kyriewen31 分钟前
2026 年了,还在用 Node.js?Bun 迁移实战:20 分钟搞定,附踩坑记录
前端·javascript·node.js
青山Coding2 小时前
Cesium应用(八):物体运动的实现思路
前端·cesium
用户41659673693552 小时前
Android WebView 加载 file:// 离线页面调试教程
android·前端
Asmewill2 小时前
curl命令学习笔记一
前端
我是一只快乐的小螃蟹2 小时前
1.2 ArrayList 源码解析
前端
星栈2 小时前
我用 Rust + Dioxus 做了个全栈跨平台笔记应用:再把新建、编辑和交付补上
前端·rust·前端框架
我是一只快乐的小螃蟹2 小时前
1.1 HashMap (JDK1.8) 源码解析
前端
爱勇宝5 小时前
小红花成长新版:模板来了,鼓励也更容易开始
前端·后端·程序员
竹林8186 小时前
Solana前端开发:我在一个NFT铸造页面上被@solana/web3.js的Connection和Transaction签名坑了两天
前端
冬奇Lab6 小时前
每日一个开源项目(第144篇):ai-website-cloner-template - 一条命令、多 Agent 并行,把任意网站逆向成 Next.js 代码
前端·人工智能·开源