浅谈几个前端异步解决方案

      Javascript语言的执行环境是单线程。即一次只能完成一个任务。如有多个任务则需排队逐个执行——前一个任务完成,再执行后一个任务。javascript

      这种执行模式实现简单,执行环境相对单纯。但随着前端业务日渐复杂,事务和请求等日渐增多,这种单线程执行方式在复杂的业务下势必效率低下,只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),每每就是由于某一段Javascript代码长时间运行(好比死循环),致使整个页面卡在这个地方,其余任务没法执行。css

       为避免和解决这种问题,JS语言将任务执行模式分为异步和同步。同步模式”就是上一段的模式,后一个任务等待前一个任务结束,而后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;”异步模式”则彻底不一样,每个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,因此程序的执行顺序与任务的排列顺序是不一致的、异步的。前端

    “异步模式”很是重要。在浏览器端,耗时很长的操做都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操做。在服务器端,”异步模式”甚至是惟一的模式,由于执行环境是单线程的,若是容许同步执行全部http请求,服务器性能会急剧降低,很快就会失去响应。java

      1.回调函数 

      异步编程最基本方法。git

      首先须要声明,回调函数只是一种实现,并非异步模式特有的实现。回调函数一样能够运用到同步(阻塞)的场景下以及其余一些场景。github

      回调函数的英文定义:A callback is a function that is passed as an argument to another function and is executed after its parent function has completed。ajax

      字面上的理解,回调函数就是一个参数,将这个函数做为参数传到另外一个函数里面,当那个函数执行完以后,再执行传进去的这个函数。这个过程就叫作回调。编程

      在JavaScript中,回调函数具体的定义为: 函数A做为参数(函数引用)传递到另外一个函数B中,而且这个函数B执行函数A。咱们就说函数A叫作回调函数。若是没有名称(函数表达式),就叫作匿名回调函数。数组

      用一个通俗的生活例子比喻一下就是:约会结束后你送你女友回家,离别时,你确定会说:“到家了给我发条信息,我很担忧你。” 而后你女友回家之后还真给你发了条信息。其实这就是一个回调的过程。你留了个参数函数(要求女友给你发条信息)给你女友,而后你女友回家,回家的动做是主函数。她必须先回到家之后,主函数执行完了,再执行传进去的函数,而后你就收到一条信息了。promise

     假定有两个函数f1和f2,后者等待前者的执行结果。

     

f1();
f2(); 
复制代码

     若f1是一个很耗时的任务,能够考虑改写f1,把f2写成f1的回调函数。

