线程

Task A --> Task B --> Task C

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

任务队列和事件循环

事件循环和任务队列.png

任务队列

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

浏览器中的事件循环

分三步:

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

代码执行都是从script(全局任务)开始。遇到 Promise,new Promise立即执行,then函数分发到微任务Event Queue

小测试:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
输出结果为:1,7,6,8,2,4,3,5,9,11,10,12。

Nodejs中的事件循环

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

处理所有 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 callback

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

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

等待异步请求和数据。

首先会执行 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 阶段

    这个阶段只处理 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接口)作参数,数组中的对象(p1、p2、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();

参考文章:
https://segmentfault.com/a/1190000007032448
https://segmentfault.com/a/1190000011198232


梁柱
135 声望12 粉丝

« 上一篇
JavaScript语句
下一篇 »
JavaScript DOM