回调、Promise、async-await

第一章 异步:现在与将来

程序中现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。

场景:等待用户输入、从数据库或文件系统中请求数据、通过网络 发送数据并等待响应,或者是在以固定时间间隔执行重复任务(比如动画)

1.1 分块的程序

最常见的块单位是函数。 从现在到将来的等待,最简单的方法是使用一个通常称为回调函数的函数。

只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点 击、Ajax 响应等)时执行,你就是在代码中创建了一个将来执行的块,也由此在这个程序 中引入了异步机制。

1.2 事件循环

一个事件循环处理的示例

var eventLoop = [];
var event;

var reportError = function(err) {
    console.log('An error happened!');
    console.log(err);
}
// 永远循环
while(true){
    // 一次tick
    if(eventLoop.length) {
        // 移出第一个事件
        event = eventLoop.shift();
        try{
            event();
        }
        catch(err){
            reportError(err);
        }
    }
}

诸如setTimeout()等方法,并不会直接将回调函数挂在事件循环队列中,而是设置一个定时器,等定时器到时后,环境会把回调函数放在事件循环中。

1.3 并行线程

并行是关于能够同时发生的事情。 并行最常见的就是进程和线程。进程和线程独立运行,并可能同时运行:多个线程能够共享单个进程的内存。 JS由于其单线程的特性,其不确定性是在函数(事件)顺序级别上,而不是多线程情况下的语句顺序级别。这种函数顺序的不确定性就是常说的竞态条件(race condition)。

1.4 并发

1.4.1 非交互

如果进程间没有互相影响的话,不确定性是完全可以接受的。

1.4.2 交互

并发的操作需要相互交流,通过作用域或DOM间接交互。如果出现这样的交互,就需要对它们的交互进行协调以避免竞态的出现。 一个可能的优化是

// 假设分页情况下
var res = [];
function response(data) {
    res[data.page] = data.items;
}
ajax('url?page=0',response);
ajax('url?page=1',response);

1.4.3 协作

并发协作:取到一个长期运行的进程,并将其分割成多个步骤或多批任务,使得其他并发“进程”有机会将自己的运算插入到时间循环队列中交替运行。 本质上是对于一个高性能消耗的操作,将其分割开进行处理。

var res = [];

function response(data) {
    // 假设1000万条数据,一次处理1000条
    var chunk = data.splice(0, 1000);

    res = res.concat(chuck.map((value)=>{
        return value * 2;
    }));

    // 如果有后续,异步触发
    if(data.length > 0) {
        setTimeout(function() {
            response(data);
        }, 0);
    }
}

1.5 任务

任务队列(job queue)。它是挂在事件循环队列的每个 tick 之后 的一个队列。在事件循环的每个 tick 中,可能出现的异步动作不会导致一个完整的新事件 添加到事件循环队列中,而会在当前 tick 的任务队列末尾添加一个项目(一个任务)。

第二章 回调

2.2 顺序的大脑

2.2.1 执行与计划

异步不是多任务,而是快速的上下文切换。 编写异步代码的困难之处就在于,这种思考/计划的意识流对我们中的绝大多数来说是不自然的。

2.3 信任问题

类型检查/规范化的过程对于函数输入是很常见的,即使是对于理论上完全可以信任的代码。

function addNumbers(x, y) {
    // 为了防止变成字符串拼接,检查类型,如果不符合就抛出错误
    if(typeof x !== 'number' || typeof y !== 'number') {
        throw new Error('Bad arguments!');
    }
    // 另一种做法是提前转换类型
    x = Number(x);
    y = Number(y);

    return x + y;
}

永远异步调用回调,即使就 在事件循环的下一轮,这样,所有回调就都是可预测的异步调用了。

第三章 Promise

回调表达程序异步和管理并发的两个主要缺陷:缺乏顺序性 和可信任性

