从0学习 vue3 reactivity源码笔记
写在前面:
这是一篇我自己学习 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
修改reactivity
与shared
目录下的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 安装依赖
在根目录下安装依赖
- 安装
typescript
与rollup
相关的依赖 @rollup/plugin-node-resolve
: 解析第三方模块,可以使我们支持第三方模块@rollup/plugin-json
: 支持解析jsonexeca
: 使用多进程--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
环境的配置相对要简单得多:
- 移除了打包全部模块的代码,仅打包一个模块
- 将 rollup 的配置从
-c
修改为-cw
形成对文件的监听打包
配置完成后,只要修改对应打包文件中的内容,就会重新打包
1.2.2 模块之间的引用
各模块之间必然存在引用其他模块的情况,为减少引用时的成本,我们对此稍作配置
在执行yarn
命令时(仅yarn
),node_modules
内会生成一个@vue
文件,文件内有两个快捷路径文件夹,reactivity
与shared
,其路径对应着packages
文件夹内的两个文件,点击可跳到对应文件下。原因是package.json
内workspaces
字段及文件内的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 的 reactive
和shallowReactive
的大部分功能。
我们可以先来测验一下这两个功能。在根目录下创建一个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 中的readonly
与shallowReadonly
方法
// 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
方法却并没有触发,这是因为我们没有收集依赖,接着我们继续完善。
- 在
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)
})
})
我们会发现,执行完第二个effect
后activeEffect
明明需要指向第一个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)
}
- 首先我们在
try
中将effect
中入栈,并且获取当前effect
,然后执行fn
- 接着在
finally
中将其出栈,将activeEffect
指回上一个effect
- 然后我们在执行这一切之前加一个防止
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
改变时有两种情况——新增和修改,我们需要将其区分开来。
- 添加标识符
// packages/reactivity/src/operators.ts
// ...
export const enum TriggerOrTypes {
ADD = 'add',
SET = 'set'
}
- 添加
trigger
方法
// packages/reactivity/src/effect.ts
// ......
export function trigger(target, type, key, newValue, oldValue) {
}
- 调用
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
}
}
- 添加
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,将其写成笔记,内容比较多,希望我能坚持做笔记。