JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。(引用阮一峰老师)
既然是单线程,那么涉及到网络请求这种耗时的事情怎么办呢,只能傻等着吗?对于灵活的Javascript来说,这不科学啊。所以,为了使浏览器非阻塞的运行任务,JS就设计了异步。
于是,所有的任务就分为两种,同步任务(synchronous)和异步任务(asynchronous)。
同步任务指,在主线程上排队执行的任务,即前一个任务执行完成,才能执行下一个任务;异步任务指的是,不进入主线程,而进入“任务队列”(task queue)的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
要完全理解异步,就需要了解 JS 的运行核心——事件循环(Event Loop)和任务队列(Task Queue)。
JS中所有的同步任务都在主线程上执行,形成一个执行栈;此外还有一个任务队列,用来存放异步任务的相关回调;一旦执行栈中的同步任务执行完毕,系统就会读取“任务队列”,检查有哪些事件待处理,并取出相关事件及回调函数放入执行栈中由主线程执行。主线程从"任务队列"中读取事件,这个过程是不断循环的,整个的这种运行机制又称为Event Loop(事件循环)。可以根据下图来加深理解:
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
回调函数是javascript中最基础的异步编程方法了。
step1(function(res1){
step2(function(res2){
step3(function(res3){ //...
});
});
});
正如上面所写的那样,把函数作为参数传入,然后回调,这种方式的弊端也显而易见,各个部分之间高度耦合(Coupling);一旦函数嵌套层次变多的话,代码维护将会十分困难,并形成所谓的“回调地狱(callback hell)”。
事件监听是javascript中非常常见的异步编程模式;
element.addEventListener("click",function(){
alert("clicked");
})
这种方式的耦合程度低,但是整个程序变成事件驱动,得不到流程控制。
发布/订阅模式通俗理解就是,订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心(顺带上下文),调度中心把这一信号传输给订阅者,那么订阅者就知道自己何时开始执行任务。
function f2() {
console.log("I'm done");
};center.subscribe("done", f2);function f1(){
setTimeout(function () {
// f的任务代码 center.publish("done");
}, 1000);
}
首先,f2向"信号中心"center"订阅"done"信号。f1执行完成后向信号中心发布“done”信号,f2收到信号之后,便开始执行。发布/订阅模式类似于事件监听,但是比事件监听更加灵活一些,我们把信号交给调度中心统一管理,可以掌握事件被订阅的次数,以及订阅者的信息,管理起来很方便。
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
首先,来创建一个Promise实例:
var promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});promise.then(function(value) {
// success
}, function(error) {
// failure
});
const fullFileName = path.resolve(__dirname, '../data/data2.json')const result = readFilePromise(fullFileName)
result.then(data => {
console.log(data) return JSON.parse(data).a
}).then(a => {
console.log(a)
}).catch(err => {
console.log(err.stack) // 这里的 catch 就能捕获 readFilePromise 中触发的 reject ,而且能接收 reject 传递的参数
})
在若干个then连续调用之后,一般会在最后跟一个.catch来捕获异常,而且执行reject时传递的参数也会在catch中获取到。这样做的好处是:
让程序看起来更加简洁,是一个串联的关系,没有分支(如果用then的两个参数,就会出现分支,影响阅读)
看起来更像是try - catch的样子,更易理解。
var readFile = require('fs-readfile-promise');readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
function* helloWorldGenerator() {
yield
'hello';
yield
'world';
return
'ending'; }
var hw = helloWorldGenerator();
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
从语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,然后通过调用遍历器对象的next方法,让指针指向下一个状态;通俗来讲,Generator函数是分段执行的,遇到yeild会暂停执行,而调用next方法会回复执行。
hw.next()// { value: 'hello', done: false }
hw.next()// { value: 'world', done: false }
hw.next()// { value: 'ending', done: true }
hw.next()// { value: undefined, done: true }
这段代码中,value表示yeild表达式后边的那个表达式的值,done表示遍历是否结束;
总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
console.log('执行了!')
}var generator = f();//此处不会执行setTimeout(function () {
generator.next();//执行函数
}, 2000);
需要注意的是,一个Generator函数中,可以执行多次yeild语句,暂停之后会记住指针的位置,即下一次从哪个位置继续执行。而return语句只能执行一次,并且不具备位置记忆功能。 yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
function *foo() {
yield 1; yield 2; yield 3; yield 4; yield 5; return 6;
}for (let v of foo()) {
console.log(v);
}// 1 2 3 4 5
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
这段代码中,首先读取一个接口,然后从返回的结构中读取信息,这简直就像是同步操作!
执行方法如下:
var g = gen();var result = g.next();result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。 对比之前的Promise, Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。那么async 函数是什么呢?它就是 Generator 函数的语法糖。
还是读取文件的例子,这次用Generator函数实现:
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
而用async函数,就写成这样:
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
对比发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,经验告诉我们。肯定不会这么简单啊。 其实,async函数确实对Generator函数做了一些改进:
进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
// 函数声明
async function foo() {}// 函数表达式
const foo = async function () {};// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
} async getAvatar(name) {
const cache = await this.cachePromise; return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);// 箭头函数
const foo = async () => {};
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
因为await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中,这样不管异步操作是否成功,后面的await都会执行。
async function main() {
try {
const val1 = await firstStep();
const val2 = await secondStep(val1);
const val3 = await thirdStep(val1, val2); console.log('Final: ', val3);
} catch (err) {
console.error(err);
}
}
好啦,以上就是对日常用到的一些异步编程方法的总结,蒽,学而时习之,不亦说乎。