3.1什么是Promise

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise(fn) {
    this._state = PENDING;
    this._value = null;
    // 用树形结构去记录then注册的hanlder,然后等到主Promise决议了,在_excuteThen中操作then返回Promise的状态和结果值
    this._handlers = [];

    if (fn) {
        this._enqueue(fn.bind(null, this._resolve.bind(this), this._reject.bind(this)));
    }
    return this;
}
/**
 * Promise的then方法。
 * 注册新的handler,挂载到handlers树上。
 * 返回该hanlder的promise引用,用于链式调用。
 * 返回的promise的_value取决于then传入的onFulfilled或onRejected函数的返回值。
 * @param {*} onFulfilled 
 * @param {*} onRejected 
 */
Promise.prototype.then = function (onFulfilled, onRejected) {
    var handler = {
        onFulfilled: onFulfilled,
        onRejected: onRejected,
        promise: new Promise(),
    };

    // 先压入堆栈,形成树形结构
    var index = this._handlers.push(handler) - 1;
    // 已经决议了,直接执行then
    if (this._state !== PENDING) {
        this._excuteThen(handler);
    }

    return this._handlers[index].promise;
};


/**
 * Promise的resolve方法。
 * 行为包括状态置位,修改结果,然后执行先前压入堆栈的then
 * @param {*} result 
 */
Promise.prototype._resolve = function (result) {
    // 只有PENDING情况下才能修改状态
    if (this._state === PENDING) {
        this._state = FULFILLED;
        this._value = result;
        this._handlers.forEach(this._excuteThen.bind(this));
    }
};


/**
 * Promise的reject方法
 * @param {*} result 
 */
Promise.prototype._reject = function (result) {
    // 只有PENDING情况下才能修改状态
    if (this._state === PENDING) {
        this._state = REJECTED;
        this._value = reason;
        this._handlers.forEach(this._excuteThen.bind(this));
    }
};


/**
 * 执行then,同时也通知then返回的promise去执行resolve或reject。
 * then中的_value最终使用的值是then中onFulfilled或onRejected的返回值。
 * @param {*} handler 
 */
Promise.prototype._excuteThen = function (handler) {
    if (this._state === FULFILLED && typeof handler.onFulfilled === 'function') {
        // 异步触发then
        this._enqueue(
        handler.promise._resolve.bind(
            handler.promise,
            handler.onFulfilled(this._value)
        )
        );
        // 同步触发then
        /* handler.promise._reject(handler.onFulfilled(this._value)); */
    }
    if (this._state === REJECTED && typeof handler.onRejected === 'function') {
        this._enqueue(
        handler.promise._resolve.bind(
            handler.promise,
            handler.onRejected(this._value)
        )
        );
        /* handler.promise._reject(handler.onRejected(this._value)); */
    }
};


/**
 * 将fn加入事件循环
 * @param {Function} fn 
 */
Promise.prototype._enqueue = function(fn){
    // process.nextTick(fn); // NodeJS
    setTimeout(fn.bind(this), 0);
};

3.2 设置超时:

// 用于超时一个Promise的工具 
function timeoutPromise(delay) {    
    return new Promise( function(resolve,reject){      
        setTimeout( function(){       
            reject( "Timeout!" );       
        }, delay );   
    } ); 
}  
 
// 设置foo()超时
Promise.race( [   
    foo(),                      // 试着开始foo()    
    timeoutPromise( 3000 )      // 给它3秒钟
] ) .then(    
    function(){  
        // foo(..)及时完成! 
    }, 
     function(err){    
         // 或者foo()被拒绝,或者只是没能按时完成      
         // 查看err来了解是哪种情况  
     } 
);

3.3 promise一旦决议就不会改变:

then()注册的回调中,出现了JavaScript异常错误怎么办?并不会执行then中的异常回调函数!!当捕获到错误,会执行下一个then的reject回调函数,或被catch捕获。

var p = new Promise( function(resolve,reject){    
    resolve( 42 );  
} );  
 
p.then(    
    function fulfilled(msg){     
        foo.bar();       
        console.log( msg ); // 永远不会到达这里 :(     
    },    
    function rejected(err){  
        // 永远也不会到达这里 
    }  
); 
// p 已经完成 为值 42,所以之后查看 p 的决议时,并不能因为出错就把 p 再变为一个拒绝。

3.4 promise链式流:

可以把多个 Promise 连接到一起以表示一系列异步 步骤,每个 Promise 的决 议就成了继续下一个步骤的信号。

两个 Promise 固有行为特性:

  • 每次你对 Promise 调用 then(..),它都会创建并返回一个新的 Promise,我们可以将其 链接起来;

  • 不管从 then(..) 调用的完成回调(第一个参数)返回的值是什么,它都会被自动设置 为被链接 Promise(第一点中的)的完成。

  • Promise. resolve(..) 会直接返回接收到的真正 Promise,或展开接收到的 thenable 值,并在持续展 开 thenable 的同时递归地前进。如果我们向封装的 promise 引入异步,一 切都仍然会同样工作

    var p = Promise.resolve( 21 );

    p.then( function(v){
    console.log( v ); // 21
    // 创建一个promise并返回
    return new Promise( function(resolve,reject){
    // 引入异步!
    setTimeout( function(){
    // 用值42填充
    resolve( v * 2 );
    }, 100 );
    } );
    } ).then( function(v){
    // 在前一步中的100ms延迟之后运行
    console.log( v ); // 42
    } );

构造ajax请求:

// 假定工具ajax({url}, {callback})存在
// Promise-aware ajax
function request(url) {
    return new Promise(function(resolve, reject) {
        // ajax(...) 回调应该是我们这个promise的resolve函数
        ajax(url, resolve);
    });
}

request('http://some.url.1/').then(function(response1) {
    return request('http://some.url.2/?v=' + response1);
}).then( function(response2){
    console.log(response2);
})
  • 调用 Promise 的 then(..) 会自动创建一个新的 Promise 从调用返回。
  • 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的) Promise 就相应地决议。
  • 如果完成或拒绝处理函数返回一个 Promise,它将会被展开,这样一来,不管它的决议 值是什么,都会成为当前 then(..) 返回的链接 Promise 的决议值。

3.5 错误处理

错误处理最自然的形式就是同步的 try..catch 结构,遗憾的是,它只 能是同步的,无法用于异步代码模式。

为了避免丢失被忽略和抛弃的 Promise 错误,一些开发者表示,Promise 链的一个最佳实践 就是最后总以一个 catch(..) 结束

3.6 Promise模式

3.6.1 Promise.all([...])

完成顺序并不重要,但是必须都要完成,门才能打开并让流程控制 继续。

Promise.all([...])允许传入一组Promise对象,调用返回的promise会收到一个完成消息,这是一个由所有传入promise的完成消息组成的数组,与指定的顺序一致。 它会在所有成员的promise都完成(fulfilled)后才会完成(fulfilled)。当其中任意一个被拒绝(rejected),它就立刻进入被拒绝(rejected)状态,并丢弃来自其他所有promise的全部结果。

  • 假定你想要同时发送两个 Ajax 请求,等它们不管以什么顺序全部完成之后,再发送第三 个 Ajax 请求
    var p1 = request( "http://some.url.1/" );
    var p2 = request( "http://some.url.2/" );
    // // Promise.all()需要传入的就是一个数组,每一项就是每一个异步函数
    Promise.all( [p1,p2] ).then(
    function(msgs){
    // 这里,p1和p2完成并把它们的消息传入
    return request(
    "http://some.url.3/?v=" + msgs.join(",")
    );
    } )
    .then( function(msg){
    console.log( msg );
    } );

3.6.2 Promise.race([...])

竞态:只响应“第一个跨过终点线的 Promise”,而抛弃其他 Promise。

第一个返回的promise为完成,它就会进入完成(fulfilled)状态;第一个返回的promise为失败,它就会进入被拒绝(rejected)状态。

应用:1. 超时模式

// 前面定义的timeoutPromise(..)返回一个promise,
 // 这个promise会在指定延时之后拒绝 
 
// 为foo()设定超时 
Promise.race( [     
    foo(),                  // 启动foo()    
    timeoutPromise( 3000 )  // 给它3秒钟
] )  .then(     
    function(){         
        // foo(..)按时完成!    
    },    
    function(err){       
        // 要么foo()被拒绝,要么只是没能够按时完成,
        // 因此要查看err了解具体原因     
    }  
); 
  1. finally:前面例子中的 foo() 保留了一些要用的资源,但是出现了超时,导致这个 promise 被忽略,finally就是用来:超时后主动释放这些保留资源,或者取消任何可能产生的副作用。
    Promise 需要一个 finally(..) 回调注册,这个回调在 Promise 决议后总 是会被调用,并且允许你执行任何必要的清理工作
    var p = Promise.resolve( 42 );

    p.then( something ) 
        .finally( cleanup )  
        .then( another ) 
        .finally( cleanup ); 
    

3.6.3 其他变体

  • none: 类似于all,但是完成和拒绝的情况互换。所有promise都被拒绝,则进入fulfilled状态。
  • any:类似于all,但是会忽略拒绝,只需要至少一个完成即可。
  • first:类似于any,但是只选取第一个完成的promise。
  • last:类似于first,但是只选取最后一个完成的promise。

另一方面,Promise也有原型方法finally,在决议后忽略是否成功,总是会执行。

3 promise API 综述

3.7.1 new Promise(..) 构造器

构造器 Promise(..) 必须和 new 一起使用,并且必须提供一个函数回调,函数接受两个函数回调,用以支持 promise 的决议。这两个函数称为 resolve(..) 和 reject(..):

  • 如果传给 resolve(..) 的是一个非 Promise、非 thenable 的立即值,这 个 promise 就会用这个值完成。
  • 如果传给 resolve(..) 的是一个真正的 Promise 或 thenable 值,这个值就会被递归展 开,并且(要构造的)promise 将取用其最终决议值或状态。

3.7.2 Promise.resolve(..) 和 Promise.reject(..)

创建成功和拒绝的Promise的快捷方式

3.7.3 then(..) 和 catch(..)

Promise 决议之后,立即会调用 这两个处理函数之一,但不会两个都调用,而且总是异步调用

  • then(..) 接受一个或两个参数:第一个用于完成回调,第二个用于拒绝回调。如果两者中 的任何一个被省略或者作为非函数值传入的话,就会替换为相应的默认回调。默认完成回 调只是把消息传递下去,而默认拒绝回调则只是重新抛出(传播)其接收到的出错原因。
  • catch(..) 只接受一个拒绝回调作为参数,并自动替换默认完成 回调。换句话说,它等价于 then(null,..)
  • then(..) 和 catch(..) 也会创建并返回一个新的promise,这个promise 可以用于实现 Promise 链式流程控制。

3.8 Promise局限性

3.8.1 顺序错误处理

如果构建了一个没有错误处理函数的Promise链,链中任何地方的任何错误都会在链中一直传播下去。

3.8.2 单一值

Promise只能有一个完成值,或一个拒绝理由。可以进行一定的值封装,但是需要每一步都进行封装和解封。

3.8.3 单决议

Promise只能被决议一次。

3.8.4 惯性

基于Promise对原有的异步回调进行改造依赖于经验,没有通用的实现手段。

3.8.5 无法取消的Promise

单独的Promise不应该可取消,但是取消一个序列(集合在一起的Promise构成的链)是合理的。

3.8.6 性能

相比于不受信任的裸回调,Promise性能上更慢一点。

第四章 async await 异步处理

4.1 async-await 和 Promise 的关系

