从0学习 vue3 reactivity源码笔记

我心飞翔 分类:javascript

写在前面:
这是一篇我自己学习 vue3 源码的笔记,记录了 vue3 reactivity 实现过程及原理。并没有认真排大纲内容。

写这个有两个原因:1. 为了学习vue3;2. 为了培养自己在学习的过程中养成做笔记的好习惯。

1. 基本构建

1.1初始化项目

1.1.1

初始化默认 package.json

yarn init -y
 

创建packages文件夹,修改package.json文件:

  {
    "name": "my-vue3",
+   "private": true,
    "version": "1.0.0",
    "main": "index.js",
    "license": "MIT",
+   "workspaces": [
+     "packages/*"
+   ]
  }
 

1.1.2

package文件夹下创建reactivity文件夹和shared文件夹,分别在这两个文件夹下 init 一个 package.json文件以及src文件夹,且在这两个src文件夹下均创建一个index.ts文件。项目目录如下

 vue
 	|- /packages
+ 	  |- /reactivity
+	    |- /src
+		|- index.ts
+	    |- package.json
+	  |- /shared
+	    |- /src
+	      |- index.ts
+	    |- package.json
   package.json
 

修改reactivityshared目录下的package.json文件

// 此处以 reactivity 为例
	 {
-    "name": "reactivity",
+		 "name": "@vue/reactivity",
     "version": "1.0.0",
     "main": "index.js", // commonJs(Nodejs)
+    "dist/reactivity.esm-bundler.js", // webpack, ES6
     "license": "MIT",
+    "buildOptions": { // 自定义配置,给 rollup 使用
+	"name": "VueReactivity", // 自定义全局包(模块)名,shared 的 global 名字无所谓
+	"formats": [ // 支持的类型
+		"cjs", // node
+		"esm-bundler", //es6
+		"global" // 全局,shared作为 vue 的共享模块不需要
+	]
+    }
   }
 

模块导出

// packages/reactivity/src/index.ts
const Reactivity = {

}

export {
  Reactivity
}
 
// packages/shared/src/index.ts
const shared = {

}

export {
  shared
}
 

1.1.3 安装依赖

在根目录下安装依赖

  1. 安装 typescriptrollup 相关的依赖
  2. @rollup/plugin-node-resolve: 解析第三方模块,可以使我们支持第三方模块
  3. @rollup/plugin-json: 支持解析json
  4. execa: 使用多进程
  5. --ignore-workspace-root-check: 忽略工作空间
yarn add typescript rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-json execa --ignore-workspace-root-check
 

1.1.4 添加 scripts 命令

在根目录下创建scripts文件夹,项目目录如下

 vue
  |- /packages
+ |- /scripts
+   dev.js // 开发环境,可以针对某个模块包进行打包
+   build.js	// 生产环境,打包所有
   package.json
 
    {
      "name": "my-vue3",
      "private": true,
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "workspaces": [
        "packages/*"
      ],
+     "scripts": {
+       "dev": "node scripts/dev.js",
+       "build": "node scripts/build.js"
+   	},
      "dependencies": {
        "@rollup/plugin-json": "^4.1.0",
        "@rollup/plugin-node-resolve": "^11.2.1",
        "execa": "^5.0.0",
        "rollup": "^2.43.0",
        "rollup-plugin-typescript2": "^0.30.0",
        "typescript": "^4.2.3"
      }
    }

 

1.1.5 构建打包命令

// scripts/build.js

const fs = require('fs')

// 同步读取 packages 目录下的文件
const targets = fs.readdirSync('packages').filter(f => {
  // 过滤出需要打包的文件
  // 保留文件夹,剔除掉文件
  if (!fs.statSync(`packages/${f}`).isDirectory()) {
		return false
  }
  return true
})

