单线程#
JavaScript 最初是设计用来在网页上和用户互动,操作 DOM 的语言,因为用途比较简单,所以被作者设计成了单线程语言。什么是单线程呢?就是同一时间只能做一件事。一般情况下,程序执行的时候是按照顺序一行行的来,不能同时执行多个程序。
console.log(0);
for (var i = 1; i < 10; i++) {
console.log(i);
}
alert(20);上面代码,先打印 0,再打印 1~9,最后弹出 20。这些都是同步程序,按照顺序排着队一步一步执行,就像去银行办理业务需要排队一样。
异步#
如果执行过程中,中间有个程序执行需要花费很长时间,那不是会把整个程序阻塞吗?导致它后面的程序无法执行,这样明显是不符合常理的,于是就有了异步,什么是异步呢?
console.log(100);
setTimeout(function () {
console.log(200);
}, 1000);
console.log(300);上面代码打印顺序是:100 300 200。
为什么先打印 300 呢,因为第二个函数setTimeout是一个定时器,已经设置需要在 1000 毫米之后再执行打印,所以先执行它后面的程序打印出 300。
如果把后面的定时 1000 毫秒改成 0,不延时执行了,重新执行整个程序打印顺序会不会就变成100 200 300呢?你可以试一下,相信执行的结果还是和上面的打印顺序一样,还是100 300 200。
为什么会这样呢?不是已经设置不延时执行了吗?因为setTimeout除了是一个定时器,本身也是一个异步函数。程序执行到异步函数的时候会先把它暂存起来,等所有的同步程序执行完毕后,最后再回过头了看下它是否可以执行,如果可以,再去执行它。
异步场景#
JavaScript 中有很多需要使用异步场景,特别是在 Node.js 中,有很多的异步程序。
- 定时函数:
setTimeout、setInterval。 - 网络请求:
ajax、get/post请求 - IO 请求:Node.js 文件系统中的
fs.readFile、fs.readDir等 - DOM 事件
- Promise
简单分析下上面提到的异步场景:
定时器是设置一定的时间,等时间到了之后再执行。
网络请求也是,因为发出请求和服务器处理请求并返回内容都需要时间去处理。如果你不想等待浪费时间的话,先不去管它,等数据返回之后再执行它。
IO 请求也是,打开文件需要一段加载内容的时间,加载好之后,才能看到文件内容,所以一般也是使用异步执行(当然如果文件小,加载时间慢,也可以用使用同步执行)。
DOM 事件绑定也是异步的,只是它的异步操作不是由系统主动调用的,而是在触发事件的时候调用的,什么时候触发,什么时候执行,所以也是异步的。
Promise 是 ES6 的异步解决方案。
回调函数#
在异步程序中,回调函数必不可少,无回调无异步。什么是回调函数?
// 定时器
setTimeout(function () {
console.log("callback"); // callback
}, 1000);
// jquery ajax请求
$.ajax({
method: GET,
url: url,
success: function (data) {
console.log(data); // callback
},
fail: function (error) {
console.log(error); // callback
},
});
// nodejs读取文件
fs.readFile("data1.json", function (err, data) {
console.log(data); // callback
});上面代码标记callback的地方就是回调函数。
JS 引擎执行到异步程序的时候,会把异步程序先暂存起来,等到所有的同步程序执行完毕后,再回过头来执行异步程序。
后面到底是怎么执行异步程序了?就是靠回调函数(callback)来执行,调用回调函数执行其中的代码。
所以异步程序一定不能缺少回调函数,不然后面“回调的时候”无法执行。
事件轮询#
再来详细说明一下上面说的 JS 程序运行异步函数的方法:
- 所用的同步任务都在主线上按照顺序执行,形成一个“执行桟”。
- 主线程之外,还存在一个“任务队列”。只要任务队列有了运行结果,就在“任务队列”中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环/事件轮询)
上面的步骤和概念来自阮一峰老师的博客。JavaScript 运行机制详解:再谈 Event Loop
Promise#
Promise 一种规范,是 ES6 中异步的解决方案。
// 用promise对象实现ajax
const PAjax = function (url) {
const promise = new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject("error: " + xhr.statusText);
}
}
};
});
return promise;
};
// then的第一个参数函数获取成功结果,第二个参数函数获取异常结果
// 也可以用catch方法来捕获异常,建议用catch方法,不用then的第二个参数函数
PAjax(url)
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});上面的代码是一个用 Promise 对象实现 ajax 的实例。
Promise 对象异步操作有 3 种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
Promise 构造函数创建实例时接受一个函数作为参数,这个函数同时接收两个参数resolve和reject。这两个参数也是函数,由 JS 引擎来提供,不需要自己部署。
resolve函数表示将 promise 对象的状态由“进行中”变成“已成功”,在异步操作成功时调用这个函数,把成功的结果作为函数的参数传入。reject函数表示将 promise 对象的状态由“进行中”变成“已失败”,在异步操作失败时调用这个函数,把失败的结果作为函数的参数传入。
Promise对象生成后可以使用 then 方法分别定制resolved状态和rejected状态的回调函数,也可以使用catch方法来捕获异常状态(rejected状态),建议使用catch方法,这样代码更加清晰简洁。
Generator#
Generator 是 ES6 中异步的解决方案。
function* Gen() {
yield 100;
yield 200;
return 300;
}
const g = Gen();
console.log(typeof g); // object
console.log(g.next()); // { value: 100, done: false }
console.log(g.next()); // { value: 200, done: false }
console.log(g.next()); // { value: 300, done: true }
console.log(g.next()); // { value: undefined, done: true }Generator 函数和普通函数有两个不一样的特征:
- function 关键字后面带有一个
*号。 - 函数内部使用 yield 表达式。
Generator 函数调用后和普通函数也不一样,函数并不执行,也不返回函数结果,而是一个指向内部状态改的指针对象,也就是遍历器对象。
使用时调用对象内部的next方法,使得指针移向下一个状态。
- 当指针遇到 yield 表达式时,next 返回一个对象,对象的值是当前 yield 表达式的值,done 属性是
false(表示遍历未完成)。 - 当指针遇到 return 表达式时,next 方法返回的对象的值是 return 表达式的 value 属性, done 属性是
true(表示遍历已完成)。 - 遍历完成后,再调用 next 方法返回的对象的值是 undefined,done 属性的值是
true。
const fs = require("fs");
const Q = require("q");
const co = require("co");
const readFilePromise = Q.denodeify(fs.readFile);
const gen = function* () {
const r1 = yield readFilePromise("data1.json");
console.log(r1.toString());
const r2 = yield readFilePromise("data2.json");
console.log(r2.toString());
};
co(gen);上面代码是一个使用Generator异步读取两个 JSON 文件的实例。
实例用了 nodejs 的fs库,还有第三方库q和co。fs是 nodejs 的文件库,q是为了更快捷封装一个 promise 对象,co的作用是执行 Generator 函数。
Async/Await#
Async/Await是 ES2017 中的异步解决方案,非常推荐使用。著名的后端框架 Koa2 就是使用了这种异步方案。
const fs = require("fs");
const Q = require("q");
const readFilePromise = Q.denodeify(fs.readFile);
const readFileAsync = async function () {
const r1 = await readFilePromise("data1.json");
console.log(r1.toString());
const r2 = await readFilePromise("data2.json");
console.log(r2.toString());
};
const result = readFileAsync();
console.log(result);是不是发现Async/Await和 Generator 非常相似,async function代替了function*,await代替了yield,另外Async/Await已经不需要co库了,它可以自己执行,使用非常方便,强烈推荐使用。
总结#
JavaScript 是非阻塞单线程语言,所以异步是无法避免的。现在异步的方案也是越来越成熟,了解异步对于学习这门语言非常有帮助。