浏览器的事件环
进程与线程
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 在当前事件环每一个步骤结束都会执行一次。