// 打包方法
const build = async (target) => {
  // 打包的目标
  console.log(target)
}
// 对多个目标打包的方法
const runParallel = (targets, iteratorFn) => {
  const res = []
	for (const item of targets) {
    // 每个打包都是一个 promise
    const p = iteratorFn(item)
    res.push(p)
  }
  return Promise.all(res)
}

// 对目标目录依次进行打包,并行打包
runParallel(targets, build)

// runParallel(targets, build).then(() => {
//   console.log('所有的包都打包完毕了')
// })
 

到这一步,我们基本完成了对多个目标进行打包的雏形,接着我们慢慢完善我们的打包方法 build

    // scripts/build.js
    const fs = require('fs')
+   const execa = require('execa') // 开启子进程进行打包,最终还是使用rollup来进行打包

......

    const build = async (target) => {
    	// 第一个参数是执行的命令 rollup,第二个是执行的参数
    	// -c 表示采用某个配置文件
    	// --environment 表示采用环境变量
    	// TARGET 目标
    	// stdio 子进程打包的信息共享给父进程
+    	await execa('rollup', ['-c', '--environment', `TARGET:${target}`, {stdio: inherit}])
    }

 

1.1.6 配置 rollup

在根目录下创建 rollup.config.js文件,我们尝试一下rollup中能否获取到打包的目标

// rollup.config.js

console.log(process.env.TARGET, '----rollup-----')
 

yarn build后在控制台中会发现正确的输出了(忽略抛错,因为我们没有导出文件) reactivity ----rollup-----以及 shared ----rollup-----

接着我们开始正式配置 rollup.config.js

// rollup 的配置
import path from 'path'
// 8. 引入插件
import json from '@rollup/plugin-json'
import resolvePlugin from '@rollup/plugin-node-resolve'
import ts from 'rollup-plugin-typescript2'
// 根据环境变量中的target属性 获取对应模块中的 package.json
// 1. 在当前目录下查找到 packages 文件夹
const packagesDir = path.resolve(__dirname, 'packages')
// 2. 找到要打包的对应的目标目录
const packageDir = path.resolve(packagesDir, process.env.TARGET)
// 封装出一个寻找对应包下的文件的方法
const resolve = p => path.resolve(packageDir, p)
// 3. 找到对应的包目录对应的 package.json
const pkg = require(resolve('package.json'))
// 获取文件名
const name = path.basename(packageDir)
// 4. 对打包类型先做一个映射表,根据提供的formats来格式化需要打包的内容
// outputConfig 是根据对应的包的 package.json 的 buildOptions 配置的输出配置
const outputConfig = {
'esm-bundler': {
file: resolve(`dist/${name}.esm-bundler.js`),
format: 'es'
},
'cjs': {
file: resolve(`dist/${name}.cjs.js`),
format: 'cjs'
},
'global': {
file: resolve(`dist/${name}.global.js`),
format: 'iife' // 立即执行函数
}
}
// 5. 对应包中的 package.json 的 buildOptions
const options = pkg.buildOptions
// 7. 创建出 rollup 配置
const createConfig = (format, output) => {
output.name = options.name
// 看需求是否需要生成sourcemap
output.sourcemap = true
// 生成rollup配置
return {
input: resolve(`src/index.ts`), // 入口
output, // 出口
plugins: [ // 插件,注意顺序
json(),
ts(),
resolvePlugin() // 解析第三方模块插件
]
}
}
// 6. rollup 最终需要导出的配置
export default options.formats.map(format => createConfig(format, outputConfig[format]))

到这一步,我们整个rollup中ts插件还需要进行额外的配置,接下来我们开始配置一下ts

npx tsc --init

执行 npx tsc --init 生成一个 tsconfig.js 文件,将该文件中的target字段与module字段的值修改为ES2015以上,此处我们修改为ESNEXT,同时,为了方便调试,我们在该文件中找到sourceMap字段,修改为true

接着,去rollup.config.js中去应用该文件

