Nodejs学习记录:异步编程

如何处理 Callback Hell

here is the Callback HellNodejs学习记录:异步编程

在早些年的时候, 大家会看到有很多的解决方案例如 Q, async, EventProxy 等等. 最后从流行程度来看 Promise 当之无愧的独领风骚, 并且是在 ES6 的 Javascript 标准上赢得了支持.

基础知识/概念推荐看阮一峰的prommis对象
关于实现案例可以看我的另一篇文章Promise初探
面试 事件/异步 相关问题

函数式编程

基础看这里函数式编程入门教程

高阶函数

常规函数参数一般只接受基本的数据类型或对象引用。
下面是常规的参数传递和返回:

function foo(x) {
  return x;
}

高阶函数则可以把函数作为参数,或是将函数作为返回值:

function foo(x) {
  return function () {
    return x;
  };
}

后续传递风格编程

后续传递风格的程序编写将函数的业务重点从返回值转移到了回调函数中:

function foo(x,bar) {
  return bar(x);
}

相对于相同的foo函数,传入的bar参数不同,则可以得到不同的结果。一个经典的例子就是数组的sort()方法

var points = [40,100,1,5,25,10];
points.sort(function(a, b) {
  return a - b;
});

通过改动sort()方法的参数,可以决定不同排序方式,从这里可以看出高阶函数的灵活性。
Node中事件的处理方式正是基于高阶函数的特性来完成。通过为相同事件注册不同的回调函数,可以灵活的处理业务逻辑。

var emitter = new events.EventEmitter();
emitter.on('event_foo', function () {
  //to do something
});

高阶函数在JavaScipt中有很多,其中ECMAScript5中提供的一些数组方法十分典型。

  • forEach()
  • reduce()
  • reduceRight()
  • filter()
  • every()
  • some()

偏函数

先上定义:
通过指定部分参数来产生一个新的定制函数的形式就是偏函数

var toString = Object.prototype.toString;

var isString = function (obj) {
  return toString.call(obj) == '[object String]';
};

var isFunction = function (obj) {
  return toString.call(obj) == '[object Function]';
};

在javascript中进行类型判断时候,我们通常会像上面代码这样定义方法。这两个函数的定义,重复定义了相似的函数,如果有更多的is*()就会产生很多冗余代码。

我们可以用isType()函数来预先指定type的值

var isType = function (type) {
  return function (obj) {
   return toString.call(obj) == '[object' + type ']';
  };
};

var isString = isType('String');
var isFunction = isTyp('Function');

可以看出引入isType()函数以后,创建isString()、isFunction()函数就变的简单很多。

偏函数应用在异步编程中也很常见。

异步编程的优势和难点

优势

难点

  1. 异常处理

我们以前用这样的代码来进行异常捕获:

try {
  JSON.parse(json);
}catch (e) {
  //todo
}

回调(callback)

需要注意的是回调是有同步异步之分的

同步synchronous callback

Nodejs学习记录:异步编程
Nodejs学习记录:异步编程

注意输出顺序,synchronous callback

异步 asynchronous callback

Nodejs学习记录:异步编程

Nodejs学习记录:异步编程

setTimeout(() => {
  console.log(1, new Date());
  setTimeout(() => {
    console.log(2, new Date());
    setTimeout(() => {
       console.log(3, new Date());
    },2000)
  }, 1000);
},1000);

AJAX

window.onload = function() {
    var http = new XMLHttpRequest();

    http.onreadystatechange = function() {
        if (http.readyState == 4 && http.status ==200){
            console.log(JSON.parse(http.response));
        }
    };
    http.open("GET","data/jx.json",true); //true异步 false同步
    http.send();
    console.log('test');
    
}

Nodejs学习记录:异步编程
当我们开启服务器,先log处理的是 test然后是服务器返回的respone的对象
这是典型的异步

console.log(JSON.parse(http.response));

如果改成这样

http.open("GET","data/jx.json",false); //true异步 false同步

就会先输出对象后输出test 但是这样错的 不要这样用哦

全部代码在这 [镜心的小树屋]()

上面的代码用jquery写这:

$.get("data/jx.json", function(data) {
        console.log(data);
    });
    console.log('test');

    
};

Promise

promise迷你书
Promise初探

