Javascript是一种同步的、阻塞的、单线程的脚本语言,运行时只能同一时间运行一个任务。在web浏览器中定义了函数和API,运行代码当某些事件发生时不同步的执行,而是采用异步的调用函数,意味着代码可以同时做几件事,不会阻塞主线程
在js中通常会遇到两种异步编程的风格: callbacks, 和基于微任务或者宏任务来执行。
宏任务的也是同步任务,但它是在下一轮事件循环中执行
异步的callbaks
异步的callback就是函数,只是当成参数传递给那些在后台执行的其他函数,当后台运行的代码结束,就调用callback函数,通知工作已经完成
document.createElement("div").addEventListener("click", () => {
console.log("callbak");
});
比如callback就是addEventListener()
的第二个参数
当回调函数作为要给参数传递给另一个函数时,仅仅是吧回调函数定义为参数传递过去,函数并没有执行,回调函数会在包含它的函数的某一给地方异步的执行。 不是所有的回调函数都是异步的,比如f``rEach
Promise
promise 对象用于标识一个异步操作的最终完成,由它产生的结果是异步进行的
promise这样的异步回调函数会被放入事件队列中,事件队列在主线程完成处理以后运行,这样它不会阻止后续的js代码执行。
promise是专门为异步操作而设计的,与callback相比有以下优点:
- 可以使用多个Promise对象的实例
then()
方法,将多个异步操作链接在一起,并将其中一个操作的结果作为参数传递给下一个操作 - promise总是严格按照投放至事件队列中的顺序被调用
- 错误处理要好的多,所有的错误都会由第一个
.catch()
函数处理, 而不像回调函数一样一层一个
promise对象必然处于以下几种状态之一:
- pending - 初始状态
- fulfilled - 意味着操作完成
- rejected - 操作失败
promise的回调函数加入到队列以后,会在由pending状态编程其他两种任一状态以后按照顺序执行
promise 的实例方法每次都会返回一个新的promise对象,会将上游的结果流入下一次的回调函数中
对于promise来说,它只是提供了一种创建微任务对象的方法,在js中创建微任务目前有诸方式
Generator
异步操作有很多种,Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。生成器函数可以在执行时暂停,后面又可以从暂停的地方继续执行
Generator函数不能作为构造函数来使用
函数开始执行,遇见第一个yield时会停下,暂时交出函数的执行权,等待第一个next方法的调用,才会继续向下执行
返回的Generator对象包含三个方法:next()
- 方法返回一个由yield表达式生成的值,是{value : any, done: bool}
对象,value标识本次yield表达式的返回值,done属性表示生成器后续是否还有yield语句,即生成器函数是否已经执行完毕并返回
在调用next方法时,如果传递了参数,那么这个参数会给上一条执行的yield语句左边的变量, 第一次的next方法
return()
- 方法返回给定的值并结束生成器, return以后,函数结束,next为undefined
throw()
- 用来向生成器抛出异常,并恢复生成器的执行. throw的调用,比如由yield捕获,如果没有错误会抛出
function* generator() {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
}
let g = generator();
try {
g.next()
g.throw('错误一');
g.throw("错误二");
} catch (e) {
console.log('外部捕获', e);
}
Async
async函数是在ECMAScript2017版本中加入的,是基于promise的语法糖,使异步代码,在async函数中呈现出同步的味道
简单使用:
async function f() {
return '成功';
}
f()
.then(v => console.log(v)) // 成功
async函数默认会返回一个promise, 如果有其他的操作需要等待async的函数的返回,要么存在promise.then
回调,要么继续使用async函数, 所以async函数存在传染性
对于async函数的错误处理可以使用try...catch
块包裹, 或者在外层使用catch()
方法
async function fn() {
try {
throw Error("NMLGB")
}catch(exx) {
// 错误
}
}
// 或者
fn() .catch(exx => exx)
async 函数可以让代码看起来是同步的,在某种程度上,也使得它的行为更加的同步。await关键字会阻塞其后的代码,直到promise完成。 在此期间,代码将被阻塞。
所以代码中出现了大量的await语句promise相继发生而变慢,每个await都会等待前一个完成。这种操作非常耗时,可以考虑让多个promise同时执行,仅选择需要的promise进行等待
基于generator函数实现一个简单的async
function api() {
return new Promise(resolve => {
setTimeout(() => resolve({ data: "xxx" }), 3000)
});
}
function* asyncFunc() {
let r = yield api();
console.log(r);
}
let g = asyncFunc();
function run(g, ...args) {
let { value, done } = g.next(...args);
if (done) {
return;
}
value.then(r => {
run2(g, r);
})
}
run(g);
queueMicrotask
一个微任务就是一个简短的函数,在创建该函数的函数执行以后,并且在执行栈为空时, 控制劝尚未返回给usr agent用来驱动脚本执行环境的事件循环之前,微任务才会执行。该函数时除promise以外另一个创建异步的API
queueMicrotask(callback)
方法必须将微任务排队以调用回调,如果回调引发异常,则报告异常。
queueMicrotask()
方法允许自定义在microtask队列中添加一个回调, 这个回调会在js执行上下文堆栈下一个为空时执行,发生在所有当执行的同步js运行完成以后。注意:这个方法执行以后并不会将控制权交给事件循环
调度大量微任务运行大量同步代码具有相同的性能缺点。两者都会阻止浏览器执行自己的工作。
很多情况下,requestAnimationFrame和requestIdleCallback是其他更好的选择 requestAnimationFrame目的就是在下一个渲染周期之前运行代码
🌰:
const cache = new Map();
const dispatch = {};
function getData(url) {
if (cache.has(url) {
return dispatch(url, cache.get(url))
}
return fetch(url).then(r => {
cache.set(url, r);
dispatch(url, r);
return r;
})
}
这种就造成函数的返回值不一致为后续的编码代码不同的局势,改变:
const cache = new Map();
const dispatch = {}; // 表示发布订阅
function getData(url) {
if (cache.has(url) {
return queueMicrotask(() => dispatch(url, cache.get(url)));
}
return fetch(url).then(r => {
cache.set(url, r);
dispatch(url, r);
return r;
})
}
这个时候,函数结果的触发时机是一直的, 但同样的也可以使用promise来替代,但是promise也是需要为一个resolve创建对象
像一些批量操作也可以使用
MutationObserver
MutationObsereer
接口提供了监视堆DOM数所做更改的能力,被设计为就的Mutaitaion Events功能的替代品,是DOM3 Events规范的一部分
构造函数时接口的一部分,用来创建一个新的观察器,会在触发指定DOM事件时,调用指定的回调函数,对dom的观察不会立即启动,必须调用observe()
实例方法,要监听那一部分的DOM以及要响应那些更改
如果被观察的元素从DOM树中删除,然后被回收掉, 此MutationObserver
将同样被删除
disconnect()
- 方法停止观察变动,可以再次调用observe方法来重新开启观察
takeRecoreds()
- 方法返回已检测到但尚未由观察者的回调函数处理的所有匹配DOM更改的列表,使变更队列保持为空。 此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改
observe
- targetNode - 一个需要观察的element节点
options -
- childList - bool - 观察目标的子节点的变化,是否有添加或者删除
- attributes - bool - 观察属性变化
- subtree - bool - 观察后代节点,默认为false ```javascript let click = document.querySelector(“.click”);
let observer = new MutationObserver(recordList => { console.log(recordList); });
observer.observe(click, { attributes: true, childList:true, subtree:true });
function update(id) { click.firstElementChild.setAttribute(“id”, id ?? “nmlgb”) } ```
API被触发时调用回调函数的时机也是异步的
update();
let i = 0;
while(i<10) {
console.log(i++);
}
// out
0-9 先输出后才是回调函数执行
IntersectionObserver
API用来观察一个元素是否可见, 会触发两次在第一次可见时,和完全离开时
IntersectionObserver 接口可用于观察相交根与一个或多个目标元素相交处的变化。例子
方法也是异步的,不会随着目标元素的滚动同步触发。规范中写明, 该API,应该采用requestIdleCallback()
实现,所有它的优先级会非常的低
requestAnimationFrame
方法告诉浏览器希望执行一个动画,并且要求浏览器在下次重绘之前执行回调函数来更新动画
这个函数的执行次数通常时60秒一次,和浏览器屏幕刷新次数相匹配,为了提高性能和电池寿命,在后台隐藏的标签也或者隐藏的ifreme里时, 函数会被暂停。(也是没有被react使用的一个原因)
回调函数会被传输一个DOMHightResTimeStamp
参数,指示当前被raf排序的回调函数被触发的是时间,在同一个帧的多个回调函数,它们每一个都会接受一个相同的时间戳
requestAnimationFrame(now => {
// now 和performance.now() 的返回值相同
console.log(1);
})
函数返回一个整形数字,为请求ID, 可以使用cancelAnimationFrame()
取消回调函数
requestIdleCallback
函数可以插入一个回调函数,回调函数会在浏览器空闲时期被调用,这使开发者能够在主事件循环上执行后台和优先级工作,而不影响延迟关键事件(比如:动画和输入), 函数一般按先进先出的顺序执行。
options:timeout
- 超时时间, 如果指定了执行超时事件timeout
, 则有可能为了在超时前去执行回调函数而打乱先进先执行的顺序
在空闲回调函数中调用requestIdleCallback()
,以便在下一次通过事件循环之前调度另一个回调。
requestIdleCallback(() => {
console.log("idle callbcak");
});
// 如果被添加,回调会在之以下代码之后执行
let i = 0;
while (i < 10) {
console.log(i++);
}
执行时机
function main() {
queueMicrotask(() => {
console.log("update start");
});
update();
queueMicrotask(() => {
console.log("update end");
});
setTimeout(() => {
console.log("timeout");
});
requestAnimationFrame(() => {
console.log("requestAnimationFrame");
});
requestIdleCallback(() => {
console.log("requestIdleCallback");
});
}
update start
MutationObserver
update end
requestAnimationFrame
requestIdleCallback
timeout
update start
MutationObserver
update end
requestAnimationFrame
timeout
requestIdleCallback
正常情况下idle的执行时机是在统一轮事件循环中,在更新完UI后会被调用。但是不正常的情况就会在下一轮的时候被调用
raf回调就是在更新UI前被执行, 但是会被getComputedStyle()
扰乱,因为这个函数会优先去计算css样式
const channel = new MessageChannel();
const port1 = channel.port1;
channel.port2.addEventListener("message", () => {
console.log("channel");
})
channel.port2.start();
还有一个API就是MessageChannel
, 属于宏任务下一轮循环开始时,在setTimeout之前执行, 比如在main函数中:
function main() {
port1.postMessage("xxx");
queueMicrotask(() => {
console.log("update start");
});
queueMicrotask(() => {
console.log("update end");
});
setTimeout(() => {
console.log("timeout");
});
requestAnimationFrame(() => {
console.log("requestAnimationFrame");
});
requestIdleCallback(() => {
console.log("requestIdleCallback");
});
}
update start
update end
MessageChannel
timeout
requestAnimationFrame
requestIdleCallback
“channel” 在timeout之前执行,“requestAnimationFrame” 不一定在之前还是之后
try…catch()捕获
语句标记要尝试的语句块,并指定一个出现异常时抛出的响应
try{ throw "错误" log("throw >") }catch(er){ cosnole.log(er) }
错误,不会打印”throw>”.因为try块中抛出的信息都会被catch捕获, 后续代码不会执行
- 最好不要嵌套 。因为try…catch是成对出现的。单独出现try是语法错误
function func() {
try {
return 1;
} finally {
return 2;
}
}
let g = func();
g // 2
finally 在和try块使用时,会到最后来执行,不应该存在返回值,避免不可预估的歧义