javascript 异步编程的5种方式


前言

javascript是单线程的一门语言,所以在执行任务的时候,所有任务必须排队,然后一个一个的执行,
在javascript中有分同步代码,和异步代码,顾名思义,同步代码,就是依此执行的代码,异步代码可能不会立即执行,得等到某一特定事件触发时才会执行,javascript有个任务队列,用来存放异步代码,任务队列中的任务又有优先级之分,微任务(microtask)的优先级大于宏任务(macrotask),在javascript中代码的执行顺序为,主线程会先执行完同步代码,并将异步代码放到任务队列中,当同步代码执行完毕,就轮询任务队列,先询问微任务,如果有则执行微任务,如果没有询问宏任务。

 

 

//异步代码
setTimeout(function () { //属于宏任务
   console.log('hello world3');
},0);
new Promise(resolve => { //属于微任务
  console.log('hello world4'); //Promise 对象会立即执行 所以new Promise里面的类似与同步代码
  resolve('hello world5');
}).then(data => {console.log(data)});

//同步代码
function main(){
  console.log('hello world');
}
console.log('hello world1');
console.log('hello world2');
main();



输出结果为:

    hello world4
    hello world1
    hello world2
    hello world
    hello world5
    hello world3

按照上面所说的顺序,同步代码先执行,那么会先输出hello world4 然后hello world1 ,hello world2,hello world 接下来执行任务队列的异步代码,先轮询微任务是否有要执行的代码,由于Promise对象属于微任务的,故先执行它,输出hello world5 ,然后执行宏任务的代码,及setTimeout的代码,输出hello world3

本例比较简单,讲述了一下javascript代码的执行流程,希望对理解异步有帮助,其中涉及的Promise对象会在本文详细介绍。

本文代码可能比较多,所有涉及的代码均在我的github上 

https://github.com/sundial-dreams/javascript_async

接下来回归正题,Javascript中异步的5种实现方法,并以ajax等为例子,实现几种异步的编写方式


javascript中的异步实现方式有以下几种

  1.  callback (回调函数)

  2.  发布订阅模式

  3. Promise对象

  4. es6的生成器函数

  5.  async/await

 

1.callback (回调函数)

 回调函数是Javascript异步编程中最常见的,由于JavaScript中的函数是一等公民,可以将其以参数形式传递,故就有了回调函数一说,熟悉nodejs的人知到,里面涉及非常多的回调,这些回调代表着,当某个任务处理完,然后需要做的事,比如读取文件,连接数据库,等文件准备好,或数据库连接成功执行编写的回调函数,又比如像一些动画处理,当动画走完,然后执行回调,举个例子
 

 function load(url,callback){
    //something
    setTimeout(callback,3000);//假设某个异步任务处理需要3s 3s后执行回调
}

load('xxx',function() {
    //do something
    console.log('hello world')
})

 

 

再来看个ajax例子 (代码 ) 

 

//ajax_callback.js

