最基础的一个响应式系统
// 存储副作用函数的桶
const bucket = new Set();
// 原始数据
const data = { text: "hello world" };
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 effect 添加到存储副作用函数的桶中
bucket.add(effect);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
bucket.forEach((fn) => fn());
},
});
function effect() {
document.body.innerText = obj.text;
}
effect();
- 利用 proxy 的夹子(get, set) 来劫持数据的读取, 读取数据时来收集副作用函数, 设置数据时出发副作用函数
一个完善的响应式系统
// 存储副作用函数的桶
const bucket = new WeakMap();
// 原始数据
const data = { foo: 1 };
// 对原始数据的代理
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);
},
});
// 读取属性时,收集依赖
function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
// detsMap 是 Map类型 Map的键target的属性名, 值是属性对应副作用函数
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
// deps 是Set 类型, 存储的是副作用函数集合
depsMap.set(key, (deps = new Set()));
}
// deps 收集对应作用函数
deps.add(activeEffect);
// 副作用函数 也会记录 服务于哪些deps
activeEffect.deps.push(deps);
}
// 设置属性时,执行副作用函数
function trigger(target, key) {
// 获取当前对象对应的副作用函数集合
const depsMap = bucket.get(target);
if (!depsMap) return;
// 获取当前对象的属性对应的副作用函数集合
const effects = depsMap.get(key);
const effectsToRun = new Set();
// 收集所有的副作用函数, 当前只在执行的副作用函数就不要再次执行了, 避免陷入死循环
// set add 和 remove 避免同时执行
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 函数
let activeEffect;
// effect 栈, 类似函数调用栈, 在effect 嵌套的情况下, 可以保证关联到对应的副作用函数
const effectStack = [];
function effect(fn, options = {}) {
const effectFn = () => {
// 先清除当前副作用函数所有的依赖项
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压栈
effectStack.push(effectFn)
// 获取执行结果
const res = fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
effectStack.pop()
// 副作用函数执行完成后,立即更新当前的副作用函数的指向
activeEffect = effectStack[effectStack.length - 1]
// 返回fn 的执行结果
return res
}
// 将 options 挂在到 effectFn 上
effectFn.options = options
// activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
effectFn.deps = []
// 执行副作用函数
if (!options.lazy) {
effectFn()
}
return 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;
}
响应式系统的数据结构
- WeakRef 对象允许您保留对另一个对象的弱引用,而不会阻止被弱引用对象被 GC 回收
- WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
- WeakMap 的 key 只能是 Object 类型, 当 key 没有其他地方被引用时,数据就会被销毁.
computed 实现
function computed(getter) { // getter = () => obj.foo + obj.bar
// value 用于缓存上次的值,提升性能
let value;
// 是否脏值
let dirty = true;
const effectFn = effect(getter, {
// 副作用函数无需立即执行
lazy: true,
// 调度器函数,
scheduler() {
// obj.foo , obj.bar 会将effectFn 收集为依赖,
// 当obj.foo 和 obj.bar 值发生改变是 会通过调度器执行effectFn, 添加脏值标记,
if (!dirty) {
//
dirty = true;
// 并重新计算 computed的值
trigger(obj, "value");
}
},
});
const obj = {
// 读取
get value() {
// 如果是脏值重新计算新值,首次执行必定会重新计算
if (dirty) {
// 执行effect 并获取到结果,会被后面的track 收集为依赖
value = effectFn();
dirty = false;
}
// 把计算属性的obj 添加到响应式数据桶中
track(obj, "value");
return value;
},
};
return obj;
}
// 调用
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value)
- computed 计算属性,实际上一个懒执行的副作用函数, 计算属性 通过对象的get取值时,手动执行副作用函数即可
- 当计算属性依赖的响应式数据 发生变化时,通过调度器scheduler 将ditry 设置为true ,下次取值就会重新算取新值
- 思考: 现在知道 使用计算属性时 为啥要 xxx.value 了吧?
watch 的实现
// 递归读取一个对象所有的属性,出发track,收集依赖,
function traverse(value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) return
seen.add(value)
for (const k in value) {
traverse(value[k], seen)
}
return value
}
function watch(source, cb, options = {}) {
let getter
// 监听的源 可能是对象 也可能是函数
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
const job = () => {
// 记录新增
newValue = effectFn()
cb(oldValue, newValue)
oldValue = newValue
}
const effectFn = effect(
// 执行 getter
() => getter(),
{
lazy: true,
scheduler: () => {
// 控制执行的时机
if (options.flush === 'post') {
// 利用了异步的微任务队列机制
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
// 是否立即执行
if (options.immediate) {
job()
} else {
// 执行并 记录本次执行的结果
oldValue = effectFn()
}
}
watch(() => obj.foo, (newVal, oldVal) => {
console.log(newVal, oldVal)
}, {
immediate: true,
flush: 'post'
})
回调的触发时机
- 当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
- 如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项;
个人理解:
通过上面的代码, 可以看出 computed 和 watch 都是基于effect方法来实现的, 主要是利用了 Effect函数在执行会读取响应数据,被收集为依赖, 当数据改变时,再通过该trigger去调用Effect,Effect优先有调度器scheduler来执行,而在调度器的内部, 就顺便 来computed 脏值dirty的状态, 和 顺便执行一下 watch的callback!