在这个大前端时代,要想从中级突破到高级,掌握框架底层原理是必须要经历的过程。本篇将以手写Vue3中的响应式原理来掌握Vue3 响应式的底层实现,突破技术瓶颈。
项目初始化和环境搭建
- 新建项目 vue-mini
- 初始化package.json, 执行
npm init -y
- 安装rollup, 执行
npm i rollup -D
- 创建项目目录(参考Vue3源码)
本篇的代码主要编写在packages/reactivity 目录下面 5. 安装ts,配置ts
js
npm i typescript tslib -D
js
{
"compilerOptions": {
"rootDir": ".",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"target": "es2021",
"noUnusedLocals": false,
"noUnusedParameters": false,
"resolveJsonModule": true,
"downlevelIteration": true,
"noImplicitAny": false,
"module": "esnext",
"removeComments": false,
"sourceMap": true,
"lib": ["esnext", "dom"],
"baseUrl": ".",
"paths": {
"@vue/*": ["packages/*/src"]
}
},
"include": ["packages/*/src"]
}
- 安装rollup相关插件
js
npm i @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript -D
- rollup打包配置 rollup.config.js
js
const typescript = require('@rollup/plugin-typescript')
const nodeResolve = require('@rollup/plugin-node-resolve')
const commonjs = require('@rollup/plugin-commonjs')
module.exports = [
{
input: 'packages/vue/src/index.ts',
output: {
sourceMap: true,
file: 'packages/vue/dist/vue.js',
format: 'iife',
name: 'Vue'
},
plugins: [
typescript({
sourceMap: true
}),
nodeResolve(),
commonjs()
]
}
]
- packages.json 里面配置打包命令
js
"scripts": {
"dev": "rollup -c -w",
"build": "rollup -c"
},
- packages/vue下面新建测试代码看是否能正常打包
packages/vue/src/index.ts
js
export function add(a, b) {
console.log(a, b);
return a + b;
}
packages/vue/examples/test/test.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>test</title>
</head>
<body>
<button id="btn">add</button>
<p>
<span>3</span>
<span>+</span>
<span>5</span>
<span>=</span>
<span id="result">?</span>
</p>
<script src="../../dist/vue.js"></script>
<script>
const { add } = Vue
document.getElementById('btn').onclick = function () {
document.getElementById('result').innerHTML = add(3, 5)
}
</script>
</body>
</html>
执行打包命令 npm run build
, 这时候就能看到vue 目录下多了个dist目录,和dist目录下的vue.js
packages/vue/dist/vue.js
js
var Vue = (function (exports) {
'use strict';
function add(a, b) {
console.log(a, b);
return a + b;
}
exports.add = add;
return exports;
})({});
这就是我们打包的产物。现在来运行下packages/vue/examples/test/test.html, 推荐大家使用vscode 中的live server 插件进行运行。
点击add
可以看到结果符合预期,说明我们的打包配置没问题。到这里项目的初始化相关配置就完成了
reactive的初步实现
看过Vue3 源码的同学,相信大概知道reactive 的过程实际调用了很多函数,大概流程如下:
packages/reactivity 下新建src, 并新建三个文件
index.ts
ts
import { reactive } from './reactive'
export { reactive }
响应式的出口文件,导出reactive方法,给用户使用
reactive.ts
ts
import { mutableHandlers } from './baseHandlers'
const reactiveMap = new WeakMap<object, any>() // 用来缓存的
export function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, reactiveMap)
}
function createReactiveObject(
target: object,
baseHandlers: ProxyHandler<any>,
proxyMap: WeakMap<object, any>
) {
const exitingProxy = proxyMap.get(target) // 通过传入的对象判断缓存里面有没有
if (exitingProxy) {
// 如果缓存里面已经有了传入对象所对应的代理对象,则直接返回代理过的对象
return exitingProxy
}
const proxy = new Proxy(target, baseHandlers) // 缓存中不存在时用传入的对象创建一个Proxy代理对象
proxyMap.set(target, proxy) // 存入缓存中
return proxy // 返回代理对象给用户
}
在这个文件中:
- 创建了
reactive
方法, 并返回createReactiveObject
的执行 - 创建了
createReactiveObject
函数,createReactiveObject
主要判断缓存中存不存在传入对象的代理,存在则直接返回,不能存则创建。 - 创建P
roxy
的handler
是从baseHandlers 中导入的mutableHandlers
baseHandlers.ts
js
class BaseReactiveHandler {
get(target: object, key: string) {
console.log('getter')
return Reflect.get(target, key)
}
}
class MutableReactiveHandler extends BaseReactiveHandler {
set(target: object, key: string, newVal: any) {
console.log('setter')
const result = Reflect.set(target, key, newVal)
return result
}
}
export const mutableHandlers: ProxyHandler<object> =
new MutableReactiveHandler()
在这个文件中:
- 看到
mutableHandlers
实际上是MutableReactiveHandler
的一个实例 - 在
MutableReactiveHandler
中处理有个set
方法,在set
中后面写完effect之后,会在这里会执行依赖的回调函数 - 在
MutableReactiveHandler
还继承了BaseReactiveHandler
BaseReactiveHandler
中有个get
方法,get
方法会在effect
写完之后,在这里调用依赖收集的方法
在packages/vue/ 新建reactive的测试用例
index.ts
ts
export { reactive } from '@vue/reactivity'
reactive.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>reactive</title>
</head>
<body>
<script src="../../dist/vue.js"></script>
<script>
const {reactive} = Vue
const obj = {
a: 1
}
const proxy = reactive(obj)
console.log(proxy)
console.log(proxy.a)
proxy.a = 5
</script>
</body>
</html>
执行打包命令:npm run build
可以看到dist/vue.js 内容更新了
js
var Vue = (function (exports) {
'use strict';
class BaseReactiveHandler {
get(target, key) {
console.log('getter');
return Reflect.get(target, key);
}
}
class MutableReactiveHandler extends BaseReactiveHandler {
set(target, key, newVal) {
console.log('setter');
const result = Reflect.set(target, key, newVal);
return result;
}
}
const mutableHandlers = new MutableReactiveHandler();
const reactiveMap = new WeakMap(); // 用来缓存的
function reactive(target) {
return createReactiveObject(target, mutableHandlers, reactiveMap);
}
function createReactiveObject(target, baseHandlers, proxyMap) {
const exitingProxy = proxyMap.get(target); // 通过传入的对象判断缓存里面有没有
if (exitingProxy) {
// 如果缓存里面已经有了传入对象所对应的代理对象,则直接返回代理过的对象
return exitingProxy;
}
const proxy = new Proxy(target, baseHandlers); // 缓存中不存在时用传入的对象创建一个Proxy代理对象
proxyMap.set(target, proxy); // 存入缓存中
return proxy; // 返回代理对象给用户
}
exports.reactive = reactive;
return exports;
})({});
来运行下packages/vue/examples/reactivity/reactive.html
可以看到现在已经能成功的得到一个Proxy 代理对象,并且触发getter和setter
effect 实现
大概流程如下
packages/reactivity/src 新增effect.ts,代码编写如下
ts
export let activeEffect: ReactiveEffect | undefined
class ReactiveEffect {
constructor(public fn) {}
run() {
activeEffect = this
return this.fn()
}
}
function effect<T = any>(fn: () => T) {
const e = new ReactiveEffect(fn)
e.run()
}
effect执行会得到一个ReactiveEffect的实例,并且赋值给activeEffect 作为当前激活变量存储
packages/reactivity/src/index.ts 出口文件新增effect 的导入导出
packages/vue/src/index.ts 出口文件新增effect 的导入导出
依赖收集实现
依赖收集主要是在getter 时,将副作用函数收集起来,方便在修改数据的时候重新执行。依赖收集之后会得到如下的数据结构:
packages/reactivity/src 下面新建dep.ts, 代码实现如下:
js
import { activeEffect } from './effect'
let targetMap = new WeakMap<object, any>()
export function track(target: object, key: unknown) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
console.log(targetMap, '依赖收集的结构')
}
修改baseHandlers.ts中的BaseReactiveHandler 的get方法,添加track 的执行
修改测试的reactive.html代码
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>reactive</title>
</head>
<body>
<p id="text"></p>
<script src="../../dist/vue.js"></script>
<script>
const {reactive, effect } = Vue
const obj = {
a: 1
}
const proxy = reactive(obj)
effect(() => {
document.getElementById('text').innerHTML = proxy.a
})
console.log(proxy)
console.log(proxy.a)
proxy.a = 5
</script>
</body>
</html>
运行reactive.html页面,就会看到控制台打印,前面途中我们看到的数据结构
依赖触发
dep.ts 新增trigger
方法
js
export function trigger(target: object, key: string, newVal: any) {
const desMap = targetMap.get(target)
if (!desMap) {
return
}
const dep = desMap.get(key)
for (let effect of dep) {
effect.fn()
}
}
修改 baseHandler.ts 中的MutableReactiveHandler
的set
方法, 新增trigger方法的调用:
修改测试的reactive.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>reactive</title>
</head>
<body>
<p id="text"></p>
<script src="../../dist/vue.js"></script>
<script>
const {reactive, effect } = Vue
const obj = {
a: 1
}
const proxy = reactive(obj)
effect(() => {
document.getElementById('text').innerHTML = proxy.a
})
console.log(proxy)
console.log(proxy.a)
proxy.a = 5
</script>
</body>
</html>
可以看到数据更新后,视图也跟着更新了。
到这里我们的响应式功能就开发完成了。来总结下吧。
总结
reactive
本身主要返回一个Proxy
,并且会缓存,每次调用reactive
方法时优先取缓存里面的effect
主要是创建一个ReactiveEffect
实例- 依赖收集在get 的时候,调用
track
方法将把副作用函数收集起来 - 依赖触发在set的时候触发将键对应的所有副作用函数全部执行一遍