目录
JS采用单线程模式工作的原因
最早js语言就是运行在浏览器端的语言,目的是为了实现页面上的动态交互。实现页面交互的核心就是DOM操作,这就决定了它必须使用单线程模型,否则就会出现很复杂的线程同步问题。
假设在js中有多个线程一起工作,其中一个线程修改了这个DOM元素,同时另一个线程又删除了这个元素,此时浏览器就无法明确该以哪个工作线程为准。所以为了避免线程同步的问题,从一开始,js就设计成了单线程的工作模式。
所以,js执行环境中负责执行代码的线程只有一个。
一个人执行一个任务,如果有多个任务,那任务需要排队,让这个人一个一个去执行。
单线程的优势和弊端
这种模式最大的优势就是更安全,更简单,缺点也很明确,就是如果中间有一个特别耗时的任务,其他的任务就要等待很长的时间,出现假死的情况。
为了解决这种问题,js有两种任务的执行模式:同步模式(Synchronous)和异步模式(Asynchronous)。
异步编程的内容概要
- 同步模式与异步模式
- 事件循环与消息队列(js如何实现异步模式)
- 异步编程的几种方式
- Promise异步方案、宏任务/微任务队列
- Generator异步方案、Async/Await语法糖
同步模式与异步模式
同步模式
同步模式 :指的是代码的任务依次执行,后一个任务必须等待前一个任务结束才能开始执行。程序的执行顺序和代码的编写顺序是完全一致的。在单线程模式下,大多数任务都会以同步模式执行。
console.log('global begin')
function bar () {
console.log('bar task')
}
function foo () {
console.log('foo task')
bar()
}
foo()
console.log('global end')
// global begin
// foo task
// bar task
//global end
// 使用调用栈的逻辑
为了避免耗时函数让页面卡顿和假死,所以还有异步模式。
异步模式
异步模式 不会去等待这个任务的结束才开始下一个任务,都是开启过后就立即往后执行下一个任务。耗时函数的后续逻辑会通过回调函数的方式定义。在内部,耗时任务完成过后就会自动执行传入的回调函数。
异步模式对于JavaScript语言非常重要,没有它就无法同时处理大量的耗时任务。对于开发者而言。单线程下面的异步最大的难点就是代码执行的顺序混乱。
console.log('global begin')
// 延时器
setTimeout(function timer1 () {
console.log('timer1 invoke')
}, 1800)
// 延时器中又嵌套了一个延时器
setTimeout(function timer2 () {
console.log('timer2 invoke')
setTimeout(function inner () {
console.log('inner invoke')
}, 1000)
}, 1000)
console.log('global end')
// global begin
// global end
// timer2 invoke
// timer1 invoke
// inner invoke
//除了调用栈,还用到了消息队列和事件循环
js线程某个时刻发起了一个异步调用,它紧接着继续执行其他的任务,此时异步线程会单独执行异步任务,执行过后会将回调放到消息队列中,js主线程执行完任务过后会依次执行消息队列中的任务。这里要强调,js是单线程的,浏览器不是单线程的,有一些API是有单独的线程去做的。
这里的同步和异步不是指写代码的方式,而是运行环境提供的API是以同步或异步模式的方式工作。
同步模式API和异步模式API的特点
同步模式的API的特点就是任务执行完代码才会继续往下走,例如:console.log
异步模式的API的特点就是下达这个任务开启的指令之后代码就会继续执行,代码不会等待任务的结束
回调函数 —— 所有异步编程方案的根基
回调函数:由调用者定义,交给执行者执行的函数
// callback就是回调函数
// 就是把函数作为参数传递,缺点是不利于阅读,执行顺序混乱。
function foo(callback) {
setTimeout(function(){
callback()
}, 3000)
}
foo(function() {
console.log('这就是一个回调函数')
console.log('调用者定义这个函数,执行者执行这个函数')
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) => {
// 创建一个XMLHttpRequest对象去发送一个请求
const xhr = new XMLHttpRequest()
// 先设置一下xhr对象的请求方式是GET,请求的地址就是参数传递的url
xhr.open('GET', url)
// 设置返回的类型是json,是HTML5的新特性
// 我们在请求之后拿到的是json对象,而不是字符串
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 => {
const { reason, promise } = event
console.log(reason, promise)
//reason => Promise 失败原因,一般是一个错误对象
//promise => 出现异常的Promise对象
event.preventDefault()
}, false)
// node
process.on('unhandledRejection', (reason, promise) => {
console.log(reason, promise)
//reason => Promise 失败原因,一般是一个错误对象
//promise => 出现异常的Promise对象
})
以上内容源于拉勾教育《大前端高薪训练营》