vue3响应式系统简易实现
从宏观的角度来实现vue3的响应式系统的机制,从副作用函数开始逐步实现一个响应式系统,本文还讲述了计算属性以及侦听器的实现原理。
响应式数据和副作用函数
副作用函数指的是会产生副作用的函数 函数影响了其他函数的执行,列如对外界的数据进行了set操作,从而在一定程度上影响了外部函数的运行。
纯函数:入参和出参,函数内部只是单纯的根据入参进行相应的操作,然后把函数的操作结果返回出来就是纯函数,不影响入参也不影响外部数据。
如下列代码所示:
function effect() {
document.body.innerText = 'hello world'; // 函数副作用 影响了body的文本节点
}
function effct(a) {
return a * 2 // 纯函数
}
响应式数据
const obj = { text: 'hello world'}
function effect() {
document.body.innerText = obj.text; // 函数副作用 影响了body的文本节点
}
obj.text = 'hello';
// 如果是响应式数据的话document.body.innerText的值会随着obj.text的变化发送响应式变化
响应式数据的实现
- 当副作用effct函数执行的时候,会触发obj.text(getter)的读取操作。
- 当修改obj.text的值时,会触发obj.text的设置(setter)操作。
const data = {
text: 'hello world'
}
// 储存当前活动的副作用函数
let activeEffct
const bucket = new Set()
// 对原始数据的代理
const obj = new Proxy(data, { // 数据源, handler一些函数属性的对象
get(target, key) {
bucket.add(activeEffct)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
bucket.forEach(fn => fn())
return true
}
})
function effct(fn) {
activeEffct = fn
fn()
}
effct(() => {
document.body.innerText = obj.text
})
setTimeoust(() => {
obj.text = 'hello'
}, 2000) // hello world -> hello
这就实现了一个简单的响应式系统的响应式系统, 当改变obj.text就会触发setter函数,执行副作用函数。
数据响应式的优化
- 优化做多个响应式数据触发全部副作用函数
const data = {
text: 'hello world',
isOk: true
}
// 储存当前活动的副作用函数
let activeEffect
// 使用weakMap和对象建立弱连接,在不会影响垃圾回收机制
const bucket = new WeakMap()
// 对原始数据的代理
const obj = new Proxy(data, { // 数据源, handler一些函数属性的对象
get(target, key) {
// 如果没有副作用函数就是普通对象
if (!activeEffect) return target[key];
// 看weakMap中有没有改对象的唯一键
let depsMap = bucket.get(target);
if (!depsMap) bucket.set(target, (depsMap = new Map()));
// 看对象中是否有属性被保存
let deps = depsMap.get(key);
// 使用set作为储存器好处是可以去重
if (!deps) depsMap.set(key, (deps = new Set()));
// 收集副作用函数
deps.add(activeEffect);
activeEffect.deps.push(deps)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue;
let depsMap = bucket.get(target);
if (!depsMap) return
// 由于不断的触发收集依赖和触发依赖 要使用一个新的set来包裹 不然会触发无限循环
let deps = new Set(depsMap.get(key));
// 执行副作用函数
deps && deps.forEach(fn => fn())
return true
}
})
function effect(fn) {
const effectFn = () => {
// 清除多余的副作用函数
cleanup(effectFn);
// 在触发副作用函数时 保证收集的函数是改副作用函数
activeEffect = effectFn;
fn();
}
// 收集包含副作用函数的set集合
effectFn.deps = [];
effectFn();
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
// 清空数组
effectFn.deps.length = 0
}
effect(() => {
document.body.innerText = obj.isOk ? obj.text : ''
})
// 可以运行
// setTimeout(() => {
// obj.text = 'hello'
// }, 2000) // hello world -> hello
可以把get中的逻辑封装成一个track函数,set中的逻辑封装成trigger函数
const obj = new Proxy(data, { // 数据源, handler一些函数属性的对象
get(target, key) {
// 如果没有副作用函数就是普通对象
if (!activeEffect) return target[key];
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue;
trigger(target, key)
return true
}
})
function track(target, key) {
if (!activeEffect) return
// 看weakMap中有没有改对象的唯一键
let depsMap = bucket.get(target);
if (!depsMap) bucket.set(target, (depsMap = new Map()));
// 看对象中是否有属性被保存
let deps = depsMap.get(key);
// 使用set作为储存器好处是可以去重
if (!deps) depsMap.set(key, (deps = new Set()));
// 收集副作用函数
deps.add(activeEffect);
activeEffect.deps.push(deps)
}
function trigger(target, key) {
let depsMap = bucket.get(target);
if (!depsMap) return
let deps = new Set(depsMap.get(key));
// 执行副作用函数
deps && deps.forEach(fn => fn())
}
实现effct嵌套
改变了一个effct函数的代码以及添加了一个effct栈
effct(() => {
console.log(obj.text)
effct(() => {
console.log(obj.isOk)
})
})
// 这种嵌套上述代码的activeEffect只会是最后一层的 所以要把加一个effctFn栈
const effectBucket = []
function effect(fn) {
const effectFn = () => {
// 清除多余的副作用函数
cleanup(effectFn);
// 在触发副作用函数时 保证收集的函数是改副作用函数
activeEffect = effectFn;
// 把当前状态保存起来
effectBucket.push(activeEffect)
fn();
// 把当前的副作用函数弹出去
effectBucket.pop()
// 如果有嵌套就会把activeEffect还原成之前的值 activeEffect就不会错乱
activeEffect = effectBucket[effectBucket.length - 1]
}
// 收集包含副作用函数的set集合
effectFn.deps = [];
effectFn();
}
调度执行
effect(() => {
console.log(obj.foo)
})
obj.foo++
console.log('结束了')
想要输出的结果是 1 结束了 2
// 让副作用函数异步执行
function effect(fn, option) {
const effectFn = () => {
// ...
}
// 收集包含副作用函数的set集合
effectFn.deps = [];
// 额外属性
effectFn.option = option // 新增
effectFn();
}
function trigger(target, key){
let depsMap = bucket.get(target);
if (!depsMap) return
let deps = new Set(depsMap.get(key));
// 执行副作用函数
deps && deps.forEach(fn => {
// 如果需要调度执行就执行调度函数
if (fn.option.schedule) { // 新增
fn.option.schedule(fn)
} else {
fn()
}
})
}
effect(() => {
console.log(obj.foo)
}, {
schedule(fn) {
console.log(obj.foo)
setTimeout(fn, 0) // 相当于异步调度
}
})
栈溢出
如果出现副作用函数中触发了setter函数的情况会出现栈溢出
function trigger(target, key) {
let depsMap = bucket.get(target);
if (!depsMap) return
let deps = depsMap.get(key);
const depsEffect = new Set()
// 防止栈溢出 如果当前函数正在执行就不需要继续触发了
deps.forEach(fn => {
if (activeEffect == fn) return
depsEffect.add(fn)
})
// 执行副作用函数
depsEffect && depsEffect.forEach(fn => {
if (fn.option.schedule) {
fn.option.schedule(fn)
} else {
fn()
}
})
}
异步一个周期执行触发一个作用函数
const data = { foo : 1 }
cosnt obj = new Proxy(...)
effect(() => {
console.log(obj.foo) // 想要输出 1 3 而不是 1 2 3, 之前写的异步调度只能保证调度是异步的
})
obj.foo++
obj.foo++
< ------------ >
// 执行函数去重队列
const jonQueue = new Set()
// 异步
const p = Promise.resolve()
let isFlusing = false
function flushJob (fn) {
// jonQueue.add(fn)
if (!isFlusing) {
isFlusing = true
p.then(() => {
jonQueue.forEach(fn => fn())
}).finally(() => {
isFlusing = false
})
}
}
effect(() => {
console.log(obj.foo)
}, {
schedule(fn) {
// setTimeout(fn, 0)
jonQueue.add(fn)
flushJob()
}
})
计算属性computed
实现计算属性首先得了解计算属性的特点: 计算属性会跟踪函数中所有的响应式数据,如果有数据发生变化就会重新计算。
const data = {foo: 1, bar: 2}
const obj = new Proxy(...)
function computed(getter) {
let value // 作为值保存
let dirty = true // 节流
const effectFn = effect(getter, {
lazy: true,
schedule() {
dirty = true
trigger(obj, 'value') // 用于触发依赖
}
})
const obj = {
get value() {
if (dirty) {
dirty = false
value = effectFn() // 获取getter函数返回结果
}
track(obj, 'value') // 用于收集依赖
}
}
return obj
}
const compu = computed(() => obj.foo + obj.bar)
effect(() => console.log(compu.value)) // 更改foo、bar的时候会触发计算属性的副作用函数
watch实现
监听数据变化来执行相应的函数
function watch(getter, Fn, option = {}) {
// 是ref等类型的响应式
// getter是函数
let getterFn = getter
if (typeof getter !== 'function') {
getterFn = () => traverse(getter)
}
effect(getterFn, {
schedule() {
const p = Promise.resolve()
if (option.flush === 'post') {
return p.then(() => fn())
}
Fn()
},
})
if (option.immediate) {
Fn()
}
}
// option 有deep深度监听、immediate初始化的时候执行一次、flush dom前后更新 'post'后 'pre'前 'sync'同步
watch(obj, () => {
console.log('watch监听到了')
}, {
immediate: true,
deep: true,
flush: 'post'
})
function traverse(value, active = new Set(), deep = false) { // 遍历对象中的每一个属性
if (typeof value === 'object' && value !== null) {
active.add(value)
for(key in value) {
if (value[key] && active.has(value[key]) && deep) {
traverse(value)
}
}
}
return value
}
总结
vue3的响应式系统就大致的实现了,这是我根据vue.js设计与实现中学习的,想着看自己能不能把响应式系统写出来。这只是响应式系统的大致实现,思路应该是类似的。希望能对大家有帮助。