本文章依据阅读源码的理解进行编写。如果有什么错误的地方,欢迎指正交流学习。
最近也在帮助想入行前端的朋友进行学习,如果有需要交流学习,可以添加微信 gdgzyw
聊天、学习、打游戏都阔以~

学习源码最快的方式就是理解概念后,自己写一个简版的功能。所以我们得先搭一个环境,这里采用测试驱动的方式进行。

初始化项目

初始化 package.json 和安装依赖

  1. yarn init -y
  2. yarn add -D @babel/core @babel/preset-env @babel/preset-typescript @types/jest babel-jest jest

添加 scripts 用于启动 jest

package.json

  1. {
  2. // ...
  3. "scripts": {
  4. "test": "jest"
  5. },
  6. // ...
  7. }

根目录创建 babel.config.js

  1. module.exports = {
  2. presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
  3. }

创建 tsconfig.json 文件

  1. {
  2. "compilerOptions": {
  3. "target": "es2016",
  4. "lib": [
  5. "DOM",
  6. "es6"
  7. ],
  8. "module": "commonjs",
  9. "types": [
  10. "jest"
  11. ],
  12. "esModuleInterop": true,
  13. "forceConsistentCasingInFileNames": true,
  14. "strict": true,
  15. "noImplicitAny": false,
  16. "skipLibCheck": true
  17. }
  18. }

至此我们的项目就初始化完成了。如果你需要用 git 来管理,可以自行 git init

编写测试用例

创建 src/reactivity/tests/effect.spec.ts 文件

  1. describe('effect', () => {
  2. it('happy path', () => {
  3. const bank = reactive({
  4. money: 100,
  5. });
  6. let myMoney;
  7. effect(() => {
  8. myMoney = bank.money * 2;
  9. });
  10. expect(myMoney).toBe(200);
  11. bank.money = 50;
  12. expect(myMoney).toBe(100);
  13. });
  14. });

创建 scr/reactivity/tests/reactive.spec.ts

  1. describe('reactive', () => {
  2. it('happy path', () => {
  3. const origin = {money: 100};
  4. const bank = reactive(origin);
  5. expect(bank).not.toBe(origin);
  6. expect(bank.money).toBe(100);
  7. });
  8. });

现在我们运行 yarn test 测试用例是跑不通的。对应的函数我们还没有创建。下面正式开始我们的编码环节。

编写 reactive 函数

通过上面的测试用例,我们可以知道,我们接收一个对象的值,并且对他进行一个拦截。所以我们可以直接返回一个 proxy 的代理对象。

  1. export function reactive(obj) {
  2. return new Proxy(obj, {
  3. get(target, key) {
  4. return Reflect.get(target, key)
  5. },
  6. set(target, key, value) {
  7. return Reflect.set(target, key, value)
  8. },
  9. })
  10. }

Reflect.get(target, key) 等同于 target[key]

接着我们为了统一出口可以创建 src/reactivity/index.ts 文件

index.ts

  1. export * from './reactive'

接着我们去 reactive.spec.ts 中引入我们的函数

  1. import {reactive} from '../index'

跑一下测试

  1. yarn test reactive

提示 PASS 至此发现这个的单侧已经跑通。接着我们可以开始写另一个单测。

编写 effect 函数

一样的,我们观察一下测试用例的参数。
可以发现他接受一个回调,所以我们参数是一个回调函数。
接着我们思考一下如何将我们上一个 reactive 的函数与这个回调函数产生关联。

学Vue3核心概念与面试官斗智斗勇(一) 收集触发依赖 - 图1

定义 tagetMap 变量,用于对象的分组。
定义 depsMap 变量,用于对象中每个 key 的依赖分组。
通过 reactive 定义对象,在 get 的时候,我们在 targetMap
中将对象添加到 Map 中作为分类。接着创建 Set 用 key 作为分类保存到 Set 中。

回顾我们的单侧流程,我们先定义了个 reactive 对象。
接着我们在 effect 函数中执行了回调函数,回调函数中我们会读取到 reactive 的值,从而触发了 get 操作。所以我们需要在 get 操作中进行依赖收集。

定义一个 ReactiveEffect 类,收集我们的回调函数

  1. let activeEffect
  2. class ReactiveEffect {
  3. private readonly _fn: any
  4. constructor(fn) {
  5. this._fn = fn
  6. }
  7. run() {
  8. activeEffect = this
  9. this._fn()
  10. }
  11. }

在初始化的时候,我们保存回调函数到 _fn 中,在我们执行 run 方法的时候。会触发我们的回调函数。

定义一个 track 的函数,完成收集依赖这个操作

  1. const targetMap = new Map()
  2. export function track(target, key) {
  3. let depsMap = targetMap.get(target)
  4. if (!depsMap) {
  5. depsMap = new Map()
  6. targetMap.set(target, depsMap)
  7. }
  8. let deps = depsMap.get(key)
  9. if (!deps) {
  10. deps = new Set()
  11. depsMap.set(key, deps)
  12. }
  13. deps.add(activeEffect)
  14. }

如果此时我们需要设置 reactive 的值,我们会触发 set 操作。所以触发依赖的操作需要在 set 中进行。

定义 trigger 函数,触发所有依赖

  1. export function trigger(target, key) {
  2. const depsMap = targetMap.get(target)
  3. const deps = depsMap.get(key)
  4. for (const dep of deps) {
  5. dep.run()
  6. }
  7. }

至此我们收集依赖和触发依赖的核心逻辑已经实现。我们现在可以跑 yarn test 进行检验。

结语

这篇文章是这个系列的开始,后续我会继续分享相关内容。慢慢完善我们对 vue3 的理解。
欢迎关注我,与我深入♂沟通。