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
:将回调函数保存起来备用,立即执行一次回调函数触发它里面一些响应数据的getter
track
: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.ts
tsimport { 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.ts
jsexport 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.ts
tsimport { 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.ts
tsimport { 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
实例, 就是 obj
ComputedRefImpl
实例, 即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.name
obj
的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.ts
tsconst 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.ts
tsimport { 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 许可协议。转载请注明出处!