zoukankan      html  css  js  c++  java
  • 说一说javascript的异步编程

    众所周知javascript是单线程的,它的设计之初是为浏览器设计的GUI编程语言,GUI编程的特性之一是保证UI线程一定不能阻塞,否则体验不佳,甚至界面卡死。

    所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,这就和火车站洗手间门口的等待一样,前面的那个人没有搞定,你就只能站在后面排队等着。

    图片来自网络

    这种模式的好处是实现起来简单,执行环境相对单纯,坏处就是只要有一个任务耗时很长,后面的任务都会必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致了整个页面卡在这个地方,其他任务无法执行。

    为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

    “同步”就是上面所说的,后面的任务等待上一个任务结束,然后再执行。

    什么是“异步”?

    所谓异步简单说就是一个任务分成两段,先执行一段,转而执行其他任务,等做好了准备转而执行第二段。

    以下是当有ABC三个任务,同步或异步执行的流程图:

    同步

    thread ->|----A-----||-----B-----------||-------C------|
    

    异步:

    A-Start ---------------------------------------- A-End   
               | B-Start ----------------------------------------|--- B-End   
               |   |     C-Start -------------------- C-End      |     |   
               V   V       V                           V         V     V      
      thread-> |-A-|---B---|-C-|-A-|-C-|--A--|-B-|--C--|---A-----|--B--|
    

    "异步"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

    本文简单梳理总结了JavaScript异步函数的发展历史如下图:

    图片来自网络

    1. 回调函数
    2. Promise
    3. Generator+co
    4. async,await

    回调函数Callbacks

    似乎一切应该从回调函数开始谈起。

    异步JavaScript

    在Javascript 中,异步编程方式只能通过JavaScript中的一等公民函数才能完成:这种方式意味着我们可以将一个函数作为另一个函数的参数,在这个函数的内部可以调用被传递进来的函数(即回调函数)。

    这也正是回调函数诞生的原因:如果你将一个函数作为参数传递给另一个函数(此时它被称为高阶函数),那么在函数内部, 你可以调用这个函数来完成相应的任务。

    回调函数没有返回值(不要试图用return),仅仅被用来在函数内部执行某些动作。

    看下面的例子:

    step1(function (value1) {
        step2(value1, function(value2) {
            step3(value2, function(value3) {
                step4(value3, function(value4) {
                    // Do something with value4
                });
            });
        });
    });
    

    这里只是做4步,嵌套了4层回调,如果更多步骤呢?显然这样的代码只是写起来比较爽但是缺点也很多。

    过度使用回调函数所会遇到的挑战:

    • 如果不能合理的组织代码,非常容易造成回调地狱(callback hell),这会使得你的代码很难被别人所理解。
    • 不能捕获异常 (try catch 同步执行,回调函数会加入队列,无法捕获错误)
    • 无法使用return语句返回值,并且也不能使用throw关键字。

    也正是基于这些原因,在JavaScript世界中,一直都在寻找着能够让异步JavaScript开发变得更简单的可行的方案。这个时候就出现了promise,它解决了上述的问题。

    Promise

    Promise 的最大优势是标准化,各类异步工具库都按照统一规范实现,即使是async函数也可以无缝集成。所以用 Promise 封装 API 通用性强,用起来简单,学习成本低。

    一个Promise代表的是一个异步操作的最终结果。

    Promise意味着[许愿|承诺]一个还没有完成的操作,但在未来会完成的。与Promise最主要的交互方法是通过将函数传入它的then方法从而获取得Promise最终的值或Promise最终拒绝(reject)的原因。要点有三个:

    • 递归,每个异步操作返回的都是promise对象
    • 状态机:三种状态转换,只在promise对象内部可以控制,外部不能改变状态
    • 全局异常处理

    1)定义

    var promise = new Promise(function(resolve, reject) {
      // do a thing, possibly async, then…
    
      if (/* everything turned out fine */) {
        resolve("Stuff worked!");
      }
      else {
        reject(Error("It broke"));
      }
    });
    

    每个Promise定义都是一样的,在构造函数里传入一个匿名函数,参数是resolve和reject,分别代表成功和失败时候的处理。

    2) 调用

    promise.then(function(text){
        console.log(text)// Stuff worked!
        return Promise.reject(new Error('我是故意的'))
    }).catch(function(err){
        console.log(err)
    })
    

    它的主要交互方式是通过then函数,如果Promise成功执行resolve了,那么它就会将resolve的值传给最近的then函数,作为它的then函数的参数。如果出错reject,那就交给catch来捕获异常就好了。

    我们可以通过调用promise的示例,了解一下propmise的一些原理及特性:

    普通调用实例:

    let fs = require('fs');
    let p = new Promise(function(resolve,reject){
      fs.readFile('./1.txt','utf8',(err,data)=>{
          err?reject(err):resolve(data);
      })
    })
    
    p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
    

    1.promise实例可以多次调用then方法

    p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
    p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
    
    

    2.promise实例可以支持then方法的链式调用,jquery实现链式是通过返回当前的this。但是promise不可以通过返回this来实现。因为后续通过链式增加的then不是通过原始的promise对象的状态来决定走成功还是走失败的。

    p.then((data)=>{console.log(data)},(err)=>{console.log(err)}).then((data)=>{console.log(data)})
    

    3.只要then方法中的成功回调和失败回调,有返回值(包括undefiend),都会走到下个then方法中的成功回调中,并且把返回值作为下个then成功回调的参数传进去。

    第一个then走成功:
    p.then((data)=>{return undefined},(err)={console.log()}).then((data)=>{console.log(data)})
    输出:undefiend
    第一个then走失败:
      p.then((data)=>{console.log(1)},(err)={return undefined).then((data)=>{console.log(data)})
    输出:undefiend
    
    

    4.只要then方法中的成功回调和失败回调,有一个抛出异常,则都会走到下一个then中的失败回调中

    第一个then走成功:
    p.then((data)=>{throw new Err("错误")},(err)={console.log(1)}).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
    输出:错误
    第一个then走失败:
      p.then((data)=>{console.log(1)},(err)={throw new Err("错误")).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
    输出:错误
    
    

    5.成功和失败 只能走一个,如果成功了,就不会走失败,如果失败了,就不会走成功;

    6.如果then方法中,返回的不是一个普通值,仍旧是一个promise对象,该如何处理?

    答案:它会等待这个promise的执行结果,并且传给下一个then方法。如果成功,就把这个promise的结果传给下一个then的成功回调并且执行,如果失败就把错误传给下一个then的失败回调并且执行。

    7.具备catch捕获错误;如果catche前面的所有then方法都没有失败回调,则catche会捕获到错误信息执行他就是用来兜儿底用的

    p是一个失败的回调:
    p.then((data)=>{console.log('成功')}).then((data)=>{成功}).catche(e){console.log('错误')}
    

    8.返回的结果和 promise是同一个,永远不会成功和失败

    var  r  = new Promise(function(resolve,reject){
       return r;
    })
    r.then(function(){
        console.log(1)
    },function(err){
        console.log(err)
    })
    

    可以看到结果一直都是pending状态

    图片来自网络

    当你没有现成的Promise时,你可能需要借助一些Promise库,一个流行的选择是使用 bluebird。 这些库可能会提供比原生方案更多的功能,并且不局限于Promise/A+标准所规定的特性。

    Generator(ECMAScript6)+co

    JavaScript 生成器是个相对较新的概念, 它是ES6(也被称为ES2015)的新特性。想象下面这样的一个场景:

    当你在执行一个函数的时候,你可以在某个点暂停函数的执行,并且做一些其他工作,然后再返回这个函数继续执行, 甚至是携带一些新的值,然后继续执行。

    上面描述的场景正是JavaScript生成器函数所致力于解决的问题。当我们调用一个生成器函数的时候,它并不会立即执行, 而是需要我们手动的去执行迭代操作(next方法)。也就是说,你调用生成器函数,它会返回给你一个迭代器。迭代器会遍历每个中断点。

    function* foo () {  
      var index = 0;
      while (index < 2) {
        yield index++; //暂停函数执行,并执行yield后的操作
      }
    }
    var bar =  foo(); // 返回的其实是一个迭代器
    
    console.log(bar.next());    // { value: 0, done: false }  
    console.log(bar.next());    // { value: 1, done: false }  
    console.log(bar.next());    // { value: undefined, done: true }  
    

    更进一步的,如果你想更轻松的使用生成器函数来编写异步JavaScript代码,我们可以使用 co 这个库,co是著名的tj大神写的。

    Co是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。

    使用co,前面的示例代码,我们可以使用下面的代码来改写:

    co(function* (){  
      yield Something.save();
    }).then(function() {
      // success
    })
    .catch(function(err) {
      //error handling
    });
    

    你可能会问:如何实现并行操作呢?答案可能比你想象的简单,如下(其实它就是Promise.all而已):

    yield [Something.save(), Otherthing.save()];  
    

    终极解决方案Async/ await

    简而言之,使用async关键字,你可以轻松地达成之前使用生成器和co函数所做到的工作。

    在这背后,async函数实际使用的是Promise,这就是为什么async函数会返回一个Promise的原因。

    因此,我们使用async函数来完成类似于前面代码所完成的工作,可以使用下面这样的方式来重新编写代码:

    async function save(Something) {  
      try {
        await Something.save(); // 等待await后面的代码执行完,类似于yield
      } catch (ex) {
        //error handling
      }
      console.log('success');
    } 
    

    使用async函数,你需要在函数声明的最前面加上async关键字。这之后,你可以在函数内部使用await关键字了,作用和之前的yield作用是类似的。

    使用async函数完成并行任务与yiled的方式非常的相似,唯一不同的是,此时Promise.all不再是隐式的,你需要显示的调用它:

    async function save(Something) {  
        await Promise.all[Something.save(), Otherthing.save()]
    }
    

    Async/Await是异步操作的终极解决方案,Koa 2在node 7.6发布之后,立马发布了正式版本,并且推荐使用async函数来编写Koa中间件。

    这里给出一段Koa 2应用里的一段代码:

    exports.list = async (ctx, next) => {
      try {
        let students = await Student.getAllAsync();
      
        await ctx.render('students/index', {
          students : students
        })
      } catch (err) {
        return ctx.api_error(err);
      }
    };
    

    它做了3件事儿

    • 通过await Student.getAllAsync();来获取所有的students信息。
    • 通过await ctx.render渲染页面
    • 由于是同步代码,使用try/catch做的异常处理

    之后还会分享node的基本概念和eventLoop(宏任务和微任务)

    (完)

    参考:
    The Evolution of Asynchronous JavaScript

  • 相关阅读:
    hdu 1042 N!
    hdu 1002 A + B Problem II
    c++大数模板
    hdu 1004 Let the Balloon Rise
    hdu 4027 Can you answer these queries?
    poj 2823 Sliding Window
    hdu 3074 Multiply game
    hdu 1394 Minimum Inversion Number
    hdu 5199 Gunner
    九度oj 1521 二叉树的镜像
  • 原文地址:https://www.cnblogs.com/chenshufang/p/9927536.html
Copyright © 2011-2022 走看看