本文主要是阅读《vue.js 设计与实现》一书所得。
副作用函数是指该函数的执行会影响其他函数的执行的函数。
🤔字面意思上可以理解这句话,但不知道为什么vue要提出这个概念,我的理解在vue实现中中读取了响应式数据的函数数,都需要在响应式数据变化时自动触发。跟产不产生副作用有什么关联呢?
响应式数据的基本实现
vue中实现响应式数据的基本思想是通过proxy构造目标对象的代理对象,从而拦截目标对象的读/写操作。在读操作时收集副作用函数,写操作时触发这些副作用函数,从而实现响应式。
在vue3中,存储这些副作用函数与目标对象之间的关联的数据结构是WeakMap
,如下图所示:WeakMap
的键是响应式数据,值是一个Map
对象,这个Map
对象的键是响应式数据的键,值是与该键相关联的副作用函数集。
下面的代码是响应式数据的基本实现:
javascript
// 存储副作用函数的桶
const bucket = new WeakMap();
// 原始数据
const data = { text: 'Hello World', ok: true };
// 对原始的数据进行代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
track(target, key);
return target[key];
},
// 拦截写入操作
set(target, key, newVal) {
// 设置属性值ß
target[key] = newVal;
trigger(target, key);
}
})
// 追踪函数,用于在get拦截操作中把副作用函数收集到"桶"中
function track(target, key) {
// 没有 activeEffect 则直接 return
if (!activeEffect) return target[key];
// 根据target从桶中取得 depsMap,也是一个 Map 类型:key --> effects
let depsMap = bucket.get(target);
// 如果不存在 depsMap,就新建一个 Map 并和 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 根据 key 从 depsMap 中取得 deps, 也是一个 Set 类型
// 里面存储着所有与当前 key 相关联的副作用函数: effects
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
}
function trigger(target, key) {
// 获得对应key的effect set
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
// 将副作用函数 effect 取出并执行
effects && effects.forEach(effect => effect());
}
// 定义一个全局变量存储被注册的副作用函数
let activeEffect;
function effect(fn) {
activeEffect = fn;
fn();
}
- ``
<font style="color:#585A5A;">effect():</font>
<font style="color:#585A5A;">bucket</font>
<font style="color:#585A5A;">track()</font>
<font style="color:#585A5A;">trigger()</font>
一些边界条件处理
effect 嵌套
computed 计算属性实现
计算属性 computed 的实现其实是通过对 effect 函数的再次包装。
我们思考下 computed 的作用和特点,computed 可以实现一些计算逻辑, 并在响应式数据变化后重新计算。
javascript
const a = ref(1)
const b = ref(2)
const sum = computed(()=>{
return a + b
})
这不就可以理解为是一个副作用函数,只是这个副作用函数返回了一个值 。
我们来看 computed 的代码实现:
typescript
// 本部分实现computed
// 计算属性实现,包括懒执行,effect嵌套问题解决
// 存储副作用函数的桶
const bucket: WeakMap<Data, Map<string, Set<EffectFn>>> = new WeakMap()
// 原始数据
type Data = {
[key: string | symbol]: number
}
const data: Data = { foo: 1, bar: 1 }
// 对原始数据的代理
export const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal
// 把副作用函数从桶里取出并执行
trigger(target, key)
return true
}
})
function track(target: Data, key: string) {
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
function trigger(target: Data, key: string) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun: Set<EffectFn> = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
// 如果存在调度器的话,交给调度器去执行副作用函数
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
// effects && effects.forEach(effectFn => effectFn())
}
// 用一个全局变量存储当前激活的 effect 函数
interface Fn {
(): void
}
interface EffectFn {
(): void;
options: {
scheduler?: (fn: Fn) => void,
lazy?: boolean
};
deps: Set<EffectFn>[]
}
let activeEffect: EffectFn;
// effect 栈
const effectStack: EffectFn[] = []
//用来注册副作用函数的函数
export function effect(fn: () => void, options = {}) {
const effectFn: EffectFn = () => {
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压栈
effectStack.push(effectFn)
const res = fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
// 将options 挂载在
effectFn.options = options
// activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
effectFn.deps = []
// 执行副作用函数
// 新增懒执行
if (!effectFn.options.lazy) {
effectFn()
}
return effectFn;
}
function cleanup(effectFn: EffectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
// 调度执行
const jobQueue: Set<Fn> = new Set()
const p = Promise.resolve()
let isFlushing = false
function flushJob() {
if (isFlushing) return
isFlushing = true
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
isFlushing = false
})
}
// 计算属性实现,包括懒执行,effect嵌套问题解决
export function computed(getter: () => void) {
// 缓存上一次计算的值
let value: any;
// dirty标志,用来标识是否需要重新计算值
let dirty = true
const effectFn = effect(getter, {
lazy: true,
// 这里shceduler充当了钩子函数的作用
scheduler() {
// 闭包
if (!dirty) {
dirty = true
// 这里做的是当计算属性中任意一个响应式数据变化时,就会触发computerd.value 绑定的副作用函数
// 补充:这里为什么要手动触发?想想在响应式数据中是访问器属性setter中触发的,而计算属性没有setter的,所以需要手动触发
trigger(res, 'value')
}
}
})
const res = {
get value() {
if (!dirty) {
value = effectFn()
dirty = false
}
// 这里其实相当于把计算属性也变成了一个响应式数据, 收集访问res.value的副作用函数
track(res, 'value')
return value
}
}
return res
}
watch 监听属性实现
javascript
watch(obj,()=>{
console.log('数据变化')
})
//
obj.foo++ // 数据变化
watch的实现就是利用effect的可调度性(scheduler选项)。以下watch的简单实现:
javascript
// watch 函数接收两个参数,source 是响应式数据,cb 是回调函数
function watch(source, cb) {
effect(
// 调用 traverse 递归地读取
() => traverse(source),
{
scheduler() {
// 当数据变化时,调用回调函数 cb
cb()
}
}
)
}
// 遍历的读取对象上的所有属性
function traverse(value, seen = new Set()) {
// 如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到 seen 中,代表遍历地读取过了,避免循环引用引起的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设 value 就是一个对象,使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理
for (const k in value) {
traverse(value[k], seen)
}
return value
}
在vue中watch的实现还有一个特色,可以获取新值和旧值,这是怎么实现的哪?这需要充分利用effect函数的**懒执行(lazy选项)**的功能:
javascript
function watch(source, cb) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值与新值
let oldValue, newValue
// 使用 effect 注册副作用函数时,开启 lazy 选项,并把返回值存储到 effectFn 中以便后续手动调用
const effectFn = effect(
() => getter(),
{
lazy: true,
scheduler() {
// 在 scheduler 中重新执行副作用函数,得到的是新值
newValue = effectFn()
// 将旧值和新值作为回调函数的参数
cb(newValue, oldValue)
// 更新旧值,不然下一次会得到错误的旧值
oldValue = newValue
}
}
)
// 这里第一次执行,手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn()
}
几个关键: