美文网首页
promise、generator、async的简单应用

promise、generator、async的简单应用

作者: 我是xy | 来源:发表于2017-10-19 14:52 被阅读0次

    javascript的运行机制是单线程处理,即只有上一个任务完成后,才会执行下一个任务,这种机制也被称为“同步”。

    “同步”的最大缺点,就是如果某一任务运行时间较长,其后的任务就无法执行。这样会阻塞页面的渲染,导致页面加载错误或是浏览器不响应进入假死状态。

    如果这段任务采用“异步”机制,那么它就会等到其他任务运行完后再执行,不会阻塞线程。

    一、es6之前实现异步的方式:

    最常用的方法是采用回调函数,但普通的回调函数并不能实现异步效果:

    (1)同步回调
    function testFn(data, callback){
      callback(data);
    }
    testFn('0', function(data2){
      for (var i=data2; i<300000000; i++);
      console.log(1);
    });
    
    console.log(2);
    

    等一秒钟左右for循环结束,控制台输出1后,才会输出2。这是因为 testFn 的回调函数 callback 是同步执行,所以for循环会阻塞后面的任务。

    (2)异步回调

    想要实现回调的异步执行,必须要借助js的其它方法,例如将上面 testFn 的回调函数放到延时器中调用,延迟的时间设置为0:

    function testFn(data, callback){
      setTimeout(function(){
        callback(data)
      },0);
    }
    

    这样控制台会先输出2,接着大约一秒钟后再输出1,可见回调函数并没有阻塞后面的任务,实现了异步效果。

    除了使用定时器,实现异步执行的方法还有:事件监听(例如点击事件“click”)、requestAnimationFrame、XMLHttpRequest(jq中ajax方法的核心)、WebSocket、Worker以及Node.js 的 fs.readFIle等。

    二、promise、generator和async/await:

    es6 引入了 generator 和 promise,es7 又增加了async/await。这三种函数有一个重要的作用,就是解决回调函数的异步执行和嵌套问题。

    (因为网络上介绍这三种方法的文章很多,所以这里只介绍个人觉得相对常用的知识点)
    (一)普通的回调嵌套:

    定义一个函数,要求只有在获取两个数据(例如"data1"和"data2")后,才会执行“console.log”任务,通过对回调函数进行嵌套就可以达到目的:

    function Test1(data, callback){
        if(data){          // 本例是同步,要实现异步可以添加延时器
            callback(data);
        };
    };
    
    Test1("data1",function(){        
        Test1("data2",function(data2){
            console.log(data2);     // 如果不传数据就不会执行回调函数
        });
    });
    

    回调函数的执行需要依赖上一个函数,这样的缺点是如果有多个回调函数,就需要嵌套很多层。这会使代码的可读性较差,并增加调试和维护的难度。

    (二)promise

    Promise是一个构造函数,它接收一个匿名函数作为参数:

    new Promise(function(resolve, reject){
      resolve(console.log(1));
      reject(console.log(2));
      console.log(3);
    })
    
    console.log(4);
    // 输出的顺序为1、2、3、4 
    

    1、因为 promise 是构造函数(带有属性或方法的函数就叫构造函数),所以必须使用 new 实例化才能调用。而作为参数的匿名函数不需要额外调用就能执行。

    2、匿名函数只能有 resolve 和 reject 两个参数。结合if判断使用的话,resolve 是条件正确时执行的方法,reject 则是错误时执行的方法。

    3、将某一任务直接放到 resolve、reject 方法里,或者放到匿名函数里并不能实现异步效果。

    function Test2(){
      return new Promise(function(resolve, reject){
        resolve(1);                     
      });
    };
    
    Test2().then(function(num){
      console.log(num);
      return num;
    }).then(function(num2){
      console.log(num2);
    })
    console.log(2);      
    // 数字输出的顺序是2、1、1
    

    4、为了防止匿名函数的自动执行,需要再定义一个函数,并将 promise 函数作为该函数的返回值。

    5、将回调函数写在 then 或 catch 方法里才能实现异步,then 接受的是 resolve 方法传递的数据,catch 则对应的是 reject。

    6、resolve 只能向第一个 then 里的函数传递数据,后面 then 里的函数只能通过前一个 then 里函数的返回值获取参数。

    function pro1(){
      return new Promise(function(resolve, reject){
        resolve(1);
      });
    };
    
    function pro2(){
      return new Promise(function(resolve, reject){
      });
    };
    
    Promise.all([ pro1(), pro2() ]).then().catch();
    Promise.race([ pro1(), pro2() ]).then().catch();
    

    7、all方法的作用:只有 pro1 和 pro2 都执行 resolve,Promise 才会执行 then 方法。如果其中有一个函数执行 reject,那么Promise 就执行catch方法。

    8、race方法的作用:pro1 和 pro2 中只要有一个状态发生改变,Promise的状态就跟着发生改变,不论是resolve还是reject。

    promise最大的作用

    (一)能把嵌套改成链式调用。对上面的普通嵌套进行改造:

    function Test2(data){
      return new Promise(function(resolve, reject){
        if(data){
          resolve(data);
        }
      });
    };
    
    Test2(1).then(function( data1 ){
      console.log( data1 );
      return 2;     
    }).then(function( data2 ){
      console.log( data2 );
    })
    
    console.log(3);
    // 输出结果为3、1、2
    

    实现效果:
    (1)先输出3,表示 then 方法里的回调函数是异步执行。
    (2)如果调用 Test2 时不传数字“1”,就不会执行 resolve 方法,这样即使第一个 then 方法里的函数有返回值“2”,也不会执行第二个 then 方法,实现了需求。

    (二)解决 ajax 不能传值给外部变量的问题
    ajax 在 success 获取的数值无法传递给外部变量,除非设置为同步模式,而 promise 的 resolve 方法可以解决这个问题。

    设一个外部变量 outsideData:

    var outsideData ;
    function TestAjax(data){
      return new Promise(function(resolve, reject){
        $.ajax({
            url: 'xxx.php';
            type: 'GET',
            datatype: 'json',
            data: ' ',
            success: function(res){
              resolve(res.data)
            }
        });
      });
    };
    
    TestAjax().then(function( data ){
      outsideData = data;
    })
    
    (三)async/await

    先说 async/await,是因为 async 函数是 Generator 函数的语法糖,容易理解,而且和 promise 函数也有很大的关联。

    用 async function 定义一个函数:

    async function Test(){      
      return 1;         
    }
    Test().then(function(num){
      console.log(num);      
    });
    console.log( Test() );
    

    得到的结果是

    1、async function 返回的是一个 promise 对象,所以该函数可以调用 then 方法,并因此实现异步执行效果。

    async function Test(){      
      await console.log( 1 );
      console.log( 2 );
      await console.log( 3 );
    }
    Test();
    console.log( 4 );
    // 结果为1、4、2、3
    

    2、await 可以代替 then 方法,以实现异步效果。

    3、但第一个 await 是同步执行。不论跟的是 promise,还是其他的函数或方法。

    4、第一个 await 后面的任务,不论有没有 await 都是异步执行。但是如果要在 async 函数里再放一个函数,那前面就必须添加 await,否则会报错。

    function proFn(){
      return new Promise(function(resolve, reject){
        resolve(2);
      });
    };
    
    async function Test(){
      var data1 = await proFn();
      console.log(1);
      console.log(data1);
    };
    Test();
    console.log(3);
    // 结果为3、1、2
    

    5、对于第一个 await,最好的方法是使其等于一个变量,然后对这个变量进行处理。

    6、如果 await 后面跟的是 promise ,那么匿名函数必须执行 resolve 方法,否则 await 后面的任务就无法执行。

    async/await 最大的作用就是替代 promise 的 then 方法:
    function proFn(data){
      return new Promise(function(){
        if(data){
          resolve(data);
        };
      });
    };
    
    async function Test(){
      var data1 = await proFn(1);
      var data1 = await proFn(2);
      console.log(data1);
      console.log(data2);
    };
    Test();
    
    console.log(3);
    // 结果为3、1、2
    

    (1)async/await 将链式调用变得更加简化。
    (2)async/await 传递参数的方式也比 then 简单,不需要通过 return 来传递参数。

    (四)generator

    和 promise、async/await 不同,generator 本身并不具有异步执行的功能。它在异步中的主要应用,是管理异步回调的执行流程。

    generator 函数的特征是使用 * 和关键词 yield:

    function* Gen(){
      yield 1;
    }
    var runGen = Gen();
    console.log(runGen.next());  
    // 输出结果为 {value: 1, done: false};
    

    1、函数名后面加小括号并不能调用 generator 函数,只是创建了一个指针对象,next 方法才会执行函数。

    2、next 方法返回的对象带有两个属性,分别是“done”和“value”。done 的值表示 generator 函数是否运行完。value 对应的是 yield 后面的值。

    function* Gen (num){
      var num2 = yield console.log(num);
      yield console.log(num2);
    }
    var runGen = Gen(1); 
    runGen.next(2);
    runGen.next(3);
    // 结果输出1和3。                      
    

    3、next 方法可以传递参数,该参数是上一个 yield 表达式的值。
    之所以没有输出“2”,就是因为第一个 next 方法并没有与之对应的"上一个 yield",所以传参无效。

    generator 函数管理流程的应用:

    使用 generator 管理异步函数,需要用到三个知识点,thunk函数、next方法得到的done属性、递归。

    1、thunk函数
    普通的多参数函数在调用时,需要一次性传入多个数据。
    thunk 函数则是把多个参数拆开,使得在调用时,数据可以分开传入。

    实现方法是定义一个函数,并且该函数的返回值也是一个函数,这样就能将参数拆开放在两个函数里:

    // 普通的多参数函数:
    function Test(data, callback){};
    // 调用普通函数:
    Test(data, callback);
    
    
    // thunk函数:
    function Test(data){
     return function(callback){
       callback(); 
     }
    }
    // 调用thunk函数:
    var runThunk = Test(data);
    runThunk(callback);
    

    将 callback 参数提取出来,放在作为返回值的匿名函数里,在调用该函数时 callback 和 data 所对应的数据就可以分两步传入。

    2、通过 done 属性控制流程:

    generator 函数代替“嵌套”去控制流程的思路,就是通过上一个 yield 的执行情况,来决定下一个 next 方法是否执行,这需要用到 done 属性:

    function Test(){
      setTimeout(function(){
        console.log(1);
      },0);     
    }
    function* Gen(){
      yield Test();
      yield console.log(2);
    }
    
    var runGen = Gen();
    var genObj = runGen.next();
    
    if(!genObj.done){
      runGen.next();
    }
    // 结果为2、1
    

    (1)直接给 next 方法外面添加 if 判断的缺点是,假如某个 yield 后面跟的是异步函数,那么其他 yield 所对应的非异步任务就会优先执行。

    如果必须保证前一个任务运行完后,才会执行下一步,就需要把 next 方法放到 value 属性里:

    function Test2(){
      return function(callback){
          console.log(3);
          callback();
      }
    }
    function* Gen2(){
      yield Test2();
      yield console.log(4);
    }
    
    var runGen2 = Gen2();
    var genObj2 = runGen2.next();
    
    genObj2.value( function(){
      runGen2.next();
    } );
    // 得到的结果是 3、4
    

    (2)想在 value 里使用 next 方法,需要将 value 变成一个函数,这就要用到 thunk 函数。而 value 后面加一个小括号,就能调用作为返回值的函数了。

    (3)如果把 next 方法直接放到 value 里,那么 next 方法得到的结果会被当成 value 的参数,先输出。所以需要给 next 方法外面再包一层函数。

    下面的函数就是最终形态:

    function Test3(){
      return function(callback){
        setTimeout(function(){
           console.log(5);
           callback();
        },0)  
      }
    }
    function* Gen3(){
      yield Test3();
      yield console.log(6);
    }
    
    var runGen3 = Gen3();
    var genObj3 = runGen3.next();
    
    genObj3.value( function(){
      if(genObj3.done) return;
      runGen3.next();
    } );
    // 结果为5、6
    
    3、使用递归自动执行 generator 函数:

    首先看看手动执行 generator 的例子:

    // 为了简洁,本例并没有使用异步
    function Thunk(num){
      return function(callback){
        console.log(num);
        callback();          
      }; 
    };
    
    function* Gen(){
      yield Thunk(1);
      yield Thunk(2);
    }
    
    var runGen = Gen();
    
    var runNext = runGen.next();
    if(runNext.done) return;
    runNext.value(function(){
      
      var runNext2 = runGen.next();
      if(runNext.done) return;
      runNext2.value(function(){
       
      });
    });
    // 结果输出1、2
    

    假如存在多个 yield,就需要写很多 next,这会令代码变得臃肿。通过观察可以看出使用 next 方法的部分存在很大的重复性,所以可以使用递归(也就是函数内部调用自身)对其进行改造。

    var  runGen = Gen();
    
    function next(err, data) {
      var runNext = runGen.next(data);
      if (runNext.done) return;
      runNext.value(next);          
    }
    next();
    

    5、修改 promise 的链式调用

    function Test(num){
      return new Promise( function(resolve, reject){
        resolve(num);
      } );
    }
    
    function* Gen(){
      yield Test(1);
      yield Test(2);
    }
    
    var runGen = Gen();
    function next(){
      var genObj = runGen.next();
                    
      if(genObj.done) return;
      genObj.value.then(function(num){
        console.log(num);
        next();
      });
    }
    next();
    
    console.log(3);
    // 得到的结果为 3、1、2
    

    总结:

    关于es6的研究到此就告一段落了。个人觉得“类”和“箭头函数”是一定要掌握的,因为这两点能简化代码结构。至于异步,从例子的长短也能看出,generator 没必要了解很深,还是交给 promise 和 ansyc/await 吧。

    三、参考:

    1、http://www.cnblogs.com/webeye/p/5383785.html (js同步的缺点)
    2、http://blog.csdn.net/tywinstark/article/details/48447135 (15楼回复)
    3、http://stackoverflow.com/questions/9516900/how-can-i-create-an-asynchronous-function-in-javascript (js实现异步的方法)
    4、http://www.ruanyifeng.com/blog/2014/10/event-loop.html (js运行机制)
    5、http://www.nowamagic.net/librarys/veda/detail/787 (浏览器假死原因)
    6、https://segmentfault.com/a/1190000003096984 (异步回调的缺点)
    7、https://www.oschina.net/translate/event-based-programming-what-async-has-over-sync (回调函数嵌套的缺点)
    8、https://segmentfault.com/q/1010000002577322 (回调函数如何实现异步)
    9、http://es6.ruanyifeng.com/#docs/promise (promise知识点)
    10、http://www.cnblogs.com/lvdabao/p/es6-promise-1.html (promise需要放到另一个函数里)
    11、https://segmentfault.com/a/1190000007535316(await知识点)
    12、http://blog.rangle.io/javascript-asynchronous-options-2016/(generator不是异步)
    13、http://es6.ruanyifeng.com/#docs/generator (generator知识点)
    14、http://www.liaoxuefeng.com/wiki/ (generator函数的调用)
    15、http://www.jianshu.com/p/87183851756f (promise使用例子)
    16、https://segmentfault.com/q/1010000011014844 (使用return返回ajax获取的数值)

    相关文章

      网友评论

          本文标题:promise、generator、async的简单应用

          本文链接:https://www.haomeiwen.com/subject/yitstxtx.html