JavaScript 默认是同步的,并且是单线程
的。这意味着代码不能创建新线程并并行运行。
一行一行的代码依次执行,例如:
1
2
3
4
5
6
|
const a = 1
const b = 2
const c = a * b
console.log(c)
doSomething()
|
一、回调
例如,无法知道用户何时会单击按钮。因此,为 click 事件定义了一个事件处理程序。此事件处理程序接受一个函数,该函数将在事件触发时调用:
1
2
3
4
|
document.getElementById('button').addEventListener('click', () => {
//item clicked
})
|
这就是所谓的回调。
1. 处理回调中的错误
任何回调函数中的第一个参数是错误对象:错误优先回调。这是 Node.js 采用的策略。
1
2
3
4
5
6
7
8
9
10
11
|
fs.readFile('/file.json', (err, data) => {
if (err) {
//handle error
console.log(err)
return
}
//no errors, process data
console.log(data)
})
|
2. 回调的问题
回调非常适合简单的情况!
然而,每个回调都会增加一层嵌套,当你有很多回调时,代码很快就会变得复杂:(回调地狱)
1
2
3
4
5
6
7
8
9
10
|
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
//your code here
})
}, 2000)
})
})
|
二、Promises (ES6)
Promise 是处理异步代码的一种方式,不会陷入回调地狱。
1. 创建 Promise
1
2
3
4
5
6
7
8
9
10
11
12
|
let done = true
const isItDoneYet = new Promise((resolve, reject) => {
if (done) {
const workDone = '这是我构建的东西'
resolve(workDone)
} else {
const why = '还在做别的事情'
reject(why)
}
})
|
2. 消费 Promise
1
2
3
4
5
6
7
8
9
10
11
12
13
|
const isItDoneYet = new Promise(/* ... as above ... */)
//...
const checkIfItsDone = () => {
isItDoneYet
.then(ok => {
console.log(ok)
})
.catch(err => {
console.error(err)
})
}
|
- 运行
checkIfItsDone()
指定在isItDoneYet
承诺解决(在then调用中)
或拒绝(在catch调用中)
时要执行的函数。
3. 链式 Promise
一个承诺可以返回到另一个承诺,创建一个承诺链。
链式 promise 的一个很好的例子是 Fetch API,可以使用它来获取资源并在获取资源时将一系列 promise 排队执行。
Fetch API 是一种基于 promise 的机制,调用fetch()相当于使用new Promise()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
const status = response => {
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response)
}
return Promise.reject(new Error(response.statusText))
}
const json = response => response.json()
fetch('/todos.json')
.then(status) // 请注意,status 函数实际上在这里被调用,并且它返回一个承诺
.then(json) // 同样,这里唯一的区别是这里的 json 函数返回一个用 data 解析的 promise
.then(data => { // ...这就是为什么 data 在这里显示为匿名函数的第一个参数
console.log('Request succeeded with JSON response', data)
})
.catch(error => {
console.log('Request failed', error)
})
|
4. 处理错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
new Promise((resolve, reject) => {
throw new Error('Error')
}).catch(err => {
console.error(err)
})
// or
new Promise((resolve, reject) => {
reject('Error')
}).catch(err => {
console.error(err)
})
|
5. 级联错误
如果在 catch() 内部引发错误,则可以附加第二个 catch() 来处理它,依此类推。
1
2
3
4
5
6
7
8
9
10
|
new Promise((resolve, reject) => {
throw new Error('Error')
})
.catch(err => {
throw new Error('Error')
})
.catch(err => {
console.error(err)
})
|
6. 协调 Promise
Promise.all()
如果需要同步不同的 promise,Promise.all()
帮助定义一个 promise 列表,并在它们全部解决后执行一些事情。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
const f1 = fetch('/something.json')
const f2 = fetch('/something2.json')
Promise.all([f1, f2])
.then(res => {
console.log('Array of results', res)
})
.catch(err => {
console.error(err)
})
// or
Promise.all([f1, f2]).then(([res1, res2]) => {
console.log('Results', res1, res2)
})
|
Promise.race()
Promise.race()
在您传递给它的第一个承诺解决(解决或拒绝)时运行,并且它只运行一次附加的回调,第一个承诺的结果得到解决。
1
2
3
4
5
6
7
8
9
10
11
|
const first = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'first')
})
const second = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'second')
})
Promise.race([first, second]).then(result => {
console.log(result) // second
})
|
Promise.any()
Promise.any()
在您传递给它的任何承诺实现或所有承诺被拒绝时解决。
它返回一个单一的承诺,该承诺以第一个已履行的承诺中的值进行解析。
如果所有 Promise 都被拒绝,则返回的 Promise 将被拒绝并带有 AggregateError
。
1
2
3
4
5
6
7
8
9
10
11
|
const first = new Promise((resolve, reject) => {
setTimeout(reject, 500, 'first')
})
const second = new Promise((resolve, reject) => {
setTimeout(reject, 100, 'second')
})
Promise.any([first, second]).catch(error => {
console.log(error) // AggregateError
})
|
7. 常见错误
未捕获的类型错误
Uncaught TypeError: undefined is not a promise
如果在控制台中收到 Uncaught TypeError: undefined is not a promise
错误,请确保使用 new Promise() 而不是 Promise()。
未处理的承诺拒绝警告
UnhandledPromiseRejectionWarning
这意味着调用的承诺被拒绝,但没有用于处理错误的捕获。 添加一个 catch 捕获
然后正确处理它。
三、async/await (ES2017)
Promise 是为了解决著名的回调地狱问题而引入的,但它们自身也引入了复杂性和语法复杂性。
async/await 是建立在 promises 之上的,用于解决这种复杂性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// Promise
const aFunction = () => {
return Promise.resolve('test')
}
aFunction().then(alert) // This will alert 'test'
// async
const aFunction = async () => {
return 'test'
}
aFunction().then(alert) // This will alert 'test'
|
更复杂的例子(多个异步函数串联):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
const promiseToDoSomething = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 10000)
})
}
const watchOverSomeoneDoingSomething = async () => {
const something = await promiseToDoSomething()
return something + '\nand I watched'
}
const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
const something = await watchOverSomeoneDoingSomething()
return something + '\nand I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then(res => {
console.log(res)
})
// I did something
// and I watched
// and I watched as well
|
四、事件循环
1. 重要性
事件循环是了解 Node.js 的最重要方面之一。
它解释了 Node.js 如何可以是异步的并且具有非阻塞 I/O,所以它基本上解释了 Node.js 作为 “杀手级应用程序” 为何如此成功。
Node.js JavaScript 代码在单线程上运行。一次只发生一件事。
这是一个实际上非常有用的限制,因为它大大简化了编程方式,而无需担心并发问题。
只需要注意如何编写代码并避免任何可能阻塞线程的事情,例如同步网络调用或无限循环。
2. 案例
举例来说,在大多数浏览器中,每个浏览器选项卡都有一个事件循环,使每个进程隔离,避免网页无限循环或繁重的处理来阻塞整个浏览器。
该环境管理多个并发事件循环,例如处理 API 调用。 Web Workers 也运行在他们自己的事件循环中。
主要需要担心代码将在单个事件循环上运行,并在编写代码时考虑到这一点,以避免阻塞它。
3. 阻塞事件循环
任何需要太长时间将控制权返回给事件循环的 JavaScript 代码都会阻塞页面中任何 JavaScript 代码的执行,甚至阻塞 UI 线程,并且用户无法四处点击、滚动页面等。
JavaScript 中几乎所有的 I/O 原语都是非阻塞的。
网络请求、文件系统操作等 阻塞是一个例外
,这就是为什么 JavaScript 如此多地基于回调,最近又有了 promise 和 async/await。
4. 调用栈
调用堆栈是一个 LIFO(后进先出
)堆栈。
事件循环不断检查调用堆栈以查看是否有任何函数需要运行。
这样做时,它会找到的任何函数调用添加到调用堆栈中,并按顺序执行每个调用。
您知道调试器或浏览器控制台中您可能熟悉的错误堆栈跟踪吗? 浏览器在调用堆栈中查找函数名称以通知您哪个函数发起当前调用:
5. 事件循环解释
1
2
3
4
5
6
7
8
9
10
11
12
|
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
bar()
baz()
}
foo()
|
当此代码运行时,首先调用 foo()。 在 foo() 中,首先调用 bar(),然后调用 baz()。
每次迭代的事件循环都会查看调用堆栈中是否有内容,并执行它:
直到调用栈为空。
1). 排队功能执行
上面的例子看起来很正常,没有什么特别之处:JavaScript 找到要执行的东西,按顺序运行它们。
让我们看看如何将函数推迟到堆栈清除为止。
setTimeout(() => {}, 0)
的用例是调用一个函数,但是代码中的每个函数都执行一次。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
baz()
}
foo()
// foo
// baz
// bar
|
当此代码运行时,首先调用 foo()。 在 foo() 中,首先调用 setTimeout,将 bar 作为参数传递,然后指示它尽可能快地立即运行,将 0 作为计时器传递。 然后调用 baz()。
此时调用堆栈如下所示:
这是程序中所有函数的执行顺序:
为什么会这样?
2). 消息队列
当 setTimeout() 被调用时,浏览器或 Node.js 启动计时器。 一旦计时器到期,回调函数被放入消息队列中。
消息队列也是用户发起的事件(如单击或键盘事件)或获取响应在您的代码有机会对其做出反应之前排队的地方。 或者还有像 onload 这样的 DOM 事件。
循环优先处理调用栈,它首先处理在调用栈中找到的所有东西,一旦里面没有任何东西,它就会去消息队列中取东西。
不必等待 setTimeout、fetch 或其他函数来做自己的工作,因为它们是由浏览器提供的,并且它们存在于自己的线程中。
例如,如果将 setTimeout 超时设置为 2 秒,则不必等待 2 秒 - 等待发生在其他地方。
6. ES6 作业队列
ECMAScript 2015 引入了 Job Queue
的概念,它被 Promises 使用(也在 ES6/ES2015 中引入)。
这是一种尽快执行异步函数结果的方法,而不是放在调用堆栈的末尾。
在当前函数结束之前解析的 Promise 将在当前函数之后立即执行。
例如,在游乐园坐过山车:
消息队列
把你放在队列的后面,在所有其他人的后面,你必须在那里等待
轮到你,
而作业队列
是快速通行证
这样就可以在完成上一次骑行后立即乘坐另一次骑行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
new Promise((resolve, reject) =>
resolve('应该在 baz 之后,在 bar 之前')
).then(resolve => console.log(resolve))
baz()
}
foo()
// foo
// baz
// 应该在 baz 之后,在 bar 之前
// bar
|
这是 Promises(和 Async/await,它建立在 Promise 之上)和通过 setTimeout() 或其他平台 API 实现的普通异步函数之间的巨大差异。
最后,这是上面示例的调用堆栈的样子: