采用单线程原因
js语言设计之初就是运行在浏览器端,目的是为了实现页面上的交互。而实现页面交互的核心就是dom操作,从而决定了它必须使用单线程模型,否则就会出现复杂的线程同步问题。如果js运行时有多个线程一起工作,其中一个线程修改了某个dom元素,而另一个线程又对这个元素进行了修改或者删除操作,此时浏览器就无法确定以哪个线程为准。所以,js语言的执行环境是单线程的。
单线程的优劣势
单线程的最大优势就是:更简单,更安全
单线程的劣势也很明显:如果代码执行过程中有一个特别耗时的任务,其他代码(任务)就要等待前一个任务完成才能执行,等待的这段时间就会给用户一种假死的感觉。
为了解决单线程的劣势,js有两种任务的执行模式:1.同步模式 2.异步模式
在正式进入两种模式之前,先来了解下js异步编程包含哪些内容。
异步编程内容概览
- 同步模式和异步模式
- 事件循环和消息队列(js实现异步模式的方式)
- 异步编程的几种方式
- Promise异步方案、宏任务/微任务队列
- Generator异步方案、Async/Await语法糖
同步模式和异步模式
同步模式
同步模式:指的是代码任务依次执行,后一个任务必须等前一个任务执行结束才能开始。程序执行顺序和代码的编写顺序一致,在单线程模式下,大多数任务都会以同步模式执行。
异步模式
异步模式:每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
异步模式对于开发者而言,单线程下的异步模式最大的难点就是代码执行顺序的混乱。对于js语言非常重要,没有它就无法同时处理大量的耗时任务。
console.log('print begin')
setTimeout(function timer1 () {
console.log('timer1 invoked')
}, 1800)
setTimeout(function timer2 () {
console.log('timer2 invoked')
setTimeout(function inner () {
console.log('inner invoked')
}, 1000)
}, 1000)
console.log('print end')
// print begin
// print end
// timer2 invoked
// timer1 invoked
// inner invoked
js线程某个时刻发起了一个异步调用,它紧接着继续执行其他的任务,此时异步线程会单独执行异步任务,执行过后会将回调放到消息队列中,js主线程执行完任务过后会依次执行消息队列中的任务。这里要强调的是,js是单线程的,浏览器是多线程的,一些API的执行是浏览器启动单独的线程去做的。
这里的同步和异步不是指写代码的方式,而是运行环境(浏览器环境或者Node环境)提供的API是以同步或异步模式的方式工作。
回调函数
回调函数是异步编程最基本的方法。
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱。
function foo(callback) {
setTimeout(function(){
callback()
}, 1000)
}
foo(function() {
console.log('这是一个回调函数')
})
还有其他的一些实现异步的方式,比如:事件监听和发布订阅,这些也都是基于回调函数之上的变体。
Promise
Promise概述
虽然回调函数是所有异步编程方案的根基。但是如果我们直接使用传统回调方式去完成复杂的异步流程,就会无法避免大量的回调函数嵌套。导致回调地狱的问题。
为了避免这个问题。CommonJS社区提出了Promise的规范,ES6中称为语言规范。
Promise是一个对象,用来表述一个异步任务执行之后是成功还是失败。
Promise基本用法
返回resolve
const promise = new Promise((resolve, reject) => {
resolve(100)
})
promise.then((value) => {
console.log('resolved', value) // resolve 100
},(error) => {
console.log('rejected', error)
})
返回reject
const promise = new Promise((resolve, reject) => {
reject(new Error('promise rejected'))
})
promise.then((value) => {
console.log('resolved', value)
},(error) => {
console.log('rejected', error)
// rejected Error: promise rejected
// at E:\professer\lagou\Promise\promise-example.js:4:10
// at new Promise (<anonymous>)
})
即便promise中没有任何的异步操作,then方法的回调函数仍然会进入到事件队列中排队。
Promise案例
使用Promise去封装一个ajax的案例
function ajax (url) {
return new Promise((resolve, rejects) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
// html5中提供的新事件,请求完成之后(readyState为4)才会执行
xhr.onload = () => {
if(this.status === 200) {
resolve(this.response)
} else {
rejects(new Error(this.statusText))
}
}
// 开始执行异步请求
xhr.send()
})
}
ajax('/api/user.json').then((res) => {
console.log(res)
}, (error) => {
console.log(error)
})
Promise的本质
本质上也是使用回调函数的方式去定义异步任务结束后所需要执行的任务。这里的回调函数是通过then方法传递过去的
Promise链式调用
常见误区
- 嵌套使用的方式是使用Promise最常见的误区。要使用promise的链式调用的方法尽可能保证异步任务的扁平化。
链式调用的理解
- promise对象then方法,返回了全新的promise对象。可以再继续调用then方法,如果return的不是promise对象,而是一个值,那么这个值会作为resolve的值传递,如果没有值,默认是undefined
- 后面的then方法就是在为上一个then返回的Promise注册回调
- 前面then方法中回调函数的返回值会作为后面then方法回调的参数
- 如果回调中返回的是Promise,那后面then方法的回调会等待它的结束
Promise异常处理
then中回调的onRejected方法
.catch()(推荐)
promise中如果有异常,都会调用reject方法,还可以使用.catch()
使用.catch方法更为常见,因为更加符合链式调用
ajax('/api/user.json')
.then(function onFulfilled(res) {
console.log('onFulfilled', res)
}).catch(function onRejected(error) {
console.log('onRejected', error)
})
// 相当于
ajax('/api/user.json')
.then(function onFulfilled(res) {
console.log('onFulfilled', res)
})
.then(undefined, function onRejected(error) {
console.log('onRejected', error)
})
.catch形式和前面then里面的第二个参数的形式,两者异常捕获的区别:
- .catch()是对上一个.then()返回的promise进行处理,不过第一个promise的报错也顺延到了catch中,而thrn的第二个参数形式,只能捕获第一个promise的报错,如果当前then的resolve函数处理中有报错是捕获不到的。
所以.catch是给整个promise链条注册的一个失败回调。推荐使用
全局对象上的unhandledrejection事件
还可以在全局对象上注册一个unhandledrejection事件,处理那些代码中没有被手动捕获的promise异常,当然并不推荐使用。
更合理的是:在代码中明确捕获每一个可能的异常,而不是丢给全局处理
// 浏览器环境
window.addEventListener('unhandledrejection', event => {
// reason => Promise 失败原因,一般是一个错误对象
// promise => 出现异常的Promise对象
const { reason, promise } = event
console.log(reason, promise)
event.preventDefault()
}, false)
// node环境
process.on('unhandledRejection', (reason, promise) => {
//reason => Promise 失败原因,一般是一个错误对象
//promise => 出现异常的Promise对象
console.log(reason, promise)
})
在说Generator之前先来了解下协程
协程
传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。
协程有点像函数,又有点像线程。它的运行流程大致如下:
- 第一步,协程A开始执行。
- 第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
- 第三步,(一段时间后)协程B交还执行权。
- 第四步,协程A恢复执行。
上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。
举例来说,读取文件的协程写法如下。
function asnycJob() {
// ...
var f = yield readFile(fileA);
// ...
}
上面代码的函数 asyncJob 是一个协程,它的奥妙就在其中的 yield 命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。
协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作。
Generator
Generator生成器函数的使用
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。 下面就是一个 Generator 函数,它不同于普通函数,是可以暂停执行的,所以函数名之前加了一个星号,以示区别。 整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。
function * foo() {
console.log('invoke start')
const value = yield 'foo'
console.log(value)
}
const g = foo()
g.next(); // invoke start {value: "foo", done: false}
g.next('end'); // end {value: undefined, done: true}
上面代码中,调用 Generator 函数会返回一个内部指针(遍历器)g。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针 g 的 next 方法,会移动内部指针指向第一个遇到的 yield 语句,上例是执行到 'foo'为止。传入 next 方法的参数会作为上个阶段异步任务的返回结果(yield 语句的返回值)。
换言之,next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法会返回一个对象,表示当前阶段的信息(value 属性和done 属性。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
Generator生成器函数错误处理
Generator 函数内部可以部署错误处理代码,捕获函数体外抛出的错误。
function * gen(x) {
let y
try {
y = yield x + 2
} catch(e) {
console.log(e)
}
return y
}
const g = gen(1)
g.next();
g.throw('error') // error
上面代码的最后一行,使用指针对象 g 的 throw 方法抛出的错误,可以被函数体内的 try...catch 代码块捕获。这意味着,出错的代码与处理错误的代码实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
Generator 函数执行异步任务
下面示例使用了嵌套写法,用来展示一个生成器函数执行的演变过程
function * main() {
yield ajax('/api/user.json')
yield ajax('/api/article.json')
yield ajax('/api/book.json')
}
const g = main()
const result = g.next()
result.value.then(data => { // 此处代码可以使用递归进行重写
const result2 = g.next(data)
if(result2.done) { return }
result2.value.then(data => {
const result3 = g.next(data)
if(result3.done) { return }
result3.value.then(data => {
g.next(data)
// ......
})
})
})
上面的代码可以使用递归来进行封装
function * request() {
yield ajax('/api/user.json')
yield ajax('/api/article.json')
yield ajax('/api/book.json')
}
const g = request()
function handleResult(result) { // 将此处封装为一个生成器函数执行器
if(result.done) { return }
result.value.then(data => {
handleResult(g.next(data))
})
}
handleResult(g.next())
将以上代码进一步封装为一个生成器函数执行器
function * request() {
yield ajax('/api/user.json')
yield ajax('/api/article.json')
yield ajax('/api/book.json')
}
function co(generator) {
const g = generator()
function handleResult(result) {
if(result.done) { return }
result.value.then(data => {
handleResult(g.next(data))
})
}
handleResult(g.next())
}
co(request)
加入promise失败处理逻辑,进行完善
function * request() {
try {
yield ajax('/api/user.json')
yield ajax('/api/article.json')
yield ajax('/api/book.json')
} catch(error) {
console.log(error)
}
}
function co(generator) {
const g = generator()
function handleResult(result) {
if(result.done) { return }
result.value.then(data => {
handleResult(g.next(data))
}, error => {
g.throw(error)
})
}
handleResult(g.next())
}
co(request)
co 函数库可以让你不用编写 Generator 函数的执行器。
上面代码中,Generator 函数只要传入 co 函数,就会自动执行。
其实,co 函数库是著名程序员 TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行。
下面看看如何使用 Generator 函数(不使用嵌套不借助co函数),执行一个真实的异步任务。
const fetch = require('node-fetch');
function * gen() {
const url = 'https://api.github.com/users/github';
const result = yield fetch(url);
}
const g = gen();
const result = g.next();
result.value.then(function(data) {
return data.json();
}).then(function(data){
g.next(data);
});
async/await
一句话,async 函数就是 Generator 函数的语法糖。
上面有一个 Generator 函数,依次读取多个json数据。
function * request() {
yield ajax('/api/user.json')
yield ajax('/api/article.json')
yield ajax('/api/book.json')
}
function co(generator) {
const g = generator()
function handleResult(result) {
if(result.done) { return }
result.value.then(data => {
handleResult(g.next(data))
})
}
handleResult(g.next())
}
co(request)
用async/await改写
async function request() {
await ajax('/api/user.json')
await ajax('/api/article.json')
await ajax('/api/book.json')
}
const promise = request() // async函数调用会返回promise对象,利于整体控制
promisethen(data => {
console.log('进行整体控制')
})
一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。
async函数的优点
async 函数对 Generator 函数的改进,体现在以下三点。
(1)内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
(2)更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。