前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue3中的响应式是如何被JavaScript实现的

Vue3中的响应式是如何被JavaScript实现的

作者头像
19组清风
发布2022-04-15 17:22:50
1.7K0
发布2022-04-15 17:22:50
举报
文章被收录于专栏:Web Front End

写在前边

Vuejs 作为在众多 MVVM(Model-View-ViewModel) 框架中脱颖而出的佼佼者,无疑是值得任何一个前端开发者去深度学习的。

不可置否尤大佬的 VueJs 中有许多值得我们深入研究的内容,但是作为最核心的数据响应式 Reactive 模块正是我们日常工作中高端相关的内容同时也是 VueJs 中最核心的内容之一。

至于 Vuejs 中的响应式原理究竟有多重要,这里我就不必累赘了。相信大家都能理解它的重要性。

不过这里我想强调的是,所谓响应式原理本质上也是基于 Js 代码的升华实现而已。你也许会觉得它很难,但是这一切只是源于你对他的未知。

毕竟只要是你熟悉的 JavaScript ,那么问题就不会很大对吧。

今天我们就让我们基于最新版 Vuejs 3.2 来稍微聊聊 VueJs 中核心模块 Reactive 是如何实现数据响应式的。

前置知识

Proxy 是 ES6 提供给我们对于原始对象进行劫持的 Api ,同样 Reflect 内置 Api 为我们提供了对于原始对象的拦截操作。

这里我们主要是用到他们的 get 、 set 陷阱。

TypeScript 的作用不言而喻了,文中代码我会使用 TypeScript 来书写。

EsBuild 是一款新型 bundle build tools ,它内部使用 Go 对于我们的代码进行打包整合。

pnpm 是一款优秀的包管理工具,这里我们主要用它来实现 monorepo

如果你还没在你的电脑上安装过 pnpm ,那么请你跟随官网安装它很简单,只需要一行 npm install -g pnpm即可。

搭建环境

工欲善其事,必先利其器。在开始之前我们首先会构建一个简陋的开发环境,便于将我们的 TypeScript 构建成为 Iife 形式,提供给浏览器中直接使用。

因为文章主要针对于响应式部分内容进行梳理,构建环境并不是我们的重点。所以我并不会深入构建环境的搭建中为大家讲解这些细节。

如果你有兴趣,可以跟着我一起来搭建这个简单的组织结构。如果你并不想动手,没关系。我们的重点会放在在之后的代码。

初始化项目目录

首先我们创建一个简单的文件夹,命名为 vue 执行 pnpm init -y 初始化 package.json 。

接下来我们依次创建:

  • pnpm-workspace.yaml文件

这是一个有关 pnpm 实现 monorepo 的 yaml 配置文件,我们会在稍微填充它。

  • .npmrc文件

这是有关 npm 的配置信息存放文件。

  • packages/reactivity目录

我们会在这个目录下实现核心的响应式原理代码,上边我们提过 vue3 目录架构基于 monorepo 的结构,所以这是一个独立用于维护响应式相关的模块目录。

当然,每个 packages 下的内容可以看作一个独立的项目,所以它们我们在 reactivity 目录中执行 pnpm init -y 初始化自己的 package.json。

同样新建 packages/reactivity/src 作为 reactivity 模块下的文件源代码。

  • packages/share目录

同样,正如它的文件夹名称,这个目录下存放所有 vuejs 下的工具方法,分享给别的模块进行引入使用。

它需要和 reactivity 维护相同的目录结构。

  • scripts/build.js文件

我们需要额外新建一个 scripts 文件夹,同时新建 scripts/build.js 用于存放构建时候的脚本文件。

此时目录如图所示。

安装依赖

接下来我们来依次安装需要使用到的依赖环境,在开始安装依赖之前。我们先来填充对应的 .npmrc 文件:

代码语言:javascript
复制
shamefully-hoist = true

默认情况下 pnpm 安装的依赖是会解决幽灵依赖的问题,所谓什么是幽灵依赖你可以查看这篇文章

这里我们配置 shamefully-hoist = true 意为我们需要第三方包中的依赖提升,也就是需要所谓的幽灵依赖。

这是因为我们会在之后引入源生 Vue 对比实现效果与它是否一致。

你可以在这里详细看到它的含义

同时,接下里让我们在 pnpm-workspace.yaml 来填入以下代码:

代码语言:javascript
复制
packages:
  # 所有在 packages/ 和 components/ 子目录下的 package
  - 'packages/**'
  # - 'components/**'
  # 不包括在 test 文件夹下的 package
  # - '!**/test/**'

因为基于 monorepo 的方式来组织包代码,所以我们需要告诉 pnpm 我们的 repo 工作目录。

这里我们指定了 packages/ 为 monorepo 工作目录,此时我们的 packages 下的每一个文件夹都会被 pnpm 认为是一个独立的项目。

接下来我们去安装所需要的依赖:

代码语言:javascript
复制
pnpm install -D typescript vue esbuild minimist -w

注意,这里 -w 意为 --workspace-root ,表示我们将依赖安装在顶层目录,所以包可以共享到这些依赖。 同时 minimist 是 node-optimist 的核心解析模块,它的主要作为即为解析执行 Node 脚本时的环境变量。

填充构建

接下来我们就来填充构建部分逻辑。

更改 package.json

首先,让我们切换到项目跟目录下对于整个 repo 的 pacakge.json 进行改造。

代码语言:javascript
复制
{
  "name": "@vue",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "node ./scripts/dev.js reactivity -f global"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "esbuild": "^0.14.27",
    "typescript": "^4.6.2",
    "vue": "^3.2.31"
  }
}
  • 首先我们将包名称修改为作用域,@vue 表示该包是一个组织包。
  • 其次,我们修改 scripts 脚本。表示当运行 pnpm run dev 时会执行 ./scripts/dev.js 同时传入一个 reactivity 参数以及 -f 的 global 环境变量。

更改项目内 package.json

接下来我们需要更改每个 repop 内的 package.json(以下简称 pck) 。这里我们以 reactivity 模块为例,share 我就不重复讲解了。

代码语言:javascript
复制
{
  "name": "@vue/reactive",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "buildOptions": {
    "name": "VueReactivity",
    "formats": [
      "esm-bundler",
      "esm-browser",
      "cjs",
      "global"
    ]
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
  • 首先,我们将 reactivity 包中的名称改为作用域名 @vue/reactive 。
  • 其次我们为 pck 中添加了一些自定义配置,分别为:
代码语言:txt
复制
- `buildOptions.name` 该选项表示打包生成 IIFE 时,该模块挂载在全局下的变量名。
代码语言:txt
复制
- `buildOptions.formats` 该选项表示该模块打包时候需要输出的模块规范。

填充scripts/dev.js

之后,让我们切换到 scripts/dev.js 来实现打包逻辑:

代码语言:javascript
复制
// scripts/dev.js
const { build } = require('esbuild');
const { resolve } = require('path');
const argv = require('minimist')(process.argv.slice(2));

// 获取参数 minimist
const target = argv['_'];
const format = argv['f'];

const pkg = require(resolve(__dirname, '../packages/reactivity/package.json'));

const outputFormat = format.startsWith('global')
  ? 'iife'
  : format.startsWith('cjs')
  ? 'cjs'
  : 'esm';

// 打包输出文件
const outfile = resolve(
  __dirname,
  `../packages/${target}/dist/${target}.${outputFormat}.js`
);

// 调用ESbuild的NodeApi执行打包
build({
  entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)],
  outfile,
  bundle: true, // 将所有依赖打包进入
  sourcemap: true, // 是否需要sourceMap
  format: outputFormat, // 输出文件格式 IIFE、CJS、ESM
  globalName: pkg.buildOptions?.name, // 打包后全局注册的变量名 IIFE下生效
  platform: outputFormat === 'cjs' ? 'node' : 'browser', // 平台
  watch: true, // 表示检测文件变动重新打包
});

脚本中的已经进行了详细的注释,这里我稍微在啰嗦一些。

