浏览器的事件环

进程与线程

CPU 正在进行的一个任务的运行过程的调度单位;
浏览器是一个多进程的应用程序;
进程是计算机调度的基本单位。

进程包括线程,线程在进程中运行,每个进程里包含多个线程运行。

tab 独立进程运行,页面的状态不会受到干扰。

任务管理器(mac 活动监视器)查看 chrome 的进程情况。浏览器有一个主进程(用户界面),每一个 tab 都会开启进程。
每一个 tab (非同源)各自有独立的渲染进程(浏览器内核 Render,渲染引擎)、网络进程(网络请求)、GPU 进程(动画与 3D 绘制)、插件进程(devtool);

渲染进程

渲染进程包括 GUI 渲染线程(渲染页面)、JS 引擎线程。GUI 渲染与 JS 引擎线程运行互斥。

GUI 渲染线程

  • 解析 HTML、CSS
  • 构建 DOM/Render 树
  • 初始布局与绘制
  • 重绘与回流

JS 引擎线程

主线程与多个辅助线程配合。

浏览器只有一个 JS 引擎,用来解析 JS 脚本、运行 JS 代码。

渲染与脚本解析执行

JS 引擎运行脚本与 GUI 渲染互斥。JS 引擎任务空闲时,进行 GUI 渲染更新。

<div>Front of you</div> <script>while (true) { }</script> <div>End of you</div>

脚本和渲染是互斥的。

<div>Front of you</div> <script> setTimeout(() => { document.getElementsByTagName('div')[0].innerText = 'Front of me'; }, 1000); </script> <div>End of you</div>

正常运行。

事件

事件触发线程:Event Loop 线程,事件环

事件线程:用户交互事件、setTimeout、Ajax

宏任务与微任务

创建线程的目的是为了实现异步的执行条件。

宏任务:

  • 宿主提供的异步方法和任务
  • script、setTimeout、UI 渲染等

微任务:

  • 语言标准(ECMA262)提供的 API 运行
  • Promise、MutationObserver

引入微任务:为了处理优先级问题,微任务在当前任务队列执行结束前,会清空微任务。

案例分析

案例1

document.body.style.backgroundColor = 'orange'; console.log('1'); setTimeout(() => { document.body.style.backgroundColor = 'green'; console.log('2'); }, 100); Promise.resolve(3).then(num => { document.body.style.backgroundColor = 'purple'; console.log(num); }); console.log(4); // 1 4 purple 3 green 2

案例2

Promise.resolve().then(() => { console.log('p1'); setTimeout(() => { console.log('s2'); }, 0); }); setTimeout(() => { console.log('s1'); Promise.resolve().then(() => { console.log('p2'); }); }); // 第一轮:p1 // 第二轮:s1 p2 // 第三轮:s2
Promise.resolve().then(() => { console.log('p1'); setTimeout(() => { console.log('s2'); }, 0); setTimeout(() => { console.log('s3'); }, 0); }); setTimeout(() => { console.log('s1'); Promise.resolve() .then(() => { console.log('p2-1'); }) .then(() => { console.log('p2-2'); }); }); // 第一轮:p1 // 第二轮:s1 p2-1 p2-2 // 第三轮:s2 // 第四轮:s3

案例3

console.log(1); setTimeout(() => { console.log(2); }, 10); new Promise((resolve) => { console.log(3); resolve(''); console.log(4); }).then(res => { console.log(5); }); console.log(6); // 1 3 4 6 // 5 // 2
console.log(1); setTimeout(() => { console.log(2); }, 10); new Promise((resolve) => { console.log(3); console.log(4); }).then(res => { console.log(5); }); console.log(6); // 1 3 4 6 // 2
console.log(1); setTimeout(() => { console.log(2); }, 10); new Promise((resolve) => { console.log(3); reject(''); console.log(4); }).then(res => { console.log(5); }); console.log(6); // 1 3 4 6 // Uncaught (in promise) // 2 // 同步代码 =》微任务代码 => UI 渲染 => 宏任务

案例4

// async/await 其实是 generator + co 的语法糖 // async 默认会返回一个 promise 实例,await 必须存在于 async 函数中 // await test() => test().then(res => {}); let res = function () { console.log(1); return new Promise((resolve) => { console.log(2); resolve(4); }); } new Promise(async () => { console.log(3); let test = await res(); console.log(test); }); console.log(5); new Promise(() => { console.log(6); }); console.log(7); // 同步任务:3 1 2 5 6 7 // 微任务:4
let res = function () { console.log(1); return new Promise((resolve) => { setTimeout(() => { new Promise((resolve) => { console.log(2); setTimeout(() => { console.log(3); }, 0); }); }, 0); resolve(5); }); } new Promise(async () => { setTimeout(() => { Promise.resolve().then(() => { console.log(4); }); }, 0); let test = await res(); console.log(test); }); setTimeout(() => { console.log(6); }, 0); new Promise(() => { setTimeout(() => { console.log(7); }, 0); }); console.log(8); // 1 8 // 5 // 4 // 2 // 6 // 7 // 3

案例5

const oBtn = document.getElementById('btn'); oBtn.addEventListener('click', () => { console.log('1'); Promise.resolve('m1').then(str => { console.log(str); }); }, false); oBtn.addEventListener('click', () => { console.log('2'); Promise.resolve('m2').then(str => { console.log(str); }); }); oBtn.click(); // 1 2 // m1 m2
const handler1 = () => { console.log('1'); Promise.resolve('m1').then(str => { console.log(str); }); } const handler2 = () => { console.log('2'); Promise.resolve('m2').then(str => { console.log(str); }); } handler1(); handler2(); // 1 2 // m1 m2
const oBtn = document.getElementById('btn'); oBtn.addEventListener('click', () => { console.log('1'); Promise.resolve('m1').then(str => { console.log(str); }); }, false); oBtn.addEventListener('click', () => { console.log('2'); Promise.resolve('m2').then(str => { console.log(str); }); }); // 特殊情况 ☆ // 用户点击按钮触发会执行两次事件循环 // JS 程序触发只会执行一次事件循环,相当于同步执行两个回调函数,添加微任务 // 1 m1 // 2 m2 // 执行栈 // script // addEvent1 cb -> 1 // m1Promise.then cb -> m1 // addEvent2 cb -> 2 // m2Promise.then cb -> m2 // 宏任务 // addEvent1 // addEvent1 cb // addEvent2 // addEvent2 cb // 微任务 // m1Promise // m1Promise.then cb // m2Promise // m2Promise.then cb
const oBtn = document.getElementById('btn'); oBtn.addEventListener('click', () => { setTimeout(() => { console.log('1'); }, 0); Promise.resolve('m1').then(str => { console.log(str); }); }, false); oBtn.addEventListener('click', () => { setTimeout(() => { console.log('2'); }, 0); Promise.resolve('m2').then(str => { console.log(str); }); }); // m1 m2 // 1 2 // 执行栈 // script // addEvent1 cb => // m1Promise.then cb => m1 // addEvent2 cb // m2Promise.then cb => m2 // setTimeout1 // setTimeout2 // 宏任务 // addEvent1 // addEvent1 cb // addEvent2 // addEvent2 cb // setTimeout1 // setTimeout1 cb // setTimeout2 // setTimeout2 cb // 微任务 // m1Promise // m1Promise.then cb // m2Promise // m2Promise.then cb

案例6

console.log('start'); const interval = setInterval(() => { console.log('setInterval'); }, 0); // setTimout1 setTimeout(() => { console.log('setTimeout1'); Promise.resolve() // promise3 .then(() => { console.log('promise 3'); }) // promise4 .then(() => { console.log('promise 4'); }) // promise5 .then(() => { // setTimout2 setTimeout(() => { console.log('setTimeout2'); // promise6 Promise.resolve() .then(() => { console.log('promise 5'); }) // promise7 .then(() => { console.log('promise 6'); }) // promise8 .then(() => { clearInterval(interval); }) }, 0); }) }, 0); Promise.resolve() // promise1 .then(() => { console.log('promise 1'); }) // promise2 .then(() => { console.log('promise 2'); }); // 执行栈 // script // start // promise1 // promise2 // setInterval cb => setInterval // setTimeout1 cb => setTimeout1 // promise3 cb => promise3 // promise4 cb => promise4 // setInterval cb => setInterval // setTimeout2 cb => setTimeout2 // promise6 cb => promise5 // promise7 cb => promise7 // promise8 cb => cleartInterval // 宏任务 // setInterval // setInterval cb // setTimeout1 // setTimeout1 cb // setInterval // setInterval cb // setTimeout2 // setTimeout2 cb // setInterval // setInterval cb // // 微任务 // promise1 // promise1.then cb // promise2 // promise2.then cb // promise3 // promise3.then cb // promise4 // promise4.then cb // promise5 // promise5.then cb // promise6 // promise6.then cb // promise7 // promise7.then cb // promise8 // promise8.then cb

