JavaScript没有async/await之前...

基于回调的异步

浏览器中的JavaScript程序是典型的事件驱动程序,即它们会等待用户点击触发,然后才真正执行,这意味着它们常常必须停止计算,等待某个事件发生。例如基于HTTP的网络事件,以下的代码就是典型的基于回调的异步:

function ajax(method, url, done, fail) {
    var xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.onload = function (e) {
        if (this.status < 300)    typeof done == "function" && done(this.response, e);
        else  typeof fail == "function" && fail(e);
    };
    xhr.onabort = xhr.onerror = xhr.ontimeout = fail;
    xhr.send(JSON.stringify(args));
    return xhr;
}

上述代码onload是我们针对即将到来的响应进行处理监听,done和fail就是我们网络事件响应成功和失败的回调。传入回调函数的最基本层面上的异步使用方式。那么异步回调的写法有什么问题?1、异步的辨识度问题,倘若不知道onload是一个“监听”,传入了DOM渲染操作在里面,结果可能会引起性能问题。2、执行顺序问题,倘若知道onload是一个“监听”,传入的回调函数似乎总是被延迟执行,这并非是一个直观线性代码结构。3、控制反转问题,倘若我知道onload是一个"监听",但我不知道它会如何处理我的回调函数,执行多次?不执行?4、继发嵌套问题,倘若下一个请求依赖上一个请求的数据,这样会在异步回调中继续执行异步回调,如果嵌套多层,代码会变得复杂和难以维护。5、异常处理问题,异步函数实际上脱离当前函数执行栈的上下文,如果出错了难以捕获到错误。针对问题1可以查阅文档解决,我们重点分析其他几个问题可能导致的结果。

正常用回调传入自己的API的时候,理所当然可以控制API代码何时调用回调,怎么调用,调用几次,然而当使用第三方的API时,就会出现”信任“问题,也称之为控制反转,传入的回调函数是否能接着执行,执行了多次怎么办?为了避免出现这样的”信任“问题,你可以在自己的回调函数中加入重重判断,可是万一又因为某个错误这个回调函数没有执行呢?万一这个回调函数有时同步执行有时异步执行呢?对于这些情况,你可能都要在回调函数中做些处理,并且每次执行回调函数的时候都要做些处理,这就带来了很多重复的代码。说到底JavaScript通过回调函数来承载异步是一个不可控的”暗箱“,我们不知道它里面会发生什么,出于”信任“,我们只会将我们的回调毫无保留的给它,让它”接力“下去。

如果回调函数是异步的,那么异步回调中仍然可能存在回调,那么就会出现回调嵌套的情况,回调嵌套的代码很需要认真看才清楚它的执行顺序。而在实际的项目中,代码会更加杂乱,为了排查问题,需要绕过很多碍眼的内容,不断的在函数间进行跳转,使得排查问题的难度也在成倍增加。当然之所以导致这个问题,其实是因为这种嵌套的书写方式跟人线性的思考方式相违和,以至于我们要多花一些精力去思考真正的执行顺序,代码上的嵌套和缩进只是这个思考过程中转移注意力的细枝末节而已。当然了,与人线性的思考方式相违和,还不是最糟糕的,实际上,还会在代码中加入各种各样的逻辑判断,当我们将这些判断都加入到这个流程中,很快代码就会变得非常复杂,以至于无法维护和更新,因此尽量避免回调的嵌套。

