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

四、最终

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

相关推荐
正小安1 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch3 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光3 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   3 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   3 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web3 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常3 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇4 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr4 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho5 小时前
【TypeScript】知识点梳理(三)
前端·typescript