让我们先来理清一些错误观念:Promise 不是对回调的替代。Promise 在回调代码和将要执
行这个任务的异步代码之间提供了一种可靠的中间机制来管理回调。
可以把Promise 链接到一起,这就把一系列异步完成的步骤串联了起来。通过与像all(..)
方法(经典术语中称为“门”)和race(..) 方法(经典术语中称为“latch”)这样更高级的 抽象概念结合起来,Promise
链提供了一个近似的异步控制流。 还有一种定义Promise 的方式,就是把它看作一个未来值(future value),对一个值的独立
于时间的封装容器。不管这个容器底层的值是否已经最终确定,都可以用同样的方法应用 其值。一旦观察到Promise
的决议就立刻提取出这个值。换句话说,Promise 可以被看作是 同步函数返回值的异步版本。
--摘自《你不知道的JavaScript 下卷》

事实上,es6已经支持原生的promise :Promise 对象

jquery中的promise(Promise/Deferred模式)

这个实现解决了文章开头我们所说的回调地狱问题

Nodejs学习记录:异步编程
我们可以从下面的例子来看:

setTimeout(() => {console.log(4);},0);
new Promise((resovle => {
  console.log(1);
  resovle();
  console.log(2);
})).then(() => {
  console.log(5);
});

console.log(3);// 问题: 为什么输出是12354,而不是12345

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject,Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。

上面代码中,Promise新建后立即执行,所以首先输出的是"1","2".然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以然后输出"3"

(1) 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

Node.js事件循环中的:Macrotask 与 Microtask

异步任务的两种分类。 在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务, 执行完毕后取出 microtask 队列中的所有任务顺序执行; 之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。
1、macro-task: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering

2、micro-task: process.nextTick, Promises(这里指浏览器实现的原生 Promise), Object.observe, MutationObserver
出处:图灵社区:Promise/A+规范

可以这样简单理解:如果你想让一个任务立即执行,那么就把它设置为Microtask,除此之外都用Macrotask比较好。因为可以看出,虽然Node是异步非阻塞的,但在一个事件循环中,Microtask的执行方式基本上就是用同步的。
在这里就是console.log(3);执行完后立即执行Promise的回掉函数,然后是setTimeout()的回掉函数
Nodejs学习记录:异步编程

构造和使用promise

首先我们应该知道:
Promise 的决议结果只有两种可能:完成或拒绝,附带一个可选的单个值。如果Promise
完成,那么最终的值称为完成值;如果拒绝,那么最终的值称为原因(也就是“拒绝的原
因”)。Promise 只能被决议(完成或者拒绝)一次。之后再次试图完成或拒绝的动作都会
被忽略。因此,一旦Promise 被决议,它就是不变量,不会发生改变。

可以通过构造器Promise(..) 构造promise 实例:

var p = new Promise( function(resolve,reject){
// ..
} );

提供给构造器Promise(..) 的两个参数都是函数,一般称为resolve(..) 和reject(..)。它
们是这样使用的。

  • 如果调用reject(..),这个promise 被拒绝,如果有任何值传给reject(..),这个值就

被设置为拒绝的原因值。

  • 如果调用resolve(..) 且没有值传入,或者传入任何非promise 值,这个promise 就完成。
  • 如果调用resove(..) 并传入另外一个promise,这个promise 就会采用传入的promise

的状态(要么实现要么拒绝)——不管是立即还是最终。

下面是通过promise 重构回调函数调用的常用方法。假定你最初是使用需要能够调用errorfirst
风格回调的ajax(..) 工具:

function ajax(url,cb) {
  // 建立请求,最终会调用cb(..)
}
// ..
ajax( "http://some.url.1", function handler(err,contents){
  if (err) {
    // 处理ajax错误
  }
  else {
    // 处理contents成功情况
  }
} );

可以将其转化为:

function ajax(url) {
  return new Promise( function pr(resolve,reject){
     // 建立请求,最终会调用resolve(..)或者reject(..)
  } );
}
// ..
ajax( "http://some.url.1" )
.then(
  function fulfilled(contents){
  // 处理contents成功情况
  },
  function rejected(reason){
  // 处理ajax出错原因
  }
);
setTimeout(() => {console.log(4);},0);
new Promise((resolve) => {
    console.log(1);
    resolve();
    console.log(2);
}).then(() => {
    console.log(5);
    new Promise((resolve) => {
        console.log(6);
        resolve();
        console.log(7);
    }).then(() => {
        console.log(8);
        setTimeout(() => {console.log(9);},0)
    })
});
console.log(3); // 输出: 123567849

Node中的promise实现