async-await是promise和generator的语法糖。简单来说:async-await 是建立在 promise机制之上的。

基本语法

async function basicDemo() {
    let result = await Math.random();
    console.log(result);
}

basicDemo();
// 0.6484863241051226
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: undefined}

async

async用来表示函数是异步的,定义的函数会返回一个promise对象,可以使用then方法添加回调函数。

async function demo01() {
    return 123;
}

demo01().then(val => {
    console.log(val);// 123
});
若 async 定义的函数有返回值,return 123;相当于Promise.resolve(123),没有声明式的 return则相当于执行了Promise.resolve();

await

await 可以理解为是 async wait 的简写。await 必须出现在 async 函数内部,不能单独使用。

function notAsyncFunc() {
    await Math.random();
}
notAsyncFunc();//Uncaught SyntaxError: Unexpected identifier

await 后面可以跟任何的JS 表达式。虽然说 await 可以等很多类型的东西,但是它最主要的意图是用来等待 Promise 对象的状态被 resolved。如果await的是 promise对象,会造成异步函数停止执行,并且等待 promise 的解决,如果等的是正常的表达式则立即执行。

function sleep(second) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(' enough sleep~');
        }, second);
    })
}
function normalFunc() {
    console.log('normalFunc');
}
async function awaitDemo() {
    await normalFunc();
    console.log('something, ~~');
    let result = await sleep(2000);
    console.log(result);// 两秒之后会被打印出来
}
awaitDemo();
// normalFunc
// VM4036:13 something, ~~
// VM4036:15  enough sleep~

实例

举例说明啊,你有三个请求需要发生,第三个请求是依赖于第二个请求的解构第二个请求依赖于第一个请求的结果。若用 ES5实现会有3层的回调,若用Promise 实现至少需要3个then。一个是代码横向发展,另一个是纵向发展。今天指给出 async-await 的实现哈~

//我们仍然使用 setTimeout 来模拟异步请求
function sleep(second, param) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(param);
        }, second);
    })
}

async function test() {
    let result1 = await sleep(2000, 'req01');
    let result2 = await sleep(1000, 'req02' + result1);
    let result3 = await sleep(500, 'req03' + result2);
    console.log(`
        ${result3}
        ${result2}
        ${result1}
    `);
}

test();
//req03req02req01
//req02req01
//req01

错误处理

上述的代码好像给的都是resolve的情况,那么reject的时候我们该如何处理呢?

最好把 await 命令放在 try...catch 代码块中。

function sleep(second) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('want to sleep~');
        }, second);
    })
}

async function errorDemo() {
    let result = await sleep(1000);
    console.log(result);
}
errorDemo();// VM706:11 Uncaught (in promise) want to sleep~

// 为了处理Promise.reject 的情况我们应该将代码块用 try catch 包裹一下
async function errorDemoSuper() {
    try {
        let result = await sleep(1000);
        console.log(result);
    } catch (err) {
        console.log(err);
    }
}

errorDemoSuper();// want to sleep~
// 有了 try catch 之后我们就能够拿到 Promise.reject 回来的数据了。

// 另一种写法
async function myFunction() {
  await somethingThatReturnsAPromise().catch(function (err){
    console.log(err);
  });
}

小心你的并行处理!!!

我这里为啥加了三个感叹号呢~,因为对于初学者来说一不小心就将 ajax 的并发请求发成了阻塞式同步的操作了,我就真真切切的在工作中写了这样的代码。await 若等待的是 promise 就会停止下来。业务是这样的,我有三个异步请求需要发送,相互没有关联,只是需要当请求都结束后将界面的 loading 清除掉即可。
刚学完 async await 开心啊,到处乱用~

function sleep(second) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('request done! ' + Math.random());
        }, second);
    })
}

async function bugDemo() {
    await sleep(1000);
    await sleep(1000);
    await sleep(1000);
    console.log('clear the loading~');
}

bugDemo();

