tasks.js是 Knockout.js 框架中负责异步任务调度的核心模块。它提供了一个高效的任务队列系统,用于处理 DOM 更新、计算属性重新计算等需要异步执行的操作。通过使用微任务调度机制,Knockout.js 能够批量处理更新操作,提高应用性能。
核心概念
为什么需要任务调度?
在现代 Web 应用中,频繁的 DOM 操作会导致性能问题。Knockout.js 通过任务调度机制将多个更新操作批量处理,避免重复的 DOM 操作。例如:
- 批量更新 - 当多个 observable 发生变化时,将相关的 DOM 更新操作合并执行
- 避免重复计算 - 计算属性的重新计算可以被批量处理
- 优化性能 - 减少浏览器重排和重绘的次数
调度机制
Knockout.js 根据浏览器支持情况选择最优的异步调度机制:
- MutationObserver - 现代浏览器首选,性能最佳
- script.onreadystatechange - IE 浏览器的备选方案
- setTimeout - 兼容性方案,性能相对较差
核心实现
调度器选择
javascript
if (window['MutationObserver']) {
// Chrome 27+, Firefox 14+, IE 11+, Opera 15+, Safari 6.1+
scheduler = (function (callback) {
var div = document.createElement("div");
new MutationObserver(callback).observe(div, {attributes: true});
return function () { div.classList.toggle("foo"); };
})(scheduledProcess);
} else if (document && "onreadystatechange" in document.createElement("script")) {
// IE 6-10
scheduler = function (callback) {
var script = document.createElement("script");
script.onreadystatechange = function () {
script.onreadystatechange = null;
document.documentElement.removeChild(script);
script = null;
callback();
};
document.documentElement.appendChild(script);
};
} else {
scheduler = function (callback) {
setTimeout(callback, 0);
};
}
这段代码根据浏览器支持情况选择最优的调度器:
- MutationObserver - 利用 DOM 变化观察器实现微任务调度
- script.onreadystatechange - 利用脚本加载事件实现近似微任务调度
- setTimeout - 使用宏任务作为备选方案
任务队列管理
javascript
var taskQueue = [],
taskQueueLength = 0,
nextHandle = 1,
nextIndexToProcess = 0;
任务队列相关变量:
taskQueue- 存储待执行任务的数组taskQueueLength- 当前任务队列长度nextHandle- 下一个任务句柄,用于任务取消nextIndexToProcess- 下一个待处理任务的索引
任务处理函数
processTasks
javascript
function processTasks() {
if (taskQueueLength) {
// Each mark represents the end of a logical group of tasks and the number of these groups is
// limited to prevent unchecked recursion.
var mark = taskQueueLength, countMarks = 0;
// nextIndexToProcess keeps track of where we are in the queue; processTasks can be called recursively without issue
for (var task; nextIndexToProcess < taskQueueLength; ) {
if (task = taskQueue[nextIndexToProcess++]) {
if (nextIndexToProcess > mark) {
if (++countMarks >= 5000) {
nextIndexToProcess = taskQueueLength; // skip all tasks remaining in the queue since any of them could be causing the recursion
ko.utils.deferError(Error("'Too much recursion' after processing " + countMarks + " task groups."));
break;
}
mark = taskQueueLength;
}
try {
task();
} catch (ex) {
ko.utils.deferError(ex);
}
}
}
}
}
处理任务队列中的所有任务,包含递归保护机制防止无限循环。
scheduledProcess
javascript
function scheduledProcess() {
processTasks();
// Reset the queue
nextIndexToProcess = taskQueueLength = taskQueue.length = 0;
}
调度处理函数,在调度器触发时执行,处理完任务后重置队列。
核心 API
schedule
javascript
schedule: function (func) {
if (!taskQueueLength) {
scheduleTaskProcessing();
}
taskQueue[taskQueueLength++] = func;
return nextHandle++;
}
调度一个任务:
- 如果任务队列为空,启动任务处理调度
- 将任务添加到队列中
- 返回任务句柄用于取消
cancel
javascript
cancel: function (handle) {
var index = handle - (nextHandle - taskQueueLength);
if (index >= nextIndexToProcess && index < taskQueueLength) {
taskQueue[index] = null;
}
}
取消指定的任务,通过将任务设置为 null 来实现。
runEarly
javascript
runEarly: processTasks
立即执行所有排队的任务,不等待调度器触发。
在 Knockout.js 中的应用
依赖检测
在依赖检测系统中,当 observable 发生变化时,相关的订阅者会被调度执行:
javascript
ko.subscribable.fn.notifySubscribers = function (valueToNotify, event) {
event = event || defaultEvent;
if (this._subscriptions[event]) {
ko.tasks.schedule(() => {
ko.utils.arrayForEach(this._subscriptions[event].slice(), function (subscription) {
subscription(valueToNotify);
});
});
}
};
DOM 更新
在绑定系统中,DOM 更新操作会被批量处理:
javascript
ko.bindingHandlers.text = {
update: function (element, valueAccessor) {
ko.tasks.schedule(() => {
ko.utils.setTextContent(element, valueAccessor());
});
}
};
优化方案(针对现代浏览器)
针对现代浏览器,我们可以简化任务调度模块的实现:
javascript
ko.tasks = (function () {
let taskQueue = [],
taskQueueLength = 0,
nextHandle = 1,
nextIndexToProcess = 0;
// 现代浏览器统一使用 queueMicrotask
const scheduler = function (callback) {
if (typeof queueMicrotask === 'function') {
queueMicrotask(callback);
} else if (window['MutationObserver']) {
// 回退到 MutationObserver
const div = document.createElement("div");
new MutationObserver(callback).observe(div, {attributes: true});
return () => { div.classList.toggle("foo"); };
} else {
// 最后的回退方案
Promise.resolve().then(callback);
}
};
function processTasks() {
if (taskQueueLength) {
// 简化递归保护
const mark = taskQueueLength;
let countMarks = 0;
for (let task; nextIndexToProcess < taskQueueLength; ) {
if (task = taskQueue[nextIndexToProcess++]) {
if (nextIndexToProcess > mark) {
if (++countMarks >= 5000) {
nextIndexToProcess = taskQueueLength;
ko.utils.deferError(new Error("'Too much recursion' after processing " + countMarks + " task groups."));
break;
}
mark = taskQueueLength;
}
try {
task();
} catch (ex) {
ko.utils.deferError(ex);
}
}
}
}
}
function scheduledProcess() {
processTasks();
// 重置队列
nextIndexToProcess = taskQueueLength = taskQueue.length = 0;
}
function scheduleTaskProcessing() {
ko.tasks['scheduler'](scheduledProcess);
}
const tasks = {
'scheduler': scheduler,
schedule(func) {
if (!taskQueueLength) {
scheduleTaskProcessing();
}
taskQueue[taskQueueLength++] = func;
return nextHandle++;
},
cancel(handle) {
const index = handle - (nextHandle - taskQueueLength);
if (index >= nextIndexToProcess && index < taskQueueLength) {
taskQueue[index] = null;
}
},
// For testing only
'resetForTesting': function () {
const length = taskQueueLength - nextIndexToProcess;
nextIndexToProcess = taskQueueLength = taskQueue.length = 0;
return length;
},
runEarly: processTasks
};
return tasks;
})();
ko.exportSymbol('tasks', ko.tasks);
ko.exportSymbol('tasks.schedule', ko.tasks.schedule);
ko.exportSymbol('tasks.runEarly', ko.tasks.runEarly);
优化要点
- 使用现代 API - 优先使用
queueMicrotask - 简化代码 - 使用
let[/](file:///Users/xianhao/jvy/nodejs/gitee/@licence/Apache-2.0/dist/index.d.ts)const和箭头函数 - 移除兼容性代码 - 删除针对 IE 的特殊处理
- 改进错误处理 - 使用现代的
Error构造函数
使用示例
基本用法
javascript
// 调度一个任务
const handle = ko.tasks.schedule(() => {
console.log('Task executed');
});
// 取消任务
ko.tasks.cancel(handle);
// 立即执行所有任务
ko.tasks.runEarly();
实际应用场景
javascript
// 在自定义订阅中使用
function MyObservable(initialValue) {
let value = initialValue;
const subscribers = [];
this.subscribe = function(callback) {
subscribers.push(callback);
return {
dispose: () => {
const index = subscribers.indexOf(callback);
if (index >= 0) {
subscribers.splice(index, 1);
}
}
};
};
this.notify = function(newValue) {
// 批量通知订阅者
subscribers.forEach(callback => {
ko.tasks.schedule(() => callback(newValue));
});
};
this.setValue = function(newValue) {
value = newValue;
this.notify(newValue);
};
}
性能优化示例
javascript
// 批量更新 DOM
ko.bindingHandlers.foreach = {
update: function(element, valueAccessor) {
const items = ko.utils.unwrapObservable(valueAccessor());
// 清空元素
ko.utils.emptyDomNode(element);
// 批量创建元素
items.forEach(item => {
ko.tasks.schedule(() => {
const childElement = document.createElement('div');
ko.applyBindingsToNode(childElement, { text: item }, item);
element.appendChild(childElement);
});
});
}
};
总结
tasks.js是 Knockout.js 中一个关键的性能优化模块,它通过异步任务调度机制实现了高效的批量更新。该模块的设计体现了现代 Web 开发中对性能优化的重视,通过合理的抽象和封装,为开发者提供了简单易用的 API 来处理异步任务。
对于现代浏览器,我们可以进一步简化其实现,利用新的 Web API 提高代码的可读性和性能,同时保持功能的完整性。这种渐进式优化的思路在现代前端开发中非常常见,有助于在保持兼容性的同时提升代码质量。