同步与异步
同步行为和异步行为是计算机科学的一个基本概念,可以理解为程序执行的两种模式;
在JavaScript这种 单线程事件循环模型 中,异步是十分重要的,异步行为可以优化计算量大导致的运行事件长的操作,它可以在等待其他操作完成的同时,即使运行其他指令,系统也能保持稳定
而异步行为也并非只针对那些计算量大或者运行事件长的,只要你不想因为等待某个操作而阻塞线程执行,那么都可以使用异步行为来优化
1)同步行为
在同步行为下,每条指令都会严格按照代码出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。
程序执行的每一步,都可以推断出程序的状态。这是因为后面的指令总是在前面的指令完成后才会执行。同步代码可以轻易的分析出程序在执行到任意代码位置时的状态。
2 ) 异步行为
异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待,所以的请求都会封装成异步操作。 相反的,我们可以将“异步”看成是“不同步伐的执行代码,且异步代码是在不同的跑道那么指令的执行顺序当然是不可预知的。
let x=3;
setTimeout(()=>x=x+4,1000);
//一秒后将箭头函数推入到执行流的最后端
在上段代码中,第1排和第2排是同步代码,但是第2排中的箭头函数是异步代码。或者说是一个回调的过程,在1秒钟以后,将该函数推入事件流执行的队列中去。
第二个指令块(加操作及赋值操作)是由系统计时器触发的,这会生成一一个入队执行的中。到底什么时候会触发这个中断,这对JavaScript运行时来说是一个黑盒, 因此实际上无法预知
为了让后续代码能够使用X,异步执行的函数需要在更新x的值以后通知其他代码,在早期的JavaScript中,只支持定义回调函数来表现异步操作完成。
let x = 3;
setTimeout(()=>{
x=x+4;
console.log(x);
})
异步返回值
假设 setTimeout 操作会返回一个有用的值。有什么好办法把这个值传给需要它的地方?广泛接受的一个策略是给异步操作提供一个回调,这个回调中要包含 ”使用异步返回值 的代码”(作为回调的参数)
function test(name){
setTimeout(()=>{
let userName = name + ",你好!";
return userName;
})
}
var a = test("张三");
console.log(a);
function test(name,fun){
setTimeout(()=>{
let userName = name + ",你好!";
fun(userName);
})
}
test("张三",acy);
function acy(userName){
console.log(userName);
}
在JS中,异步的特性并没有我们想象的那么简单,之前我们讲到:关于 JavaScript 单线程事件循环模型的特性,那么在JS代码运行时,第一轮执行的同步代码永远无法获取到回调时操作,或返回的数据
JavaScript 是单线程的,所以每次只能执行一段代码。为了调度不同代码的执行,JavaScript 维护了一个任务队列。其中的任务会按照添加到队列的先后顺序执行。setTimeout()的第二个参数只是告诉 JavaScript 引擎在指定的毫秒数过后把任务添加到这个队列。如果队列是空的,则会立即执行该代码。如果队列不是空的,则代码必须等待前面的任务执行完才能执行。
function test(name){
setTimeout(()=>{
let userName = name + ",你好!";
return userName;
})
}
var a = test("张三");
console.log(a); //输出 undefined
回调地狱
如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。在实际的代码中,这就要求嵌套回调
var num = 4;
setTimeout(() => {
num--;
console.log("1秒后:" + num);
setTimeout(() => {
num--;
console.log("1秒后:" + num);
setTimeout(() => {
num--;
console.log("1秒后:" + num);
}, 1000);
}, 1000);
}, 1000);
期约
期约的定义,使用
Promise是ES6新增的一个内置类(不兼容IE浏览器),我们叫它期约,或者约定,和容类语言类似,意思是对某个现阶段不存在的对象进行约定操作
promise是一种设计模式(承诺者模式)
该设计模式的组成:立承诺==》根据承诺执行动作
要干什么事情,状态(成功,失败),成功/失败后对应要做的事情,后续需要做的上去按照之前的承诺去执行进行了
实现步骤
1.设立一个承诺:也就是创建了一个promise实例
2.在promise实例中,传递的初始必须是一个函数,否则会报错,
(该函数是一个可执行函数,一般是一段用于管理异步。)
3、在 “new Promise” 时,会将传递进去的函数立即执行
4、该函数中有两个行参(resolve&reject),这两个参数的参数类型是新的函数
5、为了在需要的时候才去创建Promise对象,我们一般将创建Promise对象的代码封装在一个函数中。
then()方法
保证匿名内部函数的代码运行完后再执行后面的代码
then()是为期约实例添加处理程序的主要方法
var num = 4;
setTimeout(() => {
num--;
console.log("1秒后:" + num);
setTimeout(() => {
num--;
console.log("1秒后:" + num);
setTimeout(() => {
num--;
console.log("1秒后:" + num);
}, 1000);
}, 1000);
}, 1000);
then方法接收的两个参数实际上是针对期约的结果而执行的处理程序。分别是:onResolved(成功) 处理程序和onRejected(失败) 处理程序 1、这两个参数都是可选的,如果提供的话,则会在期约分别进入“兑现”和“拒绝”状态时执行。
2、当 promise 期约实例返回的结果为 resolve(兑现)时,执行then方法的onResolved 处理程序(即then的第一个参数)
3、当 promise 期约实例返回的结果为 reject(解决)时,执行then方法的onRejected 处理程序(即then的第二个参数)
var pro1 = new Promise((resolve,reject)=>{resolve()});
var pro2 = new Promise((resolve,reject)=>{reject()});
function onReslved(){
console.log("期约允许");
}
function onRejected(){
console.log("期约拒绝");
}
pro1.then(()=>{onReslved()},()=>{onRejected()});
pro2.then(()=>{onReslved()},()=>{onRejected()});
then()一定要在期约有响应的状态下执行;
期约的三种状态
待定pending
允许resolved/fulfilled
拒绝rejected
注:这里的兑现、拒绝并不是指程序运行上的成功或失败(报错),而只是指程序逻辑上的成功或失败。
(比如需要返回1:返回成功resolved,返回2失败rejected)
var pro1 = new Promise((resolve,reject)=>{resolve()});
var pro2 = new Promise((resolve,reject)=>{reject()});
function onReslved(){
console.log("期约允许");
}
function onRejected(){
console.log("期约拒绝");
}
pro1.then(()=>{onReslved()},()=>{onRejected()});
pro2.then(()=>{onReslved()},()=>{onRejected()});
期约状态、then联用:当期约实例中的状态函数被调用时,还会触发then方法的调用。并且还可以给then中的方法传递参数使用
链式操作
通过链式操作,我们可以连续的对期约求值,按顺序的进行异步操作。同样要使用到 then 方法。
例:我们在商城购买东西时,一般需要经过一下3个步骤:选择商品、下单购买、收快递。不管单独动作的执行时间是多少,但这一系列动作一定是有先后顺序的。
function task1(){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log("选择商品");
resolve(["鞋子","耐克"]);
},3000);
});
}
function task2(){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log("下单商品");
resolve("1299");
},3000);
});
}
function task3(){
return new Promise(function(resolve,reject){
setTimeout(()=>{
console.log("取快递");
resolve({name:'张三',tal:'123456'})
},3000)
})
}
// task1().then(task2().then(task3().then()))
task1().then(function(data){
console.log("您选择了商品");
console.log(data);
return task2();
}).then(function(data){
console.log("你支付了¥");
console.log(data);
return task3();
}).then(function(data){
console.log("收取快递信息为");
console.log(data)
});
console.log(a);
var b = newPro(-2);
console.log(b);
var num = 4;
catch()方法
与“try......catch”语法类似,promise对象的catch方法也是用来捕获异常的。
catch方法更多的是使用在链式操作上,我们知道异步代码的报错是不会影响到第一轮的同步代码的。但是,当我们通过链式语法操作多个异步代码时,那么前一个期约的报错,会导致后面的期约停止运行。
在一节链式操作执行完以后,可以为该段链式进行异常捕获,在后面调用catch()方法即可。
同样,catch方法也接受一个内置的匿名方法,作为报错后执行的操作。
all()方法
Promise.all() 静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象,返回一个新期约 通过all创建的合成期约,必须等到所有内部期约都执行成功后,才会执行。如果所有期约都成功解决,则合成期约的解决值就是所有内部期约解决值的数组,且按照原本迭代器顺序返回。 如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由。之后再拒绝的期约不会影响最终期约的拒绝理由。(也就是只有最先出现的内部期约拒绝理由会展示到外部,其余的拒绝理由不会展示),但是其他内部拒绝理由任然会执行,只不过不展示到外部。
function wash(){
return new Promise(function(resolve,reject){
setTimeout(()=>{
console.log("洗杯子");
return resolve("杯子");
},1000)
})
}
function boil(){
return new Promise(function(resolve,reject){
setTimeout(()=>{
console.log("烧开水");
return resolve("开水");
},2000)
})
}
function caty(){
return new Promise(function(resolve,reject){
setTimeout(()=>{
console.log("拿茶叶");
return resolve("茶叶");
},3000);
})
}
Promise.all([wash(),boil(),caty()]).then(function(a){
console.log(a)
})
race()方法
Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个方法接收一个可迭代对象,返回一个新期约。
该方法返回的新期约,只会对第一个出现反应的内部期约有效果(无论状态)。同样的,其他内部期约还是会执行,只不过不会影响到外部期约。 与all相反,race方法只要有一个内部期约有结果,那么外部新期约就会执行
function run(){
let num = Math.ceil(Math.random() * 10);
console.log("我是"+num+"选手")
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log("我跑了"+num+"秒")
resolve(num);
},num*1000);
});
}
Promise.race([run(),run(),run()]).then(function(num){
console.log("第一名是:"+num);
});
异步函数
async/await 旨在解决利用异步结构组织代码的问题。为此,ECMAScript 对函数进行了扩展,为其增加了两个新关键字:async 和 await
async
关键字用于声明异步函数。这个关键字可以在函数声明,函数表达式,箭头函数和方法上。
使用async关键字可以人函数具有异步特征,但不会进行异步的操作,总体上其代码仍然同步求值
而在参数或闭包方面,异步函数仍然具有普通 JavaScript 函数的正常行为。(与普通代码在运行上并无区别)
1、没有使用 return 关键字:没有使用 return 关键字会返回一个 undefined,只不过和普通的函数返回 undefined 不也一样,他是返回一个 Promise 对象,只不过对象“兑现”内容为空。
2、使用 return 关键字返回值:这个值会被 Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象。在函数外部调用这个函数可以得到它返回的期约。
await
异步函数数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用 await关键字可以暂停 异步函数代码 的执行,等待期约解决。 await 关键字会暂停执行 关键字 后面的异步代码,让出 JavaScript 运行时的执行线程。(简单的理解为:会在解析到await关键字时停止执行,并让出线程)
真正可以体现异步函数作用的就是 “await” 关键字,“async” 关键字只是一个用于声明异步的标识。
await 关键字的用法与 JavaScript 的一元操作一样。它可以单独使用,也可以在表达式中使用
async function foo() {
console.log(2);
console.log(await 4);
}console.log(1);
foo();
console.log(3);
只有当 await关键字 解析到一个值时,才会有效
await 关键字,它并非只是等待一个值可用那么简单。JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。