案例7

setTimeout(() => { console.log('setTimeout1'); setTimeout(() => { console.log('setTimeout3'); }, 1000); Promise.resolve().then(() => { console.log('then3'); }); }, 1000); Promise.resolve().then(() => { console.log('then1'); console.log('then4'); Promise.resolve().then(() => console.log('then6')); }); Promise.resolve().then(() => { console.log('then2'); console.log('then5'); setTimeout(() => { console.log('setTimeout2'); }, 1000); }); // then1 then4 then2 then5 then6 本轮循环产生的微任务都会在本次循环清空 // setTimeout1 then3 // setTimeout2 // setTimeout3

案例8

setTimeout(() => { console.log(1); }, 0); new Promise((resolve) => { console.log(2); resolve(); }).then(() => { console.log(3); }).then(() => { console.log(4); }); console.log(6); // new Promise 内部的代码相当于同步执行,Promise.then 才会产生微任务 // 2 6 3 4 // 1

案例9

console.log('1'); setTimeout(() => { console.log('2'); new Promise((resolve) => { console.log('3'); resolve(); }).then(() => { console.log('4'); }); }); new Promise((resolve) => { console.log('5'); resolve(); }).then(() => { console.log('6'); }) setTimeout(() => { console.log('7'); }); setTimeout(() => { console.log('8'); new Promise((resolve) => { console.log('9'); resolve(); }).then(() => { console.log('10'); }); }); new Promise((resolve) => { console.log('11'); resolve(); }).then(() => { console.log('12'); }); console.log('13'); // 1 5 11 13 6 12 // 2 3 4 // 7 // 8 9 10

案例10

async function async1 () { console.log('a1 start'); await async2(); console.log('a1 end'); } async function async2 () { console.log('async2'); } console.log('script start'); setTimeout(() => { console.log('setTimeout'); }, 0); async1(); new Promise((resolve) => { console.log('promise1'); resolve(); }).then(() => { console.log('promise2'); }); console.log('script end'); // script start、a1 start、async2、promise1、script end // a1 end、promise2 // setTimeout
async function async1 () { console.log('a1 start'); await async2(); console.log('a1 end'); } async function async2 () { new Promise((resolve) => { console.log('promise1'); resolve(); }).then(() => { console.log('promise2'); }); } console.log('script start'); setTimeout(() => { console.log('setTimeout'); }, 0); async1(); new Promise((resolve) => { console.log('promise3'); resolve(); }).then(() => { console.log('promise4'); }); console.log('script end'); // script start、a1 start、promise1、promise3、script end // promise2、a1 end、promise4 // 微任务的添加顺序和执行顺序有关,a1 end 添加顺序晚于 promise2 // setTimeout
async function async1 () { console.log('a1 start'); await async2(); /** * awaitPromsie * async2().then(() => { * setTimeout(() => { * console.log('setTimeout1'); * }) * }) */ setTimeout(() => { console.log('setTimeout1'); }) } async function async2 () { setTimeout(() => { console.log('setTimeout2'); }, 0); } console.log('script start'); setTimeout(() => { console.log('setTimeout3'); }, 0); async1(); new Promise((resolve) => { console.log('promise1'); resolve(); }).then(() => { console.log('promise2'); }); console.log('script end'); // 执行栈 // script start // a1 start // promise1 // script end // // awaitPromsie.then cb // promsie2.then -> promise2 // setTimeout3 // setTimeout2 // setTimeout1 // 宏任务 // setTimeout3 // setTimeout3 cb // setTimeout2 // setTimeout2 cb // setTimeout1 // setTimeout1 cb // // 微任务 // awaitPromsie // promsie2 // promsie2.then cb

案例11

var promise = new Promise((resolve) => { console.log(1); resolve(); }); setTimeout(() => { console.log(2); }); promise.then(() => { console.log(3); }); var promise2 = getPromise(); async function getPromise () { console.log(5); await promise; console.log(6); } console.log(8); // 1 5 8 // promise 变量是已经执行过的变量,不会反复执行 // 3 6 // 2

案例12

链式操作

