本文已参与「新人创作礼」活动,一起开启掘金创作之路
本专栏会讲述如何实现一个mini-vue
,让你了解vue
的底层原理,如果直接阅读vue
源码,那会是一件非常头疼的事情,因为许多的代码是用于处理一些边界情况的,这就导致我们很难找到核心内容,而mini-vue
实现了vue
的核心功能,忽略边界条件的判断,旨在让我们能够抓住核心,了解vue
的底层原理,并通过TDD
的思想进行开发,让你感受到TDD
带来的好处!
本节是该专栏的第一节,reactivity
是实现mini-vue
的基本,后续功能会依赖于reactivity
,因此我们从reactivity
开始,阅读本节之前,请确保你已经使用过vue
的reactivity
相关功能,了解它的作用,本节不会介绍reactivity
是什么,而是注重它的运行流程和实现原理
reactivity
模块会分为几篇文章去讲解,本篇文章是reactivity
模块的第一篇,主要讲解如何实现基本的reactive
和effect
reactive
用于创建响应式对象
effect
用于包裹副作用函数,收集响应式对象和副作用函数之间的依赖关系以及触发依赖
1. 项目搭建
首先需要创建我们的项目,需要用到的依赖有jest
、babel
、typescript
安装typescript
pnpm i typescript -Dnpx tsc --init
修改tsconfig.json
,将noImplicitAny
为false
,因为我们主要关注的是原理实现,而不关注类型,但又希望用到typescript
的一些特性,所以要允许项目中使用any
安装jest
pnpm i jest @types/jest -D
jest
集成babel
pnpm i babel-jest @babel/core @babel/preset-env -D
创建babel.config.js
module.exports = {presets: [['@babel/preset-env', {targets: {node: 'current'}}]],};
jest
集成typescript
pnpm i @babel/preset-typescript -D
修改babel.config.js
module.exports = {presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript',],};
修改package.json
添加测试脚本
"scripts": {"test": "jest"},
创建项目源码目录src/reactivity
,并写一个简单的测试用例看看环境是否搭建成功,src/reactivity/tests/index.spec.ts
describe('index', () => { it('happy path', () => { console.log('hello reactivity'); });});
终端执行pnpm test
,如果能够通过测试说明环境搭建完成
2. 实现简易版reactivity
首先我们实现一个简易版的reactivity
,这也就意味着不考虑很多额外的功能,只考虑先把最基本的功能实现,那么reactivity
最基本的功能有什么呢? 主要有两个模块:
reactive
,用于创建响应式对象,通过Proxy
实现
effect
,管理副作用函数,最基本的功能包括依赖收集和触发依赖
2.1 effect测试用例
基于TDD(Test-Driven Development)
的思想,我们先写一个简单的测试用例描述一下使用场景
// src/reactivity/tests/effect.spec.tsdescribe('effect', () => {// it 和 test 是一样的// it.skip 表示暂时跳过该测试项 因为目前需要 reactive 和 effect 而我们希望先去实现 reactive// 但又不希望 effect.spec.ts 影响整个测试的进行 因此可以用 skip 暂时跳过 等 reactive 实现后再改回来it.skip('happy path', () => { const foo = reactive({ name: 'foo', age: 20, isMale: true, friends: ['Mike', 'Tom', 'Bob'], info: { address: 'China', phone: 11011011000, }, }); let nextAge; effect(() => (nextAge = foo.age + 1)); expect(nextAge).toBe(21); // update foo.age++; expect(nextAge).toBe(22);});}
我们的需求很简单,就是利用reactive
创建一个响应式对象,然后effect
函数中会执行副作用函数fn
,当fn
所依赖的响应式对象的数据修改后,能够自动执行副作用函数fn
去更新依赖
由于reactive
和effect
函数目前都还没有实现,这个单元测试自然是无法通过的,而它们又是两个大模块,因此实现这两个模块也是有它们对应的单元测试的,可是目前这个happy path
的单元测试会妨碍我们之后编写具体某一个模块的单元测试的运行
比如我想先实现reactive
,那么我就需要先编写相应的单元测试,然后去运行单元测试,但是由于effect
模块还没实现,因此会被happy path
的这个单元测试干扰,导致无法通过所有测试用例,因此我们可以先将其标记为skip
,等我们实现完了两个模块的基本功能后再回来将标记删除,来测试happy path
是否可以通过
下面来理一下这个测试用例的流程
2.2 reactive测试用例
我们先来实现reactive
模块,仍然是先创建测试用例,根据测试用例去开发代码,这是TDD
的核心思想
describe('reactive', () => { it('happy path', () => { const foo = { bar: 1 }; const observed = reactive(foo); // observed 代理 foo 对象 expect(observed).not.toBe(foo); expect(observed.bar).toBe(1); });});
接下来我们就需要去实现reacive
函数
2.3 reactive基本实现
pnpm i jest @types/jest -D0
就是返回了一个Proxy
,代理传入的对象,并且get
和set
也是基本的功能,没有做过多额外的处理
不过之后为了管理依赖,会在get
中调用effect
模块的track
进行依赖收集,在set
中调用effect
模块的trigger
触发依赖(目前还没实现,后面会讲),我们先看看能不能通过测试用例吧 测试用例通过,说明reactive
实现基本的代理功能是没问题了,那么接下来我们就要开始处理依赖的问题了!
2.4 effect基本实现
考虑到effect
既要负责执行副作用函数,又要管理依赖,有多个功能,因此适合将他们封装到一个类中,我们首先封装一个ReactiveEffect
类
pnpm i jest @types/jest -D1
只要调用effect
函数,就会创建ReactiveEffect
对象,并执行它的run
方法,这样我们就将运行副作用函数的逻辑从effect
中转移到了ReactiveEffect
的run
方法中了
接下来要编写track
函数,用于收集依赖,trigger
函数,用于触发依赖
2.4.1 track依赖收集
根据前面的流程图,track
就是一个映射寻找的过程,首先是以依赖的对象作为key
去寻找它的属性和副作用函数之间的映射,找到这个映射后,再以依赖对象的属性作为key
去寻找副作用函数的集合,将当前激活的effect
对象加入到该集合中即可
targetMap
用一个全局变量去存储,可以使用Map
或者WeakMap
,为了让垃圾回收机制能够正常运作,建议使用WeakMap
作为targetMap
的实现,具体原因可自行了解Map
和WeakMap
的区别
当前激活的effect
对象用activeEffect
全局变量存储,每当effct
首次执行的时候,就会将activeEffect
标记为当前在执行的函数 如果该函数内部有触发响应式对象的get
拦截的话,就会执行track
进行依赖收集,而track
正是从actvieEffect
中获取到需要收集的函数的,因此activeEffect
算是建立起get
拦截器和track
之间沟通的桥梁
注意:加入到集合中的是**effect**
对象,而不是副作用函数,因为我们是通过**effect**
对象的**run**
方法统一执行副作用函数的
pnpm i jest @types/jest -D2
为了能够在track
中通过activeEffect
访问到正确的当前激活的effect
对象,我们需要在effect
对象执行run
方法的时候修改一下activeEffect
指向自己
pnpm i jest @types/jest -D3
2.4.2 trigger触发依赖
触发依赖也很简单,流程图中已经说明了,根据target
拿到depsMap
,再根据key
拿到deps
集合,遍历集合中每一个effect
对象,调用它们的run
方法即可将副作用函数执行
pnpm i jest @types/jest -D4
track
和trigger
都实现了以后,effect
单元测试的happy path
就可以通过了