JS高阶三(JS中的异步编程)
Posted 稻香Snowy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JS高阶三(JS中的异步编程)相关的知识,希望对你有一定的参考价值。
事实上,程序运行的部分和将来运行的部分之间的关系就是异步编程的核心。
任何时候,只有把一段代码包装成一个函数,并指定它在响应某个事件(定时器,鼠标点击,Ajax响应等)时执行,你就是在代码中创建了一个将来的执行的程序块,也由此在这个程序中引入异步机制。
本文主要讲述异步编程之前需要了解的其他相关知识。
目录
同步与异步
多线程
JavaScript单线程
并行于并发
javascript异步机制
并发模型
事件循环流程
再谈setTimeout(...0)
Web Works
javascript异步实现方法
一、同步与异步
所谓的同步编程,就是计算机一行一行按顺序依次执行代码,当前代码任务耗时执行会阻塞代码的执行。
同步编程,即是一种典型的请求——响应模型,当请求调用一个函数或方法后,需要等待其响应返回,然后执行后续代码。
一般情况下,同步编程,代码依次执行,能很好的保证程序的执行,但是在某些场景下,比如读取文件的内容,或者请求服务器接口数据,需要根据返回内容执行后续操作,读取文件和请求接口直到数据返回这一过程是需要时间的,javascript是不能处理其他任务的,此时页面的交互,滚动等任何操作也都会被阻塞,这显然是及其不友好的,不能接受的,而这正是需要异步编程大显身手的场景。就比如耗时任务A会阻塞任务B的执行,等到任务A执行完才会继续执行B。
而当使用异步编程时,在等待当前任务响应返回之前,可以继续执行后续代码,即当前执行任务不会阻塞后续执行。
异步编程,不同于同步编程的请求——响应模式,其实就是一种事件驱动,请求调用函数或方法后,无需立即等待响应,可以继续执行其他任务,而异步任务响应返回后可以通过状态,通知调用者。
二、多线程
前面说明了异步编程能很好的解决同步阻塞的问题,那么实现异步编程的方式有哪些呢?通常实现异步方式是多线程,如C#,即同时开启多个线程,不同操作能并行执行。耗时任务A执行的同时,在线程二中任务B也可以执行。
三、javascript单线程
javascript语言执行环境是单线程的,单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行,而使用异步实现时,多个任务可以并发执行。那么javascript的异步编程如何实现呢?
四、并行于并发
前面提到多线程的任务可以并行执行,而javascript单线程异步编程可以是实现多任务并发执行,这里有必要说明一些并行于并发的区别。
并行,指同一时刻内多任务同时进行
并发,指同一时间段内,多任务同时进行着,但是某一时刻,只有一个任务执行。
通常所说的并发链接数,是指浏览器向服务器发送请求,建立TCP连接,每秒钟服务器建立的总连接数,假如10ms能处理一个连接,那么其并发连接数就是100。
五、javascript异步机制
本节介绍javascript异步机制,首先看一个例子:
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i);
},0);
}
console.log(i);
//结果是5,5,5,5,5
应该明白最后输出的全是5:
i在此处是for循环所在上下文的变量,有且只有一个i。
循环结束时i=5
javascript单线程事件处理器在线程空间不会执行下一个事件。 如果要真正理解以上例子中的setTimeout()以及javascript异步机制,需要理解javascript的事件循环和并发模型。
六、并发模型(concurrency model)
目前,我们已经知道,javascript执行异步任务时,不需要等待响应返回,可以继续执行其他任务,而在响应返回时,会得到通知,执行回调或事件处理程序。那么这一切具体是如何完成的,又以什么规则或顺序运作的呢?接下来我们需要解答这个问题。
注:回调和事件处理程序本质上并无区别,只是在不同情况下,不同的叫法
前文已经提到,javascript异步编程是的多个人恶化可以并发执行,而实现这一功能的基础是javascript拥有一个基于事件循环的并发模型。
1.堆栈与队列
介绍javascript并发模型之前,先简单介绍堆栈与队列的区别:
堆和栈都是程序运行时内存中分配的一个数据区域,因此也称为堆区和栈区,但二者存储的数据类型和处理速度不同。堆(heep)用于复杂数据类型(引用类型)分配空间,例如数组对象,object对象;它是运行时动态分配内存的,因此存取速度较慢。栈(stack)中主要存放一些基本类型的变量和对象的引用,其优势是存取速度比堆更要快,并且栈内的数据可以共享,但是缺点是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
(1)、栈的使用规则
栈有一个很重要的特性,就是在栈中的数据可以共享。例如下面的代码定义两个变量,变量的值是数字类。
var a=3;
var b=3;
(2)、堆的使用规则
下面通过Array来看一下堆的行为,例如存在下面的代码:
var fruit1='apple';
var fruit2='orange';
var fruit3='banana';
var oArray=[fruit_1,fruit_2,fruit_3];
var newArray=oArray;
当创建数组时,就会在堆内创建一个数组对象,并且在栈中创建一个这个数组的引用。变量fruit1,fruit2,fruit3为基本数据类型,它们的值直接存放在栈中;newArray,oArray为符号数据类型(引用类型),它们的引用变量存放在栈中,指向于存放在堆中的实际对象。
注意,newArray的只等于变量引用oArray,所以它是复合数据类(引用类类型)。此时如果改变变量fruit1和fruit2,fruit3的值,那么其实是更改栈中的值;如果更改newArray或oArray的值,那么其实是更改堆中的实际对象,因此,对两个变量的引用都会发生作用。例如,首先更改newArray的值,然后看oArray的值。代码如下:
alert(oArray[1]);//返回orange
newArray[1]='berry';
alert(oArray[1]);//返回berry
同样,首先更改oArray的值,然后看newArray的值,代码如下:
alert(newArray[1]);//返回orange
oArray[1]='tomato';
alert(newArray[1]);//返回tomato
javascript堆不需要程序来显示地释放,因为堆是自动的垃圾回收来负责的,每种浏览器中的javascript解释引擎有不同的自动回收方式,但一个最基本的原则是:如果栈中不存在对堆中的某个对象的引用,那么就认为该对象已经不需要了,在垃圾回收的时候就会清除该对象占用的内存空间。因此,在不需要时应该对对象的引用释放掉,以利于垃圾回收,这样就可以提高程序的性能。释放对象的引用最常用的方法就是将其赋值为null,例如下面的代码将newArray赋值为null
newArray=null
(3)、易犯的错误
在堆和栈的使用问题上,最容易犯的错误就是String的使用,例如下面的代码:
var str=new String('abc');
var str='abc';
同样是创建两个字符串,第一种是用new关键字新建String对象,对象会存放在堆中,没调用一次就创建一个新的对象;而第二种是栈中,栈中存放“abc”和对值的引用。推荐使用第二种方式创建多个“abc”字符串,这种写法在内存中存放在一个值,有利于节省空间。同时它可以在一定程度上提高程序的运行速度,因为存储栈中,其值可以共享,并且由于栈的访问速度更快,所以对于性能的提高大有益处。 而第一种方式每次都在堆中创建一个新的String对象,而不管其字符串值是否相等以及是否有必要创建新的对象,从而加重了程序的负担。 堆的访问速度慢,对程序的性能影响也大。另外,处于逻辑运算的考虑,当两个变量进行比较时,使用堆和栈存储就会有差异。下面来看一下逻辑等于和逻辑全等于运算,深入理解一下堆和栈:
(1)、例如下面的代码,实际上只比较栈中的值:
var str1='abc';
var str2='abc';
alert(str1==str2);//true
alert(str1===str2);//true
不管逻辑等于还是逻辑全等于,都会返回true,可以看出str1和str2指向同一个值。
(2)、例如下面的代码,实际上比较堆中的值:
var str1=new String('abc');
var str2=new String('abc');
alert(str1==str2);//false
alert(str1===str2);//false
不管是逻辑等于还是逻辑全等于都会返回false,可以看出str1和str2指向的不是同一个对象。
(3)、例如下面的代码,比较堆和栈中的值
var str1=new String('abc');
var sstr2='abc';
alert(str1==str2);//true
alert(str1===str2);//false
在进行逻辑等于和逻辑全等于运算时,会首先将变量转成数据类型,然后进行对比。变量str1和str2的数据类型虽然不同,但比较运算还是返回true。但是逻辑全等于运算符与逻辑等于运算不同,它会对数据进行比较,看是否是引用的同一个数据。
以上仅仅是简单地对堆和栈的知识做一个补充,有关堆栈和队列的更多知识在这里就不做详细介绍了。
栈和队列是不同的数据结构,栈是后进先出的顺序存储数据结构,队列是先进先出的顺序存储数据结构。
2.事件循环(Event Loop)
javascript引擎负责解析,执行javascript代码,但是它不能单独运行,通常都得有一个宿主环境,一般如浏览器和node服务器,前文说到单线程在这些宿主环境中创建单一线程,提供一种机制,调用javascript引擎完成多个javascript代码块的调度,执行(是的,javascript代码都是按快执行的),这种机制称为事件循环(Event Loop)javascript执行环境中存在两个结构需要了解:
消息队列(message queue)也叫任务队列(task queue):存储待处理消息以及对应的回调函数或事件处理程序。
执行栈(execution queue),也叫执行上下文:javascript执行,顾名思义,是由执行上下文组成,当函数调用时,创建并插入一个执行上下文,通常称为执行帧栈(frame),存储着函数参数和局部变量,当函数执行结束时,弹出该执行帧。
注:关于全局代码,由于所有的代码都是全局上下文执行,所有执行栈顶部总是全局上下文就很容易理解,直到所有的代码执行完毕,全局上下文退出执行栈,栈清空了;也即是全局上下文第一个进栈,最后一个出栈的
3.任务
分析事件循环流程前,先阐述两个概念,有助于理解事件循环:同步任务和异步任务。
任务很好理解,javascript代码执行就是在完成任务,所谓的任务就是一个函数或一个代码块,比如完成一次加法计算,完成一次ajax请求;很自然就分为同步任务和异步任务。同步任务是连续的,阻塞的;而异步任务是不连续的,非阻塞的,包含异步事件及其回调。
七、事件循环
宿主环境为javascript创建线程时,会创建堆和栈,堆内存储javascript对象,栈内存储的是执行上下文;
站内执行上下文的同步任务按序执行,执行完即退栈,而当异步任务执行时,该异步任务进入等待状态(不入栈),同时通知线程:出发该事件时(或该异步操作响应返回时),向消息队列(任务队列)插入一个事件消息(异步任务)
当事件触发(异步操作响应返回)时,线程会向消息队列(任务队列)插入该事件(包括事件及其回调)
当栈内同步任务执行完毕之后,线程从消息队列(任务队列)中取出一个事件消息,将其对应的异步任务(函数)放入栈中,执行回调函数,如果未绑定回调,这个消息会被丢弃,执行完任务之后退栈。
当线程空闲(即执行栈清空)时继续拉取消息队列下一轮消息(next tick,事件循环流转一次称为一次tick)
var eventLoop=[];
var event;
var i=eventLoop.length-1;//后进先出
while(eventLoop[i]){
event=eventLoop[i--];
if(event){//如果事件回调存在
event();
}
//否则事件消息被丢弃
}
这里需要注意的一点是等待下一个事件消息的过程是同步的。
并发模式与事件循环:
var ele=document.querySelector('body');
function clickcb(event){
console.log('clicked');
}
function bindEvent(callback){
ele.addEventListener(''click,callback)
}
bindEvent(clickcb);
在javascript代码运行中,遇到同步任务时,立即入栈;遇到异步任务时,则进入等待状态,并通知线程,相应的异步事件触发(或异步操作响应返回)时,往消息队列中插入一条事件消息;而执行栈后续的同步代码执行完后,读取消息队列,得到一条消息,然后将该消息对应的异步任务放入栈中,执行相应的回调函数;一次事件循环完成了,也即处理了一个异步任务。
八、再谈setTimeout(...,0)
了解了javascript事件循环后我们再看前文关于setTimeout(...0)的例子就比较清晰了:
setTimeout(...,0)所表达的意思是:等待0秒后,往消息队列中插入一条定时器事件消息,并将第一个参数作为回调函数;当执行栈中的同步任务执行完毕时,线程会从消息队列中读取消息,将该异步任务入栈,执行;线程空闲时再从消消息队列中读取消息。
var start=new Date();
var arr=[];
setTimeout(function(){
console.log('time'+new Date().getTime()-start);
},10)
for(var i=0;i<1000000;i++){
arr.push(i);
}
试着运行上面的代码,你会发现执行多次,每次执行的结果不同。
在setTimeout异步回调函数里我们输出了异步任务注册到执行的时间间隔,发现并不等于我们指定的时间,而且每次的时间间隔也都不同,出现这种原因应该考虑到以下两点:
在读取消息队列时,得等同步任务完成,这个是需要消耗时间的;
消息队列是先进先出的,读取此异步事件消息之前,可能还存在其他消息执行也需要耗时。
九、web works
每个Web Works或一个跨域的iframe都有各自的堆栈和消息队列,这些不同的文档只能通过postMessage方法进行通信,当一方监听了message事件后,另一方才能通过该方法向其发送消息,这message事件也是异步的,当一方接受到另一方通过postMessage方法发送过来的消息之后,会想自己的消息队列插入一条消息,而后续的并发流程按照上文所述。
十、javascript异步实现
关于javascript异步编程的实现,以前有回调函数,发布订阅模式,Promise三类(这里指的是原生书写实现promise),而ES6中提出了Promise,生成器(Generator),ES7中有出现了async/await。
这里我们仅仅是简单的介绍一下这些方式的用法,详细介绍会在后面说到。
1. 回调函数的方式
通过把一个函数(callback)作为参数传入另一个函数,当满足一定条件的时候,就执行callback函数。
//这里只是一个简单的条件
function fn1(a,fn){
if(a>10 && fn instanceof Function){
fn();
}
}
function fn2(){
console.log('-------fn2------')
}
//异步调用
function fn3(fn){
setTimeout(()=>{
fn()
},1000)
}
//执行
fn1(12,fn2);
fn3(fn2);
2.发布订阅方式
pub/sub模式js设计模式中的一种,本身是借鉴于java的模式,但是在异步处理的时候非常有用。通过一个信息中心“EventCenter”来处理监听(on)和触发(triggle)。
function fn1(){
setTimeout(()=>{
//异步调用操作后得到数据
let data=fetch(...);
Event.triggle('waterFull',data);
},2000);
}
fn1();
Event.on('waterFull',(data)=>{
//对得到的数据进行处理
console.log(data);
})
通过pub/sub模式,可以在信息中心清楚的看到有多少信号来源,方便集中管理。更加方便模块化管理,但是如果整个项目都使用pub/sub模式的话,流程就变得不清晰了,数据的得到和对数据的处理时分开的,对于后期的维护也是一个很大的问题。
3.Promise
下面主要是通过具体的要求来实现Promise,不会仔细讲解。 Promise构造函数(承诺),它分为三种状态,“resolve”,“reject”,“pending”,一旦状态pending改变为其他两个状态之后,就不能再修改了,就像一个承诺一样。Promise接受两个参数resolve和reject,分别表示成功后执行和失败后执行,可以通过实例的“then()”方法传递相应的函数。
const promise=new Promise((resolve,reject)=>{
//some code这里的函数会立即执行
if(resolve) resolve(value);
else reject(err);
})
promise.then((data)=>{
console.log(data)
}).catch((err)=>{
console.log(err);
})
有关promise的详细内容,后面文章会讲到。
4.generator构造器
generator是一个构造器,generator函数执行并不会执行函数内部部分,而是返回一个构造器对象,通过构造器对象的next()函数调用函数主体,并且每当遇到“yield”都会暂停执行,并返回一个对象。
function *gen(){
console.log('....');
yield 1
yield 2
return 3
}
let g=gen();//这里执行了generator函数但是并没有执行下面
g.next();
有关generator和promise的结合使用实现异步操作,后面会有文章详细讲到
5.async/await异步处理
ES7中出现了async/await进行异步处理,使得异步操作像同步代码一样简单,方便了使用,由于'async/await'内部封装了'generator'处理,所以就很少人使用'generator'来处理了,但是在异步的推动中'generator'起到了很大的作用。
await:后面接受一个promise实例
async:返回一个promise对象
下面是一个简单的异步请求:
async function f(){
//直接得到接口返回数据,在这里会等待接口返回数据
let data=await fetch('').then(res=>res.json());
console.log(data);//接口数据
return data;
}
async function h(){
let data=await Promise.resolve(22);
console.log(data);//22
return data;//Promsie {v}
}
以上是关于JS高阶三(JS中的异步编程)的主要内容,如果未能解决你的问题,请参考以下文章