// rollup.config.js
// ......
const createConfig = (format, output) => {
output.name = options.name
// 看需求是否需要生成sourcemap
output.sourcemap = true
// 生成rollup配置
return {
input: resolve(`src/index.ts`), // 入口
output, // 出口
plugins: [ // 插件,注意顺序
json(),
ts({
tsconfig: path.resolve(__dirname, 'tsconfig.json')
}),
resolvePlugin() // 解析第三方模块插件
]
}
}
// ......

1.1.7 完成配置,进行打包

yarn build

打包完成后我们可以看下 packages文件夹下对应的模块里面会有一个dist文件夹,里面有我们一开始配置的对应文件

如:

cjs.js 为 node 的 CommonJS: exports.Reactivity = Reactivity

esm-bundler.js 为ES6:export { Reactivity }

如果有 global.js 则定义的全局变量:var VueReactivity

1.2 开发环境

1.2.1 开发环境配置

按照上面的步骤,我们完成了一个从零到使用 rollup 成功打包的过程,接下来,我们要简单配置一下dev环境

// /scripts/dev.js
// const fs = require('fs')
const execa = require('execa')
// 同步读取 packages 目录下的文件
const targets = 'reactivity'
// 打包方法
const build = async (target) => {
// 第一个参数是执行的命令 rollup,第二个是执行的参数
// -c 表示采用某个配置文件
// -cw 监听
// --environment 表示采用环境变量
// TARGET 目标
// 第三个参数 stdio 子进程打包的信息共享给父进程
await execa('rollup', ['-cw', '--environment', `TARGET:${target}`], { stdio: 'inherit' })
}
build(targets)

相对于生产环境,dev环境的配置相对要简单得多:

  1. 移除了打包全部模块的代码,仅打包一个模块
  2. 将 rollup 的配置从-c修改为-cw形成对文件的监听打包

配置完成后,只要修改对应打包文件中的内容,就会重新打包

1.2.2 模块之间的引用

各模块之间必然存在引用其他模块的情况,为减少引用时的成本,我们对此稍作配置

在执行yarn命令时(仅yarn),node_modules内会生成一个@vue文件,文件内有两个快捷路径文件夹reactivityshared,其路径对应着packages文件夹内的两个文件,点击可跳到对应文件下。原因是package.jsonworkspaces字段及文件内的package.json内的name字段配置。那么,我们就可以尝试直接在reactivity/src/index.ts中引用shared

+   import { shared } from '@vue/shared'
const Reactivity = {}
export {
Reactivity
}

我们会发现引用时ts抛错了,提示我们需要将moduleResolution选项设置为"node",且需要像"paths"选项中添加别名。

// tsconfig.json
{
"compilerOptions": {
// ......
+  	"moduleResolution": "Node",
+       "baseUrl": ".", // 未配置,则不允许使用 paths
+       "paths": {
+         "@vue/*": [
+           "packages/*/src"
+         ]
+       }
}
}

到这一步,完成了各模块间的引用,解析来我们正式开始写reactivity里的功能

1.3 为reactivity文件添加代理方法

首先在reactivity/src目录下创建一个reactive.ts文件,里面是vue的数据响应方法,内容如下。

// reactivity/src/reactive.ts
// 数据响应
export function reactive() {
}
// 第一层为数据响应
export function shallowReactive() {
}
// 仅读数据
export function readonly() {
}
// 第一层仅读
export function shallowReadonly() {
}
export {
reactive,
shallowReactive,
readonly,
shallowReadonly
}

接着修改reactivity/src/index.ts文件内容为

// reactivity/src/index.ts
export { reactive, shallowReactive, readonly, shallowReadonly } from './reactive'

1.3.1 实现数据代理

我们首先先来实现reactive功能,然后再一步一步实现其他功能及优化代码

