Node.js:ES6 模块化 & Promise
ES6 模块化
在Node.js
中,遵循的是CommonJS
的模块化规范,使用require
方法导入模块,使用moudule.exports
导出模块。
在JavaScript
中,存在很多模块化的规范,后端常用CommonJS
,前端常用AMD
、CMD
等。
这导致JavaScript
没有一个统一的方式完成模块化,增加了学习成本,为此推出了统一的ES6
模块化规范,不论前后端,只要是JavaScript
都可以支持这套规范。
规定:
- 每个
.js
文件都是一个独立的模块 - 导入模块使用
import
- 共享模块成员使用
export
想要在Node.js
中使用ES6
模块化规范,需要在配置文件package.json
中添加一个键值对:
javascript
"type": "module"
这样就可以支持ES6
模块化了。
测试:
test.js
:
javascript
console.log("hello world")
main.js
:
javascript
import './test.js'
在main.js
中,直接import
另一个文件,就会把被导入文件内部的所有代码执行一次。
运行main.js
输出结果:
javascript
hello world
但是直接import
是拿不到模块内部的变量的,这需要在模块内部进行导出操作。
默认导入导出
默认导出语法:
javascript
export default {
导出变量1,
导出变量2,
......
}
默认导入语法:
javascript
import 变量名 from '模块'
这相当于const 变量名 = require('模块')
,只要在模块内部通过默认导出,就可以在外部导入模块时,拿到指定变量。
test.js
:
javascript
let n1 = 10
let n2 = 20
function hello() {
console.log('hello world')
}
export default {
n1,
hello
}
test.js
中,把变量n1
和函数hello
进行了默认导出。
main.js
:
javascript
import test_module from './test.js'
console.log(test_module)
main.js
中,默认导入了test.js
,结果导入在test_module
中。
输出结果:
javascript
{n1: 10, hello: ƒ}
拿到了n1
和hello
两个值,但是没有拿到n2
,因为n2
没有被导出。
按需导入导出
以上的所有功能,都可以通过require
进行完成,ES6
还支持按需的导入导出,只导入需要的变量。
按需导出:
javascript
export 变量
在声明变量时,可以添加export
关键字,那么该变量就可以被按需导出,只有在用户指定的情况下,该变量才会被导出。
按需导入:
javascript
import { 变量1, 变量2 } from '模块'
导入时,使用{}
进行按需导入,把要导入的变量名写在内部,表示只导入这些变量。
test.js
:
javascript
let n1 = 10
export let n2 = 20
export function bye() {
console.log('bye bye')
}
export default {
n1
}
默认导出n1
,按需导出n2
和bye
。
main.js
:
javascript
import {n2, bye} from './test.js'
console.log(n2)
bye()
按需导入n2
和bye
。
输出结果:
javascript
20
bye bye
成功导入了两个按需导出的变量,但是没有导入n1
,因为n1
是默认导出的。
main.js
进行默认导入:
javascript
import test_moudle from './test.js'
console.log(test_moudle)
输出结果:
javascript
{n1: 10}
此时只能拿到n1
,因为n2
和bey
是按需导出的,默认导出不会导出按需导出的变量。
另外的,导入时可以同时执行按需导出和默认导出:
javascript
import test_moudle, { n2 } from './test.js'
以上代码默认导出了test_moudle
,并按需导出了n2
,这个过程中bye
没有被导出,如果想要导出bey
,只需要加到{}
内部:
javascript
import test_moudle, { n2, bye } from './test.js'
对于按需导出的变量,{}
内部的变量名必须和模块内部的变量名完全一致。但是这有可能导致命名冲突。
示例:
javascript
import { n2, bye } from './test.js'
const n2 = 1
在按需导入时,导入了n2
变量,但是当前模块也有n2
变量,这就导致命名冲突。
在按需导入时,可以进行变量重命名:
javascript
import { 变量名 as 新名 } from '模块'
示例:
javascript
import { n2 as test_n2, bye } from './test.js'
const n2 = 1
按需导入n2
时,将n2
重命名为test_n2
,这样就不会发生命名冲突了。
Promise
由于JavaScript
是单线程异步的模型,如果想让函数按照指定顺序执行,常常采用回调函数的形式。
比如以下三个函数:
javascript
function asyncOperation1(next) {
console.log('Operation 1 completed')
next()
}
function asyncOperation2(next) {
console.log('Operation 2 completed')
next()
}
function asyncOperation3(next) {
console.log('Operation 3 completed')
next()
}
每个函数都接受一个next
回调,现在希望按顺序依次执行这三个函数:
javascript
asyncOperation1(() => {
asyncOperation2(() => {
asyncOperation3(() => {
console.log('All operations completed');
});
});
});
此时就会变成一个多级嵌套的结构,虽然可以解决问题,但是这样的代码很不优雅,看起来也很费劲。
在ES6
版本后,可以使用Promise
解决这种多级嵌套结构,让函数之间解耦。
使用Promise
优化后,代码大致如下:
javascript
asyncOperation1()
.then(() => asyncOperation2())
.then(() => asyncOperation3())
.then(() => {
console.log('All operations completed');
})
可以看到,asyncOperation2
和asyncOperation3
这两个函数,都在同一级缩进中,就算后面再加多少个函数,都只会在这样的同一级缩进中:
javascript
asyncOperation1()
.then(() => asyncOperation2())
.then(() => asyncOperation3())
.then(() => asyncOperation4())
.then(() => asyncOperation5())
.then(() => asyncOperation6())
.then(() => asyncOperation7())
.then(() => {
console.log('All operations completed');
})
这样可以保证1 - 7
按顺序调用,并且避免了多级嵌套的情况。
构造
Promise
的功能,更多是完成异步任务,其可以标识一个函数的执行状态以及执行结果,并在一个异步任务接触后,进行下一步操作。
Promise
的本质是一个构造函数,格式如下:
javascript
Promise((resolve, reject) => {
// 函数体
})
该构造接收一个函数,函数内包含两个参数:
resolve
:表示函数执行成功reject
:表示函数执行失败
示例:
javascript
const p = new Promise((resolve, reject) => {
let n = Math.floor(Math.random() * 10) + 1
if (n % 2 == 0)
resolve()
else
reject()
})
这是一个随机函数,生成1 - 10
的随机数n
,依据n % 2
的结果,选择调用不同的函数resolve
或reject
,这样就会产生两种不同的状态。
这个Promise
函数构造出的对象,可以使用.then
方法传入resolve
和reject
对应的回调函数,如果Promise
的状态为resolve
就执行第一个函数,如果状态为reject
就执行第二个函数。
示例:
javascript
p.then(() => {
console.log("success!")
}, () => {
console.log("fail!")
})
p
是刚才构造出的对象,通过then
的两个参数,传入回调函数,并且.then
会等待到p
构造函数内部的函数执行完毕后,再依据其状态选择调用两个函数之一。
现在就可以尝试优化之前的asyncOperation
函数:
javascript
function asyncOperation1() {
return new Promise((resolve) => {
console.log('Operation 1 completed');
resolve();
});
}
asyncOperation1
要调用.then
方法,来指定下一个执行的函数,因此该函数的返回值必须是一个Promise
对象,在对象内部,调用resolve
就是在调用下一个函数。
以上Promise
构造函数没有用到reject
这个参数,因为它不需要分情况回调不同的函数,所以只需要一个参数即可。
类似的,后两个函数如下:
javascript
function asyncOperation2() {
return new Promise((resolve) => {
console.log('Operation 2 completed');
resolve();
});
}
function asyncOperation3() {
return new Promise((resolve) => {
console.log('Operation 3 completed');
resolve();
});
}
这样就形成了一个链式调用,上一个函数返回一个Promise
对象,并调用.then
方法传入下一个函数。这样就可以让函数按顺序执行,且可以一直追加.then
方法来拓展函数调用链条,避免嵌套式回调。
javascript
asyncOperation1()
.then(() => asyncOperation2())
.then(() => asyncOperation3())
.then(() => {
console.log('All operations completed');
})
状态
Promise
的功能,远远不止处理回调,其还有很多其它功能。
示例:
javascript
const p = new Promise((resolve, reject) => {
throw("err");
resolve()
})
p.then(() => {
console.log("success!")
console.log(p)
}, () => {
console.log("fail!")
console.log(p)
})
在Promise
中,只调用了resolve
方法,但是在调用该方法之前,throw
了一个异常,最后调用的不是resolve
而是reject
!
输出结果:
javascript
fail!
Promise {
[[PromiseState]]: 'rejected',
[[PromiseResult]]: 'err',
Symbol(async_id_symbol): 71,
Symbol(trigger_async_id_symbol): 36
}
可以看到,输出结果中,有一个[[PromiseState]]
属性,这个属性就是记录当前的Promise
状态,最终根据这状态,来决定调用resolve
还是reject
。
[[PromiseResult]]
存储着上一个函数的输出结果,这个可以直接在调用resolve()
和reject()
时作为参数传入。
示例:
javascript
const p = new Promise((resolve, reject) => {
resolve('OK!')
})
p.then(() => {
console.log("success!")
console.log(p)
}, () => {
console.log("fail!")
console.log(p)
})
这一次只调用了resolve
函数,并且调用时传入了一个参数OK!
,输出结果:
javascript
success!
Promise {
[[PromiseState]]: 'fulfilled',
[[PromiseResult]]: 'OK!',
Symbol(async_id_symbol): 71,
Symbol(trigger_async_id_symbol): 36
}
这一次的状态为fulfilled
表示成功,结果为OK!
。
Promise
分为以下三种状态:
pending
:默认状态,表示还不确定成功或失败fulfilled
:成功状态rejected
:失败状态
当一个Promise
对象创建时,为默认状态,如果调用了resolve()
就会变为fulfilled
状态,调用reject()
就会变为rejected
状态。
当Promise
对象调用then
方法时,依据当前的[[PromiseState]]
来决定下一个函数调用resolve
还是reject
。
切换状态的方法:
resolve
:从pending
变为fulfilled
reject
:从pending
变为rejected
thorw
:抛出一个异常,从pending
变为rejected
要注意的是,一个Promise
对象的状态只会切换一次,并且只能从pending
切换为fulfilled
或rejected
两者之一,后两者之间不能随意切换。
示例:
javascript
const p = new Promise((resolve, reject) => {
resolve('OK!')
reject('error')
})
这个函数中,最后p
的状态为fulfilled
,因为先执行了resolve
,从pending
切换为fulfilled
状态。第二次调用reject
,fulfilled
不能再切换为rejected
状态了。
then
在Promise
构造中,要求用户传入一个函数:
javascript
let p1 = new Promise((resolve, reject) => {
resolve('p1 OK!')
})
此时整个(resolve, reject) => {}
函数会立刻执行一次,有人就有疑问了,resolve
和reject
不是两个回调函数吗?如果没有通过.then
传入回调函数,p1
怎么知道自己要回调哪个函数?
刚讲解了Promise
对象的状态,可以得知resolve
和reject
的本质是在改变Promise
对象的状态,其实根本就没有去调用函数。
then
的作用,就是等待构造函数内部的函数执行完毕,再依据状态进行下一步操作。
示例:
javascript
let p1 = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('p1 OK!')
}, 10000)
})
console.log(p1)
在p1
中设置了一个延时,10s
之后把自己的状态设为成功。但是由于此时构成异步,console.log(p1)
会比 resolve('p1 OK!')
先执行,输出结果:
javascript
Promise {
[[PromiseState]]: 'pending',
[[PromiseResult]]: undefined,
Symbol(async_id_symbol): 71,
Symbol(trigger_async_id_symbol): 36
}
此时输出p1
的状态就是pending
,表示还不确定状态,结果也为undefined
。
修改代码:
javascript
let p1 = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('p1 OK!')
}, 10000)
})
p1.then(()=>{
console.log(p1)
})
此时p1.then
,会等待到p1
的状态确定后再执行,保证调用的顺序。
输出结果:
javascript
Promise {
[[PromiseState]]: 'fulfilled',
[[PromiseResult]]: 'p1 OK!',
Symbol(async_id_symbol): 71,
Symbol(trigger_async_id_symbol): 36
}
这次输出成功了,因为.then
会等到Promise
状态确定后,才执行内部的回调函数。
一个Promise
也可以绑定多个then
,一旦Promise
对象的状态完成切换,所有的then
都会被执行。
catch
catch
方法,可以指定一个错误处理函数reject
,当使用多个.then
调用Promise
时,可以使用.catch
处理所有Promise
发生的错误。
示例:
javascript
function func1(){
return new Promise((resolve, reject) => {
console.log('func1 successed')
resolve('OK!')
})
}
function func2(){
return new Promise((resolve, reject) => {
console.log('func2 successed')
resolve('OK!')
})
}
function func3(){
return new Promise((resolve, reject) => {
console.log('func3 error')
reject('error')
})
}
function func4(){
return new Promise((resolve, reject) => {
console.log('func4 successed')
resolve('OK!')
})
}
function funcErr() {
console.log("something err happend")
}
这四个函数都返回一个Promise
对象,最后一个函数funcErr
用于处理错误,那么调用逻辑就写为:
javascript
func1()
.then(func2, funcErr)
.then(func3, funcErr)
.then(func4, funcErr)
输出结果:
javascript
func1 successed
func2 successed
func3 error
something err happend
输出完func3
后,代码出现错误,此时触发funcErr
,不再执行func4
。
以上代码有两个优化点:
- 所有函数都使用
funcErr
处理错误,能不能一次性给他们指定错误处理函数 func3
出错后,func4
不再执行了,如果在funcErr
内部修理好了错误,能不能继续执行func4
以上问题都可以通过cache
解决。
javascript
func1()
.then(func2)
.then(func3)
.then(func4)
.catch(funcErr)
在所有then
的末尾,添加一个catch
,传入funcErr
参数,只要有任意一个函数调用出错,都会执行catch
内部的函数。
输出结果:
javascript
func1 successed
func2 successed
func3 error
something err happend
可以看到,catch
捕捉到了func3
产生的错误,但是func4
依然没有执行。
为此,可以把.cache
的位置修改一下:
javascript
func1()
.then(func2)
.then(func3)
.catch(funcErr)
.then(func4)
当.catch
执行完毕后,会返回一个Promise
对象,状态为成功,这样就可以继续调用func4
了:
输出结果:
javascript
func1 successed
func2 successed
func3 error
something err happend
func4 successed
all
all
是Promise
的一个方法,其传入一个Promise
对象的数组。
语法:
javascript
Promise.all([Promise, Promise ...])
如果所有Promise
对象执行成功,那么all
返回一个成功的Promise
对象,如果任意一个Promise
对象执行失败,返回一个失败的Promise
对象。
这里所谓的成功或失败的Promise
对象,其实就是对象的状态分别为fulfilled
和rejected
。
示例:
javascript
let p1 = new Promise((resolve, reject) => {
resolve('p1 OK!')
})
let p2 = new Promise((resolve, reject) => {
resolve('p2 OK!')
})
let p3 = new Promise((resolve, reject) => {
resolve('p3 OK!')
})
let ret = Promise.all([p1, p2, p3])
ret.then(() => {
console.log(ret)
})
此处定义了三个Promise
,最后使用Promise.all([p1, p2, p3])
,得到一个Promise
对象ret
,输出这个ret
。
此处注意,最后不能直接console.log(ret)
,要放进.then
中,因为定义p1
、p2
、p3
的过程是异步的,执行console.log(ret)
时,前三者可能还没有执行完。
输出结果:
收到的Promise
对象中,状态为成功,结果是一个数组,分别是p1 p2 p3
的输出结果。
修改代码:
javascript
let p1 = new Promise((resolve, reject) => {
resolve('p1 OK!')
})
let p2 = new Promise((resolve, reject) => {
reject('p2 err!')
})
let p3 = new Promise((resolve, reject) => {
resolve('p3 OK!')
})
let ret = Promise.all([p1, p2, p3])
ret.catch(error => {
console.log(ret)
});
此处p2
改为错误的Promise
,并且最后变为ret.catch
,如果不捕获错误的话会报错。
输出结果:
输出结果是p2
的内容。
返回值:
- 如果数组中所有的
Promise
成功,返回的Promise
也成功,结果是一个数组,包含所有Promise
的结果 - 如果数组中任意一个
Promise
失败,返回的Promise
也失败,结果是失败的那个元素的结果 - 如果数组中多个
Promise
失败,返回的Promise
也失败,结果是最早失败的那个元素的结果
race
race
也是Promise
的一个方法,其传入一个Promise
对象的数组。
语法:
javascript
Promise.race([Promise, Promise ...])
race
意为赛跑,当数组中任意一个Promise
确定成功或失败,race
直接返回这个Promise
。
其实就是得到执行速度最快的Promise
。
async
async
是一个关键字,被async
修饰的函数,返回值会变成一个Promise
对象。
语法:
javascript
async function name{
}
只需要在function
前增加一个async
关键字即可。
返回值如下:
- 如果返回值是一个非
Promise
类型,或者没有返回值,那么返回一个状态为成功的Promise
对象 - 如果返回值是一个
Promise
类型,原先是什么就返回什么 - 如果函数抛异常了,返回一个状态为失败的
Promise
对象
示例:
javascript
async function test()
{
return "hello"
}
console.log(test())
输出结果:
javascript
Promise {
[[PromiseState]]: 'fulfilled',
[[PromiseResult]]: 'hello',
Symbol(async_id_symbol): 71,
Symbol(trigger_async_id_symbol): 36
}
此时返回值变为了Promise
对象,类型是成功,原本的返回值hello
存储在了[[PromiseResult]]
中。
测试抛异常:
javascript
async function test()
{
throw "error"
}
console.log(test())
输出结果:
javascript
Promise {
[[PromiseState]]: 'rejected',
[[PromiseResult]]: 'error',
Symbol(async_id_symbol): 71,
Symbol(trigger_async_id_symbol): 36
}
得到的返回值就是一个rejected
的Promise
对象,抛出的异常存储在了[[PromiseResult]]
中。
await
await
是一个表达式,可以快速拿到Promise
存储的值。await
必须写在async
函数中,但是async
函数内可以没有await
。
await
右侧接收一个表达式:
- 如果表达式是成功的
Promise
对象,返回对象存储的值 - 如果表达式是失败的
Promise
对象,抛出一个异常 - 如果表达式不是
Promise
对象,直接返回表达式值
示例:
javascript
async function test()
{
let p = new Promise((resolve, reject)=>{
resolve('OK!')
})
console.log(await p)
console.log(await 'hello')
}
test()
输出结果:
javascript
OK!
hello