好记性不如烂笔头!通俗易懂,带你从零实现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
}

四、最终

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

相关推荐
半兽先生10 分钟前
vue video重复视频 设置 srcObject 视频流不占用资源 减少资源浪费
前端·javascript·vue.js
WebDesign_Mu37 分钟前
HTML+CSS+JS制作中国传统节日主题网站(内附源码,含5个页面)
javascript·css·html
A雄42 分钟前
2025新春烟花代码(二)HTML5实现孔明灯和烟花效果
前端·javascript·html
uhakadotcom1 小时前
YC:2025年不容错过的1000个硬科技、新质生产力的创新方向清单
前端·面试·github
咔咔库奇1 小时前
ES6的高阶语法特性
前端·ecmascript·es6
一点一木1 小时前
Can I Use 实战指南:优化你的前端开发流程
前端·javascript·css
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.1 小时前
HTML前端从零开始
前端·html
博客zhu虎康1 小时前
Vue 封装公告滚动
前端·javascript·vue.js
程序员鱼皮1 小时前
学前端 4 个月想进中厂,该怎么做?
前端·经验分享·计算机
"追风者"1 小时前
前端(十三)bootstrap的基本使用
前端·bootstrap