function f1(callback){setTimeout(function () {// f1的任务代码callback();}, 1000);}复制代码

     执行代码就变成下面这样:

f1(f2);
复制代码

     采用这种方式,咱们把同步操做变成了异步操做,f1不会堵塞程序运行,至关于先执行程序的主要逻辑,将耗时的操做推迟执行。

    另外一个例子:

//定义主函数,回调函数做为参数
function A(callback) {
    callback();  
    console.log('我是主函数');      
}

//定义回调函数
function B(){
    setTimeout("console.log('我是回调函数')", 3000);//模仿耗时操做  
}

//调用主函数,将函数B传进去
A(B);

//输出结果
我是主函数
我是回调函数

复制代码

      上面的代码中,咱们先定义了主函数和回调函数,而后再去调用主函数,将回调函数传进去。

  定义主函数的时候,咱们让代码先去执行callback()回调函数,但输出结果倒是后输出回调函数的内容。这就说明了主函数不用等待回调函数执行完,能够接着执行本身的代码。因此通常回调函数都用在耗时操做上面。好比ajax请求,好比处理文件等。

    再来一个更俗的例子:

<strong>问:你有事去隔壁寝室找同窗,发现人不在,你怎么办呢?</strong><strong>方法1</strong>,每隔几分钟再去趟隔壁寝室,看人在不<strong>方法2</strong>,拜托与他同寝室的人,看到他回来时叫一下你 前者是轮询,后者是回调。 那你说,我直接在隔壁寝室等到同窗回来能够吗? 能够啊,只不过这样本来你能够省下时间作其余事,如今必须浪费在等待上了。把原来的非阻塞的异步调用变成了阻塞的同步调用。 JavaScript的回调是在异步调用场景下使用的,使用回调性能好于轮询。复制代码

   对于回调函数,通常在同步情境下是最后执行的,而在异步情境下有可能不执行,由于事件没有被触发或者条件不知足,因此请忽略上上个例子中的小问题,并非必定回调函数就要执行。

   同时补充回调函数应用场合和优缺点:

  • 资源加载:动态加载js文件后执行回调,加载iframe后执行回调,ajax操做回调,图片加载完成执行回调,AJAX等等。
  • DOM事件及Node.js事件基于回调机制(Node.js回调可能会出现多层回调嵌套的问题)。
  • setTimeout的延迟时间为0,这个hack常常被用到,settimeout调用的函数其实就是一个callback的体现。
  • 链式调用:链式调用的时候,在赋值器(setter)方法中(或者自己没有返回值的方法中)很容易实现链式调用,而取值器(getter)相对来讲很差实现链式调用,由于你须要取值器返回你须要的数据而不是this指针,若是要实现链式方法,能够用回调函数来实现。
  • setTimeout、setInterval的函数调用获得其返回值。因为两个函数都是异步的,即:他们的调用时序和程序的主流程是相对独立的,因此没有办法在主体里面等待它们的返回值,它们被打开的时候程序也不会停下来等待,不然也就失去了setTimeout及setInterval的意义了,因此用return已经没有意义,只能使用callback。callback的意义在于将timer执行的结果通知给代理函数进行及时处理。

       回调函数这种方式的优势是比较容易理解,能够绑定多个事件,每一个事件能够指定多个回调函数,并且能够”去耦合“,有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

    2.Promise对象

      随着ES6标准的发布,处理异步数据流的解决方案又有了新的变化。promise就是这其中的一个。咱们都知道,在传统的ajax请求中,当异步请求之间的数据存在依赖关系的时候,就可能产生很难看的多层回调,这样会使代码逻辑很容易形成混乱不便于阅读和后期维护,俗称”回调地狱”(callback hell)。另外一方面,每每错误处理的代码和正常的业务代码耦合在一块儿,形成代码会极其难看。为了让编程更美好,咱们就须要引入promise来下降异步编程的复杂性。

      因此某种程度上说,promise是对上面说到的回调函数处理异步编程的一个进阶方案。首先Promise是CommandJS提出的一种规范,其目的是为异步编程提供统一接口。

      简单说,Promise的思想是,每个异步任务返回一个Promise对象,该对象有一个then方法,容许指定回调函数。形如这种形式:

f1().then(f2);
复制代码

     对于函数f1,使用Jquery实现如下改写:

function f1(){var dfd = $.Deferred();setTimeout(function () {// f1的任务代码dfd.resolve();}, 500);return dfd.promise;}复制代码

      这样写的优势在于,回调函数变成了链式写法,程序的流程能够看得很清楚,并且有一整套的配套方法,能够实现许多强大的功能。这也就是Promise处理异步编程的其中的一个方便之处。

      再举一个制定多个回调函数的例子,其形式为:

f1().then(f2).then(f3);
复制代码

      当指定发生错误时的回调函数,其形式为:

f1().then(f2).fail(f3);
复制代码

      在此补充一点,promise中,若是一个任务已经完成,再添加回调函数,该回调函数会当即执行。因此,你不用担忧是否错过了某个事件或信号。这种方法的缺点就是编写和理解,都相对比较难。

      展开谈论一下Promise:Promise实际上就是一个特殊的Javascript对象,反映了”异步操做的最终值”。”Promise”直译过来有预期的意思,所以,它也表明了某种承诺,即不管你异步操做成功与否,这个对象最终都会返回一个值给你。

      代码示例

const promise = new Promise((resolve, reject) => {
  $.ajax('https://github.com/users', (value) =>  {
    resolve(value);
  }).fail((err) => {
    reject(err);
  });
});
promise.then((value) => {
  console.log(value);
},(err) => {
  console.log(err);
});
//也能够采起下面这种写法
promise.then(value => console.log(value)).catch(err => console.log(err));
复制代码

上面的例子,会在Ajax请求成功后调用resolve回调函数来处理结果,若是请求失败则调用reject回调函数来处理错误。Promise对象内部包含三种状态,分别为pending,fulfilled和rejected。这三种状态能够类比于咱们日常在ajax数据请求过程的pending,success,error。一开始请求发出后,状态是Pending,表示正在等待处理完毕,这个状态是中间状态并且是单向不可逆的。成功得到值后状态就变为fulfilled,而后将成功获取到的值存储起来,后续能够经过调用then方法传入的回调函数来进一步处理。而若是失败了的话,状态变为rejected,错误能够选择抛出(throw)或者调用reject方法来处理。

Promise基本语法以下

  • Promise实例必须实现then这个方法

  • then()必须能够接收两个函数做为参数

  • then()返回的必须是一个Promise实例

    eg
    <script src="https://cdn.bootcss.com/bluebird/3.5.1/bluebird.min.js"></script>//若是低版本浏览器不支持Promise,经过cdn这种方式
          <script type="text/javascript">
            function loadImg(src) {
                var promise = new Promise(function (resolve, reject) {
                    var img = document.createElement('img')
                    img.onload = function () {
                        resolve(img)
                    }
                    img.onerror = function () {
                        reject('图片加载失败')
                    }
                    img.src = src
                })
                return promise
            }
            var src = 'https://www.imooc.com/static/img/index/logo_new.png'
            var result = loadImg(src)
            result.then(function (img) {
                console.log(1, img.width)
                return img
            }, function () {
                console.log('error 1')
            }).then(function (img) {
                console.log(2, img.height)
            })
         </script>
    做者:浪里行舟
    连接:https://juejin.im/post/5b1962616fb9a01e7c2783a8
    来源:掘金
    著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。
    复制代码

       Promise还能够作更多的事情,好比,有若干个异步任务,须要先作任务1,若是成功后再作任务2,任何任务失败则再也不继续并执行错误处理函数。要串行执行这样的异步任务,不用Promise须要写一层一层的嵌套代码。

       有了Promise,咱们只须要简单地写job1.then(job2).then(job3).catch(handleError); 其中job一、job2和job3都是Promise对象。

       好比咱们想实现第一个图片加载完成后,再加载第二个图片,若是其中有一个执行失败,就执行错误函数:

var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
        var result1 = loadImg(src1) //result1是Promise对象
        var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
        var result2 = loadImg(src2) //result2是Promise对象
        result1.then(function (img1) {
            console.log('第一个图片加载完成', img1.width)
            return result2  // 链式操做
        }).then(function (img2) {
            console.log('第二个图片加载完成', img2.width)
        }).catch(function (ex) {
            console.log(ex)
        })复制代码

      这里需注意的是: then 方法能够被同一个 promise 调用屡次,then 方法必须返回一个 promise 对象。上例中result1.then若是没有明文返回Promise实例,就默认为自己Promise实例即result1,result1.then返回了result2实例,后面再执行.then实际上执行的result2.then。

 Promise的经常使用方法

       除了串行执行若干异步任务外,Promise还能够并行执行异步任务

      试想一个页面聊天系统,咱们须要从两个不一样的URL分别得到用户的我的信息和好友列表,这两个任务是能够并行执行的,用Promise.all()实现以下:

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
    console.log(results); // 得到一个Array: ['P1', 'P2']
});复制代码

       有些时候,多个异步任务是为了容错。好比,同时向两个URL读取用户的我的信息,只须要得到先返回的结果便可。这种状况下,用Promise.race()实现:

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
    console.log(result); // 'P1'
});复制代码

因为p1执行较快,Promise的then()将得到结果'P1'。p2仍在继续执行,但执行结果将被丢弃。

总结:Promise.all接受一个promise对象的数组,待所有完成以后,统一执行success;

Promise.race接受一个包含多个promise对象的数组,只要有一个完成,就执行success。

对上面的例子作下修改,加深对这二者的理解:

var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
     var result1 = loadImg(src1)
     var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
     var result2 = loadImg(src2)
     Promise.all([result1, result2]).then(function (datas) {
         console.log('all', datas[0])//<img src="https://www.imooc.com/static/img/index/logo_new.png">
         console.log('all', datas[1])//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
     })
     Promise.race([result1, result2]).then(function (data) {
         console.log('race', data)//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
     })复制代码

若是咱们组合使用Promise,就能够把不少异步任务以并行和串行的方式组合起来执行。

      Promise.reject(reason): 返回一个新的promise对象,用reason值直接将状态变为rejected

const promise2 = new Promise((resolve, reject) => {
  reject('Failed');
});

const promise2 = Promise.reject('Failed');
复制代码

上面两种写法是等价的。

        Promise.resolve(value): 返回一个新的promise对象,这个promise对象是被resolved的。与reject相似,下面这两种写法也是等价的。

const promise2 = new Promise((resolve, reject) => {
  resolve('Success');
});

const promise2 = Promise.resolve('Success');
复制代码

then 利用这个方法访问值或者错误缘由。其回调函数就是用来处理异步处理返回值的。

catch 利用这个方法捕获错误,并处理。

3.Async/Await简介与用法

简介

  • async/await是写异步代码的新方式,之前的方法有回调函数Promise
  • async/await是基于Promise实现的,它不能用于普通的回调函数。
  • async/await与Promise同样,是非阻塞的。
  • async/await使得异步代码看起来像同步代码,这正是它的魔力所在。

语法

用promise示例和asyn/await示例两段代码演示:

promise

const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()
复制代码

async/await

const makeRequest = async () => {
  console.log(await getJSON())
  return "done"
}

makeRequest()
复制代码

它们有一些细微不一样:

  • 函数前面多了一个aync关键字。await关键字只能用在aync定义的函数内。async函数会隐式地返回一个promise,该promise的reosolve值就是函数return的值。(示例中reosolve值就是字符串”done”)

  • 第1点暗示咱们不能在最外层代码中使用await,由于不在async函数内。

// 不能在最外层代码中使用await
await makeRequest()

// 这是会出事情的 
makeRequest().then((result) => {
  // 代码
})
复制代码

await getJSON()表示console.log会等到getJSON的promise成功reosolve以后再执行。

相对于promise,async/await的优点有哪些

1.简洁

       由示例可知,使用Async/Await明显节约了很多代码。咱们不须要写.then,不须要写匿名函数处理Promise的resolve值,也不须要定义多余的data变量,还避免了嵌套代码。这些小的优势会迅速累计起来,这在以后的代码示例中会更加明显。

2.错误处理

        Async/Await让try/catch能够同时处理同步和异步错误。在下面的promise示例中,try/catch不能处理JSON.parse的错误,由于它在Promise中。咱们须要使用.catch,这样错误处理代码很是冗余。而且,在咱们的实际生产代码会更加复杂。

const makeRequest = () => {
  try {
    getJSON()
      .then(result => {
        // JSON.parse可能会出错
        const data = JSON.parse(result)
        console.log(data)
      })
      // 取消注释,处理异步代码的错误
      // .catch((err) => {
      //   console.log(err)
      // })
  } catch (err) {
    console.log(err)
  }
}
复制代码

使用aync/await的话,catch能处理JSON.parse错误

const makeRequest = async () => {
  try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
  } catch (err) {
    console.log(err)
  }
}复制代码

3.条件语句

下面示例中,须要获取数据,而后根据返回数据决定是直接返回,仍是继续获取更多的数据。

const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}
复制代码

