美文网首页
一道面试题 聊聊js异步

一道面试题 聊聊js异步

作者: 安石0 | 来源:发表于2019-08-02 15:06 被阅读0次

    看一道笔试题(头条)

    async function async1() {
        console.log('async1 start');
        await async2();
        console.log('async1 end');
    }
    async function async2() {
        console.log('async2');
    }
    console.log('script start');
    setTimeout(function() {
        console.log('setTimeout');
    }, 0)
    async1();
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
    console.log('script end');
    

    答案:(浏览器端:chrome 75.0.3770.142)

    /*
    script start
    async1 start
    async2
    promise1
    script end
    async1 end
    promise2
    setTimeout
    */
    

    解释:

    提前说明:
    1 js属于宿主语言,怎么执行是宿主说了算的,每个浏览器对一个特性的执行可能会不相同,浏览器环境和node环境表现可能也会不相同, 本文只讨论浏览器环境。
    2 关于异步任务执行原理(event loop)网上的解释和讨论也是多种多样的。本文从宏任务与微任务的角度进行说明。

    1 宏任务与微任务

    image.png

    Js 中,有两类任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks)。宏任务队列可以有多个,微任务队列只有一个(浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染)。

    宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI 事件.
    (消息队列,添加在执行栈的尾部)
    微任务:process.nextTick, Promise, Object.observer, MutationObserver.
    (作业队列, 优先级高于宏任务)
    Event Loop 会无限循环执行上面3步,这就是Event Loop的主要控制逻辑。其中,第3步(更新UI渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新UI成本大,所以,一般都会比较长的时间间隔,执行一次更新。


    image.png

    看一个例子

    console.log('script start');
    
    // 微任务
    Promise.resolve().then(() => {
        console.log('p 1');
    });
    
    // 宏任务
    setTimeout(() => {
        console.log('setTimeout');
    }, 0);
    
    var s = new Date();
    while(new Date() - s < 50); // 阻塞50ms
    /*
    上面之所以加50ms的阻塞,是因为 setTimeout 的 delayTime 最少是 4ms. 为了避免认为 setTimeout 是因为4ms的延迟而后面才被执行的,我们加了50ms阻塞。
    */
    // 微任务
    Promise.resolve().then(() => {
        console.log('p 2');
    });
    
    console.log('script ent');
    
    
    /*** output ***/
    
    // one macro task
    script start
    script ent
    
    // all micro tasks
    p 1
    p 2
    
    // one macro task again
    setTimeout
    

    2 async/await

    概念: 一句话,async 函数就是 Generator 函数的语法糖。(async 函数是非常新的语法功能,新到都不属于 ES6,而是属于 ES7。目前,它仍处于提案阶段,但是转码器 Babel 和 regenerator 都已经支持,转码后就能使用。)
    2.1 Generator 函数
    概念:生成器对象是由一个 generator function 返回的,并且它符合可迭代协议迭代器协议
    列子:

    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    
    var hw = helloWorldGenerator();
    hw.next()
    // { value: 'hello', done: false }
    
    hw.next()
    // { value: 'world', done: false }
    
    hw.next()
    // { value: 'ending', done: true }
    
    hw.next()
    // { value: undefined, done: true }
    

    看一个例子:
    执行三次next返回什么?

    function* foo(x) {
      var y = 2 * (yield (x + 1));
      var z = yield (y / 3);
      return (x + y + z);
    }
    
    var a = foo(5);
    // 执行这三次返回什么?
    a.next() // Object{value:6, done:false}
    a.next() // Object{value:NaN, done:false}
    a.next() // Object{value:NaN, done:true}
    

    2.2 async函数对 Generator 函数的改进,体现在以下四点。

    (1)内置执行器。

    Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

    asyncReadFile();
    上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

    (2)更好的语义。

    async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

    (3)更广的适用性。

    co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

    (4)返回值是 Promise。(本题重点)

    async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

    进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
    到这里本题就全部解答完毕了:
    同步任务>微任务>宏任务

    3拓展: 验证:async是否真的是语法糖

    源码:

    // 1.js
    async function f() {
      console.log(1)
      let a = await 1
      console.log(a)
      let b = await 2
      console.log(b)
    }
    
    

    方法一: typescript编译
    1 npm install -g typescript
    2 tsc ./1.ts --target es6
    输出的结果:

    var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
        return new (P || (P = Promise))(function (resolve, reject) {
            function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
            function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
            function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
            step((generator = generator.apply(thisArg, _arguments || [])).next());
        });
    };
    function f() {
        return __awaiter(this, void 0, void 0, function* () {
            console.log(1);
            let a = yield 1;
            console.log(a);
            let b = yield 2;
            console.log(b);
        });
    }
    
    

    结果和上面说的一样。(ps: ts大法好!)
    方法二: babel
    配置比较复杂以后补充

    4 变式:

    变式1

    async function async1() {
        console.log('async1 start');
        await async2();
        console.log('async1 end');
    }
    async function async2() {
        //async2做出如下更改:
        new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
        });
    }
    console.log('script start');
    
    setTimeout(function() {
        console.log('setTimeout');
    }, 0)
    async1();
    
    new Promise(function(resolve) {
        console.log('promise3');
        resolve();
    }).then(function() {
        console.log('promise4');
    });
    
    console.log('script end');
    

    上面代码输出什么?

    script start
    async1 start
    promise1
    promise3
    script end
    promise2
    async1 end
    promise4
    setTimeout
    

    5 拓展vue(vm.nextick)

    Vue.nextTick( [callback, context] )

    • 参数

      • {Function} [callback]
      • {Object} [context]
    • 用法

    vm.msg = 'Hello'
    // DOM 还没有更新
    Vue.nextTick(function () {
      // DOM 更新了
    })
    

    在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

    以上为vue文档对nextTick的介绍,主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。
    下面来分析一下,为什么nextTick(callback)中callback执行的时候dom一定更新好了?
    看一下nextTick源码:

    // 省略部分
    let useMacroTask = false
    let pending = false
    let callbacks = []
    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
        if (useMacroTask) {
       // updateListener的时候为true
          macroTimerFunc()
        } else {
          microTimerFunc()
         // 其他情况走的微任务
        // 它们都会在下一个 tick 执行 flushCallbacks,flushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。
    // 执行flushCallbacks的时候会执行 pending = false, callbecks.length = 0
        }
      }
      // $flow-disable-line
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    

    当我们在某个方法中调用vm.nextTick的时候,向callback中push了一个方法
    以文档的例子进行说明:

    • 首先我们改变了vm.msg的值,所以先触发了派发更新,如下图


      响应式梳理.png

      由图可知会把渲染watcher的执行逻辑先添加到callbacks里面

    • 然后再是push,框架使用者的callback,
      所以当执行flushCallbacks的时候,因为都是微任务(或者不支持promise都为宏任务),先执行渲染相关逻辑(value = this.getter.call(vm, vm) // 执行updateComponent()),而且渲染为同步任务,然后再是执行用户的逻辑。
      所以nextTick其实是主线任务(数据更新)-> 异步队列更新(dom) -> 用户定义异步任务队列执行

    最后:参考

    http://www.ruanyifeng.com/blog/2015/05/async.html
    https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7
    http://es6.ruanyifeng.com/#docs/async
    https://ustbhuangyi.github.io/vue-analysis/reactive/next-tick.html#vue-%E7%9A%84%E5%AE%9E%E7%8E%B0

    相关文章

      网友评论

          本文标题:一道面试题 聊聊js异步

          本文链接:https://www.haomeiwen.com/subject/pumvrctx.html