从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,将其写成笔记,内容比较多,希望我能坚持做笔记。

回复

我来回复
  • 暂无回复内容