这些代码看着就头痛。嵌套(6层),括号,return语句很容易让人感到迷茫,而它们只是须要将最终结果传递到最外层的Promise。

上面的代码使用async/await编写能够大大地提升可读性:

const makeRequest = async () => {
  const data = await getJSON()
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData)
    return moreData
  } else {
    console.log(data)
    return data    
  }
}
复制代码

4. 中间值

你极可能遇到过这样的场景,调用promise1,使用promise1返回的结果去调用promise2,而后使用二者的结果去调用promise3。你的代码极可能是这样的:

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return promise2(value1)
        .then(value2 => {        
          return promise3(value1, value2)
        })
    })
}
复制代码

若是promise3不须要value1,能够很简单地将promise嵌套铺平。若是你忍受不了嵌套,你能够将value 1 & 2 放进Promise.all来避免深层嵌套:

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return Promise.all([value1, promise2(value1)])
    })
    .then(([value1, value2]) => {      
      return promise3(value1, value2)
    })
}
复制代码

这种方法为了可读性牺牲了语义。除了避免嵌套,并无其余理由将value1和value2放在一个数组中。

使用async/await的话,代码会变得异常简单和直观。

const makeRequest = async () => {
  const value1 = await promise1()
  const value2 = await promise2(value1)
  return promise3(value1, value2)
}
复制代码

