简单响应式
响应式用到作用函数,何为作用函数,即该函数操作了函数外的代码,如,访问或修改全局变量。
vue 中响应式操作放在 effect
函数中,之所以叫此名字,因其内操作了 DOM,比如:
const obj = { text: '甲乙丙丁' }
function effect() {
// 此处操作了函数外的变量,故而为作用函数
document.body.textContent = obj.text
}
此时,要将 obj 变成响应式,修改其内 text
属性,页面随之更改,需拦截其读写操作。
即,读取 obj.text
时,缓存 effect
函数;修改 obj.text
时,取出 effect
并执行,便可更新。
Vue2,要兼容低版本浏览器,用 Object.defineProperty
实现拦截,但效率偏低。Vue3,采用 Proxy
,更高效。
// 缓存
// 之所以用 Set,因每次读区都会存缓存,Set 可保证唯一
const bucket = new Set()
// 原始数据
const data = { text: '甲乙丙丁' }
// 代理对象
const obj = new Proxy(data, {
// 代理读操作
get(target, key) {
// 加入缓存
bucket.add(effect)
return target[key]
},
// 代理写操作
set(target, key, newVal) {
// 更新数据
target[key] = newVal
// 取出作用函数并执行
bucket.forEach(fn => fn())
// 返回 true,表示更新成功
return true
}
})
/** 作用函数 */
function effect() {
document.body.innerHTML = obj.text
}
// 执行作用函数
effect()
// 一秒后更新数据
setTimeout(() => {
obj.text = '天地玄黄'
}, 1000)
完善响应式
欲使之完善,需减少死代码。
// 全局变量用以存储被注册的作用函数
let activeEffect
// 作用函数
function effect(fn) {
// 调用 effect 时,保存自定义作用函数
activeEffect = fn
// 执行作用函数
fn()
}
// 如此使用:
effect(() => {
document.body.textContent = obj.text
})
之所以定义全局变量 activeEffect
,乃因需要保存自定义作用函数,以便后续调用:
// ...
// 代理对象
const obj = new Proxy(data, {
// 代理读操作
get(target, key) {
// 加入缓存
if (activeEffect) {
bucket.add(activeEffect)
}
return target[key]
},
// 代理写操作
set(target, key, newVal) {
// 更新数据
target[key] = newVal
// 取出作用函数并执行
bucket.forEach(fn => fn())
// 返回 true,表示更新成功
return true
}
})
// ...
Proxy 代理的是整个对象,所有操作均出发 get
set
,换言之,即使访问或修改的属性不存也,亦会执行作用函数:
effect(() => {
console.log('执行了')
document.body.textContent = obj.text
})
setTimeout(() => {
// 属性不存在,亦打印 “执行了”
obc.notExist = '不存在'
}, 1000)
显然不合理,因而改善之。将属性与作用函数对应起来,使之成为一棵树。
原对象(WeakMap) => 属性(Map) => 作用函数(Set)
// 缓存
// 改为 WeakMap
const bucket = new WeakMap()
let activeEffect
// 原始数据
const data = { text: '甲乙丙丁' }
// 代理对象
const obj = new Proxy(data, {
// 代理读操作
get(target, key) {
// 没有作用函数,直接返回
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()))
}
// 追加作用函数,因用的 Set,不会重复
deps.add(activeEffect)
// 返回属性值
return target[key]
},
// 代理写操作
set(target, key, newVal) {
// 更新数据
target[key] = newVal
// 获取当前对象
const depsMap = bucket.get(target)
if (!depsMap) { return }
// 获取当前属性关联的作用函数
const effects = depsMap.get(key)
// 取出作用函数并执行
effects && effects.forEach(fn => fn())
}
})
整理代码:
// 缓存
// 改为 WeakMap
const bucket = new WeakMap()
let activeEffect
// 原始数据
const data = { ok: true, text: '甲乙丙丁' }
// 代理对象
const obj = new Proxy(data, {
// 代理读操作
get(target, key) {
track(target, key)
// 返回属性值
return target[key]
},
// 代理写操作
set(target, key, newVal) {
// 更新数据
target[key] = newVal
trigger(target, key)
}
})
/** 追踪变化 */
function track(target, key) {
// 没有作用函数,直接返回
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()))
}
// 追加作用函数,因用的 Set,不会重复
deps.add(activeEffect)
}
/** 触发变化 */
function trigger(target, key) {
// 获取当前对象
const depsMap = bucket.get(target)
if (!depsMap) { return }
// 获取当前属性关联的作用函数
const effects = depsMap.get(key)
// 取出作用函数并执行
effects && effects.forEach(fn => fn())
}
/** 作用函数 */
function effect(fn) {
activeEffect = fn
fn()
}
分支优化
考虑如下代码:
const obj = { ok: true, text: '甲乙丙丁' }
effect(() => {
console.log('走这儿了')
document.body.textContent = obj.ok ? obj.text : '子丑寅卯'
})
此时,设 obj.ok = false
,会触发两次打印。这是因为更改 ok
属性,触发 setter
,会执行所有作用函数,即 effects.forEach(fn => fn())
这句。
解决之道在于,执行作用函数前,清除所有依赖,执行是会重新建立依赖。移除前,需明确依赖项。
let activeEffect
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn
// fn 为传入的真实作用函数
fn()
}
// effectFn 上挂载属性 deps,用以存放关联依赖
effectFn.deps = []
// 执行即表示,将 effectFn 存于 activeEffect,并执行作用函数 fn
effectFn()
}
effectFn.deps
收集于 track
:
function track() {
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)
// 上面的 deps 就是当前作用函数的依赖集合
activeEffect.deps.push(deps)
}
反向依赖收集完毕,即可在每次执行依赖函数时,先行清除之:
let activeEffect
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
function cleanup(effectFn) {
// 遍历 effectFn.deps
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是 Set,存着依赖
const deps = effectFn.deps[i]
// 逐个删除
deps.delete(effectFn)
}
// 清空数组
effectFn.deps.length = 0
}
修改 trigger
,以避免死循环
// 死循环原因
const set = new Set([1])
set.forEach((item) => {
set.delete(1)
set.add(1)
console.log('遍历。。')
})
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) { return }
const effects = depsMap.get(key)
// 死循环源头
// effects && effects.forEach(effectFn => effectFn())
// 抽离作用函数到另一集合,再行遍历,可避免之
const effectsToRun = new Set(effects)
effectsToRun.forEach(effectFn => effectFn())
}
嵌套作用函数
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, {/* ... */})
let temp1, temp2
effect(() => {
console.log('fn1 执行了')
effect(() => {
console.log('fn2 执行了')
temp2 = obj.bar
})
temp1 = obj.foo
})
// 执行修改时
obj.foo = 4
// 输出
// fn1 执行了
// fn2 执行了
// fn2 执行了 => 应当为 fn1 执行了
之所以修改 obj.foo
执行的是内层作用函数,盖因 actvieEffect
只保存最后一次注册的作用函数。欲解之,可新增一栈,作用函数执行时,压入之,执行毕,弹出之,并始终让 activeEffect
指向栈顶。
let activeEffect
// 栈
const effectStack = []
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
// 调用前,将`effectFn` 压入栈
effectStack.push(effectFn)
fn()
// 调用毕,推出
effectStack.pop()
// activeEffect 指向栈顶
activeEffect = effectStacke.at(-1)
}
effectFn.deps = []
effectFn()
}
避免无限循环
effect(() => obj.foo = obj.foo + 1)
上述代码会栈溢出。原因是,同时读写。读取,触发追加操作,收集作用函数,接着将其加一再赋回之,触发执行作用函数。而上一个还在执行,无限递归调用自己,引发栈溢出。解决之道:
function trigger(target, key) {
// ...
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEact((effectFn) => {
// 触发的作用函数与正执行的不同,再追加
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// ...
}
调度
欲控制作用函数调用时机、次数、方式,需自定义调度。
function effect(fn, options = {}) {
const effectFn = () => { /* ... */ }
// 挂载各种选项
effectFn.options = options
effectFn.deps = []
effectFn()
}
function trigger(target, key) {
// ...
const effectsToRun = new Set()
effects && effects.forEach((effechFn) => { /* ... */ })
effectsToRun.forEach((effectFn) => {
// 如果有调度函数,则回调
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
}
// 否则直接执行
else {
effectFn()
}
})
}
计算属性
计算属性即懒执行的作用函数。处理选项 lazy
:
function effect(fn, options = {}) {
const effectFn = () => { /* ... */ }
effectFn.options = options
effectFn.deps = []
// 非 lazy 才执行
if (!options.lazy) {
effectFn
}
// 返回作用函数,以待后用
return effectFn
}
function computed(getter) {
// 用以缓存
let value
// 用以判断重新计算
let dirty = true
const effectFn = effect(getter, {
// 表明懒执行
lazy: true,
// 值变,则重新计算
scheduler() {
dirty = true
}
})
const obj = {
get value() {
// 只有值变了,才重新计算
if (dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
const data = { a: 1, b: 2 }
const obj = new Proxy(data, {/* ... */})
const res = computed(() => obj.a + obj.b)
// res.value = 3
wacch
function watch(source, cb) {
let getter
// watch 可以为 getter 方式,如 () => obj.a
if (typeof source === 'function') {
getter = source
}
// 也可以为普通对象
else {
// 递归处理对象各个属性
getter = () => traverse(source)
}
effect(
() => getter(),
{
scheduler() {
cb()
}
}
)
}
function traverse(value, seen = new Set()) {
// value 应为对象,且未被 seen 缓存
if (typeof value !== 'object' || value === null || seen.has(value)) { return }
// 缓存,以防止循环遍历
seen.add(value)
// 假定 value 为对象,递归处理
for (const key in value) {
traverse(value[key], seen)
}
// 返回
return value
}
更改时,获取新旧值:
function watch(source, cb) {
let getter
if (typeof source === 'function') {
getter = source
}
else {
getter = traverse(source)
}
// 定义新旧值
let newValue, oldValue
// lazy = true, 则返回作用函数
const effectFn = effect(
() => getter(),
{
// 激活 lazy,以返回作用函数
lazy: true,
scheduler() {
// 执行作用函数,即为新值
newValue = effectFn()
// 回调函数传入新旧值
cb(newValue, oldValue)
// 回调执行后,新值即旧值
oldValue = newValue
}
}
)
// 手动执行即为旧值
oldValue = effectFn()
}