// reactivity/src/reactive.ts
import { isObject } from '@vue/shared'
export function reactive (target){
// isObject 为 @vue/shared 里实现的方法,这里不做过多解释
// 1. 不是 object 类型就不做 Proxy 代理
if (!isObject(target)) return target
// 2. 使用 Proxy
const proxy = new Proxy(target, {
// target:目标对象。key:被获取的属性名。receiver:Proxy 或者继承 Proxy 的对象
get: function (target, key, receiver) {
// 使用 Reflect(反射)进行取值
const res = Reflect.get(target, key, receiver)
// 如果取得的是一个对象,则对该对象进行代理
// vue2 是一开始就递归,而 vue3 则是在取值是会进行代理,可以看作是一种懒代理模式
if (isObject(res)) return reactive(res)
return res
},
set: function (target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// 暂不处理
return result
}
})
// 返回 proxy
return proxy
}

接着我们再实现shallowReactive的功能

// reactivity/src/reactive.ts
import { isObject } from '@vue/shared'
export function reactive (target){
// ......
}
export function shallowReactive (target){
// isObject 为 @vue/shared 里实现的方法,这里不做过多解释
// 1. 不是 object 类型就不做 Proxy 代理
if (!isObject(target)) return target
// 2. 使用 Proxy
const proxy = new Proxy(target, {
// target:目标对象。key:被获取的属性名。receiver:Proxy 或者继承 Proxy 的对象
get: function (target, key, receiver) {
// 使用 Reflect(反射)进行取值
const res = Reflect.get(target, key, receiver)
return res
},
set: function (target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
return result
}
})
// 返回 proxy
return proxy
}

我们发现,这两个功能基本一致,区别只是在于是否需要对下一层进行代理,接下来我们对这两段代码进行优化,使其柯里化

// reactivity/src/reactive.ts
// ......
// 创建代理事件
function createProxyHandler(target, baseHandlers) {
// 1. 不是 object 类型就不做 Proxy 代理
if (!isObject(target)) return target
// 2. 数据拦截功能提取出来
const proxy = new Proxy(target, baseHandlers)
return proxy
}

我们将数据拦截部分提取出来后,还没完,这部分还存在大量重复代码,那么我们接着对这部分(baseHandlers)做处理

// reactivity/src/reactive.ts
// 1. 将对应的数据拦截部分抽离出来
export function reactive(target) {
return createProxyHandler(target, mutableHandlers)
}
export function shallowReactive(target) {
return createProxyHandler(target, shallowReactiveHandlers)
}
export function createProxyHandler(target, baseHandlers) {
// ......
}

由于baseHandlers这部分都是数据拦截部分,我们重新创建一个文件来专门对baseHandlers部分进行编码,这我们的项目能向着更加良好的方向发展。

创建baseHandlers.ts文件

// packages/reactivity/src/baseHandlers.ts
// 2. get,set 方法
const get = createGetter()
const shallowGet = createGetter(true)
const set = createSetter()
const shallowSet = createSetter(true)
// 1. 创建数据拦截
// reactive 对应的数据拦截
export const mutableHandlers = {
get,
set
}
// shallowReactive
export const shallowReactiveHandlers = {
get: shallowGet,
shallowSet
}
// 3. 创建 get 的拦截方法
function createGetter(isShallow = false) {
// 首先我们需要返回一个 get 方法
return function get(target, key, receiver) {
// target:目标对象。key:被获取的属性名。receiver:Proxy 或者继承 Proxy 的对象
// 使用 Reflect(反射)进行取值
const res = Reflect.get(target, key, receiver)
// 如果是浅代理,则不对下一层进行代理
if (isShallow) return res
// 如果取得的是一个对象,则对该对象进行代理
// vue2 是一开始就递归,而 vue3 则是在取值是会进行代理,可以看作是一种懒代理模式
if (isObject(res)) return reactive(res)
return res
}
}
function createSetter(isShallow = false) {
return function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// shallowSet 暂时不做处理
return result
}
}

至此,实现了 vue3 的 reactiveshallowReactive的大部分功能。

我们可以先来测验一下这两个功能。在根目录下创建一个example文件夹,然后创建一个1.reactive-api.html来进行测试。

<!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>
<script src="../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<script>
let { reactive, shallowReactive } = VueReactivity
const state = reactive({ name: 'jerry', age: { n: 18 } })
const state1 = shallowReactive({ name: 'jerry', age: { n: 18 } })
console.log('state.age', state.age)
console.log('state1.age', state1.age)
</script>
</body>
</html>

在浏览器中打开该文件,在控制台中能很明显的看出state.age仍是被代理的,而state1.age则只是一个普通的对象

接下来可以再对新建立的两个方法在example/1.reactive-api.html中测试。

但是在使用数据时,可能会存在以下这种情况

const state = reactive({ name: 'jerry' })
const state1 = reactive(state)

这里 state 本来就是一个被代理的对象,又被重新代理一次,这就产生了资源浪费,并且考虑到我们后面还有readonly类型,我们可以对这些情况进行处理一下。

// packages/reactivity/src/reactive.ts
// ....
// WeakMap 以对象为存储的key,并且会自动进行垃圾回收,不会造成内存泄漏
const reactiveMap = new WeakMap()
const readonlyMap = new WeakMap()
// 1. 添加一个 isReadonly 参数
function createReactiveObject (target, isReadonly, baseHandlers) {
if (!isObject(res)) return target
// 2. 如果某个对象已经被代理过了
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const exisitProxy = proxyMap.get(target)
// 3. 如果该代理已存在,则返回该数据
if (exisitProxy) return exisitProxy
const proxy = new Proxy(target, baseHandlers)
return proxy
}

接着,我们继续实现 vue3 中的readonlyshallowReadonly方法

// packages/reactivity/src/reactive.ts
import { isObject } from "@vue/shared"
import { mutableHandlers, shallowReactiveHandlers, readonlyHandlers, shallowReadonlyHandlers } 		from './baseHandlers'
// export function reactive ...
// export function shallowReactive ...
// 1. 添加 readonly 方法和 shallowReadonly 的内容
export function readonly (target) {
return createProxyHandler(target, readonlyHandlers)
}
export function shallowReadonly (target) {
return createProxyHandler(target, true, shallowReadonlyHandlers)
}
// function createProxyHandler ...

baseHandlers进行完善

// packages/reactivity/src/baseHandlers.ts
import { isObject } from '@vue/shared'
const readonlySet = (target, key) => console.warn(`set on key ${key} faild`)
// 2. 修改一下传入参数以及添加对应的仅读创建
const get = createGetter()
const shallowGet = createGetter(false, true)
const readonlyGet = createGetter(true)
const shallowReadonlyGet = createGetter(true, true)
const set = createSetter()
const shallowSet = createSetter()
// export const mutableHandlers ...
// 3. 导出仅读部分
export const readonlyHandlers = {
get: readonlyGet,
set: readonlySet
}
export const shallowReadonlyHandlers ={
get: shallowReadonlyGet,
set: readonlySet
}
// 1. 添加是否仅读
function createGetter (isReadonly = false, isShallow = false) {
// target:目标对象。key:被获取的属性名。receiver:Proxy 或者继承 Proxy 的对象
return function get (target, key, receiver) {
const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
// 不是仅读,需要收集依赖,在数据变化后更新对应的视图
}
if (isShallow) return res
// 根据是否仅读来判断下一层是仅读的还是需要响应的
if (isObject(res)) return isReadonly ? readonly(res) : reactive(res)
return res
}
}

1.4 实现 effect

packages/reactivity/src下创建effect.ts文件

