前因
目前主流的JavaScript环境都是以单线程模式去执行代码的,而采用单线程模式工作的原因与它最早的设计初衷有关,JavaScript最早是运行在浏览器中的脚本语言,它的目的是为了实现页面上的动态交互,而实现页面交互的核心就在于DOM操作,而这也导致了JavaScript必须以单线程模式来执行,否则就会出现复杂的线程同步问题。
后果
假设我们JavaScript执行环境中有多个线程同时工作,其中一个线程修改了DOM元素,而另外一个线程同时又删除了这个元素, 那么浏览器就无法判断以哪个线程的执行结果为准,为了避免这种问题的出现,从一开始JavaScript就被设计为了单线程模式工作,而这也成为这门语言最为核心的特性之一。
解决方案
要明确的是,这里所说的单线程指的是:JS执行环境中复杂执行代码的线程只有一个。
在这种情况下,容易出现某段代码执行耗时过长,假死的情况,而为了解决这种问题,JavaScript将任务的执行模式分成了两种:
- 同步模式
- 异步模式
异步编程概要
- 同步模式与异步模式
- 事件循环与消息队列
- 异步编程的几种方式
- 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')
JS执行引擎首先会把整体的代码全部加载进来,然后会在Call stack(调用栈)中压入一个匿名的调用之中,这个匿名的调用我们可以将其理解为把所有的代码打包放入了一个匿名函数中,然后去逐行执行这个匿名函数中的代码。
第一行代码 console.log('global begin')
开始执行的时候会被压入调用栈中执行,执行完毕之后就会从调用栈中弹出,接下来是两段函数的声明,这里要注意,无论是函数还是变量的声明都不会产生任何的调用,所以代码会继续往下执行。
下一行的代码是 foo()
函数的调用,它也一样会在执行时被压入调用栈之中,如此循环往复的往下执行最后调用栈被清空,整个任务也就执行完毕。
异步模式
与同步模式不同的是,异步模式不会等到前一个任务彻底结束才去执行下一个任务,对于耗时的任务执行操作,异步模式会在开启耗时任务后会立即执行下个任务,而耗时任务的后续逻辑我们会通过回调函数的方式去定义,使得耗时任务在其内部任务执行结束后会自动执行我们传入的回调函数。
基于异步模式我们可以在单线程的JavaScript中同时处理大量的耗时任务。
(注:无论是同步模式还是异步模式指的都不是我们代码的编写方式,而是指执行环境提供的API是以同步还是异步模式的方式工作的。)
回调函数
所有异步方程的解决方案都是基于回调函数
你可以把回调函数看作是一件你想要做的事情,你明确的知道这件事情应该怎么做,但是你却不清楚这件事情所依赖的任务什么时候能完成,所以最好的处理方式是将这件事情的步骤写在函数当中交给异步任务的执行者,而异步任务的执行者是清楚异步任务是什么时候结束的,那么它就可以在异步任务结束后去执行这个待处理的函数(回调函数)
回调函数的定义:由调用者定义,交给执行者执行的函数
具体的使用也很简单,就是以函数作为参数去传递
Promise 概述
如果我们直接使用传统回调方式去完成复杂的异步流程的话,很容易形成大量的异步函数嵌套,也就是我们常说的异步回调地狱问题。
而为了避免这方面的问题产生,CommonJs社区提出了Promise的规范,目的是为了给异步编程提供一种更好的统一解决方案。
这一规范在ES2015中被标准化,成为了语言规范。
那到底什么是Promise呢?
Promise实际上是一个对象,用来表示一个异步任务在执行过后其结果是成功还是失败,相当于内部对外部做了一个承诺,一开始这个承诺是处于一个待定的状态,这个状态我们称之为Pending,其结果有可能是成功(Fulfilled),也有可能是失败(Rejected)。
在结果确定的情况下,其所对应的任务也会相应执行,且明确结果的情况下将不会存在结果变更的可能。
简单的说Promise是ES2015提供的一个全局类型,我们可以使用它来构造一个Promise实例,也就是创建一个新的承诺。
而这样一个构造函数它需要接收一个函数作为参数,这个传入的函数我们可以理解为用于兑现承诺的逻辑,这一个函数会在构建Promise的过程当中被同步执行,这个函数的内部会接收两个参数,分别是 resolve
和 reject
,这两个参数也都是函数。
其中resolve
函数的作用就是将Promise对象的状态从待定(Pending)变更为成功(Fulfilled),一般来说,我们会将异步函数的执行结果通过resolve
的参数传递出去。
而在reject
函数中,我们将Promise对象的状态从待定(Pending)变更为失败(Rejected),我们会将错误的对象作为这一个函数的参数传入,作为这个承诺失败的理由传递出去。
// 这里用于'兑现'承诺
const promise = new Promise(function(resolve,reject){
resolve(100) //承诺达成
reject(new Error('promise rejected')) //承诺失败
})
在Promise实例创建完过后,我们可以使用then方法分别去指定成功或者失败过后的回调函数。
then方法第一个传入的参数是Fulfilled成功过后要执行的回调函数,第二个参数则是Rejected失败过后要执行的回调函数,我们在这个两个回调函数中分别打印其得到的参数。
promise.then(function(value){
console.log('resolved',value)
},function(error){
console.log('rejected',error)
})
Promise 使用案例
- 这里演示一个Promise封装ajax的例子,以下是实现步骤:
- 定义一个ajax的函数
- 为这个函数设置一个url的形参,用来接收请求地址
- 在函数内部我们返回一个Promise对象,相当于对外作出一个承诺
- 然后在这个Promise对象的内部执行的逻辑当中我们使用XMLHttpRequest对象去发送ajax请求
- 接着使用XMLHttpRequest实例化的xhr设置请求方式为GET,其请求地址为传入的url
- 再把响应类型xhr.responseType设置为JSON(这里要注意的是,这是HTML5的新特性,它能让我们在请求结束直接拿到一个JSON对象,而不是一个字符串)
- 通过xhr注册一个onload事件(这也是HTML5的新特性),该事件代表的是请求完成过后执行的事件
- 在onload事件当中我们再对当前的请求状态进行判断
- 如果请求的状态为200,说明请求成功,此时我们调用resolve,并向其传入请求成功的响应结果
- 如果请求的状态非200,说明请求失败,此时我们调用reject,并向起传入代表请求失败的错误信息对象(在这里我们传入的就是状态文本)
- 最后我们再使用xhr的send方法来完成这个异步请求,到这里promise版本的ajax就封装好了。
- 在ajax函数封装好后,我们先往这个函数传入一个地址
'/users.json'
来调用它,该地址指向的文件是我们提前准备好的用来模拟API接口的一个JSON文件 - 调用成功后会返回一个Promise对象,我们可以通过这个对象的then方法去指定回调,接着在成功的回调函数当中,我们打印请求的结果,在失败的回调函数中我们打印错误对象
- 此时我们只需要保存文档运行代码,就可以看到请求被正常发送了,且可以通过成功的回调函数拿到我们想要的结果
- 如果我们传入一个错误的或者是不存在的请求地址去调用该请求函数,我们就会得到一个错误对象的返回结果
// 预先准备好的JSON文件
// users.json
[
{
"name":"zce",
"age":24
},
{
"name":"alan",
"age":25
}
]
function ajax (url){
return new Promise (function(resolve,reject){
var xhr = new XMLHttpRequest()
xhr.open('GET',url)
xhr.responseType = 'json'
xhr.onload = function(){
if (this.status == 200){
resolve(this.response)
}else{
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
ajax('/users.json').then(function(res){
console.log(res)
},function(error){
console.log(error)
})
以上就是一个关于Promise基本使用的完整实例
—————————————————— END ——————————————————
菜鸡前端第一次写技术博文,还很粗糙,不足之处多多指教
作者:礁石很忙
公众号:JerrySun_2077