说明
【vue.js 设计与实现】 霍春阳 学习笔记
接上文
实现
version 0.3 (支持嵌套Effect)
嵌套的Effect,指的是在Effect中又注册了Effect。什么场景下会出现嵌套的Effect?vue中比较常见的是组件渲染。
我们对嵌套的Effect的期望是:
- 外层的Effect执行时,会引起内层的Effect执行
- 只有和内层Effect相关的变化时,只引起内层Effect执行
那我们目前的代码如果写了嵌套的Effect能正常执行吗?
1.1 内层Effect可能会多次执行
/* 此时响应式的代码同v0.21,也就是part2的中的完整代码部分 */
/**
* 以下是演示用例
*/
const proxyData = reactive({
name: '牛牛手办',
price: 10,
count: 1,
});
function render(){
console.log('执行render');
let text = '';
if(proxyData.count > 0){
let money = proxyData.count * proxyData.price;
text += `${money} 元`;
}else{
text += '0 元';
}
document.body.innerHTML = `
<h1>采购信息<h1>
<h2>总金额</h2>
<div>${text}</div>
`;
const node = document.createElement('div');
document.body.appendChild(node);
effect(renderDetail(node, proxyData));
}
function renderDetail(parentNode, product) {
return function() {
console.log('执行renderDetail')
parentNode.innerHTML = '';
const node = document.createElement('div');
node.innerHTML = `
<div>
<h2>商品详情</h2>
<p>商品名称:${product.name}</p>
<p>商品价格:${product.price}</p>
<div>
`;
parentNode.appendChild(node);
}
}
window.onload = function() {
effect(render);
// 应该引起render和renderDetail的执行
setTimeout(() => {
console.log('\n\n更新count');
proxyData.count = 30;
}, 1000);
// 应该只会引起renderDetail执行
setTimeout(() => {
console.log('\n\n更新name');
proxyData.name = '星空雨伞';
}, 1200);
}
除了执行了两次
renderDetail,其他符合我们的预期。为什么会执行两次勒?
renderDetail在render中注册的render执行了两次,renderDetail也注册了两次- 虽然依赖是
Set结构,但effectFn是有一个注册时生成的匿名函数,所以并不相同,尽管他们的原函数实际内容相同
按照这个逻辑,如果render再执行,renderDetail的执行次数还会增多
window.onload = function() {
effect(render);
// 应该引起render和renderDetail的执行
setTimeout(() => {
console.log('\n\n更新count');
proxyData.count = 30;
}, 1000);
setTimeout(() => {
console.log('\n\n更新count');
proxyData.count = 50;
}, 1100);
// 应该只会引起renderDetail执行
setTimeout(() => {
console.log('\n\n更新name');
proxyData.name = '星空雨伞';
}, 1200);
}
结果确实如此
怎么解决勒?
没想到。
这里原因是进入Set的是新创建的Effect,每次运行的结果都和前面不同,虽然注册使用的是同一个。那我们是不是可以借助注册的函数来判断?
const array = [];
function test(fn) {
const newFn = () => {
};
newFn.raw = fn;
let exist = array.find(item => item.raw === newFn.raw);
if(!exist){
array.push(newFn);
}
console.log('exist ', exist);
return newFn;
}
const detail = () =>{
console.log(detail);
}
function wrapper(){
test(detail);
}
wrapper();
wrapper();
wrapper执行了两次,但通过增加raw属性,可以判断重复。
但这种对
test的参数有要求,需要是单独声明的函数,如果换成匿名函数,并不能判断重复。
这种和重复注册(外层也)一样,处理不了,不能算bug。
看一下vue?
从结果来看,内层副作用函数也会重复执行
import { reactive} from 'vue'
import { effect } from '@vue/reactivity'
window.onload = function() {
// 此demo来自于书籍,章节4.5
const data = { foo: true, bar: true};
const obj = reactive(data);
let temp1, temp2;
effect(function effectFn1() {
console.log('执行effectFn1');
effect(function effectFn2() {
console.log('执行effectFn2');
temp2 = obj.bar;
});
temp1 = obj.foo;
});
setTimeout(() =>{
console.log('\n\n');
obj.foo = false;
}, 500);
setTimeout(() => {
console.log('\n\n');
obj.bar = false;
}, 600);
}
obj.bar修改,effectFn2执行了两次
1.2 副作用函数栈
1、修改name,触发effectFn1、effectFn2
window.onload = function() {
effect(function effectFn1() {
effect(function effectFn2() {
console.log('effect-商品价格:', proxyData.price);
});
// 和1.1测试不同点:外层的依赖晚于内存effect
console.log('effect-商品名称:', proxyData.name);
});
setTimeout(() => {
console.log('\n\n修改name')
proxyData.name = '星空雨伞';
}, 500);
}
修改name后,无依赖副作用函数,为什么勒?
- 注册
effectFn1,activeEffect等于(准确说是相关的,新的匿名函数)effectFn1 - 运行
effectFn1,activeEffect等于effectFn2,effectFn2运行完毕 - 继续
effectFn1执行,读取对象的name属性,触发trigger trigger中activeEffect等于null(第2步运行后重置了activeEffect)
矛盾点在于,activeEffect只能有一个,且执行完内层后会重置,继续回到外层函数中执行时,activeEffect不会还原。函数执行是借助栈的,同样,副作用函数也可以用栈来收集。
/* 新增 */
const effectStack = [];
function effect(effectFn) {
const formatEffectFn = () => {
for(let i = 0; i < formatEffectFn.deps.length; i++){
const effectSet = formatEffectFn.deps[i];
effectSet.delete(formatEffectFn);
}
formatEffectFn.deps.length = 0;
activeEffect = formatEffectFn;
/* 新增 */
effectStack.push(formatEffectFn);
effectFn();
/* 新增 */
effectStack.pop();
/* 修改,原来是置为null */
activeEffect = effectStack[effectStack.length - 1];
};
// 无变化 省略
}
满足期望
2、修改price,触发effectFn2(使用第一步产生的Effect)
window.onload = function() {
// 注册内容同第一步
setTimeout(() => {
console.log('\n\n修改price');
proxyData.price = 20;
}, 500);
}
满足期望
1.3 完整代码
let activeEffect = null;
// 执行副作用函数
const effectStack = [];
/**
* 注册副作用函数
* @param {Function} effectFn
*/
function effect(effectFn) {
/* 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);
effectFn();
effectStack.pop();
/* effectFn执行后,重置 */
activeEffect = effectStack[effectStack.length - 1];
};
formatEffectFn.deps = [];
formatEffectFn.raw = effectFn;
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');
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;
}
});
}
/**
* 以下是演示用例
*/
const proxyData = reactive({
name: '牛牛手办',
price: 10,
count: 1,
});
window.onload = function() {
effect(function effectFn1() {
effect(function effectFn2() {
console.log('effect-商品价格:', proxyData.price);
});
console.log('effect-商品名称:', proxyData.name);
});
// setTimeout(() => {
// console.log('\n\n修改name')
// proxyData.name = '星空雨伞';
// }, 500);
setTimeout(() => {
console.log('\n\n修改price');
proxyData.price = 20;
}, 500);
}
/*
function render(){
console.log('执行render');
let text = '';
if(proxyData.count > 0){
let money = proxyData.count * proxyData.price;
text += `${money} 元`;
}else{
text += '0 元';
}
document.body.innerHTML = `
<h1>采购信息<h1>
<h2>总金额</h2>
<div>${text}</div>
`;
const node = document.createElement('div');
node.id = 'detail-parent';
document.body.appendChild(node);
effect(renderDetail);
}
function renderDetail() {
console.log('执行renderDetail')
const parentNode = document.getElementById('detail-parent');
parentNode.innerHTML = '';
const node = document.createElement('div');
node.innerHTML = `
<div>
<h2>商品详情</h2>
<p>商品名称:${proxyData.name}</p>
<p>商品价格:${proxyData.price}</p>
<div>
`;
parentNode.appendChild(node);
}
window.onload = function() {
effect(render);
// 应该引起render和renderDetail的执行
setTimeout(() => {
console.log('\n\n更新count');
proxyData.count = 30;
}, 1000);
setTimeout(() => {
console.log('\n\n更新count');
proxyData.count = 50;
}, 1100);
// 应该只会引起renderDetail执行
setTimeout(() => {
console.log('\n\n更新name');
proxyData.name = '星空雨伞';
}, 1200);
}
*/