其次整个流程看来像是这样,首先当我们运行 npm run dev 时,相当于执行了 node ./scripts/dev.js reactivity -f global

所以在执行对应 dev.js 时,我们通过 minimist 获得对应的环境变量 target 和 format 表示我们本次打包分别需要打包的 package 和模式,当然你也可以通过 process.argv 自己截取。

之后我们通过判断如果传入的 -f 为 global 时将它变成 iife 模式,执行 esbuild 的 Node Api 进行打包对应的模块。

需要注意的是,ESbuild 默认支持 typescript 所以不需要任何额外处理。

当然,我们此时并没有在每个包中创建对应的入口文件。让我们分别创建两个 packages/reactivity/src/index.ts以及packages/share/src/index.ts作为入口文件。

此时,当你运行 npm run dev 时,会发现会生成打包后的js文件:

写在环境结尾的话

至此,针对于一个简易版 Vuejs 的项目构建流程我们已经初步实现了。如果有兴趣深入了解这个完整流程的同学可以自行查看对应 源码

当然这种根据环境变量进行动态打包的思想,我在之前的React-Webpack5-TypeScript打造工程化多页面应用中详细讲解过这一思路,有兴趣的同学可以自行查阅。

其实关于构建思路我大可不必在这里展开,直接讲述响应式部分代码即可。但是这一流程在我的日常工作中的确帮助过我在多页面应用业务上进行了项目构建优化。

所以我觉得还是有必要拿出来和大家稍微聊一聊这一过程,希望大家在以后业务中遇到该类场景下可以结合 Vuejs 的构建思路来设计你的项目构建流程。

响应式原理

上边我们对于构建稍稍花费了一些篇幅,接下来终于我们要步入正题进行响应式原理部分了。

首先,在开始之前我会稍微强调一些。文章中的代码并不是一比一对照源码来实现响应式原理,但是实现思想以及实现过程是和源码没有出入的。

这是因为源码中拥有非常多的条件分支判断和错误处理,同时源码中也考虑了数组、Set、Map 之类的数据结构。

这里,我们仅仅先考虑基础的对象,至于其他数据类型我会在之后的文章中详细和大家一一道来。

同时我也会在每个步骤的结尾贴出对应的源代码地址,提供给大家参照源码进行对比阅读。

开始之前

在我们开始响应式原理之前,我想和大家稍微阐述下对应背景。因为可能有部分同学对应 Vue3 中的源码并不是很了解。

在 VueJs 中的存在一个核心的 Api Effect ,这个 Api 在 Vue 3.2 版本之后暴露给了开发者去调用,在3.2之前都是 Vuejs 内部方法并不提供给开发者使用。

简单来说我们所有模版(组件)最终都会被 effect 包裹 ,当数据发生变化时 Effect 会重新执行,所以 vuejs 中的响应式原理可以说是基于 effect 来实现的 。

当然这里你仅仅需要了解,最终组件是会编译成为一个个 effect ,当响应式数据改变时会触发 effect 函数重新执行从而更新渲染页面即可。

之后我们也会详细介绍 effect 和 响应式是如何关联到一起的。

基础目录结构

首先我们来创建一些基础的目录结构:

  • reactivity/src/index.ts 用于统一引入导出各个模块
  • reactivity/src/reactivity.ts 用于维护 reactive 相关 Api。
  • reactivity/src/effect.ts 用户维护 effect 相关 Api。
  • reacivity/src/baseHandler.ts 用户抽离 reactive 相关陷阱逻辑。

这一步我们首先在 reactivity 中新建对应的文件:

reactive 基础逻辑处理

接下来我们首先进入相关的 reactive.ts 中去。

思路梳理

关于 Vuejs 是如何实现数据响应式,简单来说它内部利用了 Proxy Api 进行了访问/设置数据时进行了劫持。

对于数据访问时,需要进行依赖收集。记录当前数据中依赖了哪些 Effect ,当进行数据修改时候同样会进行触发更新,重新执行当前数据依赖的 Effect。简单来说,这就是所谓的响应式原理。