const LazyMan = function (name) { console.log(`Hi i am ${ name }`); function _eat (food) { console.log(`I am eating ${ food }`); } const callbacks = []; class F { sleep (timeout) { setTimeout(() => { console.log(`等待了${ timeout }秒...`); callbacks.forEach(cb => cb()); }, timeout * 1000); return this; } eat (food) { callbacks.push(_eat.bind(null, food)); return this; } } return new F(); } LazyMan('Tony').sleep(5).eat('lunch').eat('fish'); // Hi i am Tony // 等待了5秒... // I am eating lunch // I am eating finsh

宏任务与微任务

新版 nodejs,11 及以上版本与浏览器事件环的执行结果是一致的,但也存在很多不同点。

宏任务 MacroTask

  • script / ui render
  • setTimeout
  • setInternal
  • setImmediate
    • IE 新版本浏览器 edge
    • NodeJS 0.10+ 版本
  • messageChannel / requestAnimationFrame
  • 用户交互事件
  • ajax

微任务 MicroTask

  • promise.then
  • mutationObserver
  • process.nextTick
    • node 环境下的方法
    • vuejs 存在 $nextTick 方法,存在其内部实现

vue,nextTick 实现

/* @flow */ /* globals MutationObserver */ import { noop } from 'shared/util' import { handleError } from './error' import { isIE, isIOS, isNative } from './env' export let isUsingMicroTask = false // 是否使用微任务 const callbacks = [] let pending = false function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } // Here we have async deferring wrappers using microtasks. // In 2.5 we used (macro) tasks (in combination with microtasks). // However, it has subtle problems when state is changed right before repaint // (e.g. #6813, out-in transitions). // Also, using (macro) tasks in event handler would cause some weird behaviors // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109). // So we now use microtasks everywhere, again. // A major drawback of this tradeoff is that there are some scenarios // where microtasks have too high a priority and fire in between supposedly // sequential events (e.g. #4521, #6690, which have workarounds) // or even between bubbling of the same event (#6566). let timerFunc // The nextTick behavior leverages the microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, however it is seriously bugged in // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It // completely stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore next, $flow-disable-line */ if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // In problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // Use MutationObserver where native Promise is not available, // e.g. PhantomJS, iOS7, Android 4.4 // (#6466 MutationObserver is unreliable in IE11) let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // Fallback to setImmediate. // Technically it leverages the (macro) task queue, // but it is still a better choice than setTimeout. timerFunc = () => { setImmediate(flushCallbacks) } } else { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) } } export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }

Promise > MutationObserver > setImmediate > setTimeout

vue 1.0 版本还用到 MessageChannel,当前版本使用 MutationObsever。

setImmediate 和 setTimeout

setImmediate

该特性是非标准的,尽量不要在生产环境使用它。

该方法用来把一些需要长时间运行的操作放到一个回调函数中,在浏览器完成后面的其他语句后,就立刻执行这个回调函数。

该方法可能不会成为标准,目前只有最新版本的 Internet Explorer 和 Node.js 0.10+ 实现了该方法。

setImmediate(() => { console.log('over'); console.log('over'); console.log('over'); console.log('over'); }); console.log('starting'); // starting // over // over // over
const immediateId = setImmediate(func, [ param1, param2, ... ]); // Immediate { // _idleNext: null, // _idlePrev: null, // _onImmediate: [Function (anonymous)], // _argv: undefined, // _destroyed: false, // [Symbol(refed)]: true, // [Symbol(asyncId)]: 2, // [Symbol(triggerId)]: 1 // }

immediateId 是 setImmediate 方法设置的唯一 ID,可以作为 window.clearImmediate 的参数。

该方法可以替代 setTimeout(fn, 0) 执行繁重的操作。

setTimeout

const timeoutId = setTimeout(function[, delay, arg1, arg2, ...]);

delay 延迟的毫秒数,函数的调用会在延迟之后发生。默认为 0,尽快执行。

setTimeout 实际延迟比设定值更久,最小延迟 >= 4ms。浏览器中,setTimeout / setInternal 的最小间隔是 4 ms。
通常是由于函数嵌套导致,或者是由于已经执行的 setInternal 的回调函数阻塞导致。

未被激活的 tabs 的定时最小延迟 >= 1000 ms。1000 ms 的间隔值可以通过 dom.min_background_timeout_value 改变。

Firefox 从 version 5 开始采用这种机制,chrome 从 version 11 开始采用。

