说明
【vue.js 设计与实现】 霍春阳 学习笔记
接上文
实现
version 0.4 (调度执行)
1. 案例(需求)
function render() {
console.log('执行render');
let money = proxyData.count * proxyData.price;
let text = `采购信息:总金额 = ${money}`;
document.body.innerHTML = text;
}
window.onload = function() {
effect(render);
setTimeout(function () {
console.log('更新count');
proxyData.count++;
console.log('\n\n更新price');
proxyData.price = proxyData * 2;
}, 500);
}
先后修改count和price,目前会两次触发trigger和render。
这两个是执行同一个副作用函数,如果能更新数据完成后,再执行一次render,性能会更好。(就是vue.js连续多次修改响应数据,但只会触发一次渲染)
如何实现勒?
2. 响应系统支持副作用函数调度执行
调度执行指的是当
trigger执行触发副作用函数执行时,有能力决定副作用函数执行的时机、次数以及方式
注册时存储调度器
/**
* 注册副作用函数
* @param {Function} effectFn
* @param {Object} options // 新增
* @param {Function} [options.scheduler] 副作用函数执行调度器
*/
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);
effectFn();
effectStack.pop();
/* effectFn执行后,重置 */
activeEffect = effectStack[effectStack.length - 1];
};
formatEffectFn.deps = [];
formatEffectFn.raw = effectFn;
/* 新增 */
formatEffectFn.options = options;
formatEffectFn();
}
trigger触发执行时将执行副作用的控制权交给调度器
/**
* 触发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');
}
});
}
3. 案例所需的调度器书写
有几点需要注意:
- 任务队列(需要执行的副作用函数)是
Set,这样可以去重 - 需要一个值标记任务已经执行了,使的在任务完成前,即使多次调用任务也只会执行一次
- 副作用函数的执行放到微任务中,这样才能让响应式对象的多个属性更新(同步)完成后再执行副作用函数
window.onload = function() {
// 任务(副作用函数)队列
const jobQueue = new Set();
let isRunning = false;
// 用它将任务添加到微任务队列中
const promise = Promise.resolve();
const runJob = () => {
console.log('执行runJob');
if(isRunning) {
console.log('任务队列不执行');
return;
}
console.log('准备执行任务队列');
isRunning = true;
promise
.then(() => {
console.log('微任务执行');
jobQueue.forEach(job => {
console.log('副作用函数即将执行');
job();
})
isRunning = false;
console.log('微任务执行完毕,isRunning还原');
})
.catch(() => {
isRunning = false;
console.log('微任务执行完毕(有错误),isRunning还原');
});
}
effect(render, {
scheduler: function(effectFn) {
jobQueue.add(effectFn);
runJob();
}
});
setTimeout(function () {
console.log('更新count');
proxyData.count++;
console.log('\n\n更新price');
proxyData.price = proxyData.price * 2;
}, 500);
}
如我们期望的,render只执行了一次
4. 完整代码
let activeEffect = null;
// 执行副作用函数
const effectStack = [];
/**
* 注册副作用函数
* @param {Function} effectFn
* @param {Object} options // 新增
* @param {Function} [options.scheduler] 副作用函数执行调度器
*/
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);
effectFn();
effectStack.pop();
/* effectFn执行后,重置 */
activeEffect = effectStack[effectStack.length - 1];
};
formatEffectFn.deps = [];
formatEffectFn.raw = effectFn;
/* 新增 */
formatEffectFn.options = options;
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;
}
});
}
/**
* 以下是演示用例
*/
const proxyData = reactive({
name: '牛牛手办',
price: 10,
count: 1,
});
function render() {
console.log('执行render');
let money = proxyData.count * proxyData.price;
let text = `采购信息:总金额 = ${money}`;
document.body.innerHTML = text;
}
window.onload = function() {
// 任务(副作用函数)队列
const jobQueue = new Set();
let isRunning = false;
// 用它将任务添加到微任务队列中
const promise = Promise.resolve();
const runJob = () => {
console.log('执行runJob');
if(isRunning) {
console.log('任务队列不执行');
return;
}
console.log('准备执行任务队列');
isRunning = true;
promise
.then(() => {
console.log('微任务执行');
jobQueue.forEach(job => {
console.log('副作用函数即将执行');
job();
})
isRunning = false;
console.log('微任务执行完毕,isRunning还原');
})
.catch(() => {
isRunning = false;
console.log('微任务执行完毕(有错误),isRunning还原');
});
}
effect(render, {
scheduler: function(effectFn) {
jobQueue.add(effectFn);
runJob();
}
});
setTimeout(function () {
console.log('更新count');
proxyData.count++;
console.log('\n\n更新price');
proxyData.price = proxyData.price * 2;
}, 500);
}