前提
线程与进程
在操作系统(此处说的系统是引入了线程概念的系统)中一个应用要想执行必须有一定的执行资源,而执行资源大致分为两个部分一个是执行时需要用的内存,一个是CPU执行权。而系统分配给每个应用的内存空间是在进程中的,且一个应用只有一个进程。而一个进程中可以有多个线程,多个线程可以共享进程中的系统分配给进程的内存空间,而进程则管理和分配内存空间给多线程。多线程则对CPU执行权进行竞争切换使用。
单核CPU的在执行程序时,即使开启多个进程,多个进程在宏观上是同时执行的,但是在微观上多个进程依然是按照一定顺序执行的,这个顺序的安排则是取决于进程的线程争取到的CPU执行时间片,一个执行时间片内,对于单核CPU有且只有一个进程的其中一个线程能获取到单核CPU的执行权。假如单核CPU先后开启了进程A、进程B、进程C,三个进程中的线程通过竞争从CPU处获取到不同时间片的执行时间:首先进程A中的线程二获取到执行权1秒,1秒过后暂停进程A中的线程二并将执行权给进程C中的线程一0.5秒,0.5秒过后暂停进程C中的线程一并将执行权给进程B中的线程三2秒,2秒过后暂停进程B中的线程三并将执行权给进程C中的线程三0.2秒,0.2秒过后暂停进程C中的线程三并将执行权给进程A中的线程一3秒…一直到三个进程最后销毁,三个进程中的线程根据争取到的执行时间片而不断交替使用CPU的执行权。如此同一个进程下的多线程共享进程中的内存环境,而多线程的执行顺序在宏观上看是同时执行的,但是在微观上看则是通过竞争交替使用CPU的执行权。
如此进程就成了系统(引入了线程概念的系统)中分配与管理内存的基本单位,线程则是独立调用和独立运行的基本单位。
通过上面可知单核CPU同一段时间内只能让一个线程使用它的执行权,这样就造成了进程中的多线程是交替使用CPU执行线程中的程序代码的,这种交替顺序执行多线程我们称为并发。但是如果CPU不是单核而是多核,那么多线程中的不同线程就可以同时使用不同的核进行执行,我们称为并行。
使用Java语言来描述线程的状态:
- 使用
Thread类
创建出来的Thread实例对象
只是一个线程对象,这个对象代表了一个线程真正运行时需要用到的数据。此时线程处于新建状态
- 当使用
Thread实例对象
调用start()
时,JVM会以Thread实例对象
为参考,在进程中创建一个真正的可以执行的线程,并将这个线程加入到竞争CPU执行时间片的队列中。此时线程处于可运行状态
- 当处于
可运行状态
下的线程获取到了CPU的执行时间片从而得到了执行权,那么线程会开始执行run()
方法中的代码。此时线程处于运行状态
- 当处于
运行状态
下的线程的执行时间片的时间消耗光了,且线程run()
方法中的代码没有执行完,那么此时线程会被强制停止,解除CPU的执行权,线程再次变为可运行状态
,重新开始竞争CPU执行时间片。此时线程处于可运行状态
- 当处于
运行状态
下的线程的执行时间片耗光前,线程run()
方法中的代码执行完成后,会将线程结束并退出进程,释放分配给该线程的资源。此时线程处于死亡状态
- 上面1~5就是一个没有开发者干涉的线程生命周期状态,而如果开发者想要通过代码去干涉线程的生命周期,那只有当线程处于
运行状态
下,执行了开发者编写的干涉线程生命周期的代码时,才能干涉成功,开发者的干涉目前只能产生两种效果,分别是将正在执行的线程提前结束变为死亡状态
,以及将正在执行的线程提前解除其CPU执行权,并且将线程阻塞。被阻塞的线程是因为某种原因而放弃了CPU执行权,暂时停止执行代码,线程依然具有活性,在某一个时刻线程会重新加入竞争CPU执行时间片的队列中。被阻塞的线程处于阻塞状态
。造成线程阻塞的原因大致有四种:
6.1、睡眠阻塞
:在Java中通过sleep()
方法让当前运行的线程被阻塞一段时间,这段时间的多少由sleep()
方法的参数来决定,睡眠时间一过,被阻塞的线程将被重新加入竞争CPU执行时间片的队列中
6.2、挂起阻塞
:在Java中,当线程A处于运行状态
时执行了线程B调用join()
方法的代码后,线程A会被阻塞,直到线程B执行结束处于死亡状态
后,线程A才会重新加入竞争CPU执行时间片的队列中。这种线程阻塞是因为要先完成某个任务而阻塞线程的,线程可以说是被暂时挂起来等待任务完成。
6.3、等待阻塞
:在Java中通过wait()
方法让当前运行的线程被阻塞,被阻塞的线程会被放入到等待池,直到通过notify()
等方法唤醒等待池中被阻塞的线程,被唤醒的线程会重新加入竞争CPU执行时间片的队列中
6.4、同步阻塞
:处于运行状态
时的线程执行代码去获取某个对象的同步锁时,若该同步锁被别的线程占用,则JVM会阻塞线程并把该线程放入锁池中,当同步锁没有线程在使用时,被阻塞的线程会占用该同步锁,并重新加入竞争CPU执行时间片的队列中
进程是线程的容器,进程是有独立的内存空间的,多线程所使用的内存就进程的内存空间,进程在创建并开启后,必定会有一个线程运行着,这个线程我们称之为主线程,猜测:主线程应该是与进程一起创建并开启的,所以我们可以将没有多线程情况下的主线程的运行状态当作是进程的运行状态,如此多进程本质上可以认为是多个主线程
多进程与多线程的对比:
对比维度 | 多进程 | 多线程 | 总结 |
---|---|---|---|
数据共享、同步 | 数据共享复杂,需要用IPC;数据是分开的,同步简单 | 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 | 各有优势 |
内存、CPU | 占用内存多,切换复杂,CPU利用率低 | 占用内存少,切换简单,CPU利用率高 | 线程占优 |
创建销毁、切换 | 创建销毁、切换复杂,速度慢 | 创建销毁、切换简单,速度很快 | 线程占优 |
编程、调试 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程占优 |
可靠性 | 进程间不会互相影响 | 一个线程挂掉将导致整个进程挂掉 | 进程占优 |
分布式 | 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 | 适应于多核分布式 | 进程占优 |
协程
子例程:某个主程序代码中的一部分代码,该代码执行特定的任务并且与主程序中的其他代码相对独立。
子例程又被称为子程序、过程、方法、函数等。在主程序代码中可以调用子例程来执行子程序的代码。
函数就是一种子程序,利用函数名称,可以接收回传值。
函数中的函数体代码就是子程序代码
,而调用函数的代码的所在代码整体相对于子程序代码我们可以称之为主程序代码
。
主程序代码
在执行时,CPU执行的是主程序代码
,当主程序代码
执行函数调用时,主程序代码
将暂停执行,CPU改为执行子程序代码
,当子程序代码
执行完毕后,CPU才会从主程序代码
暂停的地方重新开始继续执行。(注意:此处的CPU执行切换只是为了方便理解而这样说的,CPU实质上并没有进行执行切换,与线程并发时的CPU执行切换并不是同一个意思)
有一对相对对立的程序代码:程序A、程序B,程序A在执行了一段代码后,CPU暂停执行并去执行程序B的程序代码,程序B在执行了一段代码后,CPU又暂停执行并重新恢复程序A的执行,接着程序A又执行了一段代码后,CPU暂停执行并重新恢复程序B的执行……就这样程序A与程序B之间通过代码控制进行执行代码的切换,这种功能概念我们称之为协程。而上面的主程序代码
调用函数从而改为执行子程序代码
的过程就是一种特殊的协程过程,特殊之处在于:只能从主程序代码
中切换到子程序代码
,不能从子程序代码
中切换到主程序代码
。
为了让开发人员实现从执行子程序代码
过程中切换到重新恢复执行主程序代码
,或者从执行主程序代码
过程中切换到重新恢复执行子程序代码
,又或者多个子程序代码
之间进程切换,并且这种切换必须是由开发人员使用代码控制的,所以各种开发语言需要在编译/解释器中实现协程功能。
协程与多线程很相似,主程序就好比主线程,而多个子例程就好比多线程中除主线程以外的线程,其核心都是多个独立程序执行代码之间进行相互切换,但是线程的切换控制开发人员无法决定,且切换间隔时间极短需要一定的资源开销,而协程的切换控制则是由开发人员来决定,且切换间隔时间相对于线程切换而言很长没有资源开销。
协程常见用例:
-
状态机:假设有一个功能的实现可以分为多个状态,而要完成这个功能的实现,则需要在不同的状态时执行不同的操作,那么我们可以将多状态的执行操作直接放在在一个子例程中实现,这样在子例程中的代码可读性就很高,但是有些状态之间不是连续的执行,有时有可能需要等待一段时间才能接着执行下一个状态的操作,所以此时就可以用到协程中子例程执行暂停功能。这里的状态由子例程代码执行时的 暂停/执行 点确定。多用于多异步任务的顺序执行
// 这是一个用于实现状态机的子例程 function* getNetworkData(){ // 当本函数的函数体代码执行时,此处是一个 暂停/执行 点 // 此时状态机的状态是:开启网络请求 var p1 = new Promise((resolve,reject)=>{ var settings = { "async": true, "crossDomain": true, "url": "https://www.baidu.com/", "method": "GET", "headers": { "cache-control": "no-cache", "postman-token": "86087ec3-4666-c7b8-b711-d7144ad9e6ab" } } var request = $.ajax(settings); request.done((response)=>{ resolve(response); }).error((errorData)=>{ reject(errorData); }); }); let requestResult = yield p1; // 当本函数的函数体代码执行时,此处是一个 暂停/执行 点 // 此时状态机的状态是:处理请求结果 console.log(requestResult); } // 上面子例程中网络请求功能分为请求和处理两种状态,功能代码一目了然,可读性很高
-
角色模型:每个角色都有自己的功能过程(根据不同的功能逻辑分离为多个子例程代码,每个子例程代表了一个角色的功能执行过程),本来各个角色功能过程的执行是按照顺序一个个去执行,但我们想要并发的同时去执行多个角色的功能过程,这时就需要用到协程中暂停一个子例程的执行并执行另一个子例程的功能,从而让多个子例程执行像多线程并发执行一样相互切换执行权。多用于多异步任务的并发执行
// 用于生产的子例程 function* produce(produceNum){ produceNum; // 生产次数 for(let i = 0; i < produceNum; i++){ // 生产了一些东西…… let produceResult = {name:`产品${(i+1)}`}; console.log('生产者生产出了: ' + produceResult.name); // 每生产出一样产品就暂停并让消费者消费后在继续生产下一件东西 yield produceResult; } } // 用于消费的子例程 function* consume(){ while(true){ let product = yield; // 消费了一些东西 console.log('消费者消费了: ' + product.name); // 每消费完一件产品就暂停并让生产者生产下一件东西 } } function productAndConsume(produceNum){ // 初始化生产者和消费者 let produceter = produce(produceNum); let consumeter = consume(); consumeter.next(); let produceResult = produceter.next(); while(!produceResult.done){ consumeter.next(produceResult.value); produceResult = produceter.next(); } consumeter.return(); } productAndConsume(10); // 宏观上生产和消费同时进行,但是实质是生产和消费的并发执行
-
产生器:让子例程被调用时可以输出多个返回值,这就需要用到协程暂停子例程执行的同时会输出数据的功能了,这个功能也能用于实现数据结构的通用遍历。多用于遍历数据
function* iterEntries(obj) { let keys = Object.keys(obj); for (let i=0; i < keys.length; i++) { let key = keys[i]; yield [key, obj[key]]; } } // 执行子例程在暂停执行的时候,可以输出产生数据, var g1 = iterEntries({a:1,b:2,c:3}); g1.next(); // {value:["a", 1], done:false} g1.next(); // {value:["b", 2], done:false} g1.next(); // {value:["c", 3], done:false} g1.next(); // {value:undefined, done:false} // 数据结构的通用遍历 for(let attr of iterEntries({a:'a1a',b:'b2b',c:'c3c'})){ console.log(attr); }// 输出打印: ["a", "a1a"]、["b", "b2b"]、["c", "c3c"]
异步与同步
异步执行程序代码:有两段程序代码,先执行了一段程序代码,而另一段程序代码会在一段时间后执行,这段时间是相对不确定
的,有可能很长,有可能为0,如此后执行的程序代码相对与先执行的程序代码就是异步执行。
function applyResponse(response){
// 处理网络请求结果的函数
console.log(response);
}
function openNetwork(){
// 开启网络请求的函数
var settings = {
"async": true,
"crossDomain": true,
"url": "https://www.baidu.com/",
"method": "GET",
"headers": {
"cache-control": "no-cache",
"postman-token": "86087ec3-4666-c7b8-b711-d7144ad9e6ab"
}
}
$.ajax(settings).done(applyResponse);
}
// openNetwork 和 applyResponse 是两个子程序,当执行完了 openNetwork 后,
// 在不确定的时间间隔后,applyResponse函数才被执行
openNetwork();
同步执行程序代码:有两段程序代码,这两段代码先后执行,且两者执行的时间间隔是相对确定
的,如此这两段程序代码就是同步执行。
function fun2(){
console.log('执行了一段子程序代码2');
}
function fun1(){
console.log('执行了一段子程序代码1');
}
// fun1 和 fun2 是两个子程序,在执行完fun1后,经过确定的时间(就是执行了console.log('需要停一下')),在执行了fun2
// 如此fun1与fun2的执行是同步的。
fun1();
console.log('需要停一下');
fun2();
JS异步编程的实现
要想实现异步编程在JS中大部分都是用的回调函数,即使是监听、Promise对象等,本质也是回调函数,只不过用了不同的思想,不同的实现方式。
回调函数
自我理解:当JS执行一段程序代码时,代码其中一部分是将一个函数A按照执行条件记录注册,而不是立即执行函数A,当这一段程序代码执行完后,过了一段不确定的时间后,JS解释器收到了一个符合函数A调用执行的条件后,会将函数A立即加入到事件队列中等待执行,当事件队列走到函数A后,就会调用执行函数A,如此函数A的执行相对于注册函数A的代码执行而言就形成了一个异步执行,这个函数A我们称之为回调函数。
回调函数:
function() he{
console.log('函数he就是回调函数');
}
setTimeout(he,1000);
// 函数he的声明和setTimeout函数的调用是一段同时执行的代码,而函数he的调用则是异步执行。
注册回调函数的代码的上下文执行环境
在代码执行完后就会移出上下文执行栈
,所以在回调函数被调用时,回调函数的上下文执行环境
的上一层执行环境不会是注册回调函数的代码的上下文执行环境
。
回调函数用于异步编程本身没有问题,但是如果有多个异步任务需要顺序执行时,那就产生多个异步回调函数嵌套问题,多个回调函数嵌套在编程时会形成横向发展,而我们在编程时最好是竖向发展,且多个回调函数的嵌套,耦合度还很高。
多个回调函数的嵌套:
var settings = {
"async": true,
"crossDomain": true,
"url": "https://www.baidu.com/",
"method": "GET",
"headers": {
"cache-control": "no-cache",
"postman-token": "86087ec3-4666-c7b8-b711-d7144ad9e6ab"
}
}
$.ajax(settings).done(
function (response) {
console.log('回调函数1');
$.ajax(settings).done(
function (response) {
console.log('回调函数2');
$.ajax(settings).done(
function (response) {
console.log('回调函数3');
// ... 还可以继续嵌套下去
}
);
}
);
}
);
Promise
为了解决多层回调函数的嵌套以及高耦合问题,而发展出了Promise
。
每个Promise
对象在设置回调函数时都会返回一个新的Promise
对象,新的Promise
对象上会存储回调函数的结果,如此Promise
将回调函数的嵌套编程改成了链式编程(每次设置了回调函数后都会再次返回一个Promise
对象,拿着新Promise
对象再去设置新的回调函数,以达到链式编程):
var settings = {
"async": true,
"crossDomain": true,
"url": "https://www.baidu.com/",
"method": "GET",
"headers": {
"cache-control": "no-cache",
"postman-token": "86087ec3-4666-c7b8-b711-d7144ad9e6ab"
}
}
let p1 = new Promise((resolve,reject)=>{
console.log('开启第一个异步任务');
var request = $.ajax(settings);
request.done((response)=>{
resolve(response);
});
request.error((error)=>{
reject(error);
});
});
p1.then(()=>{
console.log('第一个异步任务执行了成功的回调函数');
return new Promise((resolve,reject)=>{
console.log('开启第二个异步任务');
var request = $.ajax(settings);
request.done((response)=>{
resolve(response);
});
request.error((error)=>{
reject(error);
});
});
}).then(()=>{
console.log('第二个异步任务执行了成功的回调函数');
return new Promise((resolve,reject)=>{
console.log('开启第三个异步任务');
var request = $.ajax(settings);
request.done((response)=>{
resolve(response);
});
request.error((error)=>{
reject(error);
});
});
}).then(()=>{
console.log('第三个异步任务执行了成功的回调函数');
}).catch(()=>{
console.log('三个异步任务中的某一个任务出现了错误');
})
但是Promise
对回调函数的改进依然存在一些缺陷:
Promise
的最大问题是代码冗余,如果有多个需要顺序执行的异步任务,那么编程时会按照执行顺序要求,以链式编程注册多个回调函数,但是Promise
为了达到链式编程的目的,每注册一次回调函数就会生产一个新的Promise
对象,如此就会产生很多Promise
对象,这样与回调函数嵌套一对比就显得代码很冗余了。- 而且原来的异步执行任务被
Promise
包装了一下,不管什么操作,一眼看去都是一堆then
,被包装Promise
包装的任务的语义变得很不清楚。
Generator
既然使用Promise
包装异步执行任务后,语义会变得不清楚,那我们可以考虑使用协程的状态机模版来编写异步任务,将异步任务分为两个状态,一个是开启异步任务的状态,一个是处理任务结果的状态,如此这两个状态下的代码可以整合在一起放在一个函数G中,语义也就非常清楚了,而异步任务的回调函数在 开启异步任务状态下的代码执行完后 函数G暂停执行时 进行注册,回调函数被执行时,再次恢复函数G的执行。
JS中实现了协程功能的是Generator函数:
function* getNetworkData(){
try{
// 此时状态机的状态是:开启网络请求
var settings = {
"async": true,
"crossDomain": true,
"url": "https://www.baidu.com/",
"method": "GET",
"headers": {
"cache-control": "no-cache",
"postman-token": "86087ec3-4666-c7b8-b711-d7144ad9e6ab"
}
}
var request = $.ajax(settings);
// 暂停函数执行,并在外部注册回调函数,回调函数中恢复了函数执行,并将结果传递进函数中
let requestResult = yield request;
// 此时状态机的状态是:处理请求结果
console.log(requestResult);
}catch(e){
return e;
}
}
var g1 = getNetworkData();
g1.next().value.done((r)=>{
g1.next(r);
}).error((e)=>{
g1.throw(e);
});
当有多个异步任务需要顺序执行时,也可以使用Generator函数,让功能语义很清晰:
function* getNetworkData(){
try{
var settings = {
"async": true,
"crossDomain": true,
"url": "https://www.baidu.com/",
"method": "GET",
"headers": {
"cache-control": "no-cache",
"postman-token": "86087ec3-4666-c7b8-b711-d7144ad9e6ab"
}
}
let resultArray = [];
// 第一次网络请求
var request1 = $.ajax(settings);
let requestResult1 = yield request1;
resultArray.push(requestResult1);
// 第二次网络请求
var request2 = $.ajax(settings);
let requestResult2 = yield request2;
resultArray.push(requestResult2);
// 第三次网络请求
var request3 = $.ajax(settings);
let requestResult3 = yield request3;
resultArray.push(requestResult3);
console.log(resultArray);
}catch(e){
return e;
}
}
g1.next().value.done((r)=>{
g1.next(r).value.done((r)=>{
g1.next(r).value.done((r)=>{
g1.next(r);
}).error((e)=>{
console.log(g1.throw(e).value);
});
}).error((e)=>{
console.log(g1.throw(e).value);
});
}).error((e)=>{
console.log(g1.throw(e).value);
});
从上面的代码我们可以看出在注册回调函数的时候,依然出现了回调函数嵌套,导致代码横向发展,但是是这个问题依然可以解决,因为这种回调函数的嵌套是为了让每次异步任务的回调函数有返回结果时,都能自动恢复Generator函数的执行同时也将返回结果(无论成功与否)都输入到Generator函数中,所以为了这种相同目的的回调函数注册我们就可以使用递归函数来解决回调函数嵌套问题,具体实现参考下一节。
Generator自动执行
使用Generator函数编写顺序执行多异步任务时,编写声明Generator函数代码时按照多异步任务执行顺序逻辑编写就行,但是在编写执行这个Generator函数的代码时,就会因为异步任务的回调函数嵌套,导致代码横向发展,为了解决这个问题,我们需要声明一个通用的自动执行Generator函数的函数。这个自动执行Generator函数的函数是为了顺序执行多异步任务,也是为了让Generator函数被执行时看起来好像一次调用就完成了函数内的所有异步任务。
为了让声明的自动执行Generator函数的函数可以通用,在声明Generator函数时必须遵循一个规则:
Generator函数内部只执行 开启异步任务 和 处理异步任务的结果,在开启了一个异步任务后会将可以注册该异步任务回调函数的对象通过yield
关键字输出到Generator函数外部,同时暂停Generator函数的执行。
编写自动执行Generator函数的函数的思路:
- 先调用Generator函数获取到Generator实例对象。接着调用实例对象开始执行Generator函数(这一步后半部分其实可以使用下面的
第3步
)。 - 当Generator函数暂停执行并返回
yield
关键字后表达式的值时,要先判断Generator函数是否是暂停执行而不是结束执行了,如果是暂停执行,才使用函数输出的对象给异步任务注册回调函数,回调函数中需要进行异步任务是否成功判断,然后判断是成功就执行成功的逻辑(执行下面第3步
),失败就执行失败的逻辑(执行下面第4步
)。而如果Generator函数是结束执行了,那整个自动执行函数也就结束了,最后我们可以使用一个函数将上述注册回调函数的逻辑进行包装一下。。 - 异步成功需要执行的逻辑是重新恢复Generator函数的执行,且将任务成功返回的数据输入到Generator函数中,同时也要考虑到Generator函数恢复执行时有可能会抛出错误,所以需要使用
try/catch
捕获错误,并对恢复执行时出现的错误进行处理。当Generator函数再次暂停执行时(不管是因为抛出错误而暂停,还是Generator函数结束了执行,还是Generator函数输出了又一个可注册回调函数的对象),此时需要重复上面第2步
的逻辑,如此我们可以使用一个函数将上述异步成功执行的逻辑进行包装一下。 - 异步失败需要执行的逻辑也是重新恢复Generator函数的执行,但是要使用
throw
将失败返回的数据输入到Generator函数中,让Generator函数去处理异步任务失败的逻辑,同时也要考虑到Generator函数恢复执行时有可能会抛出错误,所以需要使用try/catch
捕获错误,并对恢复执行时出现的错误进行处理。当Generator函数再次暂停执行时(不管是因为抛出错误而暂停,还是Generator函数结束了执行,还是Generator函数输出了又一个可注册回调函数的对象),此时需要重复上面第2步
的逻辑,如此我们可以使用一个函数将上述异步失败执行的逻辑进行包装一下。
function* getNetworkData(){
try{
var settings = {
"async": true,
"crossDomain": true,
"url": "https://www.baidu.com/",
"method": "GET",
"headers": {
"cache-control": "no-cache",
"postman-token": "86087ec3-4666-c7b8-b711-d7144ad9e6ab"
}
}
let resultArray = [];
// 第一次网络请求
var request1 = $.ajax(settings);
let requestResult1 = yield request1;
resultArray.push(requestResult1);
// 第二次网络请求
var request2 = $.ajax(settings);
let requestResult2 = yield request2;
resultArray.push(requestResult2);
// 第三次网络请求
var request3 = $.ajax(settings);
let requestResult3 = yield request3;
resultArray.push(requestResult3);
return resultArray;
}catch(e){
return e;
}
}
function voluntaryCarryOut(gen){
return new Promise((resolve,reject)=>{
var generator = gen();
// 本函数在执行注册了第一个回调函数后,函数其实已经执行完成了,那么它的上下文执行环境也就不存在了,
// 所以没法使用return返回Generator函数最终的返回值,
// 所以需要一个Promise对象来将Generator函数最终的返回值返回给调用本函数的用户
requestSuccess();
function registerCallBack(nextResult){
if(nextResult.done){
// Generator函数执行结束了
resolve(nextResult.value);
}else{
// Generator函数暂停执行中
nextResult.value.done(requestSuccess).error(requestFail);
}
}
function requestSuccess(respone){
let nextResult;
try{
nextResult = generator.next(respone);
}catch(e){
// Generator函数中没有对抛出的异常进行处理,那我们就将异常作为返回值
reject(e);
}
registerCallBack(nextResult);
}
function requestFail(error){
let throwResult;
try{
throwResult = generator.throw(error);
}catch(e){
// Generator函数中没有对抛出的异常进行处理,那我们就将异常作为返回值
reject(e);
}
registerCallBack(throwResult);
}
});
}
voluntaryCarryOut(getNetworkData);
上面的通用自动执行Generator函数的函数只能针对ajax
异步网络请求,但我们需要的是针对大部分异步任务的自动执行,所以在Generator函数中开启异步任务时,我们可以使用Thunk函数
或者Promise
去处理一下,在将Thunk函数
对象或者Promise
对象作为暂停执行时的输出数据。而声明自动执行Generator函数的函数时就可以使用Thunk函数
或者Promise
来编写了。
Thunk函数
编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好。一个争论的焦点是"求值策略",即函数的参数到底应该何时求值。
var x = 1;
function f(m) {
return m * 2;
}
f(x + 5)
上面代码先定义函数f,然后向它传入表达式x + 5。请问,这个表达式应该何时求值?
一种意见是"传值调用"(call by value),即在进入函数体之前,就计算x + 5的值(等于 6),再将这个值传入函数f。C 语言就采用这种策略。
f(x + 5)
// 传值调用时,等同于
f(6)
另一种意见是“传名调用”(call by name),即直接将表达式x + 5传入一个临时函数体中,只在用到它的时候才会调用这个临时函数求值。Haskell 语言采用这种策略。
f(x + 5)
// 传名调用时,等同于
function adverb(){ x + 5 };
f(adverb); // adverb()*2
传值调用和传名调用,哪一种比较好?
回答是各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。
function f(a, b){
return b;
}
f(3 * x * x - 2 * x - 1, x);
上面代码中,函数f的第一个参数是一个复杂的表达式,但是函数体内根本没用到。对这个参数求值,实际上是不必要的。因此,有一些计算机学家倾向于"传名调用",即只在执行时求值。
编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
上面代码中,函数 f 的参数x + 5被一个函数替换了。凡是用到原参数的地方,对Thunk函数求值即可。
这就是 Thunk 函数的定义,它是“传名调用”的一种实现策略,用来替换某个表达式。
总结:Thunk函数
就是一个临时的函数,这个临时函数就是将某个相对复杂表达包装为一个函数,当相对复杂表达示需要被使用时,就可直接调用这个临时函数以替代相对复杂表达式被使用时的效果。猜测:其核心就是将复杂的东西进行黑箱封装,让其对外可以简单使用。
JavaScript语言是传值调用,它的Thunk函数含义有所不同,但核心不变,将复杂化为黑箱。
Generator函数暂停时,我们希望输出的数据是一个只需要设置一次回调函数的数据对像,那么我们就需要将 多次设置回调函的表达式调用 化为 单次设置回调函数的表达式调用 作为数据输出,也需要将 设置回调函数时需要多参数的表达式 化为 只需要设置回调函数的单参数表达式 作为数据输出 。如此两种不同回调函数设置方法的异步任务在Generator函数中进行一下Thunk函数黑箱处理后,就都可以使用同一个 自动执行Generator函数的函数 去进行自动执行。
多次设置 化为 单次设置 的黑箱处理Thunk函数:
var settings = {
"async": true,
"crossDomain": true,
"url": "https://www.baidu.com/",
"method": "GET",
"headers": {
"cache-control": "no-cache",
"postman-token": "86087ec3-4666-c7b8-b711-d7144ad9e6ab"
}
}
var request = $.ajax(settings);
// 此处设置回调函数就需要设置两次
request.done((respone)=>{
}).error((error)=>{
});
// 使用Thunk函数概念包装下,在调用只需要设置一次回调函数
var thunk = function(callBack){
request.done((respone)=>{
callBack.(null,respone);
}).error((error)=>{
callBack.(error,null);
});
}
// 可以让所有ajax返回值只用设置一个回调函数
var ajaxThunk = function(ajaxRequest){
return function(callBack){
ajaxRequest.done((respone)=>{
callBack(null,respone);
}).error((error)=>{
callBack(error,null);
});
}
}
多参数调用 化为 单参数调用 的黑箱处理Thunk函数:
var fileName = '/as/asd.txt';
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// 使用Thunk函数概念包装下
var thunk = function(callback){
fs.readFile(fileName, callback);
}
// 可以让所有读取文件的函数调用时只需要设置一个回调函数
var thunk1 = function(fileName){
return function(callback){
fs.readFile(fileName, callback);
}
}
// 这里设置回调函数的函数有一个要求,回调函数的参数必须在末尾
// 让所有设置回调函数的函数调用都变为只需要设置一个回调函数
var manyParam = function(fn){
// 将需要设置回调函数的函数先转化下
return function(){
// 将除了回调函数的参数传递进行来
let args = Array.prototype.slice.call(arguments);
return function(callBack){
// 设置回调函数
args.push(callBack);
fn.apply(this,args);
}
}
}
// Thunkify模块
function thunkify(fn){
if(Object.prototype.toString.call(fn) === Object.prototype.toString.call(function(){})){
return function(){
var args = Array.prototype.slice.call(arguments);
var ctx = this;
return function(done){
var called;
args.push(function(){
// 限制回调函数被设置以后只能被单次调用
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
}else{
return undefined;
}
};
当暂停Generator函数执行并输出的数据类型恒定是一个只用设置回调函数的函数对象时,我们就可以声明一个通用的 自动执行Generator函数 的函数。
function* getNetworkData(){
try{
var settings = {
"async": true,
"crossDomain": true,
"url": "https://www.baidu.com/",
"method": "GET",
"headers": {
"cache-control": "no-cache",
"postman-token": "86087ec3-4666-c7b8-b711-d7144ad9e6ab"
}
}
let resultArray = [];
// 第一次网络请求
let requestResult1 = yield ajaxThunk($.ajax(settings));
resultArray.push(requestResult1);
// 第二次网络请求
let requestResult2 = yield ajaxThunk($.ajax(settings));
resultArray.push(requestResult2);
// 第三次网络请求
let requestResult3 = yield ajaxThunk($.ajax(settings));
resultArray.push(requestResult3);
return resultArray;
}catch(e){
return e;
}
}
function voluntaryCarryOut(gen){
return new Promise((resolve,reject)=>{
var generator = gen();
// 本函数在执行注册了第一个回调函数后,函数其实已经执行完成了,那么它的上下文执行环境也就不存在了,
// 所以没法使用return返回Generator函数最终的返回值,
// 所以需要一个Promise对象来将Generator函数最终的返回值返回给调用本函数的用户
requestSuccess();
function registerCallBack(nextResult){
if(nextResult.done){
// Generator函数执行结束了
resolve(nextResult.value);
}else{
// Generator函数暂停执行中
nextResult.value(function(error,respone){
if(error){
requestFail(error);
}else{
requestSuccess(respone);
}
});
}
}
function requestSuccess(respone){
let nextResult;
try{
nextResult = generator.next(respone);
}catch(e){
// Generator函数中没有对抛出的异常进行处理,那我们就将异常作为返回值
reject(e);
}
registerCallBack(nextResult);
}
function requestFail(error){
let throwResult;
try{
throwResult = generator.throw(error);
}catch(e){
// Generator函数中没有对抛出的异常进行处理,那我们就将异常作为返回值
reject(e);
}
registerCallBack(throwResult);
}
});
}
voluntaryCarryOut(getNetworkData);
总结:Thunk函数的概念运用于 自动执行Generator函数的函数 中时,其目的是想让开发者在定义Generator函数的时候,开启异步任务后暂停Generator函数执行并输出的数据是一个只需要定义异步任务回调函数的函数对象,而只要返回值是这种类型,就可以根据这种情况声明出一通用的 自动执行Generator函数 的函数。
Promise
将Promise作为暂停Generator函数执行并输出的数据类型时,只需要使用Promise包装一下异步任务就可以了,这个包装也比Thunk函数的黑箱封装简。
function* getNetworkData(){
try{
var settings = {
"async": true,
"crossDomain": true,
"url": "https://www.baidu.com/",
"method": "GET",
"headers": {
"cache-control": "no-cache",
"postman-token": "86087ec3-4666-c7b8-b711-d7144ad9e6ab"
}
}
let resultArray = [];
// 第一次网络请求
let requestResult1 = yield new Promise((resolve,reject)=>{
$.ajax(settings).done((respone)=>{resolve(respone)}).error((error)=>{reject(error)});
});
resultArray.push(requestResult1);
// 第二次网络请求
let requestResult2 = yield new Promise((resolve,reject)=>{
$.ajax(settings).done((respone)=>{resolve(respone)}).error((error)=>{reject(error)});
});
resultArray.push(requestResult2);
// 第三次网络请求
let requestResult3 = yield new Promise((resolve,reject)=>{
$.ajax(settings).done((respone)=>{resolve(respone)}).error((error)=>{reject(error)});
});
resultArray.push(requestResult3);
return resultArray;
}catch(e){
return e;
}
}
当暂停Generator函数执行并输出的数据类型恒定是Promise对象时,我们就可以声明一个通用的 自动执行Generator函数 的函数。
function voluntaryCarryOut(gen){
return new Promise((resolve,reject)=>{
var generator = gen();
// 本函数在执行注册了第一个回调函数后,函数其实已经执行完成了,那么它的上下文执行环境也就不存在了,
// 所以没法使用return返回Generator函数最终的返回值,
// 所以需要一个Promise对象来将Generator函数最终的返回值返回给调用本函数的用户
requestSuccess();
function registerCallBack(nextResult){
if(nextResult.done){
// Generator函数执行结束了
resolve(nextResult.value);
}else{
// Generator函数暂停执行中
nextResult.value.then((respone)=>{
requestSuccess(respone);
},(error)=>{
requestFail(error);
});
}
}
function requestSuccess(respone){
let nextResult;
try{
nextResult = generator.next(respone);
}catch(e){
// Generator函数中没有对抛出的异常进行处理,那我们就将异常作为返回值
reject(e);
}
registerCallBack(nextResult);
}
function requestFail(error){
let throwResult;
try{
throwResult = generator.throw(error);
}catch(e){
// Generator函数中没有对抛出的异常进行处理,那我们就将异常作为返回值
reject(e);
}
registerCallBack(throwResult);
}
});
}
voluntaryCarryOut(getNetworkData);
总结:Promise运用于 自动执行Generator函数的函数 中时,其目的是想让开发者在定义Generator函数的时候,开启异步任务后暂停Generator函数执行并输出的数据是一个Promise对象,而只要返回值是这种类型,就可以根据这种情况声明出一通用的 自动执行Generator函数 的函数。
co模块
上面的Thunk函数
和Promise
在 自动执行Generator函数的函数中 的应用,只能写出对应的通用执行,开发者在定义Generator函数时还得注意yield
关键字后面表达式的返回值。但是我们需要的是一个yield
关键字后面表达式的返回值可以是大部分类型。所以就有了co模块
。
co模块中的co函数
也是一个自动执行Generator函数的函数,但是这个函数允许Generator函数在暂停时,输出的数据类型可以时大部分类型,其核心是函数内部会将Generator函数暂停时输出的数据类型转化为Promise。
co函数:
/*
* 本方法用于将Generator函数中的多个异步任务按照函数定义时定义的先后顺序自动执行。并将最后一个异步任务的结果返回
* @param genFunction 被用于自动执行的Generator函数
* @param genParam 参数一Generator函数被调用是需要的参数
* @return 返回值为一个Promise对象,这个对象的then方法的回调函数可以获取到Generator函数中的多个异步任务顺序执行后的结果
*/
function co(genFunction, ...genParam) {
// 下面多个函数调用时都需要是被此对象调用,这个对象就是多个函数的共同调用对象,也就是同一个上下文对象
let context = this;
return new Promise((resolve,reject)=>{
let genControl;
if (Object.prototype.toString.call(genFunction) ===
Object.prototype.toString.call(function*(){})) {
// 本方法的第一个参数是 Generator函数 时
// 调用参数一Generator函数,获取该函数的返回值Generator类实例对象,
// 这个返回值是用来控制参数一Generator函数函数体执行的控制器。
genControl = genFunction.apply(context, genParam);
}else if (Object.prototype.toString.call(genFunction) ===
Object.prototype.toString.call((function*(){})())) {
// 本方法的第一个参数是 Generator函数 调用后的返回值时。
// 将参数一赋值给genControl变量。
genControl = genFunction;
}else {
// 如果两种类型都不是那就直接认为整体执行失败,并将失败的原因告诉调用co函数的开发者
reject(new Error('co function first param must Generator function or Generator Object'));
}
/*
// 使用 Generator函数控制器 恢复 Generator函数的执行,并获取开启异步任务的Promise对象。
let result;
try{
// 恢复Generator函数中某一段函数体代码的执行,恢复执行的代码一般是异步任务开启前的配置,
// 以及最后开启异步任务的代码,开启异步任务的代码一般会作为 yield 关键字后面的表达式,
// JS代码执行了 yield 关键字后面的表达式(也就开启了异步任务)后,会暂停Generator函数体代码的执行,
// 并将表达式的返回值赋值给此处 result 变量。
result = genControl.next(undefined);
}catch(error){
// ****** 开启异步任务都出错了,那也没得说,直接认为整体执行失败,将失败的原因告诉调用co函数的开发者 ******
// 如果开启异步任务的代码在执行时抛出错误,且没有被捕获,那么会在此处捕获
// 对此次恢复Generator函数执行时抛出的错误进行处理。
// 处理方法:改变 co函数 返回的Promise对象的状态 为已失败,并使用return说明不在继续执行
return reject(error);
}
// 我们将根据上面获取的Promise对象去获取异步任务的执行结果,在根据执行结果去进行相应的操作。
if (result.done) {
// ****** 不管Generator函数中如何执行的只要因为return语句导致Generator函数结束执行,那就直接认为整体执行成功,并将return后面的值告诉调用co函数的开发者 ******
// 如果Generator函数已经因为各种原因结束执行了,那么此处直接 改变 co函数 返回的Promise对象的状态 为已完成
resolve(result.value);
}else{
// 如果 Generator函数 还可以继续执行,那么我们将根据异步任务的执行结果去决定怎样继续恢复Generator函数的恢复
result.value.then((resolveResult)=>{
// 我们认为异步任务执行成功
let result;
try{
// 将上次异步任务的结果作为数据,输入Generator函数将要恢复执行的代码中
// 本次恢复执行的代码是再次开启另一个异步任务,这个异步任务的开启需要上个异步任务的结果作为参考
result = genControl.next(resolveResult);
}catch(error){
return reject(error);
}
// 此处再次获取到一个开启异步任务后的Promise对象,所以还是先判断,在设置Promise的回调函数
if (result.done) {
// ...
}else{
// ...
}
},(rejectResult)=>{
// 我们认为异步任务执行失败
let result;
try{
// ****** 即使异步任务执行失败了,但是我们依然需要将失败的原因告诉Generator函数的定义者,让定义者根据异步任务失败的原因去进行下一步的执行 ******
// 将异步任务执行失败的原因作为参数输入给Generator函数
result = genControl.throw(rejectResult);
}catch(error){
// ****** 如果Generator函数的定义者没有去捕获异步任务执行失败的原因,那么说明定义者都不在呼,我们还管啥,直接认为整体执行失败,并将失败的原因告诉调用co函数的开发者 ******
return reject(error);
}
// 因为定义者对上次异步任务的失败结果进行了处理,所以此处再次获取到一个开启异步任务后的Promise对象,所以还是先判断,在设置Promise的回调函数
if (result.done) {
// ...
}else{
// ...
}
});
}
// 上面代码可以使用递归进行转换
// 代码段一:
let result;
try{
result = genControl.next(undefined);
}catch(error){
return reject(error);
}
processingNextResult(result);
function processingNextResult(nextResult){
if (nextResult.done) {
resolve(nextResult.value);
}else{
nextResult.value.then((resolveResult)=>{
// 代码段二:
let result;
try{
result = genControl.next(resolveResult);
}catch(error){
return reject(error);
}
processingNextResult(result);
},(rejectResult)=>{
// 代码段三:
let result;
try{
result = genControl.throw(rejectResult);
}catch(error){
return reject(error);
}
processingNextResult(result);
});
}
}
*/
// 上面代码中 代码段一 和 代码段二 代码一致,可以抽取出为一个对立函数,代码段三也可以抽取出
singleAsyncSuccess(undefined);
function singleAsyncSuccess(successResult){
let result;
try{
result = genControl.next(successResult);
}catch(error){
return reject(error);
}
processingNextResult(result);
}
function singleAsyncFail(failSuccess){
let result;
try{
result = genControl.throw(failSuccess);
}catch(error){
return reject(error);
}
processingNextResult(result);
}
function processingNextResult(nextResult){
if (nextResult.done) {
// 这里resolve的是Generator函数结束执行时return出来的值
resolve(nextResult.value);
}else{
// 为了让Generator函数在暂停执行时的返回值对象的value属性的值可以是多样化的类型值
// 所以此处先将这个多样化的类型值转为Promise对象
// nextValueToPromise方法中目前可以将Thunk函数、数组(元素最好都是Promise,即使不是方法内部也会转为Promise)、
// 对象(属性值最好都是Promise,即使不是方法内部也会转为Promise)、
// Generator函数、Generator函数调用后的返回值、包含有then方法的对象等转为Promise对象
let nextResultValue = nextValueToPromise.call(context, nextResult.value);
// 将转换后的值在进行一次判断
if (nextResultValue && isPromise(nextResultValue)) {
// 如果是Promise对象那就设置回调函数。
nextResultValue.then(singleAsyncSuccess,singleAsyncFail);
}else{
// 如果不是,那就将一个yield关键后表达式的值的类型错误输入到Generator函数中,
// 告诉Generator函数定义者,在定义函数的yield关键字后表达式的值出现了类型错误。
singleAsyncFail(new TypeError('You may only yield a function, promise, generator, array, or object, '+ 'but the following object was passed: "' + String(nextResult.value) + '"'));
}
}
}
});
}
/*
* 方法描述:将Generator函数暂停执行时返回值对象中的value属性的值都转为Promise对象。
*/
function nextValueToPromise(nextValue){
// 参数为null、undefined、false等
if (!nextValue) { return nextValue; }
// 参数就是Promise对象
if (isPromise(nextValue)) { return nextValue; }
// 参数是Generator函数或者Generator函数调用后的返回值
if (isGeneratorFunction(nextValue) || isGeneratorObject(nextValue)) {
// 将参数作为调用co函数的参数,co函数调用返回值就是Promise对象
return co.call(this, nextValue)
}
// 参数是一个Thunk函数,但是这里我们只能判断参数是否是函数对象,没法具体判断
if ('[object Function]' === Object.prototype.toString.call(nextValue)) {
// 将Thunk函数转为Promise对象。
return thunkToPromise.call(this,nextValue);
}
// 参数是一个含有then方法的对象
if (nextValue.then && '[object Function]' === Object.prototype.toString.call(nextValue.then)) {
// 将含有then方法的对象转为Promise对象。
return Promise.resolve(nextValue);
}
// 参数是一个数组对象
if (Array.isArray(nextValue)) {
// 必须保证数组中的所有元素必须是Prosmise对象
let promiseArray = nextValue.map(nextValueToPromise,this);
// 当数组中的所有Promise都是resolve时那么all返回的Promise就是resolve,只要有一个reject,那all返回的Promise就是reject
// 虽然可以直接将数组作为all方法的参数,但是数组中的元素如果是Thunk函数,那就不可以这样了。
// 通过all方法就可以实现多异步任务的并发执行了
return Promise.all(promiseArray);
}
// 参数是一个对象
if (Object === nextValue.constructor) {
return objectToPromise.call(this,nextValue);
}
// 如果参数的类型不在上述判断中,那就只能直接返回
return nextValue;
}
/*
* 方法描述:判断是否是Generator函数
*/
function isGeneratorFunction(obj){
return '[object GeneratorFunction]' === Object.prototype.toString.call(obj);
}
/*
* 方法描述:判断是否是Generator函数
*/
function isGeneratorObject(obj){
return '[object Generator]' === Object.prototype.toString.call(obj);
}
/*
* 方法描述:将Thunk函数转为Promise对象
*/
function thunkToPromise(fn){
return new Promise((resolve,reject)=>{
// 调用Thunk函数,去设置回调函数。注意 这里的this就是thunkToPromise函数中的this
fn.call(this,(error, respone)=>{
// Thunk函数有一定的局限性,因为你无法规定回调函数的参数顺序,
// 有可能错误是在参数列的末尾。
if (error) {
reject(error);
}else if (arguments.length >2 ) {
resolve(respone);
}
});
});
}
/**
* 方法描述:将 一个普通对象 中的大部分属性值转化为 Promise对象,并将转化后的Promise对象的存储进一个数组中
*/
function objectToPromise(obj){
// 结果对象
let resultObj = {};
// Promise数组
let promiseArray =[];
// 获取到所有可遍历属性的名字
let allAttrNames = Object.keys(obj);
for(let attrName of allAttrNames){
// 将对象中可遍历的属性的值转为Promise对象
let promiseAttrValue = nextValueToPromise.call(this,obj[attrName]);
// 判断属性值转为Promise对象是否成功
if (promiseAttrValue && isPromise(promiseAttrValue)) {
// 对于能转为Promsie对象的属性,先在结果对象中占个位置
resultObj[attrName] = undefined;
// 然后设置Promsie对象成功的回调函数,并在成功的回调函数中将结果存储到结果对象中
// 如果Promsie对象最后是失败的话,promiseAttrResult中依然有失败的记录
let promiseAttrResult = promiseAttrValue.then((resolveResult)=>{
resultObj[attrName] = resolveResult;
})
// promiseAttrResult这个Promise主要作用是用来判断异步任务是否成功了。
// 将 属性的Promsie对象 的结果Promise存储进数组中。
promiseArray.push(promiseAttrResult);
}else{
// 对于不能转为Promise对象的属性的值,直接存储到结果对象中
resultObj[attrName] = obj[attrName];
}
}
// 如果对象中那些 转为Promise的属性都成功了,那就将 resultObj 对象作为成功的结果
// 而如果出现错误,根据Promise特性,也会将错误传递出来。
// 通过all方法就可以实现多异步任务的并发执行了
return Promise.all(promiseArray).then(()=>{
return resultObj;
});
}
/*
* 方法描述:判断参数是不是Promise对象
*/
function isPromise(obj){
return '[object Promise]' === Object.prototype.toString.call(obj);
}