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。

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

  • 相关阅读:
    liunx各命令及全称
    window启动数据库服务命令
    拉取github指定分支上的代码
    python项目学习
    客户展示 增删改查
    登录 注册功能 表梳理
    java简历
    go语言数组
    go语言 变量作用域
    go语言函数
  • 原文地址:https://www.cnblogs.com/yangzhou33/p/8824738.html
Copyright © 2011-2022 走看看