// packages/reactivity/src/effect.ts
// 1. 导出 effect 
/**
* 
* @param fn effect 第一个参数
* @param options effect 的配置
*/
export function effect (fn, options = {}) {
// effect 需要是响应式的,能够做到数据变化重新执行
const effect = createReactiveEffect(fn, options)
// 默认 effect 需要先执行
if (!options.lazy) { // 配置了 lazy 时不需要执行
effect()
}
return effect
}
// effect 标识,用于区分不同的 effect
let uid = 0
// 创建响应式 effect
function createReactiveEffect (fn, options) {
const effect = function reactiveEffect() {
// 执行页面上传入的 fn
fn()
}
// 给 effect 添加上标识
effect.id = uid++
// 用于标识这个事响应式的 effect
effect._isEffect = true
// 保留 effect 中对应的原函数
effect.raw = fn
// 保留用户属性
effect.options = options
return effect
}

重新在example目录下创建一个html文件用来测试effect

  <script>
let { effect, reactive } = VueReactivity
let state = reactive({ name: 'jerry', age: 12 })
effect(() => {
app.innerHTML = `${state.name}`
state.age++
})
state.name = 'xxxx'
</script>

打开浏览器,使用 reactive的数据effect方法被成功触发。

但是我们发现,我们修改了state数据,这个effect方法却并没有触发,这是因为我们没有收集依赖,接着我们继续完善。

  1. effect中添加一个track方法用来收集依赖
// packages/reactivity/src/effect.ts
// ......
// 用于收集依赖
export function track (target, type, key) {
console.log(target, type, key)
}

在同级目录下添加一个operators.ts文件,用于标记追踪类型

// packages/reactivity/src/operators.ts
export const enum TrackOpTypes {
GET = 'get'
}

在获取数据时添加track方法

// packages/reactivity/src/baseHandlers.ts
// ...
import { track } from './effect'
// ...
function createGetter(isReadonly = false, isShallow = false) {
return function get (target, key, receiver) {
const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
// 1. 添加收集依赖,在执行 effect 时取值,收集 effect 依赖
track(target, TrackOpType.GET, key)
}
// ......
}
}

此时再打开浏览器查看,会发现取了多少次值,就会触发多少次track方法,但我们并不能知道是哪个effect触发的,所以我们需要添加一个activeEffect的变量记录着是哪个effect触发

// packages/reactivity/src/effect.ts
let uid = 0
// 存储当前的 effect
let activeEffect
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
// 存储当前 effect
activeEffect = effect
// 5. 函数执行时会取值,需要执行 get 方法
return fn()
}
// 制作一个 effect 标识,用于区分 effect
effect.id = uid++
// 用于标识这个是响应式 effect
effect._isEffect = true
// 保留 effect 对应的原函数
effect.raw = fn
// 在 effect 上保存用户属性
effect.options = options
return effect
}
export function track (target, type, key) {
console.log(target, type, key)
}

倘若当我们在页面写下如下代码时:

effect(() => {
console.log(state.name)
effect(() => {
console.log(state.age)
})
})

我们会发现,执行完第二个effectactiveEffect明明需要指向第一个effect,却仍指向了第二个effect,这时候我们就需要一个栈来存储了

// packages/reactivity/src/effect.ts
let uid = 0
// 存储当前的 effect
let activeEffect
let effectStack = []
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
// 4. 为了防止 effect 被重复入栈,重复执行 fn
if (!effectStack.includes(effect)) {
// 2. 使用 try 防止 fn 执行抛错,使用 finally 保证这个 effect 执行完成后activeEffect能指向下一个 effect 
try {
// 1. 将当前 effect 入栈
effectStack.push(effect)
// 存储当前 effect
activeEffect = effect
// 函数执行时会取值,需要执行 get 方法
return fn()
} finally {
// 3. 将当前 effect 出栈
effecStack.pop()
activeEffect = effectStack[effectStack.length -1]
}
}
}
// 制作一个 effect 标识,用于区分 effect
effect.id = uid++
// 用于标识这个是响应式 effect
effect._isEffect = true
// 保留 effect 对应的原函数
effect.raw = fn
// 在 effect 上保存用户属性
effect.options = options
return effect
}
export function track (target, type, key) {
// 5. 这样属性 key 就和 effect 一一对应起来了
console.log(target, type, key, activeEffect)
}
  1. 首先我们在try中将effect中入栈,并且获取当前effect,然后执行fn
  2. 接着在finally中将其出栈,将activeEffect指回上一个effect
  3. 然后我们在执行这一切之前加一个防止effect被重复入栈的判断

