什么是MessageChannel
messageChannel
允许我们建立一个消息通道,并通过两端的端口发送消息实现通信。 其以 DOM Event
的形式发送消息,属于浏览器 宏任务(macro task)的一种。
MessageChannel
实例有两个只读属性:
-
port1: 消息通道的第一个端口,连接源上下文通道。
-
port2: 消息通道的第二个端口,连接目标上下文通道。
基本用法
const { port1, port2 } = new MessageChannel();
port1.onmessage = function (event) {
console.log('收到来自port2的消息:', event.data); // 收到来自port2的消息: pong
};
port2.onmessage = function (event) {
console.log('收到来自port1的消息:', event.data); // 收到来自port1的消息: ping
port2.postMessage('pong');
};
port1.postMessage('ping');
使用场景
深拷贝(序列化,反序列化)
当消息包含
函数
、Symbol
等不可序列化的值时,就会报无法克隆的DOM异常。
let foo = {
a: 1,
b: [1, 3, 4]
};
function deepClone(obj){
return new Promise((resolve)=>{
let {port1,port2}=new MessageChannel()
port1.postMessage(obj)
port2.onmessage=(e)=>resolve(e.data)
})
}
deepClone(foo).then(res=>console.log(res))
实现React调度器
为什么选择messageChannel
选择 MessageChannel
的原因,是首先异步得是个宏任务,因为宏任务中会在下次事件循环中执行,不会阻塞当前页面的更新
setTimeout的缺点
React 源码的实现
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker environments.
// We prefer MessageChannel because of the 4ms setTimeout clamping.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
// We should only fallback here in non-browser environments.
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
localSetImmediate的优势
- 相对与 messageChannel 它不会阻塞nodejs的进程;
- 相对于 setTimeout 没有 4ms 延迟。
手动实现
const ImmediatePriority = 1; // 立即执行的优先级, 级别最高 [点击事件,输入框,]
const UserBlockingPriority = 2; // 用户阻塞级别的优先级, [滚动,拖拽这些]
const NormalPriority = 3; // 正常的优先级 [redner 列表 动画 网络请求]
const LowPriority = 4; // 低优先级 [分析统计]
const IdlePriority = 5;// 最低阶的优先级, 可以被闲置的那种 [console.log]
// 获取当前时间
function getCurrentTime() {
return performance.now();
}
class SimpleScheduler {
constructor() {
this.taskQueue = []; // 任务队列
this.isPerformingWork = false; // 当前是否在执行任务
// 使用 MessageChannel 处理任务调度
const channel = new MessageChannel();
this.port = channel.port2;
channel.port1.onmessage = this.performWorkUntilDeadline.bind(this);
}
// 调度任务
scheduleCallback(priorityLevel, callback) {
const curTime = getCurrentTime();
let timeout;
// 根据优先级设置超时时间
switch (priorityLevel) {
case ImmediatePriority:
timeout = -1;
break;
case UserBlockingPriority:
timeout = 250;
break;
case LowPriority:
timeout = 10000;
break;
case IdlePriority:
timeout = 1073741823;
break;
case NormalPriority:
default:
timeout = 5000;
break;
}
const task = {
callback,
priorityLevel,
expirationTime: curTime + timeout // 直接根据当前时间加上超时时间
};
this.push(this.taskQueue, task); // 将任务加入队列
if (!this.isPerformingWork) {
this.isPerformingWork = true;
this.port.postMessage(null); // 触发 MessageChannel 调度
}
}
// 执行任务
performWorkUntilDeadline() {
this.isPerformingWork = true;
this.workLoop();
this.isPerformingWork = false;
}
// 任务循环
workLoop() {
let curTask = this.peek(this.taskQueue);
while (curTask) {
const callback = curTask.callback;
if (typeof callback === 'function') {
callback(); // 执行任务
}
this.pop(this.taskQueue); // 移除已完成任务
curTask = this.peek(this.taskQueue); // 获取下一个任务
}
}
// 获取队列中的任务
peek(queue) {
return queue[0] || null;
}
// 向队列中添加任务
push(queue, task) {
queue.push(task);
queue.sort((a, b) => a.expirationTime - b.expirationTime); // 根据优先级排序,优先级高的在前 从小到大
}
// 从队列中移除任务
pop(queue) {
return queue.shift();
}
}
// 测试
const scheduler = new SimpleScheduler();
scheduler.scheduleCallback(LowPriority, () => {
console.log('Task 1: Low Priority');
});
scheduler.scheduleCallback(ImmediatePriority, () => {
console.log('Task 2: Immediate Priority');
});
scheduler.scheduleCallback(IdlePriority, () => {
console.log('Task 3: Idle Priority');
});
scheduler.scheduleCallback(UserBlockingPriority, () => {
console.log('Task 4: User Blocking Priority');
});
scheduler.scheduleCallback(NormalPriority, () => {
console.log('Task 5: Normal Priority');
});
Vue实现 nextTick
到了2.5版本,Vue引入MessageChannel,nextTick的实现优先使用setImmediate
,平台不支持则使用MessageChannel
,再不支持才使用Promise
,最后用setTimeout
兜底。
不过到了2.6版本以后,nextTick又改回原来的Promise实现。
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(nextTickHandler)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = nextTickHandler
timerFunc = () => {
port.postMessage(1)
}
} else
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(nextTickHandler)
}
} else {
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}