异步、任务队列、事件循环

线程

Task A --> Task B --> Task Chtml

  • 每一个线程一次只能执行一个任务。
  • 每一个任务顺序执行,只有前面的结束了,后面的才能开始。
  • JavaScript 是单线程的:即便有多个内核,也只能在单一线程上运行多个任务,此线程称为主线程(main thread)。

任务队列和事件循环

事件循环和任务队列.png

任务队列

宏任务:script(全局任务),setTimeout,setInterval,setImmediate,I/O,UI rendering
微任务:process.nextTick,Promise,Object.observer,MutaionObserver
执行栈:任务在执行栈上执行。前端

浏览器中的事件循环

分三步:node

  1. 取一个宏任务来执行。执行完毕后,下一步。
  2. 取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。
  3. 更新UI渲染。

代码执行都是从script(全局任务)开始。web

Nodejs中的事件循环

Node.js采用V8做为js的解析引擎,而I/O处理方面使用了本身设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不一样操做系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现。
node.png
每次事件循环都包含了6个阶段:
1.timersajax

处理全部 setTimeout 和 setInterval 的回调。

这些回调被保存在一个最小堆(min heap) 中. 这样引擎只须要每次判断头元素, 若是符合条件就拿出来执行, 直到遇到一个不符合条件或者队列空了, 才结束 Timer Phase.
Timer Phase 中判断某个回调是否符合条件的方法:消息循环每次进入 Timer Phase 的时候都会保存一下当时的系统时间(T1),而后只要看上述最小堆中的回调函数设置的启动时间(T0)是否超过进入 Timer Phase 时保存的时间(T0<T1), 若是超过就拿出来执行.
Nodejs 为了防止某个 Phase 任务太多,每一个 Phase 执行回调都有个最大数量. 若是超过数量的话也会强行结束当前 Phase 而进入下一个 Phase. 这一条规则适用于消息循环中的每个 Phase.
2. I/O callbacksegmentfault

执行你的 fs.read, socket 等 IO 操做的回调函数, 同时也包括各类 error 的回调.

3.Idle, Prepare 阶段
内部使用,不讨论.
4. poll 阶段api

等待异步请求和数据。

首先会执行 watch_queue 队列中的 IO 请求, 一旦 watch_queue 队列空, 则整个消息循环就会进入 sleep , 从而等待被内核事件唤醒.
当js层代码注册的事件回调都没有返回的时候,事件循环会阻塞在poll阶段。看到这里,你可能会想了,会永远阻塞在此处吗?固然 Poll Phase 不能一直等下去.数组

  1. 它首先会判断后面的 Check Phase 以及 Close Phase 是否还有等待处理的回调. 若是有, 则不等待, 直接进入下一个 Phase.
  2. 若是没有其余回调等待执行, 它会给 epoll 这样的方法设置一个 timeout.

Nodejs 就是经过 Poll Phase, 对 IO 事件的等待和内核异步事件的到达来驱动整个消息循环的.
5. check 阶段promise

这个阶段只处理 setImmediate 的回调函数.

6. close callback 阶段
专门处理一些 close 类型的回调. 好比 socket.on('close', ...). 用于资源清理.
总结:
1.Node.js 的事件循环分为6个阶段
2.浏览器和Node 环境下,microtask 任务队列的执行时机不一样
Node.js中,microtask 在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。
浏览器端,microtask 在事件循环的 macrotask 执行完以后执行
搜狗截图20200316184546.png浏览器

同步和异步

  • 同步会阻塞代码执行
  • 异步不会阻塞代码执行

JavaScript是一种同步的、阻塞的、单线程的语言,在这种语言中,一次只能执行一个操做。但web浏览器定义了函数和API,容许咱们当某些事件发生时不是按照同步方式,而是异步地调用函数(好比,时间的推移,用户经过鼠标的交互,或者获取网络数据)。这意味着您的代码能够同时作几件事情,而不须要中止或阻塞主线程。

前端使用异步的场景有哪些?

  • 网络请求,如ajax、图片加载
  • 定时任务:setTimeout,setInverval
setTimeout()指定的延迟时间以后来执行代码。
clearTimeout()取消setTimeout设置。
setInterval()每隔指定的时间执行代码
clearInterval()取消setInterval()设置
  • DOM事件绑定:

用addEventListener注册一个类型的事件的时候,浏览器会有一个单独的模块去接收这个东西,当事件被触发的时候,浏览器的某个模块,会把相应的函数扔到异步队列中,

  • ES6中的Promise

回调函数

回调函数callback(执行结束回来调用的函数)

定义:回调函数是一个函数,它做为参数传递给另外一个函数,并在父函数完成后执行。
请注意,不是全部的回调函数都是异步的,有一些是同步的。
如:Array.prototype.forEach()

forEach() 须要的参数是一个回调函数,回调函数自己带有两个参数,数组元素和索引值。它无需等待任何事情,当即运行。

const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];

gods.forEach(function (eachName, index){
  console.log(index + '. ' + eachName);
});

一般所说的回调函数是指异步的。
回调函数的问题:

  1. 回调地狱:致使的调试困难,和大脑的思惟方式不符。
  2. 控制反转:把本身程序一部分的执行控制交给某个第三方致使请求并发回调函数执行顺序没法肯定。

Promise

基本用法

通常状况下是有异步操做时,使用Promise对这个异步操做进行封装。

三种状态

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操做成功完成。
  • rejected: 意味着操做失败。

Promise有两种状态改变的方式,既能够从pending转变为fulfilled,也能够从pending转变为rejected。一旦状态改变,就「凝固」了,会一直保持这个状态,不会再发生变化。当状态发生变化,promise.then绑定的函数就会被调用。
promises.png

resolvereject

new Promise((resolve, reject) => {
  setTimeout(() => {
    //resolve("Hello World");
    reject("error");
  }, 100)
}).then(data => {
  console.log(data);
}, error => {
  console.log(error);
});

resolve函数的做用:在异步操做成功时调用,并将异步操做的结果做为参数传递出去;
reject函数的做用:在异步操做失败时调用,并将异步操做报出的错误做为参数传递出去。
promise对象的错误,会一直向后传递,直到被捕获。即错误总会被下一个catch所捕获。then方法指定的回调函数,若抛出错误,也会被下一个catch捕获。catch中也能抛错,则须要后面的catch来捕获。

基本api

.then()

then()有两个参数,分别为Promise从pending变为fulfilledrejected时的回调函数(第二个参数非必选)。这两个函数都接受Promise对象传出的值做为参数
简单来讲,then就是定义resolvereject函数的,

.catch()

该方法是.then(undefined, onRejected)的别名,用于指定发生错误时的回调函数。

new Promise((resolve, reject) => {
  setTimeout(() => {
    //resolve("Hello World");
    reject("error");
  }, 100)
}).then(data => {
  console.log(data);
}, error => {
  console.log(error);
});

//等同于
new Promise((resolve, reject) => {
  setTimeout(() => {
    //resolve("Hello World");
    reject("error");
  }, 100)
}).then(data => {
  console.log(data);
}).catch(error => {
  console.log(error);
});

.all()

Promise.all(iterable)

该方法用于将多个Promise实例,包装成一个新的Promise实例。
Promise.all方法接受一个数组(或具备(迭代器)Iterator接口)做参数,数组中的对象(p一、p二、p3)均为promise实例(若是不是一个promise,该项会被用Promise.resolve转换为一个promise)。它的状态由这三个promise实例决定。

  • 当p1, p2, p3状态都变为fulfilled,p的状态才会变为fulfilled,并将三个promise返回的结果,按参数的顺序(而不是resolved的顺序)存入数组,传给p的回调函数,如例3.8。
  • 当p1, p2, p3其中之一状态变为rejected,p的状态也会变为rejected,并把第一个被reject的promise的返回值,传给p的回调函数。

这多个 promise 是同时开始、并行执行的,而不是顺序执行。

let p1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, "first");
});
let p2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000, "second");
});
let p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "third");
});
Promise.all([p1, p2, p3]).then(arr => {
  console.log(arr);
});
//[ 'first', 'second', 'third' ]
let p1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, "first");
});
let p2 = new Promise((resolve, reject) => {
  setTimeout(reject, 2000, "second");
});
let p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "third");
});
Promise.all([p1, p2, p3]).then(arr => {
  console.log(arr);
}, error => {
  console.log(error);
});
//second

.race()

Promise.race(iterable)

Promise.race方法一样接受一个数组(或具备Iterator接口)做参数。当p1, p2, p3中有一个实例的状态发生改变(变为fulfilledrejected),p的状态就跟着改变。并把第一个改变状态的promise的返回值,传给p的回调函数。

let p1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, "first");
});
let p2 = new Promise((resolve, reject) => {
  setTimeout(reject, 2000, "second");
});
let p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "third");
});
Promise.race([p1, p2, p3]).then(arr => {
  console.log(arr);
}, error => {
  console.log(error);
});
//third

在第一个promise对象变为resolve后,并不会取消其余promise对象的执行

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("first");
    console.log(1);
  }, 3000)
});
let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("second");
    console.log(2);
  }, 2000)
});
let p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("third");
    console.log(3);
  }, 1000)
});
Promise.race([p1, p2, p3]).then(arr => {
  console.log(arr);
}, error => {
  console.log(error);
});
// 3
// third
// 2
// 1

.resolve()

Promise.resolve('Success');

/*******等同于*******/
new Promise(function (resolve) {
    resolve('Success');
});

.reject()

Promise.reject(new Error('error'));

/*******等同于*******/
new Promise(function (resolve, reject) {
    reject(new Error('error'));
});

Promise常见问题

建立promise的流程:

  1. 使用new Promise(fn)或者它的快捷方式Promise.resolve()Promise.reject(),返回一个promise对象
  2. fn中指定异步的处理
    处理结果正常,调用resolve
    处理结果错误,调用reject

情景1:若是在then中抛错,而没有对错误进行处理(即catch),那么会一直保持reject状态,直到catch了错误

function taskA() {
    console.log(x);
    console.log("Task A");
}
function taskB() {
    console.log("Task B");
}
function onRejected(error) {
    console.log("Catch Error: A or B", error);
}
function finalTask() {
    console.log("Final Task");
}
var promise = Promise.resolve();
promise
    .then(taskA)
    .then(taskB)
    .catch(onRejected)
    .then(finalTask);
    
-------output-------
Catch Error: A or B,ReferenceError: x is not defined
Final Task
Promise.resolve().then(() => {
    console.log('ok1');
    throw 'throw error1'
}).then(() => {
    console.log('ok2');
}, err => {
    // 捕获错误
    console.log('err->', err);
}).then(() => {
    // 该函数将被调用
    console.log('ok3');
    throw 'throw error3'
}).then(() => {
    // 错误捕获前的函数不会被调用
    console.log('ok4');
}).catch(err => {
    console.log('err->', err);
});

//ok1
//err-> throw error1
//ok3
//err-> throw error3
console.log('script start');

async function async1() {
    console.log('async1');
    await async2();
    console.log('end');
}

async function async2() {
    console.log('async2');
}

setTimeout(function () {
    console.log('setTimeout');
}, 0);

async1();

new Promise(function (resolve) {
    console.log('promise1');
    resolve();
}).then(function () {
    console.log('promise2');
});

console.log('script end');

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

情景二:在异步回调中抛错,不会被catch

new Promise((resolve, reject) => {
  throw "error"
}).then(data => {
  console.log(data);
}).catch(err => {
  console.log(err);
});
//error

//对比
new Promise((resolve, reject) => {
  setTimeout(() => {
    throw "error";
  }, 1000)
}).then(data => {
  console.log(data);
}).catch(err => {
  console.log(err);  //This is never called
});

promise的优缺点

优势:

  1. 结束回调地狱
  2. Promise老是严格按照它们放置在事件队列中的顺序调用。
  3. 错误处理要好地多

缺点:
Promise一旦运行,不能终止掉。

Promise 加载一张图片

promise.js
function loadImg(src) {
    return new Promise((resolve, reject) => {
        let img = document.createElement("img");
        img.src = src;
        img.onload = function (url) {
            resolve(img);
        };
        img.onerror = function () {
            reject(err);
        }
    });
}

const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg';
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg';

loadImg(url1).then((img) => {
    console.log("图片1加载成功");
    console.log(`图片1的宽度为:${img.width}`);
}).then(() => {
    console.log("准备加载图片2");
    return loadImg(url2);
}).then((img) => {
    console.log("图片2加载成功");
    console.log(`图片2的宽度为:${img.width}`);
}).catch((err) => {
    console.log(err);
});
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
<script src="./promise.js"></script>
</html>

Generator

Generator解决了回调函数处理异步流程的第一个问题:不符合大脑顺序、线性的思惟方式。

Async/Await

  • 同步的书写方式,逻辑和数据依赖都很是清楚,只须要把异步的东西用Promise封装出去,而后使用await调用就能够了,也不须要像Generator同样须要手动控制next()执行。
  • Async/Await是Generator和Promise的组合,彻底解决了基于回调的异步流程存在的两个问题,多是如今最好的JavaScript处理异步的方式了。
// 使用setTimeout模拟异步
function ajax (url){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log(url + ' result.');
            resolve(url + ' result.');
        }, 100);
    });
}

async function ajaxAsync () {
    var aResult = await ajax('/api/a'); 
    console.log('aResult: ' + aResult);
    var bResult = await ajax('/api/b'); 
    console.log('bResult: ' + bResult);
}

ajaxAsync();

参考文章:
http://www.javashuo.com/article/p-odwhqqel-bp.html
http://www.javashuo.com/article/p-ogqwcufq-go.html

相关文章
相关标签/搜索