完成这些以后,我们在track中就能将属性effect一一对应起来了,从而进行收集依赖,代码如下

// packages/reactivity/src/effect.ts
// ......
// 让某个对象中的属性收集当前他对应的 effect 函数
const targetMap = new WeakMap()
export function track(target, type, key) {
if (activeEffect === undefined) return
// 1. 在 targetMap 表中查找 target
let depsMap = targetMap.get(target)
// 2. 判断 targetMap 映射表中是否存在该对象
if (!depsMap) {
// 2.1 映射表中不存在该对象就将存储起来,将其值设置成一个空的 Map
targetMap.set(target, (depsMap = new Map()))
}
// 3. 在 depsMap 表中查找属性 key
let dep = depsMap.get(key)
// 4. 判断 targetMap 的 target 属性的 depsMap 映射表中能否拿到 key
if (!dep) {
// 4.1 该映射表中不存在该属性 key 则存储起来,其值使用 Set 存储
depsMap.set(key, dep = new Set()) // 使用 Set 存储防止重复
}
// 5. 判断 targetMap 的 target 属性的 depsMap 的 key 属性是否存储了对应的 effect
if (!dep.has(activeEffect)) {
// 5.1 该 key 的 effect 不存在则添加这个 effect
dep.add(activeEffect)
}
console.log(targetMap)
}

然后我们在浏览器中打开测试页面会看到 console 出来的对应关系,即便是页面上有多个 effect并且这多个effect中都是用了state的值,这些依赖关系也都能被正确的收集起来。

既然我们收集到了effect与 ' state' 的依赖关系,那么我们就可以在 ' state' 发生改变时触发对应的effect。值得注意的是,state改变时有两种情况——新增和修改,我们需要将其区分开来。

  1. 添加标识符
// packages/reactivity/src/operators.ts
// ...
export const enum TriggerOrTypes {
ADD = 'add',
SET = 'set'
}
  1. 添加trigger方法
// packages/reactivity/src/effect.ts
// ......
export function trigger(target, type, key, newValue, oldValue) {
}
  1. 调用trigger
// packages/reactivity/src/baseHandlers.ts
// ......
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
const oldValue = target[key]
// 1. 判断是否有值
// 数组且是设置下标时需要处理
const hadKey = isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 2. 判断条件进行触发
if (!hadKey) {
// 新增
trigger(target, TiggerOrTypes.ADD, key, value)
} else if (hasChanged(oldValue, value)) {
// 判断新旧值不相等后做修改操作触发
trigger(target, TriggerOrTypes.SET, key, value, oldValue)
}
return result
}
}
  1. 添加 trigger内容
// packages/reactivity/src/effect.ts
// ......
export function trigger(target, type, key, newValue, oldValue) {
// 1. 判断该属性是否收集过 effect 
const depsMap = targetMap.get(target)
if (!depsMap) return
// 2. 需要将所有的 effect 全都存到一个集合中,最终一起执行
const effects = new Set() // 依旧需要去重
// 5. 添加最终需要执行的 effect
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
effects.add(effect)
})
}
}
// 数组比较特殊,需要单独处理
// 3. 判断修改的是否为数组的长度
if (isArray(target) && key === 'length') {
depsMap.forEach((dep, key)) {
if (key === 'length' || key > newValue) {
// 3.1 映射表中的 key 是否为 'length'
// state.arr.length = 5   template > {{state.arr.length}}
// 3.2 依赖中依赖的数据的下标 是否大于数组新的 length
// effect > state.arr[2]    other > state.length = 1
// 4. 将该数据符合条件的 effect 都添加到最终要执行的 effects 中
add(dep)
}
}
} else {
// 6 需要判断 key 是否为 undefined
if (key !== void) {
// 不是 undefined 那肯定就是修改
add(depsMap.get(key))
}
switch (type) {
case TriggerOrTypes.ADD:
if (isArray(target) && isIntegerKey(key)) {
add(depsMap.get('length'))
}
}
}
// 7. 最终一次性触发所有的 effect
effects.forEach(effect => effect())
}