关于 Effect 你可以暂时的将它理解成为一个函数,当数据改变函数(Effect)重新执行从而函数执行导致页面重新渲染。

Target 实现目标

在开始书写代码之前,我们先来看看它的用法。我们先来看看 reactive 方法究竟是如何搭配 effect 进行页面的更新:

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="app"></div>
  <script src="https://unpkg.com/vue@next"></script>
  <script>
    const {
      reactive,
      effect
    } = Vue

    const obj = {
      name: '19Qingfeng'
    }

    // 创建响应式数据
    const reactiveData = reactive(obj)

    // 创建effect依赖响应式数据
    effect(() => {
      app.innerHTML = reactiveData.name
    })

    // 0.5s 后更新响应式数据
    setTimeout(() => {
      reactiveData.name = 'wang.haoyu'
    }, 500)
  </script>
</body>

</html>

不太了解 Effect 和响应式数据的同学可以将这段代码放在浏览器下执行试试看。

首先我们使用 reactive Api 创建了一个响应式数据 reactiveData 。

之后,我们创建了一个 effect,它会接受一个 fn 作为参数 。这个 effect 内部的逻辑非常简单:它将 id 为 app 元素的内容置为 reactiveData.name 的值。

注意,这个 effect 传入的 fn 中依赖了响应式数据 reactiveData 的 name 属性,这一步通常成为依赖收集。

当 effect 被创建时,fn 会被立即执行所以 app 元素会渲染对应的 19Qingfeng 。

当 0.5s 后 timer 达到时间,我们修改了 reactiveData 响应式数据的 name 属性,此时会触发改属性依赖的 effct 重新执行,这一步同样通常被称为触发更新。

所以页面上看起来的结果就是首先渲染出 19Qingfeng 在 0.5s 后由于响应式数据的改变导致 effect 重新执行所以修改了 app 的 innerHTML 导致页面重新渲染。

这就是一个非常简单且典型的响应式数据 Demo ,之后我们会一步一步基于结果来逆推实现这个逻辑。

基础 Reactive 方法实现

接下来我们先来实现一个基础版的 Reactive 方法,具体使用 API 你可以参照👉这里。

上边我们提到过 VueJs 中针对于响应式数据本质上就是基于 Proxy & Reflect 对于数据的劫持,那么自然我们会想到这样的代码:

代码语言:javascript
复制
// reactivity/src/reactivity.ts

export function isPlainObj(value: any): value is object {
  return typeof value === 'object' && value !== null;
}

const reactive = (obj) => {
  // 传入非对象
  if (!isPlainObj(obj)) {
    return obj;
  }

  // 声明响应式数据
  const proxy = new Proxy(obj, {
      get() {
          // dosomething
      },
      set() {
          // dosomething
      }
  });

  return proxy;
};

上边的代码非常简单,我们创建了一个 reactive 对象,它接受传入一个 Object 类型的对象。

我们会对于函数传入的 obj 进行校验,如果传入的是 object 类型那么会直接返回。

接下来,我们会根据传入的对象 obj 创建一个 proxy 代理对象。并且会在该代理对象上针对于 get 陷阱(访问对象属性时)以及 set (修改代理对象的值时)进行劫持从而实现一系列逻辑。

依赖收集

之前我们提到过针对于 reactive 的响应式数据会在触发 get 陷阱时会进行依赖收集。

这里你可以简单将依赖收集理解为记录当前数据被哪些Effect使用到,之后我们会一步一步来实现它。

代码语言:javascript
复制
// reactivity/src/reactivity.ts

export function isPlainObj(value: any): value is object {
  return typeof value === 'object' && value !== null;
}

const reactive = (obj) => {
  // 传入非对象
  if (!isPlainObj(obj)) {
    return obj;
  }

  // 声明响应式数据
  const proxy = new Proxy(obj, {
    get(target, key, receiver) {
      // 依赖收集方法 track
      track(target, 'get', key);

      // 调用 Reflect Api 获得原始的数据 你可以将它简单理解成为 target[key]
      let result = Reflect.get(target, key, receiver);
      // 依赖为对象 递归进行reactive处理
      if (isPlainObj(result)) {
        return reactive(result);
      }

      // 配合Reflect解决当访问get属性递归依赖this的问题
      return result;
    },
    set() {
      // dosomething
    }
  });

  return proxy;
};