Android 版本的 firefox 对未被激活的后台 tabs 使用 15 min 的最小延迟间隔时间,并且这些 tabs 也能完全不被加载。

当 Web Audio API AudioContext 正在播放音频时,Firefox 50 不会再限制后台 tabs 的加载。后续的 Firefox 51 版本,即使在没有音频播放的时候,也不再限制后台 tabs 的加载。

两者对比

https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout

setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); }); // setImmediate // setTimeout
setImmediate(() => { console.log('setImmediate'); }); setTimeout(() => { console.log('setTimeout'); }, 0); // setTimeout // setImmediate

setImmediate 和 setTimeout 是相似的

  • setImmediate 在本次的 poll 阶段完成时执行
  • setTimeout 在设置时间结束时执行

它们的执行顺序依赖于 context,如果它们都存在于主模块,它们的执行时机被进程性能影响。
所以上述代码的执行顺序其实是不能够被确定的,它们会被进程性能所影响。

setImmediate 相对于 setTimeout 的优势是,如果当前执行在 I/O 循环,setImmediate 会在所有 timers 执行之前执行。

const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });

此时执行顺序是固定的,setImmediate 会优先于 setTimeout。

MessageChannel 与 postMessage

Channel Messaging API 的 MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据。

此特性在 Web Worker 中可用。

const channel = new MessageChannel(); console.log(channel); // MessageChannel { // port1: MessagePort [EventTarget] { // active: true, // refed: false, // [Symbol(kEvents)]: SafeMap(2) [Map] { // 'newListener' => [Object], // 'removeListener' => [Object] // }, // [Symbol(events.maxEventTargetListeners)]: 10, // [Symbol(events.maxEventTargetListenersWarned)]: false, // [Symbol(kNewListener)]: [Function (anonymous)], // [Symbol(kRemoveListener)]: [Function (anonymous)], // [Symbol(nodejs.internal.kCurrentlyReceivingPorts)]: undefined // }, // port2: MessagePort [EventTarget] { // active: true, // refed: false, // [Symbol(kEvents)]: SafeMap(2) [Map] { // 'newListener' => [Object], // 'removeListener' => [Object] // }, // [Symbol(events.maxEventTargetListeners)]: 10, // [Symbol(events.maxEventTargetListenersWarned)]: false, // [Symbol(kNewListener)]: [Function (anonymous)], // [Symbol(kRemoveListener)]: [Function (anonymous)], // [Symbol(nodejs.internal.kCurrentlyReceivingPorts)]: undefined // } // } const { port1, port2 } = channel; console.log(port1); console.log(port2); // MessagePort {onmessage: null, onmessageerror: null} // MessagePort {onmessage: null, onmessageerror: null}

MessageChannel 继承于 MessagePort 构造函数。MessagePort 原型上存在 postMessage 方法。

<div id="J-msg1">No Message</div> <button id="J-btn1">Send Message</button> <div id="J-msg2">No Message</div> <button id="J-btn2">Send Message</button> <script> const oMsg1 = document.querySelector('#J-msg1'), oMsg2 = document.querySelector('#J-msg2'), oBtn1 = document.querySelector('#J-btn1'), oBtn2 = document.querySelector('#J-btn2'); const channel = new MessageChannel(); const { port1, port2 } = channel; oBtn1.addEventListener('click', sendMessage1, false); oBtn2.addEventListener('click', sendMessage2, false); port1.onmessage = getMessage1; port2.onmessage = getMessage2; function sendMessage1 () { port1.postMessage('I am PORT-1'); } function sendMessage2 () { port2.postMessage('I am PORT-2'); } function getMessage1 (e) { oMsg1.textContent = e.data; } function getMessage2 (e) { oMsg2.textContent = e.data; } </script>

模块化案例

// demo.js const channel = new MessageChannel(); const { port1, port2 } = channel; const oTitle = document.querySelector('h1'); port1.onmessage = (e) => { oTitle.textContent = e.data; console.log('dom rendering'); port1.postMessage('dom rendered'); } export default port2;
// index.js import port2 from './demo.js'; ;(() => { port2.postMessage('This is new title'); port2.onmessage = (e) => { console.log(e.data); } })();
<h1>Title</h1> <script type="module" src="./index.js"></script>

requestAnimationFrame 与 setInterval

window.requestAnimationFrame() 要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

如果你想在浏览器下一次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 window.requestAnimationFrame()。

