Javascript异步

1,078 阅读7分钟

浏览器的进程与线程

  • 浏览器是多进程的,比如打开多个窗口可能就对应着多个进程,这样可以确保页面之间相互没有影响,一个页面卡死也不会影响其他的页面
  • 对于浏览器进程来说,它是多线程的:GUI渲染线程、JS引擎线程、事件触发线程、定时触发器线程、异步http请求线程

1、GUI渲染线程

负责渲染浏览器页面,解析html+css,构建DOM树,进行页面的布局和绘制操作,同事页面需要重绘或者印发回流时,都是该线程负责执行

2、JS引擎线程

负责解析和运行JS脚本,一个页面中永远都只有一个JS线程来负责运行JS程序,这就是我们常说的JS单线程

注意:JS引擎线程和GUI渲染线程永远都是互斥的,所以当我们的JS脚本运行时间过长时,或者有同步请求一直没返回时,页面的渲染操作就会阻塞,就是我们常说的卡死了

3、事件触发线程

接受浏览器里面的操作事件响应。如在监听到鼠标、键盘等事件的时候, 如果有事件句柄函数,就将对应的任务压入队列

4、定时触发器线程

定时触发器线程 浏览器模型定时计数器并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 它必须依赖外部来计时并触发定时

5、异步http请求线程

异步http请求线程 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行

JS单线程

因为只有JS引擎线程负责处理JS脚本程序,所以说JS是单线程的。可以理解的是js当初设计成单线程语言的原因是因为js需要操作dom,如果多线程执行的话会引入很多复杂的情况,比如一个线程删除dom,一个线程添加dom,浏览器就没法处理了。虽然现在js支持webworker多线线程了,但是新增的线程完全在主线程的控制下,为的是处理大量耗时计算用的,不能处理DOM,所以js本质上来说还是单线程的。

1、同步异步

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行

2、任务队列

  • 任务队列就是用来存放一个个带执行的异步操作的队列,是一个先进先出的数据结构,排在前面的事件,优先被主线程读取
  • 每完成一个任务,都会去检查任务队列中还有没有任务需要执行,直到任务队列被清空,这个操作叫做事件轮询(Event Loop)
  • Event Loop只是负责告诉还有没有任务待执行,该执行哪些任务,真正的逻辑还是在主线程逐段逐段去中执行的
  • 任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中

在ES6中又将任务队列分为宏任务队列和微任务队列

宏任务队列(macrotask queue)

macrotask是由宿主环境分发的异步任务,事件轮询的时候总是一个一个任务队列去查看执行的。一次宏任务结束之后总会去检查还有没有宏任务需要处理。

微任务队列(microtask queue)

是由js引擎分发的任务,总是添加到当前宏任务末尾执行。如果在处理microtask期间,如果有新添加的microtasks,会被添加到当前微任务队列的末尾执行,在当前的微任务队列没有被清空时,是不会执行下一个宏任务的

宏任务清单和微任务清单

宏任务:

(1)定时器(setTimeout、setInterval)、异步请求(ajax/fetch/axios)、DOM事件(click)

它们分属于三个线程来分别处理

  • 对于定时器,定时触发器线程在接收到代码时就开始计时,时间到了将回调函数扔进队列
  • 对于异步请求,异步http请求线程立即发起http请求,请求成功后将回调函数扔进队列
  • 对于DOM事件,事件触发线程会先监听dom,直到dom被点击了,才将回调函数扔进队列

(2)I/O(比如node中的fs.readFile())、requestAnimationFrame

  • requestAnimationFrame 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。
const element = document.getElementById('some-element-you-want-to-animate'); 
let start;

function step(timestamp) {
  if (start === undefined)
    start = timestamp;
  const elapsed = timestamp - start;

  // `Math.min()` is used here to make sure that the element stops at exactly 200px.
  element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';

  if (elapsed < 2000) { // Stop the animation after 2 seconds
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);

微任务:process.nextTick(node环境)、MutationObserver、Promise.then catch finally

  • process.nextTick用于node环境
  • MutationObserver接口提供了监视对DOM树所做更改的能力,它是一个构造函数
// 选择需要观察变动的节点
const targetNode = document.getElementById('some-id');

// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };

// 当观察到变动时执行的回调函数
const callback = function(mutationsList, observer) {
    // Use traditional 'for loops' for IE 11
    for(let mutation of mutationsList) {
        if (mutation.type === 'childList') {
            console.log('A child node has been added or removed.');
        }
        else if (mutation.type === 'attributes') {
            console.log('The ' + mutation.attributeName + ' attribute was modified.');
        }
    }
};

// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);

// 以上述配置开始观察目标节点
observer.observe(targetNode, config);

// 之后,可停止观察
observer.disconnect();
  • Promise.then catch finally

new Promise里面的代码是在主线程中直接执行的,只有then/catch/finally会被加入微任务队列,它在什么时候执行,取决于他被哪个宏任务resolve或者reject

JS异步处理方式

1、callback

将函数作为参数传递,在异步任务执行环境中调用,但是多层嵌套容易引起回调地狱

2、Promise

使用Promise可以解决回调地狱的问题,是一种以广度换深度的解决方式

可以参考Promise/A+规范来了解Promise的解析过程

  • Promise.resolve() 可以有四种传参方式
/* 跟 Promise 对象 */
Promise.resolve(Promise.resolve(1))
// Promise {state: "resolved", data: 1, callbackQueue: Array(0)}

/* 跟 thenable 对象 */
var thenable = {
  then: function(resolve, reject) {
    resolve(1)
  }
}

Promise.resolve(thenable)
// Promise {state: "resolved", data: 1, callbackQueue: Array(0)}

/* 普通参数 */
Promise.resolve(1)
// Promise {state: "resolved", data: 1, callbackQueue: Array(0)}

/* 不跟参数 */
Promise.resolve()
// Promise {state: "resolved", data: undefined, callbackQueue: Array(0)}
  • Promise.reject() 原封不动地返回参数值
  • Promise.all(arr) 在参数数组中所有元素都变为决定态后,然后才返回新的 promise
const p1 = request(`http://some.url.1`)
const p2 = request(`http://some.url.2`)

Promise.all([p1, p2])
  .then((datas) => { // 此处 datas 为调用 p1, p2 后的结果的数组
    return request(`http://some.url.3?a=${datas[0]}&b=${datas[1]}`)
  })
  .then((data) => {
    console.log(msg)
  })
  • Promise.race 只要参数数组有一个元素变为决定态,便返回新的 promise
// race 译为竞争,同样是请求两个 url,当且仅当一个请求返还结果后,就请求第三个 url
const p1 = request(`http://some.url.1`)
const p2 = request(`http://some.url.2`)

Promise.race([p1, p2])
  .then((data) => { // 此处 data 取调用 p1, p2 后优先返回的结果
    return request(`http://some.url.3?value=${data}`)
  })
  .then((data) => {
    console.log(data)
  })
  • Promise.wrap(fn) 将一个普通函数promise化,使用到了闭包
function foo(a, b, cb) {
  ajax(
    `http://some.url?a=${a}&b=${b}`,
    cb
  )
}

const promiseFoo = Promise.wrap(foo)

promiseFoo(1, 2)
  .then((data) => {
    console.log(data)
  })
  .catch((err) => {
    console.log(err)
  })
  • then/catch/done
Promise.resolve(1)
  .then((data) => {console.log(data)}, (err) => {console.log(err)}) // 链式调用,可以传一个参数(推荐),也可以传两个参数
  .catch((err) => {console.log(err)}) // 捕获链式调用中抛出的错误 || 捕获变为失败态的值
  .done()                             // 能捕获前面链式调用的错误(包括 catch 中),可以传两个参数也可不传

3、async/await

假设一个业务需要分步完成,每个步骤都是异步的,而且依赖上一步的执行结果,甚至依赖之前每一步的结果,就可以使用Async Await来完成 可以解决promise无限then的问题,还可以实现try/catch

function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}
function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}
function step2(m, n) {
    console.log(`step2 with ${m} and ${n}`);
    return takeLongTime(m + n);
}
function step3(k, m, n) {
    try{
        let content1 = await read('./2.promise/100.txt', 'utf8');
        let content2 = await read(content1, 'utf8');
        return content2;
    } catch(e) { // 如果出错会catch
        console.log('err', e)
    }
}

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt().then(function(data){
    console.log('data',data);
},function(err){
    console.log('err1',err);
})

如果用promise来实现

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

3、Generator/co

直接参考 Generator用法详解+co

感谢 @Jiasm / @WebJ2EE / @牧云云 / @WZZ41998 提供的优质文章

JS异步开发总结

微任务、宏任务与Event-Loop

JS 异步系列 —— Promise 札记

Async/Await