上边我们填充了在 Proxy 中的 get 陷阱的逻辑:

  • 当访问响应式对象 proxy 中的属性时,首先会针对于对应的属性进行依赖收集。主要依靠的是 track 方法。
  • 之后如果访问该响应式对象 key 对应的 value 仍为对象时,会再次递归调用 reactive 方法进行处理。

需要注意的是递归进行 reactive 时是一层懒处理,换句话说只有访问时才会递归处理并不是在初始化时就会针对于传入的 obj 进行递归处理。

当然这里的依赖收集主要依靠的就是 track 方法,我们会在稍后详解实现这个方法。

依赖收集

接下来我们来看看 set 陷阱中的逻辑,当触发对于 proxy 对象的属性修改时会触发 set 陷阱从而进行触发对应 Effect 的执行。

我们来一起看看对应的 set 陷阱中的逻辑:

代码语言:javascript
复制
// reactivity/src/reactivity.ts

export function isPlainObj(value: any): value is object {
  return typeof value === 'object' && value !== null;
}

const reactive = (obj) => {
  // 传入非对象
  if (!isPlainObj(obj)) {
    return obj;
  }

  // 声明响应式数据
  const proxy = new Proxy(obj, {
    get(target, key, receiver) {
      // 依赖收集方法 track
      track(target, 'get', key);

      // 调用 Reflect Api 获得原始的数据 你可以将它简单理解成为 target[key]
      let result = Reflect.get(target, key, receiver);
      // 依赖为对象 递归进行reactive处理
      if (isPlainObj(result)) {
        return reactive(result);
      }

      // 配合Reflect解决当访问get属性递归依赖this的问题
      return result;
    },
    // 当进行设置时进行触发更新
    set(target, key, value, receiver) {
      const oldValue = target[key];
      // 配合Reflect解决当访问get属性递归依赖this的问题
      const result = Reflect.set(target, key, value, receiver);
      // 如果两次变化的值相同 那么不会触发更新
      if (value !== oldValue) {
        // 触发更新
        trigger(target, 'set', key, value, oldValue);
      }

      return result;
    },
  });

  return proxy;
};

同样,我们在上边填充了对应 set 陷阱之中的逻辑,当设置响应式对象时会触发对应的 set 陷阱。我们会在 set 陷阱中触发对应的 trigger 逻辑进行触发更新:将依赖的 effect 重新执行。

关于为什么我们在使用 Proxy 时需要配合 Refelct ,我在这篇文章有详细讲解。感兴趣的朋友可以查看这里👉 [为什么Proxy一定要配合Reflect使用?]。

上边我们完成了 reactive.ts 文件的基础逻辑,遗留了两个核心方法 track & trigger 方法。

在实现着两个方法之前,我们先来一起看看 effect 是如何被实现的。

effect 文件

effect 基础使用

让我们把视野切到 effcet.ts 中,我们稍微来回忆一下 effect Api 的用法:

代码语言:javascript
复制
const {
  reactive,
  effect
} = Vue

const obj = {
  name: '19Qingfeng'
}

// 创建响应式数据
const reactiveData = reactive(obj)

// 创建effect依赖响应式数据
effect(() => {
  app.innerHTML = reactiveData.name
})

effect 基础原理

上边我们看到,effect Api 有以下特点:

  • effect 接受一个函数作为入参。
  • 当调用effect(fn) 时,内部的函数会直接被调用一次。
  • 其次,当 effect 中的依赖的响应式数据发生改变时。我们期望 effect 会重新执行,比如这里的 effect 依赖了 reactiveData.name 上的值。

接下来我们先来一起实现一个简单的 Effect Api:

代码语言:javascript
复制
function effect(fn) {
  // 调用Effect创建一个的Effect实例
  const _effect = new ReactiveEffect(fn);

  // 调用Effect时Effect内部的函数会默认先执行一次
  _effect.run();

  // 创建effect函数的返回值:_effect.run() 方法(同时绑定方法中的this为_effect实例对象)
  const runner = _effect.run.bind(_effect);
  // 返回的runner函数挂载对应的_effect对象
  runner.effect = _effect;

  return runner;
}

这里我们创建了一个基础的 effect Api,可以看到它接受一个函数 fn 作为参数。

当我们运行 effect 时,会创建一个 const _effect = new ReactiveEffect(fn); 对象。

同时我们会调用 _effect.run() 这个实例方法立即执行传入的 fn ,之所以需要立即执行传入的 fn 我们在上边提到过:当代码执行到 effect(fn) 时,实际上会立即执行 fn 函数。

我们调用的 _effect.run() 实际内部也会执行 fn ,我们稍微回忆下上边的 Demo 当代码执行 effect(fn) 时候相当于执行了:

代码语言:javascript
复制
// ...
effect(() => { app.innerHTML = reactiveData.name })

会立即执行传入的 fn 也就是 () => { app.innerHTML = reactiveData.name } 会修改 app 节点中的内容。

同时,我们之前提到过因为 reactiveData 是一个 proxy 代理对象,当我们访问它的属性时实际上会触发它的 get 陷阱。

代码语言:javascript
复制
// effect.ts

export let activeEffect;

export function effect(fn) {
  // 调用Effect创建一个的Effect实例
  const _effect = new ReactiveEffect(fn);

  // 调用Effect时Effect内部的函数会默认先执行一次
  _effect.run();

  // 创建effect函数的返回值:_effect.run() 方法(同时绑定方法中的this为_effect实例对象)
  const runner = _effect.run.bind(_effect);
  // 返回的runner函数挂载对应的_effect对象
  runner.effect = _effect;

  return runner;
}


/**
 * Reactive Effect
 */
export class ReactiveEffect {
  private fn: Function;
  constructor(fn) {
    this.fn = fn;
  }

  run() {
    try {
         activeEffect = this;
        // run 方法很简单 就是执行传入的fn
        return this.fn();
    } finally {
        activeEffect = undefined
    }
   
  }
}

这是一个非常简单的 ReactiveEffect 实现,它的内部非常简单就是简单的记录了传入的 fn ,同时拥有一个 run 实例方法当调用 run 方法时会执行记录的 fn 函数。

同时,我们在模块内部声明了一个 activeEffect 的变量。当 我们调用运行 effect(fn) 时,实际上它会经历以下步骤:

  • 首先用户代码中调用 effect(fn)
  • VueJs 内部会执行 effect 函数,同时创建一个 _effect 实例对象。立即调用 _effect.run() 实例方法。
  • 重点就在所谓的 _effect.run() 方法中。
  • 首先,当调用 _effect.run() 方法时,我们会执行 activeEffect = this 将声明的 activeEffect 变成当前对应的 _effect 实例对象。
  • 同时,run() 方法接下来会调用传入的 fn() 函数。
  • 当 fn() 执行时,如果传入的 fn() 函数存在 reactive() 包裹的响应式数据,那么实际上是会进入对应的 get 陷阱中
  • 当进入响应式数据的 get 陷阱中时,不要忘记我们声明全局的 activeEffect 变量,我们可以在对应响应式数据的 get 陷阱中拿到对应 activeEffect (也就是创建的 _effect) 变量。

接下来我们需要做的很简单:

在响应式数据的 get 陷阱中记录该数据依赖到的全局 activeEffect 对象(_effect)(依赖收集)也就是我们之前遗留的 track 方法。

同时:

当改变响应式数据时,我们仅仅需要找出当前对应的数据依赖的 _effect ,修改数据同时重新调用 _effect.run() 相当于重新执行了 effect(fn)中的 fn。那么此时不就是相当于修改数据页面自动更新吗?这一步就被称为依赖收集,也就是我们之前遗留的 trigger 方法。

track & trigger 方法

让我们会回到之前遗留的 track 和 trigger 逻辑中,接下来我们就尝试去实现它。

