异步

536 阅读6分钟

自己学习总结;

提到异步,就会想到为什么要有异步?异步能解决什么问题?

JavaScript是单线程语言。

* 什么是单线程**?**

只有一个线程,同时只能做一件事,两段js不能同时执行。

* 为什么JavaScript是单线程**?**

为了避免DOM渲染冲突; 首先:浏览器需要渲染DOM,js 可以修改DOM结构、js执行的时候,浏览器DOM渲染会暂停,两段js不能同时执行,都修改DOM 就会发生冲突。(webworker 支持多线程,但是不能访问DOM,所以js 必须是单线程)

* 异步出现的原因**?**

如果JavaScript中不存在异步,只能自上而下执行,若一行解析时间过长,那么下面的代码就会被阻塞,阻塞就意味着会卡死,用户体验很差,所以js就需要有异步执行---为了解决JavaScript单线程问题。

事件轮询 event-loop

事件轮询指的是计算机系统的一种运行机制。

"EventLoop是一个程序结构,用于等待和发送消息和事件。" 简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"EventLoop线程"(可以译为"消息线程")。

event-loop是实现异步的具体解决方案。

* 什么是event-loop/运行流程**?**

  1. 同步代码(主线程)直接执行;
  2. 异步函数先放在异步队列中,在异步队列中区分宏任务macrotask 和微任务 microtask,(根据任务分类,又将任务队列分为宏观任务队列(microtask queue)和微观任务队列(microtask queue)。任务队列的读取顺序是先读取所有微观任务队列执行后再读取一个宏观任务队列,再读取所有微观任务队列,再读取一个宏观任务队列…)
  3. 待同步函数执行完毕,轮询执行 异步队列中的函数;
  4. 以上步骤不断重复执行,就形成了事件轮询event-loop;

es6标准中,任务又分为两种类型,宏任务(macrotask)和微任务(microtask)。 

宏任务:由宿主环境提供,比如:setTimeout、setInterval、网络请求、用户I/O、script(整体代码)、UI rendering、setImmediate(node)。 

微任务:语言标准(ECMAScript)提供,如:process.nextTick(node环境中,要先于其他微任务执行)、Promise、Object.observe、MutationObserver。 

Promise

promise的出现也是为了解决回调地狱问题,是一种异步请求解决方案。promise是对异步回调的一个封装。

promise标准状态:

三种状态:pending等待、fulfilled已完成、rejected已拒绝

初始状态是pending

状态变化: pending变为fulfilled, 或者pending变为rejected; 状态变化不可逆;

promise标准then:

   promise实例必须实现then这个方法;

   then() 必须可以接受两个函数作为参数(resolve 成功和 reject失败的返回参数)

   then() 返回的必须是一个promise实例;(promise串联,当前的那个promise实例,否则一 直返回之前那个promise实例)

可以实现请求串联,解决无限回调问题;

promise.all()

promise.all()接收一个promise对象的数组,待全部执行完成后,统一执行success,返回一个datas数组(包含了promise的所有返回内容);

let  result1 = new Promise((resolve, reject) => {
  resolve('成功了')
})

let result2 = new Promise((resolve, reject) => {
  resolve('success')
})

//待全部完成之后,统一执行 success
Promise.all([result1, result2]).then(datas => {
    //接收到的datas是一个数组,依次包含了多个promise返回的内容
    console.log(datas[0]);
    console.log(datas[1]);
})

promise.race()

promise.race接收一个包含多个promise对象的数组,只要有一个完成,就执行success,返回一个data

let  result1 = new Promise((resolve, reject) => {
  resolve('成功了')
})

let result2 = new Promise((resolve, reject) => {
  resolve('success')
})

Promise.race([result1, result2]).then(data => {
    //data即首先执行成功的promise的返回值
    console.log(data);
})

async/await

异步带来的问题:就是callback(callback拆分(可以写很多then))返回结果的问题,也就是异步编写执行的逻辑顺序不一致的问题。

使用await函数必须用async标识;await后面跟的是一个promise实例;( 需要引入babel-polyfill 来转译识别await函数)

