Nodejs学习记录:异步编程

如何处理 Callback Hell

here is the Callback Hell图片描述javascript

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

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

函数式编程

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

高阶函数

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

function foo(x) {
  return x;
}

高阶函数则能够把函数做为参数,或是将函数做为返回值:git

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

后续传递风格编程

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

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

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

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

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

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

图片描述
图片描述

注意输出顺序,synchronous callback

异步 asynchronous callback

图片描述

图片描述

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');
    
}

图片描述
当咱们开启服务器,先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模式)

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

图片描述
咱们能够从下面的例子来看:

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 任务,周而复始,直至两个队列的任务都取完。
一、macro-task: script(总体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering

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

能够这样简单理解:若是你想让一个任务当即执行,那么就把它设置为Microtask,除此以外都用Macrotask比较好。由于能够看出,虽然Node是异步非阻塞的,但在一个事件循环中,Microtask的执行方式基本上就是用同步的。
在这里就是console.log(3);执行完后当即执行Promise的回掉函数,而后是setTimeout()的回掉函数
图片描述

构造和使用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 能够被
绑定,用其完成值来恢复这个生成器的运行。

上面的代码能够这样改造:

图片描述

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

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

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

如今es6已经内置了Generator 函数
es6 Generator 函数的语法
图片描述
迭代器执行next()时候老是返回一个对象,对象里面有两个属性

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

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

如下koa@2.4 application.js 源码
图片描述

回调 -> promise -> yield

图片描述

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);
})

图片描述

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

图片描述

实例-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
相关文章
相关标签/搜索