这样,我们实现了对象和数组类型的数据响应

1.5 ref 与 toRef

首先我们需要了解到的是,reactive 的内部采用的是 proxy,而 ref的最终底层实现是defineProperty(www.babeljs.cn/)

packages/reactivity/src目录下创建ref.ts文件

实现 ref 与 shallowRef

//packages/reactivity/src/ref.ts
// 1. ref
export function ref(value) {
// value 可以是普通类型,那我我们就需要将普通类型转换成对象
// 当然 value 也可以是对象,如果是对象,那么我们最终还是使用 proxy
return createRef(value)
}
export function shallowRef(value) {
return createRef(value, true)
}
// 3.2 
const convert = val => isObject(val) ? reactive(val) : val
// 3. 代理数据实例
class Refimpl<T> {
private _value: T // 最终返回的值
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow = false){
// 3.1 如果是 shallowRef 不管是普通类型还是 object 类型,仅代理这一层,否则判断_rawValue 是普通类型还是object类型,如果是object类型,则交由 reactive 使用 proxy 进行代理
this._value = _shallow ? _rawValue : convert(_rawValue)
}
// 使用时最终通过 .value 获取数据
get value() {
// 要实现响应式执行 effect,肯定是需要数据追踪的,我们直接调用 track 就好了
track(this, TrackOpTyoes.GET, 'value')
// 返回值
return this._value
}
set value(newValue) {
// 判断新来的 value 是否与上一次的 value 相等
if (hasChanged(newValue, this.rawValue)) {
// 更新上一次的 value 为新 value,且将当前值设置为新值
this.rawValue = newValue
// 同理3.1
this._value = this._shallow ? newValue : convert(newValue)
// 触发 effect
trigger(this, TriggerOrTypes.SET, 'value', newValue)
}
}
}
// 2. createRef 返回实例
function createRef(rawValue, shallow = false) {
return new Refimpl(rawValue, shallow)
}

toRef 与 toRefs

// packages/reactivity/src/ref.ts
// ......
// 2. 转化
class ObjectRefImpl {
public readonly __v_isRef = true
constructor(private readonly _target, private readonly _key){}
get value() {
// 如果原对象是响应式的就会进行依赖收集
return this._target[this.key]
}
set value() {
// 如果原对象是响应式的,就会触发更新
this._target[this.key] = newVal
}
}
// toRef 1. 将对象的的值转成 ref
export function toRef(target, key) {
// vue 源码中还有一个判断是否已经是ref isRef(target[key]) 如果已经是ref了,就返回target[key]
return new ObjectRefImpl(target, key)
}
// 3. toRefs
export function toRefs(target) {
// 需要判断是数组还是对象
const ret = isArray(target) ? new Array(target.length) : {}
for (let key in target) {
ret[key] = toRef(target, key)
}
// 返回所有的属性
return ret
}

基本上 vue3 reactivity 内的主要API都实现了,虽然还有很多细节及功能没有做处理,但是基本上理解这么多就够了,毕竟是学习框架而不是写一个框架(虽然我也不配)。

最近准备再精读一遍李兵老师的《浏览器的工作原理与实践》,立一个flag,将其写成笔记,内容比较多,希望我能坚持做笔记。

回复

我来回复
  • 暂无回复内容