JS异步的几种方式

2019 Java 开发者跳槽指南.pdf (吐血整理)….>>>

JavaScript是一门基于对象的弱类型语言(大家没意见吧),由于V8引擎没有锁机制,JS的执行环境是单线程的,在日常业务中你不可避免地会遇到异步处理。下面将浅谈几种异步方式
友情提示:阅读本文大概需要10分钟

前言

        JS的任务执行模式只有两种:同步、异步。在同步操作中,如果某个请求或者资源出现死锁或假死,都会造成GUI渲染阻塞,页面基本挂掉的情况,于是,有很多场景下须用到异步处理。同步操作,任务遵循队列顺序,异步操作,就相当于并线了,因此异步任务不具有阻塞效应。
        在浏览器端,耗时长的操作都应该异步执行,避免阻塞,比如Ajax操作;做服务器端,异步 将是唯一的模式,因为如果运行同步执行,那么服务器性能将快速被吃掉,服务失去响应,下面将具体介绍几种异步处理方式。

1.回调函数

        回调函数是异步操作最原始的方法,简单来说就是执行一个操作A,操作A执行完毕之后会执行操作B,依次回调,并且后者依赖前者,条理清晰,缺点也很明显,回调嵌套很深会造成 回调地狱(Callback hell)。

ajax(url, () => {
    // 操作A
    ajax(url1, () => {
        // 操作B
        ajax(url2, () => {
            // 操作C
        })
    })
})

        回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误,不能直接 return。

2.延时器setTimeout

        延时器本身就是异步操作,它不取决于代码的执行顺序,取决于某个事件是否发生。

fun1.on('done', fun2);
// 待fun1发生done事件,就执行fun2。

function fun1() {
  setTimeout(function () {
    // ...
    fun1.trigger('done');
  }, 1000);
}
// fun1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行fun2。

        优点:每个事件都可以指定若干个回调函数,可以"去耦合",有利于实现模块化
        缺点:整个程序都要变成事件驱动型,无法保障顺序,运行流程不清晰,不方便看出主流程

发布/订阅模式

        发布订阅模式,基于一个主题/事件通道,希望接收通知的对象(称为subscriber)通过自定义事件订阅主题,被激活事件的对象(称为publisher)通过发布主题事件的方式被通知。
        简单来说:就和用户订阅微信公众号道理一样,公众号可以被若干用户同时订阅,当公众号有新增内容时候,只要发布就好了,用户就能接收到最新的内容。(不等同于 观察者模式)
举个栗子:

jQuery.subscribe('done', f2);
// 操作一:f2向信号中心jQuery订阅done信号
function f1() {
  setTimeout(function () {
    // ...
    jQuery.publish('done');
  }, 1000);
}
// 操作二:f1执行完成后,向中心jQuery发布done信号,从而引发f2的执行。
jQuery.unsubscribe('done', f2);
// 操作三:f2完成执行后,可以取消订阅(unsubscribe)

优点:观察者和订阅者之间没有依赖,松散耦合,改进代码管理和潜在的复用。
附上观察者模式实现代码(JavaScript版)

//观察者列表
function ObserverList(){
  this.observerList = [];
}
ObserverList.prototype.add = function( obj ){
  return this.observerList.push( obj );
};
ObserverList.prototype.count = function(){
  return this.observerList.length;
};
ObserverList.prototype.get = function( index ){
  if( index > -1 && index < this.observerList.length ){
    return this.observerList[ index ];
  }
};
ObserverList.prototype.indexOf = function( obj, startIndex ){
  var i = startIndex;
  while( i < this.observerList.length ){
    ifthis.observerList[i] === obj ){
      return i;
    }
    i++;
  }
  return -1;
};
ObserverList.prototype.removeAt = function( index ){
  this.observerList.splice( index, 1 );
};

//目标
function Subject(){
  this.observers = new ObserverList();
}
Subject.prototype.addObserver = function( observer ){
  this.observers.add( observer );
};
Subject.prototype.removeObserver = function( observer ){
  this.observers.removeAt( this.observers.indexOf( observer, 0 ) );
};
Subject.prototype.notify = function( context ){
  var observerCount = this.observers.count();
  for(var i=0; i < observerCount; i++){
    this.observers.get(i).update( context );
  }
};

//观察者
function Observer(){
  this.update = function(){
    // ...
  };
}

3.Promise

