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

四、最终

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

相关推荐
腾讯TNTWeb前端团队1 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰4 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪4 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪5 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy5 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom6 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom6 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom6 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom6 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试