说明
【vue.js 设计与实现】 霍春阳 学习笔记
接上文
实现
Version 0.5(支持计算属性 computed)
要如何设计computed?
const amount = computed(() => {
return proxyData.count * proxyData.price;
});
// 读取计算属性
console.log(amount.value);
- 计算属性依赖了响应式对象,是响应式对象的副作用函数
- 希望是在读取计算属性时,再得到运行结果。其他Effect,是一注册就执行
如何实现?
1. init
/**
* 注册计算属性
* @param {*} getter 计算属性对象是的getter函数
*/
function computed(getter) {
// lazy: 注册Effect时,不执行副作用函数
const effectFn = effect(getter, {lazy: true});
const obj = {
get value() {
// 读取value时才执行
return effectFn();
}
}
return obj;
}
此时,需要修改一下effect函数
2. effect函数支持延迟执行
/**
* 注册副作用函数
* @param {Function} effectFn
* @param {Object} options // 新增
* @param {Function} [options.scheduler] 副作用函数执行调度器
* @param {Function} [options.lazy] 延迟执行
*/
function effect(effectFn, options = {}) {
/* effectFn对象,增加deps属性 */
const formatEffectFn = () => {
/* start-清理关联的set */
for(let i = 0; i < formatEffectFn.deps.length; i++){
const effectSet = formatEffectFn.deps[i];
effectSet.delete(formatEffectFn);
}
// 重置数组
formatEffectFn.deps.length = 0;
/* end */
// 一定要放在清理set之后,因为执行effectFn会重新建立关联
activeEffect = formatEffectFn;
effectStack.push(formatEffectFn);
/* 新增:暂存执行结果 */
const result = effectFn();
effectStack.pop();
/* effectFn执行后,重置 */
activeEffect = effectStack[effectStack.length - 1];
/* 新增:返回结果 */
return result;
};
formatEffectFn.deps = [];
formatEffectFn.raw = effectFn;
formatEffectFn.options = options;
/* 新增:如果是延迟执行,此时不执行副作用函数 */
if(!options.lazy) {
formatEffectFn();
}
return formatEffectFn;
}
- [✓]
computed对象是在读取值后才进行计算,符合预期 - [⚠️]依赖的响应式对象属性发生变更后,触发
trigger,计算属性重新执行了。此时不执行更好,希望读取amount.value时再执行。
3. 计算属性增加缓存
此时有个小问题,读取value时,副作用函数都会再运行,即使依赖的对象没有发生变更。
我们需要给计算结果添加缓存,需要借助Part 4实现的调度执行
function computed(getter) {
/** 新增:1. 标记响应对象的有关属性是否发生改变 */
let isDirty = true;
let val;
// lazy: 注册Effect时,不执行副作用函数
const effectFn = effect(getter, {
lazy: true,
/* 新增:2. 调度器中(依赖发生改变触发trigger,执行调度器),重置isDirty */
scheduler: function() {
isDirty = true;
console.log('change isDirty=true');
}
});
const obj = {
get value() {
/* 新增:3. 如果数据脏(改变)了,再执行副作用函数,否则返回缓存值 */
if(isDirty){
val = effectFn();
isDirty = false;
}else{
console.log('计算属性不需要重新计算');
}
return val;
}
}
return obj;
}
- [✓] 依赖未发生改变,多次读取计算属性得值,未重复执行副作用函数
- [✓] 依赖发生改变,计算属性未立即执行副作用函数,而是再次读取时才执行。解决了上文提到的的⚠️
4. 计算属性支持响应式(嵌套Effect)
我们注册一个依赖计算属性的Effect
可以看到,计算属性重新读取后,值更新了,但依赖于它的副作用函数却未重新执行。
render依赖amountamount并不是响应式对象(reactive创建的) 所以amount变化时,也不会触发render的重新执行。从代码执行的角度来说,这其实也是嵌套的Effect。
要支持这种场景,在computed内部进行track和trigger即可
{
...
scheduler: function() {
isDirty = true;
console.log('change isDirty=true');
/** 新增:1. 计算属性依赖的响应式对象发生变更时,手动调用obj的trigger */
trigger(obj, 'value');
}
}
const obj = {
get value() {
if(isDirty){
val = effectFn();
isDirty = false;
}else{
console.log('计算属性不需要重新计算');
}
/* 新增:2. 读取value时,手动调用track函数,追踪obj */
console.log('读取属性value');
track(obj, 'value');
return val;
}
}
过程:
render执行时读取amount.valueamount.value读取时effectFn执行,proxyData.price、proxyData.count关联computed getter- 调用
track,此时的activeEffect是render,所以track执行后,amount.value关联了render
priceData.price更新后,触发了computed gettercomputed getter有调度器,触发了scheduler执行scheduler中触发了amount.value的依赖—render由此,计算属性也支持响应式了
5. 完整代码
let activeEffect = null;
// 执行副作用函数
const effectStack = [];
/**
* 注册副作用函数
* @param {Function} effectFn
* @param {Object} options // 新增
* @param {Function} [options.scheduler] 副作用函数执行调度器
* @param {Boolean} [options.lazy] 延迟执行
*/
function effect(effectFn, options = {}) {
/* effectFn对象,增加deps属性 */
const formatEffectFn = () => {
/* start-清理关联的set */
for(let i = 0; i < formatEffectFn.deps.length; i++){
const effectSet = formatEffectFn.deps[i];
effectSet.delete(formatEffectFn);
}
// 重置数组
formatEffectFn.deps.length = 0;
/* end */
// 一定要放在清理set之后,因为执行effectFn会重新建立关联
activeEffect = formatEffectFn;
effectStack.push(formatEffectFn);
const result = effectFn();
effectStack.pop();
/* effectFn执行后,重置 */
activeEffect = effectStack[effectStack.length - 1];
return result;
};
formatEffectFn.deps = [];
formatEffectFn.raw = effectFn;
formatEffectFn.options = options;
/* 如果是延迟执行,此时不执行副作用函数 */
if(!options.lazy) {
formatEffectFn();
}
return formatEffectFn;
}
// 存储副作用函数
const bucket = new WeakMap();
/**
* 收集target对象key的副作用函数
* @param {Object} target
* @param {String|Symbol} key
* @return {void}
*/
function track(target, key){
if(!activeEffect) return;
let targetMap = bucket.get(target);
if(!targetMap){
targetMap = new Map();
bucket.set(target, targetMap);
}
let effectSet = targetMap.get(key);
if(!effectSet){
effectSet = new Set();
targetMap.set(key, effectSet);
}
effectSet.add(activeEffect);
/* effect中关联和它相关的set */
activeEffect.deps.push(effectSet);
}
/**
* 触发target对象key的副作用函数执行
* @param {Object} target
* @param {String|Symbol} key
* @return {void}
*/
function trigger(target, key) {
console.log('触发trigger');
const targetMap = bucket.get(target);
if(!targetMap) return;
const effectSet = targetMap.get(key);
if(!effectSet) return;
const effectsToRun = new Set(effectSet);
effectsToRun.forEach(effectFn => {
/* 守卫条件 */
if(effectFn !== activeEffect){
console.log('取出effectFn');
if(effectFn.options.scheduler) {
console.log('effectFn执行由调度器来控制');
effectFn.options.scheduler(effectFn);
}else{
effectFn();
}
}else{
console.log('不执行effectFn');
}
});
}
/**
* 创建响应式对象
* @param {*} obj
* @returns
*/
function reactive(obj){
return new Proxy(obj, {
get(target, key) {
// console.log(`触发get,key = ${key}`);
/* 存入副作用函数 */
track(target, key);
return target[key];
},
set(target, key, newVal) {
console.log(`触发set,key = ${key}`);
target[key] = newVal;
// 取出并执行副作用函数
trigger(target, key);
return true;
}
});
}
/**
* 注册计算属性
* @param {*} getter 计算属性对象是的getter函数
*/
function computed(getter) {
let isDirty = true;
let val;
// lazy: 注册Effect时,不执行副作用函数
const effectFn = effect(getter, {
lazy: true,
/* 调度器中(依赖发生改变触发trigger,执行调度器),重置isDirty */
scheduler: function() {
isDirty = true;
/** 计算试行依赖的响应式对象发生变更时,手动调用obj的trigger */
trigger(obj, 'value');
}
});
const obj = {
get value() {
if(isDirty){
val = effectFn();
isDirty = false;
}else{
console.log('计算属性不需要重新计算');
}
console.log('读取属性value');
/* 读取value是,手动调用track函数,追踪obj */
track(obj, 'value');
return val;
}
}
return obj;
}
/**
* 以下是演示用例
*/
const proxyData = reactive({
name: '牛牛手办',
price: 10,
count: 2,
});
window.onload = function() {
const amount = computed(() => {
console.log('执行计算');
return proxyData.count * proxyData.price;
});
console.log('computed延迟执行');
const render = function() {
document.body.innerHTML =`总金额为:${amount.value}`;
}
effect(render);
setTimeout(() => {
console.log('\n\n涨价之后');
proxyData.price = 20;
}, 500);
}