ES6的新特性之一

        ES6的新特性之一,Promise本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。这明显是异步操作。Promise有三种状态:Pending(初始)、Fulfilled(成功)、Rejected(失败),需要注意的是这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,比如说一旦状态变为 resolved ,就不能再次改变为Fulfilled,具有唯一性。

// 上面栗子的改写
ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))
promise的链式调用

1.每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因)
2.如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调
3.如果then中出现异常,会走下一个then的失败回调
4.在 then中使用了return,那么 return 的值会被Promise.resolve() 包装(见例1,2)
5.then中可以不传递参数,如果不传递会透到下一个then中(见例3)
6.catch 会捕获到没有捕获的异常

let fs = require('fs')
function read(url{
  return new Promise((resolve, reject) => {
    fs.readFile(url, 'utf8', (err, data) => {
      if (err) reject(err)
      resolve(data)
    })
  })
}
read('./name.txt')
  .then(function(data{
    throw new Error() //then中出现异常,会走下一个then的失败回调
  }) //由于下一个then没有失败回调,就会继续往下找,如果都没有,就会被catch捕获到
  .then(function(data{
    console.log('data')
  })
  .then()
  .then(nullfunction(err{
    console.log('then', err)// then error
  })
  .catch(function(err{
    console.log('error')
  })

4.Generators

        Generator 是 ES6 中新增的语法,和 Promise 一样,都可以用来异步编程

    // 使用 * 表示这是一个 Generator 函数
    // 内部可以通过 yield 暂停代码
    // 通过调用 next 恢复执行
    functiontest() {
        let a = 1 + 2;
        yield 2;
        yield 3;
    }
    let b = test();
    console.log(b.next()); // > { value: 2, done: false }
    console.log(b.next()); // > { value: 3, done: false }
    console.log(b.next()); // > { value: undefined, done: true }

        从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是Generator 函数的简单实现。
完整的栗子:

    function generator(cb{
        return (function() {
            var object = {
                next0,
                stopfunction() {}
            };
            return {
                nextfunction() {
                    var ret = cb(object);
                    if (ret === undefinedreturn {
                        valueundefined,
                        donetrue
                    };
                    return {
                        value: ret,
                        donefalse
                    };
                }
            };
        })();
    }
    // 如果你使用 babel 编译后可以发现 test 函数变成了这样
    function test() {
        var a;
        return generator(function(_context{
            while (1) {
                switch ((_context.prev = _context.next)) {
                    // 可以发现通过 yield 将代码分割成几块
                    // 每次执行 next 函数就执行一块代码
                    // 并且表明下次需要执行哪块代码
                    case 0:
                        a = 1 + 2;
                        _context.next = 4;
                        return 2;
                    case 4:
                        _context.next = 6;
                        return 3;
                        // 执行完毕
                    case 6:
                    case "end":
                        return _context.stop();
                }
            }
        });
    }

5.async / await

        一个函数如果加上 async ,那么该函数就会返回一个 Promise,并且await 只能在 async 函数中使用。

function sleep() {
 return new Promise(resolve => {
 setTimeout(() => {
 console.log('finish')
 resolve("sleep");
 }, 2000);
 });
}
async function test() {
 let value = await sleep();
 console.log("object");
}
test()

        上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。async 和 await 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。
在举个栗子:

var a = 0
var b = async () => {
 a = a + await 10
 console.log('2', a) // -> '2' 10
//首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generators ,generators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来因为 await 是异步操作,所以会先执行 console.log('1', a)这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10,然后后面就是常规执行代码了
 a = (await 10) + a
 console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
并发请求
let fs = require('fs')
function read(file{
  return new Promise(function(resolve, reject{
    fs.readFile(file, 'utf8'function(err, data{
      if (err) reject(err)
      resolve(data)
    })
  })
}
function readAll() {
  read1()
  read2()//这个函数同步执行
}
async function read1() {
  let r = await read('1.txt','utf8')
  console.log(r)
}
async function read2() {
  let r = await read('2.txt','utf8')
  console.log(r)
}
readAll() // 2.txt 3.txt
// async/await目前算是比较好的方案

参考书籍

JavaScript高级程序设计
JavaScript设计模式
InfoQ前端面试指南

最后

今天的分享就到这里,有问题欢迎大家留言,谢谢~

发布时间:2019.01.23
软文作者:思语
个人站点:limeini.com
GitHub: https://github.com/GitLuoSiyu


原文始发于微信公众号(程序员思语):JS异步的几种方式