Vue3中有三大特性响应式、运行时、编译器,我将分三篇文章进行讲解。本篇将讲解Vue3的响应式篇章,会依次讲解以下内容
Proxy数据劫持解决了definePrototype的什么问题依赖收集的设计思路reactive、ref、computed、scheduler、watch的实现
带你手撸Vue3响应式。本文所有代码 vue3_study

TS编写代码,使用Rollup构建。如果不熟悉TS的同学可以使用npx tsc xx.ts -t esnext命令转一下.
vue3_study/1.begin这里是一个干净的项目工程骨架

我先说一下大致情况
node_modules安装到了外层(node_module有递归往上查找的特性)packages/*/src 借助tsconfig.js把路径映射为 "@vue/*" 这样通过@vue/引入模块了。
packages/*/src 每个目录内容都一样只有一个index.ts,里面没内容。npm安装,(我开始是用pnpm安装,在测试computed时遇到了问题)npm run dev, demo 在 vue/examples/*.html 借助 vscode Live Server插件启动基本情况就这些, 具体看代码吧!
首先来看一下vue2响应式弊端:
Map、Set、Class等无法响应式proxy实现数据响应性demo
jsconst isObject = val => val !== null && typeof val === 'object'
const toProxy = new WeakMap() // obj:observed
function reactive(obj) {
if (!isObject(obj)) return obj
if (toProxy.has(obj)) return toProxy.get(obj)
const observed = new Proxy(obj, {
get(target, p, receiver) {
const res = Reflect.get(target, p, receiver)
// return res;
// 体验Proxy惰性劫持
return isObject(res) ? reactive(res) : res
},
set(target, p, value, receiver) {
const result = Reflect.set(target, p, value, receiver)
console.log('set', p, value, result)
return result
},
deleteProperty(target, p) {
const res = Reflect.deleteProperty(target, p)
console.log('delete', p, res)
return res
}
})
toProxy.set(obj, observed)
return observed
}
var state = reactive({
foo: 'foo',
bar: { a: 1 }
})
// 1.获取
state.foo // ok
// 2.设置已存在属性
state.foo = 'fooooooo' // ok
// 3.设置不存在属性
state.dong = 'dong' // ok
// 4.删除属性
delete state.dong // ok
// 5.嵌套对象也支持
state.bar.a = 3
总结:
proxy数据劫持是惰性(用时)劫持, 性能要好很多proxy远比存取器劫持方式强大很多,支持删除属性、新增属性、属性检测、对象遍历等js// 思考 为什 Proxy 要搭配 Reflect 使用呢?
// 通过查看mdn文档知道
// Reflect相关的方法最后一个参数 receiver 是绑定this的意思
var obj = {
firstName: '张',
lastname: '三',
get fullName() {
return this.firstName + this.lastname
}
}
var observed1 = new Proxy(obj, {
get(target, p, receiver) {
console.log('触发get', p)
return target[p]
},
set(target, p, value, receiver) {
target[p] = value
return true
}
})
observed1.fullName
// 打印 “触发get fullName” // 只打印一次
var observed2 = new Proxy(obj, {
get(target, p, receiver) {
const res = Reflect.get(target, p, receiver)
console.log('触发get', res)
return res
},
set(target, p, value, receiver) {
const res = Reflect.set(target, p, value, receiver)
console.log('set', p, value)
return res
}
})
observed2.fullName
// 打印 触发get 张
// 打印 触发get 三
// 打印 触发get 张三
建立响应数据key和更新函数之间的对应关系。
用法:
jsconst state = reactive({
foo: 'Foo'
});
// 设置响应函数
effect(() => console.log(state.foo)) ;
// 用户修改关联数据会触发响应函数
state.foo = 'xxx';
设计思路
实现三个函数:
effect:将回调函数保存起来备用,立即执行一次回调函数触发它里面一些响应数据的gettertrack:getter中调用track,把前面存储的回调函数和当前target,key之间建立映射关系trigger:setter中调用trigger,把target,key对应的响应函数都执行一遍
target,key和响应函数映射关系

html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const {reactive, effect} = Vue;
const obj = reactive({
name: '张三'
});
effect(() => {
document.getElementById('app').innerText = obj.name
});
setTimeout(() => {
obj.name = '李四';
}, 2000);
</script>
</html>
reactivity/reactive.tstsimport { trigger, track } from './effect'
// Map缓存 obj:observed
export const reactiveMap = new WeakMap<object, any>()
export function reactive(target: object): any {
if (reactiveMap.has(target)) {
return reactiveMap.get(target)
}
const observed = new Proxy(target, {
get(target: object, p: string | symbol, receiver: object) {
const res = Reflect.get(target, p, receiver)
track(target, p)
return res
},
set(target: object, p: string | symbol, value: unknown, receiver: object) {
const res = Reflect.set(target, p, value, receiver)
trigger(target, p)
return res
}
})
reactiveMap.set(target, observed)
return observed
}
reactivity/effect.tsjsexport function effect<T = any>(fn: () => T) {
const _effect = new ReactiveEffect<T>(fn)
_effect.run()
}
/**
* targetMap 大致结构如下
* {target: Map<{key: Set<Effect>}>}
* target | depsMap
* obj | key | Dep
* k1 | effect1,effect2...
* k2 | effect3,effect4...
*/
const targetMap = new WeakMap<object, Map<any, Set<ReactiveEffect>>>()
export let activeEffect: ReactiveEffect | undefined
export class ReactiveEffect<T = any> {
constructor(public fn: () => T) {
this.fn = fn
}
run() {
activeEffect = this
this.fn()
}
}
export function track(target: object, p: unknown) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(p)
if (!dep) {
depsMap.set(p, (dep = new Set()))
}
dep.add(activeEffect!)
}
export function trigger(target: object, p: unknown) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(p)
if (!dep) return
triggerEffects(dep)
}
export function triggerEffects(effects: Set<ReactiveEffect>) {
for (let effect of effects) {
effect.run()
}
}
注: packages/*/src文件夹的所有文件都有通过index.ts聚合导出,如packages/reactivity/src/index.ts
tsexport * from './reactive';
export * from './effect';
后面不在强调这个细节
运行效果

ref支持 复杂数据(对象类型) 和简单数据(基本类型)reactive的依赖收集这一步不难我先上代码
reactivity/ref.tstsimport { isObject } from '@vue/shared'
import { activeEffect, ReactiveEffect } from './effect'
import { reactive } from './reactive'
export function ref(value: unknown) {
return new RefImpl(value)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Set<ReactiveEffect> = undefined
constructor(value: T) {
this._value = toReactive(value)
this._rawValue = value
}
get value() {
// 收集依赖
if (activeEffect) {
this.dep ??= new Set<ReactiveEffect>()
this.dep.add(activeEffect)
}
return this._value
}
}
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value as object) : value
运行效果

我们在修改一下ref让它支持简单数据

运行效果

思路
reactivity/computed.tstsimport { ReactiveEffect, activeEffect } from './effect'
export function computed(getterOrOptions) {
// 这里只考虑简单情况 即 getterOrOptions 为 getter
const getter = getterOrOptions
return new ComputedRefImpl(getter)
}
export class ComputedRefImpl<T> {
public dep?: Set<ReactiveEffect> = undefined
private _value!: T
private readonly effect: ReactiveEffect<T>
constructor(getter) {
this.effect = new ReactiveEffect(getter)
}
get value() {
this.dep ??= new Set<ReactiveEffect>()
this.dep.add(activeEffect!)
this._value = this.effect.run()
return this._value
}
}

运行效果

reactivity/computed.ts 改动如下

reactivity/effect.ts改动

运行效果

这一步理解起来还是比较费劲的,建议用断点调试多调试几遍

我说一下大致流程
ReactiveEffect实例, 就是 objComputedRefImpl实例, 即computedObj。computedObj有个effect属性, 与 obj类型相同,不同点是前者(computedObj.effect)会标识computed属性且它的effect属性拥有scheduler方法,这样 trigger()是调用的是 effect.scheduler() 而不再是 effect.run()effect() 完成依赖收集,targetMap依赖如下

obj.name赋值 触发 setter ---> triggger() ---> 执行副作用,就是上图的ReactiveEffect实例 即computedObj.effect.sheduler() 即 effect的第一个参数
ComputedRefImpl实例(这里是computedObj)有个标志_dirty,初始值是true, 在第三步的时候已经触发getter,完成依赖收集后变成false了, 由于现在执行依赖收集副作用触发.effect.sheduler()方法再次把_dirty标记置为true并执行它的副作用(effect的第一个参数)
ComputedRefImpl实例的getter, 再次执行副作用,这时副作用是指 第二步 computed()的第一个参数() => '姓名:' + obj.nameobj的getter(), 进行副作用收集,其实前面第三步已经收集了,这里等于什么都没做,逐级回退调用栈,函数就执行结束了文采不够,解释的比较牵强,源码的艺术只可意会不可言谈😂,还是要多调试自己品味才行。
总结: computed也是要完成依赖收集,setter 触发副作用从它开始逐级执行副作用(effect的回调参数 --> computed的回调参数 --> 从reactive代理对象取值返回),是一个递归过程
我们在改一下案例 测试computed的缓存性

断点调试发现 computedObj.dep 有两个 ReactiveEffect的实例
第一个副作用是effect()里面的回调参数 第二个副作用是computed()的回调参数
当执行到定时器中的代码 触发setter, 执行依赖(前面分析过只有一个依赖即computed()生成的的副作用)
执行scheduler()将_dirty标记为true,
第一次执行computedObj.value 由于_dirty为false 再执行它的副作用, 这里就造成递归,正确的做法是先执行存值器 将_dirty标记为false 再执行schedule() 这样就不会造成死循环了
如果我们先执行computedObj.dep中的schedule()

第一次执行computedObj.value,_dirty为false 再执行它的副作用, 这次是先执行了schedule() _dirty为false 不在执行副作用,就结束了。
computed的缓存性如何体现呢?
computedObj.value,进入存值器,这时_dirty为true 会执行副作用重新生成值,computedObj.value,进入存值器,这时_dirty为false 就不在重新计算值了。注: 这一块很难解释, 耐心多调试方能理解。
这一次运行就正常了

因为watch默认是懒执行(immediate: false)的, 所以我们effect要适配,让它支持懒执行。

effect支持scheduler参数

scheduler的完整实现
前面的调度方法是同步执行,如果我们想异步执行,Vue提供了queuePostFlushCb,这个方法比较独立,是一个不错的手写题
runtime-core/src/scheduler.tstsconst cbs: Function[] = []
export function queuePostFlushCb(cb: Function) {
cbs.push(cb)
queueFlush()
}
let isFlushPending = false
async function queueFlush() {
// console.log('queueFlush', isFlushPending)
if (!isFlushPending) {
isFlushPending = true
await Promise.resolve()
let _cbs = [...new Set(cbs)]
for (let cb of _cbs) {
cb()
}
cbs.length = 0
isFlushPending = false
}
}
jsconst {
reactive,
effect,
queuePostFlushCb,
} = Vue
const obj = reactive({
count: 1
})
effect(() => {
// 初始化执行
console.log(obj.count);
}, {
scheduler(){
// 发生变更执行这里
queuePostFlushCb(() => {
console.log(obj.count);
})
}
})
obj.count = 2
obj.count = 3
// 打印 1
// 打印两次 3
运行结果与前面一致, 这里就不截图了
Vue3的watch 是放在 runtime-core/src/apiWatch.ts文件中的。
先来实现watch的骨架和immediate功能
runtime-core/src/apiWatch.tstsimport { ReactiveEffect, isReactive } from '@vue/reactivity'
import { queuePostFlushCb } from './scheduler'
export function watch(
source: any,
cb: Function,
{ immediate, deep }: WatchOptions = {}
) {
let getter: () => any
if (isReactive(source)) {
getter = () => source
deep = true
} else {
getter = () => {}
}
let oldValue = {}
const job = () => {
const newValue = effect.run()
if (deep || Object.is(newValue, oldValue)) {
cb(newValue, oldValue)
oldValue = newValue
}
}
const scheduler = () => queuePostFlushCb(job)
const effect = new ReactiveEffect(getter, scheduler)
if (immediate) {
job()
} else {
oldValue = effect.run()
}
return () => effect.stop()
}
export interface WatchOptions {
immediate?: boolean
deep?: boolean
}
在看一下测试用例和效果

前面的案例watch没有实现响应式,为什么呢?
其实是因为 watch的effect(就是watch的第二个参数)没有触发getter, 我们只需递归遍历一下watch监听的对象即可以完成依赖收集
修改 runtime-core/src/apiWatch.ts 增加代码如下


在看一下测试用例

本文作者:郭敬文
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!