loading 确实是等待请求都结束完才清除的。但是你认真的观察下浏览器的 timeline 请求是一个结束后再发另一个的(若观察效果请发真实的 ajax 请求)
那么,正常的处理是怎样的呢?

async function correctDemo() {
    let p1 = sleep(1000);
    let p2 = sleep(1000);
    let p3 = sleep(1000);
    await Promise.all([p1, p2, p3]);
    console.log('clear the loading~');
}
correctDemo();// clear the loading~

恩, 完美。看吧~ async-await并不能取代promise.

await in for 循环

最后一点了,await必须在async函数的上下文中的。

// 正常 for 循环
async function forDemo() {
    let arr = [1, 2, 3, 4, 5];
    for (let i = 0; i < arr.length; i ++) {
        await arr[i];
    }
}
forDemo();//正常输出
// 因为想要炫技把 for循环写成下面这样
async function forBugDemo() {
    let arr = [1, 2, 3, 4, 5];
    arr.forEach(item => {
        await item;
    });
}
forBugDemo();// Uncaught SyntaxError: Unexpected identifier

第五章 程序性能

5.1 Web Worker

JavaScript目前并没有支持多线程执行的功能。 目前部分浏览器支持通过Web Worker的形式来实现任务并行。通常Web Worker只加载JS文件,浏览器为它启动一个独立的线程,让这个文件在这个线程中为独立的程序运行。

var w1 = new Worker('http://some.url.com/webworker.js');

Worker之间以及它们和主程序之间,不会共享任何作用域或资源,而是通过一个基本的事件消息机制相互联系。比如

w1.addEventListener('message', function(evt) {
    // evt.data
});
w1.postMessage('something to say');

专用Worker和创建它的主程序是一对一的关系,这个message要么来自这个Worker,要么来自主页面。 Worker可以实例化它的子Worker,称为subworker。 要想关闭一个worker,只要在主程序中对Worker调用terminate()方法(类似浏览器标签页来关闭页面)。

5.1.1 Worker环境

在Worker内部是无法访问主程序的任何资源的(变量、DOM)。 它可以用来执行网络操作(ajax,websocket)以及设置定时器,并可以访问几个重要的全局变量和功能的本地复本,比如navigator、location、JSON和applicationCache。 还可以通过importScript(...)方法向Worker加载额外的JavaScript脚本:

// Worker内部,该操作是同步的
importScript('foo.js', 'bar.js');

Web Worker的应用:

  • 处理密集型数学计算
  • 大数据集排序
  • 数据处理
  • 高流量网络通信

5.1.2 数据传递

如果要传递一个对象,可以使用结构化克隆算法,把这个对象复制到一边。 或者可以使用Transferable对象,发生对象所有权的转移,数据本身不做移动。一旦所有权发生转移,它原来的位置上就会变为null或不可访问,消除多线程编程作用域共享带来的混乱。比如使用postMessage()方法发送一个Transferable对象:

// 假设foo是一个Uint8Array
postMessage(foo.buffer, [foo.buffer]);

5.1.3 共享Worker

使用SharedWorker可以创建一整个站点或app的所有页面实例都可以共享的中心Worker。

var w1 = new SharedWorker('http://some.url.com/sharedWorker.js');

SharedWorker使用端口来作为不同程序的唯一标识符,调用程序必须使用Worker的port对象用于通信。

w1.port.addEventListener('message', handleMessages);
w1.port.postMessage('something');

// 端口连接初始化
w1.port.start();

同时在SharedWorker内部,还需要处理额外的connect事件,为这个特定的连接提供了端口对象。保持多个连接独立的最简单办法就是使用port上的闭包,把这个连接上的事件侦听和传递定义在connect的处理函数内部:

// 在SharedWorker内部
addEventListener('connect', function(evt) {
    // 这个连接分配的端口
    var prot = evt.ports[0];

    port.addEventListener('message', function(evt) {
        // ...

        port.postMessage('something');

        // ...
    });
    port.start();
});

