社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
本人自学前端小白一个,最近初学ES6中的异步编程与Promise 对象的使用,但是由于视频教程讲解的比较模棱两可,所以便自己上网搜了一些资料,按照网上资料加上自己所学,写下了以下对于异步编程以及Promise对象的使用语法的一些简要的理解,如果其中有错误的地方,望各位大佬们指正!!
在理解异步之前,先理解同步的概念。
由于javascript是单线程
的,只能在JS引擎的主线程上运行的,所以js代码只能一行一行的执行,不能在同一时间执行多个js代码任务,这就导致如果有一段耗时较长的计算,或者是一个ajax请求等IO操作,如果没有异步的存在,就会出现用户长时间等待,并且由于当前任务还未完成,所以这时候所有的其他操作都会无响应【同步】。
在 JavaScript 当中,事件任务的调用执行顺序都是在 **任务队列容器【先来的先加载,后来的后加载】**中定义的,在任务队列中,事件任务都是以排队的形式分布,它们会按照代码中声明的顺序进行排队,JS的设计模式是单线程的,单线程的意思就是一次只完成一个任务,如果有多个任务,那么就必须排队,一个一个的执行,而这些乖乖排队的函数任务 也叫作 同步任务。
同步:指的就是后一个任务等待前一个任务执行完毕后,再执行,执行顺序与任务的排列顺序一致。
所以在程序执行中,如果用户想先执行某个操作,等一会再执行另外一个操作,这时由于单线程的同步机制,用户想要预先执行的这个操作必须要等待前面一个操作完成后,才能执行,这样可能会在程序中发生阻塞的情况。
看案例:
同步阻塞就是在执行的过程当中,主线程会一直盯着同步任务是否已经完成,未完成让其他任务先等着,完成了就执行下一个任务,在此过程中需要消耗时间来等待任务完成。
JS中的同步阻塞示例:
{
//这是一个阻塞式函数,将一个文件复制到另外一个文件上。
function copyfile(afile,bfile){
let result = copyfile(afile,bfile)
return result
}
//调用`copyfile` 函数,将一个大文件复制到另外一个文件中,将耗时一小时。意味着这个函数的结果将在一个小时后返回。
console.log('start copying ---')
let a = copyfile('A.txt','b.txt') // 这行程序将耗时一小时
if(a){
console.log('Finished!') // 这行程序将在一小时后执行
}else {
throw new Error('Copy File Failed--') // 这行程序将在一小时后执行
}
console.log('处理一下别的事情') // 这行程序将在一小时后执行
console.log("Hello World, 整个程序已加载完毕,请享用"); // 这行程序将在一小时后执行
}
// 最终报错:Maximum call stack size exceeded【超过最大调用堆栈大小】
以上案例可以看出,因为copyfile
函数返回值的过程需要漫长的时间,所以线程也无法继续执行下去,只能等待。所以可以看出,同步执行 发生的阻塞情况极大的影响了整个程序的执行效率以及耗时,用户体验就跟个傻逼一样。这时候就需要异步来解决此问题。
异步:也叫 非阻塞模式加载,专门用来解决一些在某种特定情况下才执行的行为。指的是在主线程运行程序的时候,能够脱离当前主线程的调用,由另外一种机制来代理执行此任务,两者之间互不影响,在此期间,主线程并不会管异步任务的执行,而当异步任务执行完毕之后,会主动通知主线程异步任务执行完毕。同时,在另外一个机制执行异步任务的时候,还可以让异步任务执行 异步微任务。
打个比方,所有人都在排队买票,每个人都是一个任务,排到张三的时候,张三说现在还不能确定买哪一个车次的票,需要等到明天才能决定,所以我先在这里等着。这种同步情况肯定是不合理的,会造成阻塞。最好的办法就是让张三站在一边,先想好买什么票,再开始排队,这样就不耽搁大家时间了。像这种执行某任务,它不能马上完成或者要满足某个条件才能完成的任务,就叫做 异步任务。
异步微任务:由异步任务调用出来的 ,它基于异步任务,但是又与异步任务有着不相同的其他小任务。
这就好比张三在排队的时候,退出排队另想买票时间的时候,处于等待状态【在任务队列中同步等待】,突然领导跟你打电话跟你交代任务,先暂停怎么买票的想法与老板打电话【执行异步微任务】,你打完电话之后,就继续想怎么买票的事情【还处于异步任务中】,最终在第二天到了【遇到并执行异步任务】,终于买好票了【异步任务执行完毕】,张三就直接回家休息了【弹栈】,这整个过程就是异步的操作。
console.log("程序时间:"+new Date().getTime());
setTimeout(function () {
console.log("暂停一秒:"+new Date().getTime());
}, 1000);
console.log('这是暂停一秒之后的时间:'+new Date().getTime());
以上代码体现了异步非阻塞式的情况,在主线程中,定时器设定了一个异步任务并执行,在此期间,主线程并不会管定时器是否已经完成,它会接着去执行下一步的代码,当定时器的异步任务执行完毕之后会主动通知主线程异步任务执行完毕。
这就是非阻塞【不需要程序等待任务完成,而是直接跳过需要等待的任务,执行下一个任务。】
老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。 1 老张把水壶放到火上,立等水开。(同步阻塞) 老张觉得自己有点傻 2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。 3 老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大 4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞) 老张觉得自己聪明了。
所谓同步异步,只是对于水壶而言。 普通水壶,同步;响水壶,异步。 虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。 同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。 立等的老张,阻塞;看电视的老张,非阻塞。 情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
但是JS是单线程的,那么是如何实现同步任务跟异步任务同时执行的呢?
答案是 事件轮询机制(Event Loop)
其实JS运行的环境在浏览器,且浏览器内部是多线程的,那么 JS 可以借助浏览器内部为程序中的异步任务开辟一个子线程,专门用于调用当前执行栈中正在执行的主任务【函数】中的异步任务。
也就是说JS提供给开发者的模式是单线程模式,但是在浏览器内部的处理机制是多线程同时运行的,相当于是浏览器内部专门为异步任务开辟了一条新的队列,异步任务则在这边这个队列中执行,JS 的调用栈还是继续执行它的 Event Loop。
为什么 JS 不用多线程?
这主要跟javascript的历史有关,js最开始只是为了处理一些表单验证和DOM操作而被创造出来的,所以主要为了语言的轻量和简单采用了
单线程的模式。
多线程模型相比
单线程要复杂很多,比如多线程需要处理线程间资源的共享问题,还要解决状态同步等问题。如果JS是多线程的话,当你要执行往div中插入一个DOM的操作的同时,另一个线程执行了删除这个div的操作,这个时候就会出现很多问题,我们还需要为此增加锁机制等。
原理:
JavaScript 写好的事件任务是排成一个队列【单线程】进入浏览器的,在浏览器这个执行环境下开始执行的时候,遇到同步立刻执行【主线程】,遇到异步开辟新的线程【子线程】。主线程上所有同步任务执行完毕,去循环子线程执行异步任务。
实现过程:
当JS脚本加载到浏览器,JS引擎首先会对JS 脚本进行预编译,在此阶段中,JS引擎会将检测到的 同步任务 以及 CallBack 异步任务分别放进任务队列容器中,并按照队列顺序【先加载先调用,后面的等待】的形式将第一个 同步任务推进调用栈中,使用主线程来执行函数,如果里面没有异步任务,那么就继续执行,如果检测到了异步任务,浏览器会开辟一个子线程,主线程会将异步任务放置在子线程当中等待合适时机被子线程调用执行,如:用户点击了某个控件产生回调函数。而在此过程当中,主线程会不断的检查查看任务队列容器中是否还有事件任务,如果有则继续拿取,这样不断循环,称之为 Event Loop 事件循环机制。
对于任务队列
,其实是有更细的分类。其被分为 微任务(microtask)队列
& 宏任务(macrotask)队列
宏任务: setTimeout、setInterval等,会被放在宏任务(macrotask)队列。
微任务: Promise的then、Mutation Observer等,会被放在微任务(microtask)队列。
Event Loop的执行顺序是:
首先执行执行栈里的任务。
执行栈清空后,检查微任务(microtask)队列,将可执行的微任务全部执行。
取宏任务(macrotask)队列中的第一项执行。
回到第二步。
注意: 微任务队列每次全执行,宏任务队列每次只取一项执行。
原理如图:
在JavaScript 中,实现异步操作的其实就是随处可见的回调函数。
回调函数是在满足了某种条件之后执行的任务,如果不满足就不执行,这就与同步任务不同,只要定义了同步任务必须执行,且其他同步任务必须等待此任务完成,回调函数不需要谁等它,它只要个触发条件。
而回调函数的表现形式主要有以下:
在操作某个对象的时候,对象会有固定的API来开发,这些API都有各自特定的功能且大多数都由回调函数作为参数,当对象执行了API之后,回调函数就会立即执行。
触发前提:必须为对象API定义了回调函数,只有定义了回调函数,那么回调函数就会随着事物执行了某个操作而触发回调函数,从而实现 异步辅助对象完成某个功能,不受主线程的影响。
回调函数的执行,最直观的例子就是 setTimeout定时器 以及 Ajax 数据交互操作。
定时器设定一个回调函数,规定在 n 秒后执行这个回调函数:
setTimeout(function (){
let hello = 'hello world'
console.log('1 s 后输出 内容'+ `${hello}`)
//1 s 后输出 内容hello world
},1000)
事件控制通常使用在操作 DOM元素的时候定义。
DOM 事件规定用户在操作DOM元素的时候满足某种条件则执行回调函数 来处理相关业务。例如:用户点击onclick, 鼠标移入 onmouseover… 等等状态。
{
document.querySelector('div').onclick = (e)=>{
throw new Error('点击元素,执行回调函数')
//Uncaught Error: 点击元素,执行回调函数
}
}
观察者模式也叫 发布/订阅模式,可以通过 Proxy 与 Reflect 实现代理监听异步操作。
{
// 代理器
function validator(target,validator){
// 返回一个代理对象
return new Proxy(target,{
_validator: validator, // 传入配置项【内含类型判断函数】
set(target,key,value,proxy){
// 重载原始数据对象的 set() 函数,让代理映射对象来拦截用户对数据更新的操作
if(target.hasOwnProperty(key)){
// 判断传入的数据对象中是否有 key 值,如果有为true
let va = this._validator[key]
//调用类型判断函数,判断当前传入的 key 值是否符合条件
if(!!va(value)){ // 如果 传入的 key 值条件为true
console.warn(`${key} 的值变成了 ${value}` )
// ${key} 在当前作用域中查找 key 变量【包括形参】,并提取此变量的内容。【类似于JSP的EL 表达式】
return Reflect.set(target,key,value,proxy)
// 则正式通过 Reflect.set() 为原始数据对象更改属性值。
}else {
throw Error(`不能将${key} 设置到 ${value} `) //否则抛出错误信息
}
}else {
throw Error(`${value} 不存在`) // 否则抛出错误信息
}
}
})
}
//配置项
const personValidators = {
// 定义类型判断函数。
// 一个人的属性构成,name 属性必须为 string类型
name(val){
return typeof val === 'string'
},
// 一个人的属性构成,age 属性必须为 number 类型 并且 大于 18
age(val){
return typeof val === 'number' && val > 18
}
}
// 原始数据对象构造类
class Person{
//定义一个人的构成。有 name 名称属性, age 年龄属性
constructor (name,age){
this.name = name
this.age = age
return validator(this,personValidators)
//this 指向当前创建的实例对象。
/* 在 Person 类对象构造时,调用代理器函数,返回一个代理对象,那也就是说,在Person初始化一个实例对象的时候, Person的实例对象就已经被代理器开始代理其中构造Person实例对象传入的 name 属性 与 age 属性被作为原始数据存在。*/
}
}
const person = new Person('lilei',18)
// person:代理对象。
console.info(person)
// 代理对象更改属性数据
person.name = 'Han meimei'
console.info(person)
}
虽然使用函数与事件控制两种形式已经满足了很多异步操作。但同时回调函数也有一些问题:
一、“回调地狱”
因为回调函数的特点:回调函数是作为异步任务的参数定义的,将回调函数嵌套在异步任务身上。如果此时需要有多个回调函数嵌套的情况,比如说后一个请求需要上一个请求的返回结果,过去的常规操作都是使用 CallBack 回调函数来层层嵌套,但当嵌套过多,就会出现 CallBack hell 问题【代码结构混乱,无法正常进行错误捕捉和处理【try catch 错误捕捉通常在回调函数中使用】,后期维护性查。】。例如:
foo(){
// 处理 foo 函数的业务
foo2 (){
// 处理 foo2 函数的业务
foo3(){
// 处理 foo3 函数的业务
foo4(){
try {
...
}catch (err){
...
}
}
}
}
}
以上函数结构,foo 函数调用了 foo2 函数,foo2 函数调用了foo3 函数,依次类推,在 foo4() 函数中设定了一个回调函数,回调函数中有错误捕捉代码,如果每个调用者都有一个回调函数作为参数的话。可以设想,这样的代码结构可读性极难阅读,且不好维护。
二、回调函数的执行不符合自然语言的线性思维方式,不容易理解,也就是代码结构不直观,没有体现出层次感,可读性差。
三、控制反转(控制权在其他人的代码上),假如我们的程序中使用的异步任务是别人的库,我们把对应的回调函数传进去,我们并不能知道异步任务在调用回调函数之外做了什么事情,不易于程序后期维护。
综上所诉,ES6为了提供多层回调函数嵌套的代码结构简洁,易读,同时维护性强,提供了 Promise对象及语法。
关于Promise对象简要理解,在下一个文章介绍:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!