嵌套引起的另一个问题回调地狱,其导致的问题远非嵌套导致的可读性降低和难以维护而已,代码变得难以复用——回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全身。且堆栈信息被断开,当函数执行的时候,会创建该函数的执行上下文压入栈中,当函数执行完毕后,会将该执行上下文出栈。如果 A 函数中调用了 B 函数,JavaScript 会先将 A 函数的执行上下文压入栈中,再将 B 函数的执行上下文压入栈中,当 B 函数执行完毕,将 B 函数执行上下文出栈,当 A 函数执行完毕后,将 A 函数执行上下文出栈。这样的好处在于,如果中断代码执行,可以检索完整的堆栈信息,从中获取任何想获取的信息。可是异步回调函数并非如此,比如执行异步的时候,其实是将回调函数加入任务队列中,代码继续执行,直至主线程完成后,才会从任务队列中选择已经完成的任务,并将其加入栈中,此时栈中只有这一个执行上下文,它的调用者根本不在调用栈里,如果回调报错,无法向调用者抛出异常,也无法获取调用该异步操作时的栈中的信息,不容易判定哪里出现了错误。此外,因为是异步的缘故,使用 try catch 语句也无法直接捕获错误。一个补救措施是使用回调参数严密跟踪和传播错误并返回值,但这样非常麻烦,容易出错。另一个补救措施可以通过借助外层变量确定回调的层次和位置捕获,但当多个异步计算同时进行,由于无法预期完成顺序,必须借助外层作用域的变量,但外层的变量,也可能被其它同一作用域的函数访问并且修改,容易造成误操作。

Promise

新东西的出现,总是为了解决旧方式难以调和的矛盾。比如Promise。

Promise有效的解决了控制反转问题,控制反转其本质是无法信任的API内部的回调会被如何对待,是一种被动的接受异步状态,但如果能够把控制反转再反转回来,也称之为Promise范式,希望无法信任的API只需给予一个异步的结果,那么代码将变得主动接管。在promise中使用then来承接回调。

那么问题是是否可信任反转后的结果?Promise中有三种状态,只有异步操作的结果决定当前是哪一种状态,任何操作都无法改变,而且一旦状态发生改变就不会再次改变,适用于一次性事件(导致也Promise不适合多次触发的事件,但是ES18加入了for-await支持异步迭代),从而确保then中的回调只能执行一次,并且then能确保then必须是一次异步,这样保证了Promise反转后的结果是可信任的!如下是promise封装的ajax:

function ajax(url) {
  return new Promise((resolve, reject) => {
    var xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.onload = () => resolve(xhr.responseText);
    xhr.onerror = () => reject(xhr.statusText);
    xhr.onabort = xhr.onerror = xhr.ontimeout = fail;
    xhr.send(JSON.stringify(args));
    return xhr;
  });
};
// 使用
ajax('xxxURL')
.then(res => {
    if (res.status < 300){
        typeof done == "function" && done(this.response, e);
    }else{
        typeof fail == "function" && fail(e);
    }
})
.catch((e)=>{throw new Error('error:', e)})

// 使用fetch会直接返回一个promise对象,从而更加简洁
fetch(method, url).then((res)=>{
    if (res.status < 300){
        typeof done == "function" && done(this.response, e);
    }else{
        typeof fail == "function" && fail(e);
    }
}).catch((e)=>{throw new Error('error:', e)})

promise另一个优点在于简洁的容错机制,回调地狱中的错误处理不容易判定哪里出现了错误(到底是异步的错误还是回调的错误不得而知),而且 try catch 语句也无法在异步条件下直接捕获错误,另外回调还需要对每一个错误都需要做预处理,导致传入的第一个参数必须是错误对象,因为其原来的上下文环境结束,无法捕捉错误,只能当参数传递。Promise中的链式调用一旦reject或者抛出错误那么直接到catch实例方法中统一处理,而不是手动传参或者冗杂的容错判断。我们来看一个真实而更精细化错误处理的案例:

fetch('xxxurl')// p1
// 1%的失败概率是正常的,是种可恢复的错误,不应该直接抛错 c1,wait=p4,p5
.catch(e => wait(500).then(fetch('xxxurl'))
// 响应的处理 p2, b1
.then(res => {
       if(res.status >= 300){
        return null
    }
    let type = res.headers.get('content-type');
    if(type !== 'application/json'){
        throw new TypeError('balalala')
    }
    return res.json() //返回一个promise
})
// 响应的解析p3, b2
.then(profile => {
    if(profile){
        done(profile)
    }else{
        fail(profile)
    }
})
// 不可恢复错误汇总c2, b3
.catch(e => {
    if(e instanceof NetwordError){
        // 网络故障
    }else if(e instanceof TypeErroe){
        // 格式问题
    }else{
        // 其他意料之外的错误
    }
})

对上述代码的解释:第一种情况网络出现故障不可恢复,p1 NetworkError => c1 => p4、p5 reject => p2、p3 reject => c2 => b3。第二种情况低概率网络负载问题可恢复,p1 NetworkError => c1 => p4、p5 resolve => p3 => b1 => p3 => b2。第三种情况404,p2 resolve => b1 null => p3 resolve => b2。 第四种情况响应类型问题,p2 TypeError => p3 reject => c2 => b3。

Generator

在没有Generator之前,Promise看上去只是回调的包装器,其本质是将代码包裹成回调函数,由于异步,带来的弊端就是脱离函数的执行上下文栈,只能通过传参将有用的前一个结果传递给后一个结果。Promise并没有解决这个本质型的问题,而是留给了Generator。

如果在一个函数中遇到异步暂停执行异步后的代码,而等到异步的结果再恢复执行,这样回调嵌套就会消失,代码也不分割,像写同步函数一样写异步函数。Generator正是基于这样的考虑,它和之前的异步处理方式有着根本性变革,保证了执行上下文环境。如下代码用Generator改造的ajax:

function ajax(method, url){
    let xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.onload = function (e) {
        if (this.status < 300)    return this.response
        else  return e
    };
    xhr.onabort = xhr.onerror = xhr.ontimeout = fail;
    xhr.send(JSON.stringify(args));
}
function* gen(method, url){
    let res = yield ajax(method, url)
    let res1 = JSON.parse(res)
       let res2 = yield ajax(res1.data)
    let res3 = JSON.parse(res2)
    return [res1,res3]
}
var g = gen(method, url) //遇到第一个yield停止执行
g.next() // 手动执行第一个yield
g.next() // 手动执行第二个yield

Generator最大的问题就是再次获取执行权的问题,因为它返回的是一个遍历器对象,因此每次都需要手动获取,而不会在异步之后自动得到执行权。可以与回调或Promise结合获取自动执行权,Thunk函数和co模块正是以此来达到Generator的自动流程管理。而与Promise结合会融合两者共同的优点,如统一的错误处理,控制反转再反转,支持并发等。

async & await

async & await内置自动执行器,而且async返回一个Promise,它取决于await的Promise结果,等同于是Promise.resolve(awaitPromise),因此整体看起来像是Generator和promise的语法糖包装。以下是babel polyfill的例子:

async function _getData(method,url){
    var res1 = await ajax(method,url)
    var res2 = await ajax(method,url,res1.data)  
    console.log('async end')
}
_getData(method,url).then((res)=>{
    //result res1
}).then((res1)=>{
    //result res2
}).catch((e)=>{
    //error
})

最后解析下上段代码的官方babel:

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { 
    try { 
        var info = gen[key](arg); 
        var value = info.value; 
    } catch (error) { 
        reject(error); 
        return;
    } 
    //自执行
    if (info.done) { 
        resolve(value); 
    } else { 
        Promise.resolve(value).then(_next, _throw); 
    } 
}
function _asyncToGenerator(fn) { 
    return function () { 
        var self = this, args = arguments; 
        //用Promise包装
        return new Promise(function (resolve, reject) { 
            var gen = fn.apply(self, args); 
            function _next(value) { 
                asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); 
            } 
            function _throw(err) { 
                asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); 
            } 
            _next(undefined); 
        }); 
    }; 
}
function _getData() {
    //传入匿名的Generator
  _getData = _asyncToGenerator(function* () {
    yield ajax();
    console.log('async end');
  });
  return _getData.apply(this, arguments);
}


你可能感兴趣的:(javascript)