5.2 SIMD(单指令多数据)

单指令多数据是一种数据并行(data parallelism)的方式,这不是把程序逻辑分成并行的块,而是并行处理数据的多个位。现代CPU通过数字“向量”(特定类型的数组),以及可以在所有这些数字上并行操作的指令,来提供SIMD功能。这是利用低级指令级并行的底层运算。

5.3 asm.js

5.3.1 如何使用asm.js优化

var a = 42;
var b = a;

// asm优化,确保b是32位整型
var b = a | 0;

5.3.2 asm.js模块

对JS性能影响最大的因素是内存分配、垃圾收集和作用域访问。可以使用asm.js模块来解决这些问题。 对于一个asm.js模块来说,需要明确地导入一个严格规范的命名空间——stdlib,以导入必要的符号。同时还需要声明一个堆(heap)并将其传入var heap = new ArrayBuffer(0x10000); //64k堆,asm.js就可以在这个缓冲区存储和获取值,不需要付出任何内存分配和垃圾收集的代价。 asm的目标是针对特定的任务处理提供一种优化的方法,比如数学运算和游戏中的图像处理。

第六章 性能测试与调优

6.1 性能测试

使用Benchmark.js、mocha、karma等测试框架。

6.5 微性能

如果某个变量只在一个位置被引用,而别处没有任何引用,那么它的值就会被在线化,即直接用值替换变量。 当递归可以进行展开,引擎就会对其进行采用循环的方式实现。 非关键路径上的优化没有必要,而应该注重可读性。关键路径要注重性能优化。

6.6 尾调用优化

使用TCO可以将尾递归转为普通的循环

function tco(f) {
    var value,
        active = false,
        accumulated = [];

    
    // 在f内部进行递归调用的时候,其实调用的是这个返回的accumulator
    // 这里需要注意的是,要将f中的递归调用函数名使用accumulator被赋予的变量名
    return function accumulator() {
        accumulated.push(arguments);
        if(!active) {
            active = true;
            /**
             * 由于每次f.apply都会导致accumulated被push进一个新的arguments,
             * 所以这个while一直要到全部执行结束才会跳出,
             * 但这种结构只会保持最多两层的调用栈
             */
            while(accumulated.length) {
                value = f.apply(this, accumulated.shift());
            }
            active = false;
            return value;
        }
    }
}

var sum = tco(function(x, y) {
    if (y > 0) {
        // 再次调用的其实是闭包返回的accumulator
        return sum(x + 1, y - 1)
    }
    else {
        return x
    }
});
st=>start: 开始
ed=>end: 结束

cond1=>condition: 检查accumulator.length
(如果是第一次进入,
或者上一次的递归调用中做了push,
这个循环就会继续)
cond2=>condition: 需要进行递归

opm0=>subroutine: 初始化闭包内的变量,
调用闭包返回的函数,
执行accumulated.push(arguments),
第一次的active判断必然通过
op1-1=>operation: while循环
op1-2=>operation: 取出上次的运行结果,
传参给f.apply
op2-1=>operation: 执行真正的操作
op2-2=>operation: 进入第二层递归调用
op2-3=>operation: 执行
accumulated.push(arguments),
记录这一次递归调用的结果
op2-4=>operation: 第二层中!active为false,返回
op2-5=>operation: 回到第一层的while循环
op3-1=>operation: 递归调用条件已经不符合,可以返回结果,不再做push
op4-1=>operation: 已经执行完毕,value得到最后的结果

st->opm0->op1-1->cond1
cond1(yes,right)->cond2
cond1(no)->op4-1->ed
cond2(yes,right)->op1-2->op2-1->op2-2->op2-3->op2-4->op2-5(left)->op1-1
cond2(no,right)->op3-1(right)->op1-1
posted @ 2019-06-27 15:53  欢笑还  阅读(654)  评论(0编辑  收藏  举报