<div id="box" style="width: 100px; height: 100px; background-color: red;"></div> <script type="module" src="./index.js"></script>
// index.js const oElem = document.getElementById('box'); let start; function step (timestamp) { if (start === undefined) start = timestamp; const elapsed = timestamp - start; oElem.style.transform = `translateX(${ Math.min(0.1 * elapsed, 200) }px)`; if (elapsed < 2000) { window.requestAnimationFrame(step); } } window.requestAnimationFrame(step);

回调函数会被传入 DOMHighResTimeStamp 参数,DOMHighResTimeStamp 指示当前 requestAnimationFrame() 排序的回调函数被触发的时间。在同一帧中的多个回调函数,它们每一个都会接收到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为 1ms。

// setTimeout 版本实现 const oElem = document.getElementById('box'); let px = 0; let t = null function step () { px++; oElem.style.transform = `translateX(${ px }px)`; if (px >= 200) { clearInterval(t); } } t = setInterval(step, 1000 / 60);

requestAnimationFrame VS setInterval

  • 布局绘制逻辑不同
    • setInterval:回调逻辑存在多次 DOM 操作,就会进行多次计算、绘制
    • requestAnimationFrame:把所有DOM 操作集中起来,一次性进行统一计算、统一绘制,性能较好
  • 窗口最小化时,运行情况不同
    • setInterval:一直执行回调函数
    • requestAnimationFrame:最小化时,暂停程序执行;页面打开时,从暂停的位置重新开始
  • 是否导致无意义的回调执行,重绘重排(计时间隔小于刷新率)
    • setInterval(step, 0):导致多次无意义的回调执行,
    • requestAnimationFrame:只会在下次重绘时执行

MutationObserver 与 nextTick

MutationObserver 接口提供了监视对 DOM 树所做更改的能力。

它被设计为旧的 MutationEvents 功能的替代品,该功能是 DOM3 Events 规范的一部分。

  • MutationObserver 构造函数
    • 创建并返回一个新的 MutationObserver,它会在指定的 DOM 发生变化时被调用。
  • disconnect
    • 阻止 MutationObserver 实例继续接收通知,直到再次调用 observe 方法,该观察者对象包含的回调函数都不会再被调用。
  • observe
    • 配置 MutationObserver 在 DOM 更改匹配给定选项时,通过其回调函数开始接收通知。
  • takeRecords
    • 从 MutationObserver 的通知队列中删除所有待处理的通知,并将它们返回到 MutationRecord 对象的新 Array 中。

mutate v,mutation 变化。
observe v,observer n. 观察者。

object => ob => 相反的,对面的
oppsite => 相反的 => ob op => 对面的,相反的
ject => 物体,object => 对面的物体 => 对象,物件
ob serve/keep=> 看对面的东西,observe 观察

MutationObserver 使用案例

<div id="app"> <h1>Loading...</h1> </div> <script type="module" src="./index.js"></script>
function callback (target) { console.log(target); } function cb (mutationList, observer) { mutationList.forEach(mutation => { callback(mutation.target); }); } const oTarget = document.getElementById('app'); const oTitle = oTarget.querySelector('h1'); const observer = new MutationObserver(cb); observer.observe(oTarget, { attributes: true, // 监视元素属性变更 childList: true, // 监视目标节点添加或删新的子节点 subtree: true, // 将监视范围扩展至目标节点整个节点树中的所有节点 }); oTitle.innerText = 'This is a title'; oTitle.className = 'title'; const oParent = document.createElement('p'); oParent.innerText = 'This is content'; oTarget.appendChild(oParent);
Promise.resolve().then(() => { console.log('Promise'); }); setTimeout(() => { console.log('setTimeout'); }, 0); process.nextTick(() => { console.log('nextTick'); }); // nextTick // Promise // setTimeout
process.nextTick(() => { console.log('nextTick1'); }); Promise.resolve().then(() => { console.log('Promise'); }); process.nextTick(() => { console.log('nextTick2'); }); setTimeout(() => { console.log('setTimeout'); }, 0); process.nextTick(() => { console.log('nextTick3'); }); // nextTick1 // nextTick2 // nextTick3 // Promise // setTimeout

node 中的 nextTick 作为微任务优先于 promise 执行。

process.nextTick 同一阶段立即执行,微任务。setImmediate 在一个 event 完成或者下一个 tick 执行。

nextTickQueue 在当前事件环每一个步骤结束都会执行一次。