function ajax(object, callback) {
  function isFunction(func) { // 是否为函数
    return typeof func === 'function';
  }

  function isObject(object) { //是否为对象
    return typeof object === 'object';
  }

  function toQuerystring(data) { //对象转成查询字符串 例如{a:1,b:2} => a=1&b=2 或{a:[1,2],b:3} => a=1&a=2&b=3
    if (!isObject(data) || !data) throw new Error('data not object');
    var result = '';
    for (var key in data) {
      if (data.hasOwnProperty(key)) {
        if (isObject(data[key]) && !Array.isArray(data[key])) throw new Error('not support error');//除去对象
        if (Array.isArray(data[key])) { 
          data[key].forEach(function (v) {
             result += key + '=' + v + '&'   
          });
        } else {
          result += key + '=' + data[key] + '&';
        }
      }
    }
    return result.substr(0, result.length - 1);//去掉末尾的&
  }

  var url = object.url || '';
  var method = object.method.toUpperCase() || 'GET';
  var data = object.data || Object.create(null);
  var async = object.async || true;
  var dataType = object.dataType || 'json';//相应的数据类型 可选json ,text, xml

  
  var xhr = new XMLHttpRequest();
  
  url = ajax.baseUrl + url;
  data = toQuerystring(data);
  method === 'GET' && (url += '?' + data) && (data = null); //get 请求 => url 后面加上 ?a=1&b=2这种
  
  try {
    xhr.open(method, url, async);
    method === 'POST' && (xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'));//post请求需要设置请求头为 application/x-www-form-urlencoded 类型
    console.log(data);
    xhr.send(data);
    xhr.onreadystatechange = function () {//监听事件
      if (this.readyState === 4) {
        if (this.status === 200)
          if (isFunction(callback))
            switch (dataType) {
              case 'json': {
                callback(JSON.parse(this.responseText));//完成时执行传进来的回调
                break
              }
              case 'text': {
                callback(this.responseText);//重点在这,ajax接到数据就执行传入的函数
                break
              }
              case 'xml': {
                callback(this.responseXML);
                break
              }
              default: {
                break;
              }
            }
      }
    }
  } catch (e) {
    console.log(e);
  }
}

ajax.get = function (url, data, callback) { //get方法
  this({url: url, method: 'GET', data: data}, callback);
};
ajax.post = function (url, data, callback) { //post方法
  this({url: url, method: 'POST', data: data}, callback);
};
ajax.baseUrl = '';


以上是个完整的ajax实例,当ajax完成执行回调
以下是使用koa实现的一个简易的服务端,模拟处理ajax的响应,之后的例子都会用这个来模拟ajax响应
 

//koa_test_server.js

const Koa = require('koa');
const Router = require('koa-router');
const bodyparser = require('koa-bodyparser');
const app = new Koa();
const api = new Router();
api.get('/api/test1', async ctx => { //处理get请求
  ctx.res.setHeader('Access-Control-Allow-Origin', '*');//允许跨域访问
  let querystring = ctx.querystring;
  console.log(querystring);
  ctx.body = JSON.stringify({
    errno: false,
    data: 'it is ok',
    message: `you send me ${querystring} type is GET`
  });
}).post('/api/test2', async ctx => {//处理post请求
  ctx.res.setHeader('Access-Control-Allow-Origin', '*');//允许跨域访问
  let data = ctx.request.body;
  console.log(data);
  ctx.body = JSON.stringify({
    errno: false,
    data: 'it is ok',
    message: `you send me ${JSON.stringify(data)} type is POST`
  })
});
app.use(bodyparser());
app.use(api.routes()).use(api.allowedMethods());
app.listen(3000, () => {
  console.log('listen in port 3000')
});



简单使用如下
 

//test.html

  ajax.baseUrl = 'http://localhost:3000';
  ajax.get('/api/test1',{name: 'dpf', age: 19},function (data) {
    //do something such as render page
    console.log(data);
  });
  ajax.post('/api/test2',{name: 'youname', age: 19}, function (data) {
    //do something such as render page
    console.log(data);
  });


结果如下:

回调的好处就是容易编写,缺点就是过多的回调会产生回调地狱,代码横向扩展,代码可读性变差
不过回调还有很多应用,而且回调也是最常用的实现Javascript异步的方式。

 

 

2.发布订阅模式

发布订阅模式是设计模式的一种,并不是javascript特有的内容,所以javascript可以用发布订阅模式来做异步,那么其他语言如C++ java python php 等自然也能,其他语言实现的均在我的github里。

简单介绍一下发布订阅模式,发布订阅是两个东西,即发布和订阅,想象一下,有家外卖,你可以点外卖,这就是订阅,当你的外卖做好了,就会有人给你打电话叫你去取外卖,这就是发布,简单来说,发布订阅模式,有一个事件池,用来给你订阅(注册)事件,当你订阅的事件发生时就会通知你,然后你就可以去处理此事件,模型如下


接下来简单实现这个发布订阅模式

//async_Event.js

//单对象写法 Event 就相当于事件中心
const Event = function () { //使用闭包的好处 : 把EventPool私有化,外界无法访问EventPool
  const EventPool = new Map();//使用es6 map来存 event,callback 键值对
  const isFunction = func => typeof func === 'function';

  const on = (event, callback) => { //注册事件
    EventPool.get(event) || EventPool.set(event, []);
    if (isFunction(callback)) {
      EventPool.get(event).push(callback);
    }
    else {
      throw new Error('callback not is function')
    }
  };
  const addEventListenr = (event, callback) => { //on方法别名
    on(event, callback)
  };
  const emit = (event, ...args) => { //触发(发布)事件
    //让事件的触发为一个异步的过程,即排在同步代码后执行
    //也可以setTimeout(fn,0)
    Promise.resolve().then(() => {
      let funcs = EventPool.get(event);
      if (funcs) {
        funcs.forEach(f => f(...args))
      } else {
        throw new Error(`${event} not register`)
      }
    })
  };
  const send = (event, ...args) => {//emit方法别名
    emit(event,...args)
  };
  const removeListener = event => {//删除事件
    Promise.resolve(() => {//删除事件也为异步的过程
      if(event){
        EventPool.delete(event)
      }else{
        throw new Error(`${event} not register`)
      }
    })
  };

  return {
    on, emit, addEventListenr, send
  }
}();


简单使用

 

 Event.on('event', data => {//注册事件,名为event
      console.log(data)
    });
setTimeout(() => {
      Event.emit('event','hello wrold')
    },1000);

//1s后触发事件,输出hello world


使用发布订阅模式,修改之前的ajax例子
 

//仅看这部分代码
xhr.onreadystatechange = function () {//监听事件
      if (this.readyState === 4) {
        if (this.status === 200)
            switch (dataType) {
              case 'json': {
                Event.emit('data '+method,JSON.parse(this.responseText));//触发事件
                break
              }
              case 'text': {
                Event.emit('data '+method,this.responseText);
                break
              }
              case 'xml': {
                Event.emit('data '+method,this.responseXML);
                break
              }
              default: {
                break;
              }
            }
         }
    }

使用如下


//test.html
//注册事件
Event.on('data GET',data => {
      //do something such as render page
      console.log(data)
    });

Event.on('data POST',data => {
      //do something such as render page
      console.log(data)
    });
//使用ajax    
ajax.baseUrl = 'http://localhost:3000';
ajax.get('/api/test1',{name: 'dpf', age: 19});
ajax.post('/api/test2',{name: 'youname', age: 19});


 

发布订阅模式是很重要的一种设计模式,在JavaScript中应用非常广泛,比如一些前端框架比如React,Vue等,都有使用这一设计模式,nodejs使用的就更多了。

使用发布订阅模式的好处是事件集中管理,修改方便,缺点就是,代码可读性下降,事件容易冲突。

 

 

3.Promise对象

Promise对象是异步编程的一种解决方案,比传统的回调函数和事件更合理更强大。
Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果,相比回调函数,Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。


Promisel对象的两个特点:

1.对象状态不受外界影响。

Promise对象有三种状态:pending(进行中),fulfilled(已成功),rejected(已失败),当异步操作有结果时可以指定pending状态到fulfilled状态或pending状态到rejected状态的转换,状态一旦变为fulfilled,或rejected则这个Promise对象状态不会在改变。

2.一旦状态改变,就不再变化,任何时候都可以得到这个结果。

 

上面的看不懂也没关系,先把基本格式记住,然后多写写,也就理解了
 

//基本格式
let promise = new Promise((resolve, reject) => {//Promise对象接受一个函数
  try {
    setTimeout(() => {//模拟某异步操作 , 若操作成功返回数据 
      resolve('hello world'); //resolve() 使pending状态变为 fulfilled,需要注意resolve()函数最多只能接收1个参数,若要传多个参数,需要写成数组,或对象,比如resolve([1,2,2,2])或resolve({data,error})
      reject(); //状态已变为fulfilled 故下面这个reject()不执行
    }, 1000);
  }catch (e) {
    reject(e) //操作失败 返回Error对象 reject() 使pending状态变为rejected
  }
});

promise.then((data) => {
  console.log(data)   //resolve()函数里面传的值
},(err) => {
  console.log(err) //reject()函数里传的值
});

//1s后输出hello world


Promise对象的几个方法

1. then(fulfilled,rejected)方法:

异步任务完成时执行的方法,其中fulfilled(data)和rejected(err)分别是单参的回调函数,fulfilled对应的是成功时执行的回调,rejected对应的是失败时执行的回调,fulfilled函数的所接参数为resolve()函数传的值,rejected函数的参数则为reject()函数所传的值。

2. catch(rejected)方法:

then(null,rejected)的别名 捕获Promise对象中的错误

3. Promise.resolve(data):

等价于new Promise(resolve => {resolve(data)})

4.Promise.all([promise1,promise2,...,promisen]):

用于多个Promise对象的执行,执行时间取最慢的那个,例如:

 

let promise1 = new Promise(resolve => {
  setTimeout(() => {
    resolve(1);
  }, 1000);
});
let promise2 = new Promise(resolve => {
  setTimeout(() => {
    resolve(2)
  }, 2000)
});
let promise3 = new Promise(resolve => {
  setTimeout(() => {
    resolve(3)
  }, 3000)
});
let start = Date.now();
Promise.all([promise1, promise2, promise3]).then(([data1, data2, data3]) => {//使用数组解构获得每个Promise对象的data
  console.log(`datas = ${data1},${data2},${data3} total times = ${Date.now() - start}ms`);
});

//输出结果为 datas = 1,2,3 total times = 3000ms

5.Promise.race([promise1,promise2,...,promisen]):

和Promise.all类似,不过它取Promise对象中最快的那个。

6.Promise.reject(err):

等价于new Promise((resolve,reject) => reject(err))

对有了Promise对象有了基本的理解,然后可以用它来替代回调函数的模式,比如一个图片加载例子
 

//回调形式
function asyncLoadImage_callback(url,callback) {//异步加载图片
  var proxyImage = new Image();//图片代理
  proxyImage.src = url;
  proxyImage.onload = callback;//加载完时执行回调
}
asyncLoadImage_callback('xxx', function () {
  image.src = 'xxx'//让真正的图片对象显示
});

//Promise对象形式
function asyncLoadImage_Promise(url) {
  return new Promise((resolve,reject) => {
    var proxyImage = new Image();
    proxyImage.src = url;
    proxyImage.onload = resolve;
    proxyImage.onerror = reject;
  })
}
asyncLoadImage_Promise('xxx')
  .then(() => {
  image.src = 'xxx'//让真正的图片对象显示
 }).catch(err => console.log(err));


使用Promise对象的好处比较明显,除了写起来有一些麻烦而已,不过设计Promise可不仅仅让你这么用用,替代回调函数就完事了的,在后面的生成器实现异步,及async/await中,Promise将发挥巨大作用。

接下来将介绍将回调函数形式与Promise对象形式的相互转换,以下纯属个人兴趣,其实nodejs里的util包里面又promisify和callbackify两个函数,专门实现这个的。

1.回调函数形式转换为Promise对象形式
 

//promisify.js
//callback => Promise
/**
 *
 * @param fn_callback
 * @returns {function(...[*]): Promise<any>}
 */
function promisify(fn_callback) { //接收一个有回调函数的函数,回调函数一般在最后一个参数
  if(typeof fn_callback !== 'function') throw new Error('The argument must be of type Function.');
  return function (...args) {//返回一个函数
    return new Promise((resolve, reject) => {//返回Promise对象
      try {
        if(args.length > fn_callback.length) reject(new Error('arguments too much.'));
        fn_callback.call(this,...args,function (...args) {
          args[0] && args[0] instanceof Error && reject(args[0]);//nodejs的回调,第一个参数为err, Error对象
          args = args.filter(v => v !== undefined && v !== null);//除去undefined,null参数
          resolve(args)
        }.bind(this));//保证this还是原来的this
      } catch (e) {
        reject(e)
      }
    })
  }
}



简单使用
 

//nodejs的fs.readFile为例
let asyncReadFile = promisify(require('fs').readFile);
asyncReadFile('async.js').then(([data]) => {
  console.log(data.toString());
}, err => console.log(err));

//将上面的asyncLoadImage_callback转换为例
let asyncLoadImage = promisify(asyncLoadImage_callback);
asyncLoadImage.then(() => {
  image.src = 'xxx'//让真正的图片对象显示
});



2. Promise对象形式转换为回调函数形式
 

//callbackify.js
//Promise => callback
/**
 * 
 * @param fn_promise
 * @returns {Function}
 */
function callbackify(fn_promise) {
  if(typeof fn_promise !== 'function') throw new Error('The argument must be of type Function.');
  return function (...args) {
    let callback = args.pop();//返回一个函数 最后一个参数是回调
    if(typeof callback !== 'function') throw new Error('The last argument must be of type Function.');
    if(fn_promise() instanceof Promise){
      fn_promise(args).then(data => {
        callback(null,data)//回调执行
      }).catch(err => {
        callback(err,null)//回调执行
      })
    }else{
      throw new Error('function must be return a Promise object');
    }
  }
}



简单使用
 

let func =  callbackify(timer => new Promise((resolve, reject) => {
  setTimeout(() => {resolve('hello world')},timer);
}));
func(1000,function (err,data) {
  console.log(data)//1s后打印hello world
});


接下来对之前的ajax例子进行改写,将回调形式变为Promise形式,可以直接改写,或使用promisify函数

 

先看改写的


//ajax_promise.js
function ajax(object) {
  return new Promise(function (resolve,reject) {//返回个Promise对象
  //省略部分代码
    try {
     //省略部分代码 
      xhr.onreadystatechange = function () {//监听事件
        if (this.readyState === 4) {
          if (this.status === 200) {
            switch (dataType) {
              case 'json': {
                resolve(JSON.parse(this.responseText));//ajax有结果,resolve一下结果
                break
              }
              case 'text': {
                resolve(this.responseText);
                break
              }
              case 'xml': {
                resolve(this.responseXML);
                break
              }
              default: {
                break;
              }
            }
          }else{
            reject(new Error('error'))
          }
        }
      }
    } catch (e) {
      reject(e)
    }
  });
}

ajax.get = function (url, data) { //get方法
    return this({url: url, method: 'GET', data: data});
};
ajax.post = function (url, data) { //post方法
    return this({url: url, method: 'POST', data: data});
};
ajax.baseUrl = '';



简单使用

 

//test.html
ajax.baseUrl = 'http://localhost:3000';
ajax.get('/api/test1',{name: 'dpf', age: 19}).then(data => {
      console.log(data)
    });
ajax.post('/api/test2',{name: 'youname', age: 19}).then(data => {
      console.log(data)
    });

不修改原代码是最好的,故看第二种方式
 

//test.html
ajax = promisify(ajax);
ajax.baseUrl = 'http://localhost:3000';
ajax.get = (url,data) => ajax({url: url, method: 'GET', data: data});
ajax.post = (url,data) => ajax({url: url, method: 'POST', data: data});
ajax.get('/api/test1', {name: 'dpf', age: 19}).then(([data]) => {
    console.log(data)
  });
ajax.post('/api/test2', {name: 'youname', age: 19}).then(([data]) => {
    console.log(data)
  });


Promise对象目前是比较流行的异步解决方案,相比回调函数而言,代码不再横向扩展,而且没有回调地狱这一说,好处还是挺多的,不过也有不足,就是写起来费劲(相比回调而言),不过Promise对象仍然是javascript的一个重要的知识点,而且在后面的生成器函数实现异步,async/await实现异步中会有广泛应用,希望通过刚刚的讲解,读者能对Promise对象有个基本的认识。


4.Generator(生成器)函数

 

Generator(生成器)函数,在python中就是创建迭代对象的,在Javascript中也是如此,不过无论在python还是JavaScript中Generator函数都还有另一个功能,即实现异步。
Generator函数是ES6提供的一种异步编程解决方案,其行为类似于状态机。
首先看一个简单的Generator例子
 

function *gen(){//声明一个生成器
   let t1 = yield "hello"; //yield 表示 产出的意思 用yield来生成东西
   console.log(t1);
   let t2 = yield "world";
   console.log(t2);
}
let g = gen();
/*next()返回一个{value,done}对象,value为yield表达式后面的值,done取值为true/false,表示是否  *生成结束*/  
let x = g.next();//{value:"hello",done:false}   启动生成器


/**
 * 通过给next()函数里传值 这里的值会传递到第一个yield表达式里 即相当于gen函数里 let t1 = "aaaa" */
let y = g.next("aaaa");//{value:"world",done:false}
g.next("bbbb");//{value:undefined,done:true}
console.log(x.value,y.value);

/*
输出

aaaa
bbbb
hello world
*/


上面的例子中,如果把gen函数当成一个状态机,则通过调用next()方法来跳到下一个状态,即下一个yield表达式,给next()函数传值来把值传入上一个状态中,即上一个yield表达式的结果。


在介绍Generator函数的异步时,先简单介绍一下Generator函数的几个方法

1.next()方法:

生成器函数里面的yield表达式并没有值,或者说总返回undefined,next()函数可以接受一个参数,该参数就会被当作yield表达式的值。

2.throw()方法:

在函数体外抛出一个错误,然后在函数体内捕获。例如
 

function *gen1(){
    try{
        yield;
    }catch(e){
        console.log('内部捕获')
    }
}
let g1 = gen1();
g1.next();
g1.throw(new Error());

/*
输出
内部捕获
*/


 

3.return()方法:

返回给定值,并终结生成器。例如
 

function *gen2(){
    yield 1;
    yield 2;
    yield 3;
}
let g2 = gen1();
g2.next();//{value:1,done:false}
g2.return();//{value:undefined,done:true}
g2.next();//{value:undefined.done:true}


4.yield*表达式(类似于python的yield from):

在生成器函数中调用另一个生成器函数。例如
 

function *gen3(){
    yield 1;
    yield 2;
    yield 3;
}
function *gen4(){
    yield 4;
    yield * gen3();
    yield 5;
}
//等价于
function *gen4(){
    yield 4;
    yield 1;
    yield 2;
    yield 3;
    yield 5;
}


在使用Generator(生成器)函数做异步时,先引入协程(来自python的概念)这个概念,可以理解为 "协作的函数",一个协程本质就是子函数,不过这个子函数可以执行到一半,可以暂停执行,将执行权交给其他子函数,等稍后回收执行权的时候,还可以继续执行,跟线程非常像,在c++/python/java中一个线程的单位也是一个子函数(java的run方法),线程之间的切换,就相当于函数的切换,不过这个切换代价非常大,得保存很多跟线程相关东西,而协程则没那么复杂,所以协程又被称为纤程,或轻量级线程。

协程的执行流程大致如下:

1.协程A开始执行。

2.协程A执行到一半,进入暂停,执行权转移给协程B。

3.(一段时间后)协程B交还执行权。

4.协程A恢复执行

其中协程A就是异步任务,因为其分多段执行。

接下来将介绍使用Generator函数来实现协程,并做到异步。
首先来看一个简单的例子
 

const fs = require('fs');
function* gen(){//生成器函数
    let data = yield asyncReadFile(__dirname+'/ajax_promise.js'); 
    console.log(data); //文件读取成功 则输出
    let data2 = yield timer(1000);
    console.log(data2); //过1s后输出 hello world
}
let it = gen();
it.next();
function timer(time){//异步任务 
    setTimeout(() => it.next('hello world'),time)
}
function asyncReadFile(url) {//异步任务 读取文件
    fs.readFile(url,(err,data) => {
        it.next(data.toString())
    })
}


可以看出通过暂缓it.next()方法的执行,来实现异步的功能,如果仅看gen的函数里面内部,比如
let data = yield asyncReadFile(__dirname+'/ajax_promise.js'); 这一段,可以理解为data等待异步读取文件asyncReadFile的结果,如果有了结果,则输出,gen继续向下执行,不过每一个异步函数,比如asyncReadFile的实现却变麻烦了,这个时候就要借助Promise对象,例子如下
 

const promisify = require('./promisify');
function timer(time,callback){
    setTimeout(() => callback(), time)
}
const asyncReadFile = promisify(require('fs').readFile);//借用之前的promisify方法,将callback形式转换为Promise
const asyncTimer = promisify(timer);
function *gen(){
    let [data] = yield asyncReadFile('./a.mjs');//生成一个Promise对象
    console.log(data);
    yield asyncTimer(1000);
    console.log('hello world');
}
let g = gen();
let {value} = g.next(); //{value:asyncReadFile('./a.mjs'),done:false}
value.then(data => {//相当于asyncReadFile('./a.mjs').then(data => {})
    let {value} = g.next(data);//{value:asyncTimer(1000),done:false}
    value.then(data => {//相当于asyncTimer(1000).then(data => {})
        g.next(data);//{value:undefined,done:true}
    })
});


可以看出上面的借助Promise对象例子,在异步处理上可以有更通用的实现,即生成器执行器,
 

//run.js
function run(gen){//传入一个生成器函数
    let g = gen();
    function next(data){
        let result = g.next(data);
        let {value,done} = result;
        if(done) return value;//done为true时结束递归
        if (Array.isArray(value)) value =  Promise.all(value);//如果yield表达式后面跟的是一个数组,可以将其转换为Promise.all
        if(!value instanceof Promise) value = Promise.resolve(value)//不是Promise对象,则转成Promise对象
        value.then((data) => {
            next(data);//递归调用
        });
    }
    next();//启动生成器
}



借助run执行器函数,运行上面的gen只需要run(gen)即可
最后让我们来继续改写之前的ajax例子,这次使用Generator函数,代码如下
 

//test.html
  ajax = promisify(ajax);
  ajax.baseUrl = 'http://localhost:3000';
  ajax.get = (url,data) => ajax({url: url, method: 'GET', data: data});
  ajax.post = (url,data) => ajax({url: url, method: 'POST', data: data});
  run(function*(){
    let [[data1],[data2]] = yield [ajax.get('/api/test1', {name: 'dpf', age: 19}),ajax.post('/api/test2', {name: 'youname', age: 19})];//相当于Promise.all
    console.log(data1,data2)
  });


使用Generator函数无疑是解决异步的优于callback(回调),及Promise对象的好方法,没有callback回调地狱,Promise对象的过长then链,异步代码看起来跟同步代码一样,可读性,和维护性都较好。

 

5.async/await(javascript异步的终极解决方案)

es6中使用Generator函数来做异步,在ES2017中,提供了async/await两个关键字来实现异步,让异步变得更加方便。
async/await本质上还是基于Generator函数,可以说是Generator函数的语法糖,async就相当于之前写的run函数(执行Generator函数的函数),而await就相当于yield,只不过await表达式后面只能跟着Promise对象,如果不是Promise对象的话,会通过Promise.resolve方法使之变成Promise对象。async修饰function,其返回一个Promise对象。await必须放在async修饰的函数里面,就相当于yield只能放在Generator生成器函数里一样。一个简单的例子
 

//封装一个定时器,返回一个Promise对象
const timer = time => new Promise((resolve,reject) => {
    setTimeout(() => resolve('hello world'),time)
});

async function main() {//async函数
    let start = Date.now();
    let data = await timer(1000);//可以把await理解为 async wait 即异步等待(虽然是yield的变体),当Promise对象有值的时候将值返回,即Promise对象里resolve(data)里面的data,作为await表达式的结果
    console.log(data,'time = ',Date.now() - start,'ms')//将会输出 hello world time =  1002 ms
}
main();



可以看到async/await使用起来非常方便,其实async/await的原理也非常简单,就是把Generator函数和执行器包装在一起,其实现如下
 

//spawn.js 
//之前的run函数的变体,只不过多了错误处理,然后返回的是Promise对象
function spawn(genF){
    return new Promise((resolve,reject) => {
        let g = genf();
        function next(nextF){
            let next;
            try{
                next = nextF();
            }catch(e){
                reject(e)
            }
            if(next.done) return resolve(next.value);
            Promise.resolve(next.value)
                   .then(data => next(() => g.next(data)))
                   .catch(err => next(() => g.throw(err)));
        }
        next(() => g.next(undefined))
    })
}



所以之前的async function main() {} 就等价于 function main() { return spawn(function *() {}) },了解async的内部原理可以有助于理解和使用async。

接下来看使用async/await来改进之前的ajax的例子
 

//test.html
ajax = promisify(ajax);
ajax.baseUrl = 'http://localhost:3000';
ajax.get = (url,data) => ajax({url: url, method: 'GET', data: data});
ajax.post = (url,data) => ajax({url: url, method: 'POST', data: data});
 
(async function() {
    let [data1,data2] = await Promise.all([ajax.get('/api/test1', {name: 'dpf', age: 19}),ajax.post('/api/test2', {name: 'youname', age: 19})]);
    console.log(data1,data2)
})() 


到此,这篇文章已经接近尾声,总结一下JavaScript实现异步的这五种方式的优缺点


1.callback(回调函数):写起来方便,不过过多的回调会产生回调地狱,代码横向扩展,过多的回调不易于维护和理解

2.发布订阅模式:通过实现个事件管理器,方便管理和修改事件,不同的事件对应不同的回调,通触发事件来实现异步,不过会产生一些命名冲突的问题(在原来的Event.js基础上加个命名空间,防止命名冲突即可),事件到处触发,可能代码可读性不好。

3.Promise对象:本质是用来解决回调产生的代码横向扩展,及可读性不强的问题,通过.then方法来替代掉回调,而且then方法接的参数也有限制,所以解决了,回调产生的参数不容易确定的问题,缺点的话,个人觉得,写起来可能不那么容易,不过写好了,用起来就就方便多了。

4.Generator(生成器)函数:记得第一次接触Generator函数是在python中,而且协程的概念,以及使用生成器函数来实现异步,也是在python中学到的,感觉javascript有点是借鉴到python语言中的,不过确实很好的解决了JavaScript中异步的问题,不过得依赖执行器函数。

5.async/await:这种方式可能是javascript中,解决异步的最好的方式了,让异步代码写起来跟同步代码一样,可读性和维护性都上来了。

最后文章中的所有代码,均在我的github上
[https://github.com/sundial-dreams/javascript_async]()

,希望这篇文章能让你对JavaScript异步有一定的认识。


 

展开阅读全文

没有更多推荐了,返回首页