码迷,mamicode.com
首页 > 其他好文 > 详细

异步三部曲之回调

时间:2018-04-15 12:06:57      阅读:159      评论:0      收藏:0      [点我收藏+]

标签:def   问题   程序   mac   hand   listen   进程   book   很多   

概述

这是我看你不知道的JavaScript(中卷)的读书笔记,供以后开发时参考,相信对其他人也有用。

异步机制

分块的程序:我们写的代码有一部分是{现在运行的},其余的则是{将来运行的}。

我们不把它们分开写,因为它们是有联系的,比如{将来运行的代码}需要部分{现在运行的代码}的变量,那么怎么使这些变量在{现在运行的代码}运行结束后仍然存在并且能被{将来运行的代码}调用?答案很简单,就是闭包,我们把{将来运行的代码}放在一个函数作用域中,使它能够使用外部作用域的变量,而且,即使外部作用域被销毁,这些变量也一直存在。而产生这个闭包的函数就被称为回调函数

我们写的代码中,可能不止一个地方需要在将来运行,一般的情况是,js的主线程运行完{现在运行的代码}之后,继续运行{将来运行的代码1},运行完之后继续运行{将来运行的代码2}。。。所以当运行{现在运行的代码}的时候,{将来运行的代码1},{将来运行的代码2}。。。这些将来运行的代码放在哪儿?答案是放在一个队列里面,这个队列被称为任务队列

于是,主线程在运行完{现在运行的代码}之后,会拿出任务队列中的{将来运行的代码1}运行,运行完之后继续拿出任务队列中的{将来运行的代码2}运行。。。这种主线程不断拿出任务队列中的代码运行的机制被称为事件循环

需要注意的是,可能有这么一个情况,在运行{将来运行的代码1}的时候,又发现了一个{将来运行的代码x},这个时候会重新创建一个任务队列2,并且把{将来运行的代码x}塞进去,等之前的任务队列中的代码运行完之后再来运行任务队列2中的代码。

需要注意的第二点是,{将来运行的代码1},{将来运行的代码2},,,{将来运行的代码x}的运行顺序并不一定是队列中先进先出的顺序,通常情况是,各自满足一定条件之后才运行,比如多少秒之后,或者接收到某个数据之后。

需要注意的第三点是,在es6之前,这个任务队列并不是js创建的,而是浏览器实现的,它一般被用来运行settimeout和ajax等异步操作。

在es6,js规范了任务队列,这个任务队列叫做microtask,而之前的任务队列被叫做macrotask,microtask用来运行promise等异步操作,并且运行在同一个事件循环的macrotask之前。

并发

由于js的异步机制,导致js在运行的时候能看起来好像同时处理多个任务,这种同时发生的情况就叫做并发。实现并发还有另一个机制,就是多个进程或线程同时运行,这种多个进程或线程同时运行的情况,就叫做并行

在并发时候有一个很重要的情况,就是未来执行的代码的执行先后顺序会对最终结果产生影响。示例如下,块2和块3执行顺序的不同会造成a,b最后取值的不同。这种代码运行顺序的不确定性就被称为竞态条件。

//块 1:
var a = 1;
var b = 2;
//块 2( foo() ):
a++;
b = b * a;
a = b + 3;
//块 3( bar() ):
b--;
a = 8 + b;
b = a * 2;

一个很现实的异步竞态条件例子如下:

var a, b;
function foo(x) {
    a = x * 2;
    baz();
}
function bar(y) {
    b = y * 2;
    baz();
}
function baz() {
    console.log(a + b);
}
// ajax(..)是某个库中的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

怎么处理这种竞态条件呢?方法是加一个判断(所以判断在异步中非常常用)。

var a, b;
function foo(x) {
    a = x * 2;
    if (a && b) {
        baz();
    }
}
function bar(y) {
    b = y * 2;
    if (a && b) {
        baz();
    }
}
function baz() {
    console.log( a + b );
}
// ajax(..)是某个库中的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

这种需要2个异步同时完成就叫做门(gate),另一种是我们只需要最先完成的异步的数据,这种情况就叫做闩(latch),实例如下:

var a;
function foo(x) {
    if (!a) {
        a = x * 2;
        baz();
    }
}
function bar(x) {
    if (!a) {
        a = x / 2;
        baz();
    }
}
function baz() {
    console.log( a );
}
// ajax(..)是某个库中的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

回调函数

我们上面说了,我们一般把未来执行的代码包裹在一个回调函数里面,等满足某个条件之后再执行,比如下列代码:

listen( "click", function handler(evt){
    setTimeout( function request(){
        ajax( "http://some.url.1", function response(text){
            if (text == "hello") {
                handler();
            }
            else if (text == "world") {
                request();
            }
        } );
    }, 500) ;
} )

初看之下,回调函数貌似看起来非常清晰,但是这只是表面的,再来看下面这段伪代码,其中doABCDEF都是异步函数。

doA( function(){
    doB();
    doC( function(){
        doD();
    } )
    doE();
} );
doF();

实际运行顺序并不是ABDEF,而是AFBCED。当嵌套更多的时候,会更加复杂,需要看半天才能知道执行顺序。这就是著名的回调地狱。(注意,回调地狱并不是说嵌套太多了由于缩进写起来不方便,而是嵌套多了之后可读性很差。)

信任问题

回调地狱只是回调问题的一部分,还有一些更加深入的问题需要考虑。比如下面这个例子:

// A
ajax( "..", function(..){
    // C
} );
// B

执行A和B以后我们会执行异步代码块C。就是说,现在异步代码块C获得了程序的全部控制权,可以控制作用域中的全部变量和方法。

这个时候,我们有理由担心:

  1. 异步代码块C根本不执行怎么办?
  2. 异步代码块C调用所需要的变量也是异步的没拿到怎么办?(调用过早)
  3. 异步代码块C调用太晚了怎么办?
  4. 异步代码块C获得的ajax数据不符合规范怎么办?
  5. 异步代码块C执行的时间太长怎么办?可能永久执行?
  6. 错误被吞掉怎么办?

更一般的情况是,我们有时候执行的这个代码块C是一个第三方函数,我们看不见。这个时候由于代码块C能够调用作用域中的全部变量和方法,如果这个第三方函数对这些变量乱改怎么办?

上面就是回调函数带来的信任问题,根源是我们把控制权交给了回调函数C。

当然,上面的问题有补救方法,但是要处理所有这些问题依然非常麻烦。细心的人可能看出来了,上面有部分问题是由于异步和同步同时进行导致的,而这也引出了一个非常有效的建议:永远异步调用回调,即使在事件循环的下一轮。比如下面的代码:

function result(data) {
    console.log( a );
}
var a = 0;
ajax( "..pre-cached-url..", result );
a++;

如果ajax获得数据的速度比console.log的IO端口读写更快的话(cache储存),会打印0,否则会打印1。

需要说明的是,即使我们在回调函数中遇到了这么多问题呢,但是在小项目中,我们实际遇到的问题会少很多,所以用回调还是很安全的。

异步三部曲之回调

标签:def   问题   程序   mac   hand   listen   进程   book   很多   

原文地址:https://www.cnblogs.com/yangzhou33/p/8836203.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!