🤔nextTick 与 React scheduler的底层实现,MessageChannel😁

42 阅读3分钟

什么是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的缺点

image.png

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的优势
  1. 相对与 messageChannel 它不会阻塞nodejs的进程;
  2. 相对于 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)
    }
  }