实例:

import “babel-polyfill”;function loadimg(src){     var promise = new Promise(function(resolve,reject){          var img = document.createElement("img");          img.onload =function(){ resolve(img); }          img.onerror=function(){ reject("图片加载失败"); }    })}
var src1 = "https://www.baidu.com/img/img1.png";
var src2 = "https://www.baidu.com/img/img2.png";
const load = async function(){
   const result1 = await loadimg(src1);
   console.log(result1);
   const result2 = await loadimg(src2);
   console.log(result2);
}
load();

基本语法:

使用了promise,但是并没有和promise冲突,完全是同步的写法,再也没有回调函数,但是改变不了js单线程、异步的本质。

Generator函数

generator函数是ES6提供的一种异步编程解决方案

在JavaScript中,一个,函数一旦开始执行,就会运行到最后或者遇到return结束,不会被其他代码打断,也不能从外部传入值到函数体内,而generator函数出现则打破了函数的完整运行成为可能。

generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。

generator和函数不同的是,generator由function*定义(注意多出的*号),并且,除了return语句,还可以用yield返回多次。

注意:与return的区别?

每次遇到yield函数就会停止执行,下一次在从该位置继续向后执行,而return 不具备记忆位置的功能。

以斐波那契数列函数为例:

function* fib(max) {   
 var
        t,
        a = 0,
        b = 1,
        n = 0;
    while (n < max) {
        yield a;
        [a, b] = [b, a + b];
        n ++;
    }
    return;
}

直接调用一个generator和调用函数不一样,fib(5)仅仅是创建了一个generator对象,还没有去执行它。

调用generator对象有两个方法,一是不断地调用generator对象的**next()**方法

var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}

next()方法会执行generator的代码,然后,每次遇到yield x;就返回一个对象{value: x, done: true/false},然后“暂停”。返回的value就是yield的返回值,done表示这个generator是否已经执行结束了。如果**done****true**,则**value**就是**return**的返回值

当执行到donetrue时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。

第二个方法是直接**for ... of**循环迭代generator对象,这种方式不需要我们自己判断done

'use strict'

function* fib(max) {
    var
        t,
        a = 0,
        b = 1,
        n = 0;
    while (n < max) {
        yield a;
        [a, b] = [b, a + b];
        n ++;
    }
    return;
}
for (var x of fib(10)) {
    console.log(x); // 依次输出0, 1, 1, 2, 3, ...
}

generator和普通函数相比,有什么用?

因为generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能。

generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码(数据请求)。

详细的generator参考:

www.liaoxuefeng.com/wiki/102291…

当前异步解决方案有jQuery deferred、promise、async/await、Generator

思维导图:

补充:

目前最新的浏览器中已经没有宏任务的说法,取而代之的是各种队列任务;

 随着浏览器的复杂度急剧提升,w3c不再使用宏任务队列的说法

在目前Chrome的实现中,至少包含了下面的队列:

- 延时队列:用于存放计时器到达后的回调任务**优先级中**setTimeout和setInterval的回调函数;

- 交互队列(鼠标事件、键盘事件): 用于存放用户操作后产生的事件处理任务,**优先级高**;

- 微任务队列(w3c规定,封顶):用户存放需要最快执行的任务,用于存放Promise的回调函数,**优先级最高**;

如何理解JS 的事件循环:

  •  事件循环又叫做消息循环(官方叫event loop,浏览器实现叫message loop),是浏览器渲染主线程的工作方式。

  •  在Chrome的源码中,它开启一个不会结束的for循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。

  • 过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的 浏览器环境,取而代之的是一种更加灵活多变的处理方式。

  • 根据W3C官方的解释,每个任务有不同的类型,同类型的任务必须在同一 个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行;

注意:

 且process.nextTick优先级大于promise.then; await实际上是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码;因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。

 由于process.nextTick指定的回调函数是在本次”事件循环”触发,而setImmediate指定的是在下次”事件循环”触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查”任务队列”);