这里我们将在 effect.ts 中来实现这两个方法,将它导出提供给 reactive.ts 中使用。

思路梳理

上边我们提到过,核心思路是当代码执行到 effect(fn) 时内部会调用对应的 fn 函数执行。当 fn 执行时会触发 fn 中依赖的响应式数据的 get ,当 get 触发时我们记录到对应 声明的(activeEffect) _effect 对象和对应的响应式数据的关联即可。

当响应式数据改变时,我们取出关联的 _effect 对象,重新调用 _effect.run() 重新执行 effect(fn) 传入的 fn 函数即可。

看到这里,一些同学已经反应过来了。我们有一份记录对应 activeEffect(_effect) 和 对应的响应式数据的表,于是我们自然而然的想到使用一个 WeakMap 来存储这份关系。

之所以使用 WeakMap 来存储,第一个原因自然是我们需要存储的 key 值是非字符串类型这显然只有 map 可以。其次就是 WeakMap 的 key 并不会影响垃圾回收机制

创建映射表

上边我们分析过,我们需要一份全局的映射表来维护 _effect 实例和依赖的响应式数据的关联:

于是我们自然想到通过一个 WeakMap 对象来维护映射关系,那么如何设计这个 WeakMap 对象呢?这里我就不卖关子了。

我们再来回忆下上述的 Demo :

代码语言:javascript
复制
// ...
const {
  reactive,
  effect
} = Vue

const obj = {
  name: '19Qingfeng'
}

// 创建响应式数据
const reactiveData = reactive(obj)

// 创建effect依赖响应式数据
effect(() => {
  app.innerHTML = reactiveData.name
})

// 上述Demo的基础上增加了一个effect依赖逻辑
effect(() => {
   app2.innerHTML = reactiveData.name
})

首先针对于响应式数据 reactiveData 它是一个对象,上述代码中的 effect 中仅仅依赖了这个对象上的 name 属性。

所以,我们仅仅需要关联当前响应式对象中的 name 属性和对应 effect 即可。

同时,针对于同一个响应式对象的属性比如这里的 name 属性被多个 effect 依赖。自然我们可以想到一份响应式数据的属性可以被多个 effect 依赖。

根据上述的分析最终 Vuejs 中针对于这份映射表设计出来了这样的结构:

当一个 effect 中依赖对应的响应式数据时,比如上述 Demo :

全局的 WeakMap 首先会根据当前 key 响应式对象的原始对象,也就是 reactive 对应的原始对象(未代理前的 obj) 作为 key 值,value 为一个 Map 对象。

同时 effect 内部使用了上述对象的某个属性,那么此时 WeakMap 对象的该对象的值(对应为一个 Map)。我们会在这个 Map 对象中设置 key 为使用到的属性,value 为一个 Set 对象。

为什么对应属性的值为一个 Set ,这非常简单。因为该属性可能会被多个 effect 依赖到。所以它的值为一个 Set 对象,当该属性被某个 effect 依赖到时,会将对应 _effect 实例对象添加进入 Set 中。

也许有部分同学乍一看对于这份映射表仍然比较模糊,没关系接下来我会用代码来描述这一过程。你可以结合代码和这段文字进行一起理解。

track 实现

接下来我们来看看 track 方法的实现: f

trigger 方法

baseHandler 文件

依赖收集

派发更新

阶段总结

写在结尾

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022/04/12 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前边
  • 前置知识
  • 搭建环境
    • 初始化项目目录
      • 安装依赖
        • 填充构建
          • 更改 package.json
          • 更改项目内 package.json
          • 填充scripts/dev.js
        • 写在环境结尾的话
        • 响应式原理
          • 开始之前
            • 基础目录结构
              • reactive 基础逻辑处理
                • 思路梳理
                • Target 实现目标
                • 基础 Reactive 方法实现
              • effect 文件
                • effect 基础使用
                • effect 基础原理
              • track & trigger 方法
                • 思路梳理
                • 创建映射表
                • track 实现
                • trigger 方法
              • baseHandler 文件
              • 依赖收集
              • 派发更新
              • 阶段总结
              • 写在结尾
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档