我们通过继承Node的events模块完成简单的实现
此处看《深入浅出》4.3 有空整理归纳下

generator(es6)生成器 + Promise

可以把一系列promise 以链式表达,用以代表程序的异步流控制
一般我们这么写链式promise
step1()
.then(
  step2,
  step2Failed
)
.then(
  function(msg) {
    return Promise.all( [
      step3a( msg ),
      step3b( msg ),
      step3c( msg )
    ] )
  }
)
.then(step4);

但是,还有一种更好的方案可以用来表达异步流控制,而且从编码规范的角度来说也要比很
长的promise 链可取得多。我们可以使用生成器来表达我们的异步流控制。

生成器可以yield 一个promise,然后这个promise 可以被
绑定,用其完成值来恢复这个生成器的运行。

上面的代码可以这样改造:

Nodejs学习记录:异步编程

表面看来,这段代码似乎比前面代码中的等价promise 链实现更冗长。但是,它提供了一
种更有吸引力,同时也是更重要、更易懂、更合理且看似同步的编码风格(通过给“返 回”值的= 赋值等)。特别是由于try..catch
出错处理可以跨过这些隐藏的异步边界。

Generator很多语言中都有,本质上是协程,下面就来看一下协程,线程,进程的区别与联系:

进程:操作系统中分配资源的基本单位
线程:操作系统中调度资源的基本单位
协程:比线程更小的的执行单元,自带cpu上下文,一个协程一个栈
一个进程中可能存在多个线程,一个线程中可能存在多个协程,进程、线程的切换由操作系统控制,而协程的切换由程序员自身控制。异步i/o利用回调的方式来应对i/o密集,同样的使用协程也可以来应对,协程的切换并没有很大的资源浪费,将一个i/o操作写成一个协程,这样进行i/o时可以吧cpu让给其他协程。
js同样支持协程,那就是yield。使用yield给我们直观的感受就是,执行到了这个地方停了下来,其他的代码继续跑,到你想让他继续执行了,他就是会继续执行。
这是用 q这个库实现的Generator 函数用法教程戳这里
Nodejs学习记录:异步编程

现在es6已经内置了Generator 函数
es6 Generator 函数的语法
Nodejs学习记录:异步编程
迭代器执行next()时候总是返回一个对象,对象里面有两个属性

  • value 状态的返回值
  • done 生成器函数内部是否迭代完毕

所以控制代码的执行进度,所以用生成器函数来解决异步就很容易,不再需要回调,甚至不再需要promise,通过同步的方法(鲸鱼注:现在node 8 以后已经全面支持 async/await 替代yield)
并且koa框架在v3版本中将不再支持yield

以下[email protected] application.js 源码
Nodejs学习记录:异步编程

回调 -> promise -> yield

Nodejs学习记录:异步编程

function p(time){
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Date());
    }, time)
  });
}

p(1000).then((data) => {
  console.log(1, data);
  return p(1000);
}).then((data) => {
  console.log(2, data);
  return p(2000);
}).then((data) => {
  console.log(3, data);
})

Nodejs学习记录:异步编程

function p(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Date());
    }, time)
  });
}

co(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)
    })
  }
}

Nodejs学习记录:异步编程

实例-async与await实现(es7)

体验异步的终极解决方案-ES7的Async/Await

function p(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
       resolve(new Date());
    }, time)
  });
}

(async function(){
  let time1 = await p(1000);
  console.log(1, time1);

  let time2 = await p(1000);
  console.log(2, time2)

  let time3 = await p(2000);
  console.log(3, time3);
})()



function* foo(x){
  let y = x * (yield);
  return y;
}

let it = foo(6);
let res = it.next();  // res是什么
res = it.next(7);     // res是什么

异步场景(常见的异步操作)

定时器setTimeout
postmessage
WebWorkor
CSS3 动画
XMLHttpRequest
HTML5的本地数据
等等…

React中的fetch

实战教程(6)使用fetch
fetch就是一种可代替 ajax 获取/提交数据的技术,有些高级浏览器已经可以window.fetch使用了。相比于使用 jQuery.ajax 它轻量(只做这一件事),而且它原生支持 promise ,更加符合现在编程习惯。

参考

《你不知道的JavaScript》
再谈回调、异步与生成器
Generator 函数的含义与用法
Javascript异步编程的4种方法
Understanding Async Programming in Node.js
JavaScript 运行机制详解:再谈Event Loop
https://developer.mozilla.org...
EventProxy
Async/await

相关推荐