5. 错误栈

下面示例中调用了多个Promise,假设Promise链中某个地方抛出了一个错误:

const makeRequest = () => {
  return callAPromise()
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => {
      throw new Error("oops");
    })
}

makeRequest()
  .catch(err => {
    console.log(err);
    // output
    // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
  })
复制代码

Promise链中返回的错误栈没有给出错误发生位置的线索。更糟糕的是,它会误导咱们;错误栈中惟一的函数名为callAPromise,然而它和错误没有关系。(文件名和行号仍是有用的)。

然而,async/await中的错误栈会指向错误所在的函数:

const makeRequest = async () => {
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  throw new Error("oops");
}

makeRequest()
  .catch(err => {
    console.log(err);
    // output
    // Error: oops at makeRequest (index.js:7:9)
  })
复制代码

在开发环境中,这一点优点并不大。可是,当你分析生产环境的错误日志时,它将很是有用。这时,知道错误发生在makeRequest比知道错误发生在then链中要好。

6. 调试

最后一点,也是很是重要的一点在于,async/await可以使得代码调试更简单。2个理由使得调试Promise变得很是痛苦:

  • 不能在返回表达式的箭头函数中设置断点

const markRequest = () => {
    return callAPromise ()
        .then (() => callAPromise())
        .then (() => callAPromise())
        .then (() => callAPromise())
        .then (() => callAPromise())

}
复制代码

  • 若是你在.then代码块中设置断点,使用Step Over快捷键,调试器不会跳到下一个.then,由于它只会跳过异步代码。使用await/async时,你再也不须要那么多箭头函数,这样你就能够像调试同步代码同样跳过await语句。

    const markRequest = async () => {
        await callAPromise()
        await callAPromise()
        await callAPromise()
        await callAPromise()
        await callAPromise()
    }复制代码

总结

      对于经常使用的不一样异步编程处理方案,我的观点是针对不一样的业务场景可根据状况选择合适高效的方案,各有优点劣势,不必顶一个踩一个,虽然技术不断发展优化,但有些技术不至于淘汰如此之快,存在即合理。

相关文章
相关标签/搜索