zoukankan      html  css  js  c++  java
  • 异步三部曲之回调

    概述

    这是我看你不知道的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。

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

  • 相关阅读:
    mysql 函数 存储过程 事件(event) job 模板
    protobuf 无proto 解码 decode 语言 java python
    mitmproxy fiddler 抓包 填坑
    android adb 常用命令
    android机器人 模拟 踩坑过程
    RabbitMQ添加新用户并支持远程访问
    Windows下RabbitMQ安装及配置
    Java mybatis mysql 常用数据类型对应关系
    easyExcel 踩坑
    linux防火墙查看状态firewall、iptable
  • 原文地址:https://www.cnblogs.com/yangzhou33/p/8824738.html
Copyright © 2011-2022 走看看