美文网首页
异步的解决方案

异步的解决方案

作者: 209bd3bc6844 | 来源:发表于2017-07-22 16:35 被阅读0次

    回调

    回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
    简单的说回调函数是参数传递函数,并指定它在响应某个事件(定时器、鼠标点 击、Ajax 响应等)时执行这个参数传递的函数。这个参数就是回调。回头调用。回调传参

    假定有两个函数f1和f2,后者等待前者的执行结果。
    如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。

        function f1(callback){
        setTimeout(function () {
          // f1的任务代码
          callback(); // 这是回调   因为是setTimeout计时后触发的。
        }, 1000);
      }
        function f1(callback){
       callback(); // 这不是回调   因为是callback()自己直接调用的。
      }
    
    

    执行代码就变成下面这样:f1(f2);
    回调会造成的问题:

    • 层次的嵌套,俗称回调金字塔

    还有什么问题吗?仅仅是代码看起来混乱吗?

    顺序的大脑
    //伪代码
    doA(function(){
      doB();
      doC(function() {
        doD();
      });
      doE();
    });
    doF();
    

    无论多么熟悉JS异步的人,要完全搞懂这段代码实际的运行顺序,恐怕也得思考一番。
    为什么会这样? → 因为人的大脑是顺序的,天生适合顺序的思考,难以理解(不是不能理解)非顺序的东西。
    无论是在书写还是在阅读这段代码的时候,我们的大脑都会下意识地以为这段代码的执行逻辑是这样的doA→doB→doC→doD→doE→doF,然而实际运行逻辑很可能(假设)是这样的doA→doF→doB→doC→doE→doD

    思想实验

    我们来进行两种游戏

    1. 第一种游戏,举办方提前跟你说:“这游戏总共有X关。第一关你应该做.....然后在....(地方)进入第二关。第二关你应该做....然后在....(地方)进入第三关。……"。我称之为呆板的游戏。
    2. 第二种游戏,举办方提前跟你说:”你只管从这个门口进去,等你快到下一关的时候,自然会有人出来给你提示。“我称之为灵活的游戏。
      对应到代码当中,我们便能发现回调的另一个严重问题:硬编码。
      前后的操作被回调强制硬编码绑定到一起了。在调用函数A的时候,你必须指定A结束之后该干什么,并且显式地传递进去。这样,其实你已经指定了所有的可能事件和路径,代码将变得僵硬且难以维护。同时,在阅读代码的时候,由于必须记住前后的关联操作,这也加重了大脑记忆的负担

    Promise

    Promise 是 ES 2015 原生支持的,他把原来嵌套的回调改为了级联的方式。

    var a = new Promise(function(resolve, reject) {
      setTimeout(function() {
          resolve('1')
      }, 2000)
    })
    a.then(function(val) {
        console.log(val)
    })
    

    如果要涉及到多个异步操作的顺序执行问题,我们可以这样写:

    var a = new Promise(function(resolve, reject) {
      setTimeout(function() {
          resolve('1')
      }, 2000)
    })
    
    a
      .then(function(val){
        console.log(val)
        return new Promise(function(resolve, reject) { // 必须加return不然promise不止到这个then函数中又是个promise。只有通过返回值判断一下。
          setTimeout(function() {
              resolve('2')
          }, 2000)
        })
      })
      .then(function(val) {
        console.log(val)
      })
    

    以Ajax为例。我们都知道,在Ajax执行成功之后,指定的回调函数会被放入”任务队列“中。JS执行引擎在主线程空闲的时候便会轮询任务队列,执行其中的任务。
    我们仔细想想,是不是漏了一个关键点:”我知道最终是JS引擎执行了这个回调函数。但是,到底是谁调度这个回调函数的?到底是谁在特定的时间点把这个回调函数放入任务队列中去?“
    答案是宿主环境,在本例中也就是浏览器。是浏览器检测到Ajax已经成功返回,是浏览器主动将指定的回调函数放到”任务队列”中,JS引擎只不过是执行而已。
    由此,我们澄清了一件(可能令人震惊)的事情: 在回调时代,尽管你已经能够编写异步代码了。但是,其实JS本身,从来没有真正內建直接的异步概念,直到ES6的出现。
    事实就是如此。JS引擎本身所做的只不过是在不断轮询任务队列,然后执行其中的任务。JS引擎根本不能做到自己主动把任务放到任务队列中,任务的调度从来都是宿主完成的。举个形象的例子就是:“JS引擎就像是流水线上的工人,宿主就像是派活的老板。工人只知道不断地干活,不断地完成流水线上出现的任务,这些任务都是老板给工人指定的。工人从来没有(也不能)自己给自己派活,自己给自己的流水线上放任务。”
    ES6从本质上改变了在哪里管理事件循环,这意味着在技术上将其纳入了JavaScript引擎的势力范围,而不再是由宿主来管理。
    为什么说ES6的Promise才真正有了异步呢?因为Promise是基于任务队列(job queue)。可以js方面的在事件循环中插队。并不是宿主环境控制的。
    Promise 是一个 Job,所以必然异步的,因为 then 总是返回 Promise,xxx.then(a => a) 的效果实际上是 return new Promise(resolve => resolve(a)),所以then也是异步。

    es6引入了Generator

    为什么会出现Generator

    本质上,generator是一个函数,它执行的结果是一个iterator迭代器,每一次调用迭代器的next方法,就会产生一个新的值。迭代器本身就是用来生成一系列值的,同时也广泛应用于扩展运算符...、解构赋值和for...of循环遍历等地方。

    generator函数的函数是分段的。第一次执行next的时候,程序会执行到第一个yield,然后返回{ value:1, done:false },表示yield后面返回1,但是函数Hello还没执行完,函数既不会退出,也不会往下执行。

    function p(time) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(new Date());
        }, time)
      });
    }
    
    function* delay(){
      let time1 = yield p(1000);
      console.log(1, time1);
    
      let time2 = yield p(1000);
      console.log(2, time2)
    
      let time3 = yield p(2000);
      console.log(3, time3);
    }
    
    function co(gen){
      let it = gen();
      next();
      function next(arg){
        let ret = it.next(arg);
        if(ret.done) return;
        ret.value.then((data) => {
          next(data)
        })
      }
    }
    
    co(delay);// 这里写的co函数只适用于yield后跟promise。
    

    co库的简单实现

    await/async

    ES7中提供async函数,利用它,不需要依赖co库,也一样可以解决这个问题。
    使用方法和 co 非常类似,同时也支持同步写法的异常捕获。async的返回值为Promise 对象。

    function a() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(1)
        }, 2000)
      })
    }
    
    var b = async function() {
      var val = await a()
      console.log(val)
    }
    
    b()
    

    使用await/async或者Generator进行http请求的时候经常是配合这Promise的。

    function timeout(ms) {
      return new Promise((resolve) => {
        setTimeout(resolve, ms);
      });
    }
    
    async function asyncPrint(value, ms) {
      await timeout(ms);
      console.log(value)
    }
    
    asyncPrint('hello world', 50);
    

    async既可以处理promise又可以处理普通的,不过此时相当于同步,而且更有语义化。

    async function asyncPrint() {
      await yibu1();
      await yibu2();
    }
    
    

    使用async/await yibu1请求完了再去请求yibu2。如果使用Promise。是不等yibu1的结果就去请求yibu2。就等于async可以把本身的回调或者then里的内容拿出来当做同步代码写。但是一般http请求函数都没有返回值,只有请求有了结果才会有结果,所以await后一般跟着promise。不然await也不知道异步函数请求结束了呀。

    yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

    所以

    async function hhh () {
        var ll = await setTimeout(function() {
            console.log('ss')
        }, 3000)
        console.log('ll')
    }
    hhh()
    

    回调主要有两个问题,信任问题和顺序问题,Promise解决信任问题,Generator解决顺序问题。但我认为Generator也不是把顺序问题完完全全的解决了。因为如果

    async function async1(value, ms) {
      console.log(value)
      await new Promise()
      async function async2(value, ms) {
          // 
      }
      console.log(value1)
    } 
    

    假设 console.log(value1)本身是一个很费时的同步任务,并且不需要等待async2执行完成。这时候,我们还是会先执行console.log(value1) ,后执行async2的回调。await会使得async函数卡住不动,所以只能解决一层一层的嵌套的顺序问题,不能解决这种同一级别的顺序问题。
    async函数的返回值是一个Promise,上面这个问题可以改为

    async function async1(value, ms) {
      console.log(value)
      await new Promise()
      console.log(value1)
    } 
    async1().then(function(){
      async2(value, ms) 
    })
    

    四种异步解决方案
    回调与generate

    相关文章

      网友评论

          本文标题:异步的解决方案

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