¶从JavaScript的单线程谈起
什么是单线程, 大白话: 同一时间, 只能做一件事情. JavaScript的诞生主要是为了操作DOM, 如果有多个线程, 假设线程A在它的时间片里去修改DOM_FOO, 但是来不及修改完就得退出CPU, 进入就绪状态, 等待下一次调度, 接着线程B占用CPU, 然而它的任务是删除DOM_FOO, 而且线程B这哥们比较麻溜儿的在一个时间片里就把DOM_FOO给干掉了, 功成身退, 等到线程A再来执行任务的时候压根就找不到DOM_FOO,这下就尴尬了😂, 完不成任务该咋整啊, 要主人来一套锁机制吧, 会不会成本太高, 布兰登巴巴一开始就把它设计成了单线程(而且与浏览器渲染DOM共用一个线程), 并被瑞安大大发扬光大, 膜拜一下我JS两大男神😍
JavaScript的事件轮询Event Loop: 同步任务在执行环境栈ECS(Execution Context Stack)中运行, 主线程执行一次叫做一个tick, 异步任务有了结果后进入任务队列TQ(Task Quene)执行, task分为macro task(如 setImmediate, MessageChannel, setTimeout)和 micro task(如Promise.then), 每个macro task结束后, 所有的micro task 都会被handle, 主线程空了就回去读取任务队列, 而且定时器的执行时间到了才能返回主线程, 如此重复重复再重复
setTimeout(fn, 0), 在任务队列尾部添加事件, HTML5标准中规定延迟时间不低于4ms, 所以你写的0, 实际执行时在4ms以上
主流屏幕刷新率在60hz, DOM的变动不是立即执行, 而是每16毫秒执行一次
node.js中process.nextTick方法可以在当前执行栈的尾部和下一次Event Loop(主线程读取任务队列)之前触发回调函数,它指定的任务优先于所有异步任务之前执行, node.js中setImmediate方法则是在当前任务队列的尾部添加事件, 相当于window中的setTimeout(fn, 0)
¶Vue中的nextTick
见官方文档, nextTick的应用场景, 大白话, 数据变化后, DOM在下一个tick才会重新渲染
nextTick代码在vue源码核心的工具类里vue/src/core/util/next-tick.js, 对外暴露两个函数, withMacroTask 和 nextTick
1 |
|
nextTick在vue/src/core/global-api/index.js作为静态方法被绑定在Vue的构造函数上
nextTick在vue/src/core/instance/render.js绑定在Vue原型对象上并且this会指向调用$nextTick的实例
对macro task的实现, 依次判断是否支持原生的setImmediate和原生的MessageChannel, 否则降级为setTimeout(flushCallbacks, 0)
对micro task的实现, 判断是否支持原生的Promise, 否则降级为macro task
nextTick函数, 把传入的回调函数cb压入callbacks数组, 最后一次性地根据useMacroTask条件执行macroTimerFunc或者是 microTimerFunc, 而它们都会在下一个tick执行flushCallbacks
flushCallbacks拷贝并遍历callbacks并执行相应的回调函数
如果nextTick函数没有接收到cb而且支持Promise, 则提供Promise化的调用