node.js异步和Promise

·  阅读 363

Node.js的非阻塞I/O

定义:I/O即Input/Output,一个系统的输入和输出,对外系统信息的一个交换; 可以比较为说话和听别人说话,说话就是输出(output),听就是输入(input) 阻塞I/O和非阻塞I/O的区别就是在于系统接受输入再到输出期间,能不能接受其他输入;

例子:吃饭

  • 去食堂吃饭:排队打饭,等前面的人打完饭了,轮到你打饭,等你打完饭然后开始吃饭(阻塞I/O)
  • 去餐厅点菜:坐下,点菜,服务生招呼完你之后又去招呼别人,这期间你可以干自己的事,等厨房把你的菜做好然后端给你吃(非阻塞I/O)

食堂阿姨和服务生=系统,输入=点菜,输出=端菜 食堂阿姨只能一份一份的打菜,你向食堂阿姨点菜(输入),阿姨给你打菜(输出),这就是阻塞I/O,给你打菜期间不能别人点菜; 服务生点完菜之后可以服务其他人,这就是非阻塞I/O,在你的菜好端给你(输出)之前可以接受其他人的点菜(输入)

要理解非阻塞I/O要先确定系统边界,上面那个例子如果把厨师看作系统怎么都不能理解非阻塞I/O;

node.js正常情况下所有的i/o操作都是非阻塞的,它会把大量计算能力分发到其他的C++线程,等其他C++线程计算完毕之后再把计算结果回调给nodejs线程,nodejs在把这个线程的结果返回给你的应用程序

//非阻塞IO
//第二个参数传入一个回调函数,执行中可以计算1+1
var result = null;
glob(__dirname + '/**/*', function (err, res) {
  result = res;
  console.log(result);
});
console.log(1 + 1);
复制代码

Callback回调函数

nodejs有大量的非阻塞I/O,这些非阻塞I/O程序的运行结果是需要通过回调函数来获取的,通过回调函数来编程的方式就是异步编程

callback函数要遵循一个规则,第一个参数是error,后面的参数才是结果

function interview(callback) {
  setTimeout(() => {
    if (Math.random() < 0.8) {
      callback('success');
    } else {
      throw new Error('fail');
    }
  }, 500);
}
try {
  interview(function (val) {
    console.log(val);
  });
} catch (e) {
  console.log('error', e);
}
复制代码

上面这段代码回调函数throw error并不会被try catch捕获到,而是直接抛给nodejs全局 image.png 为什么会这样子呢,因为try catch调用栈是很有关系的,代码会存在函数调用函数的情况的,每个函数调用其他函数的时候会在调用栈里加一层,一层层累加形成一个调用的链条体系,在程序里就形成了一个栈结构 try catch的机制就是,比如在调用栈的第五项加入一个try catch,栈顶元素抛出一个错误,这个错误会逐层往上抛,抛到第五项的时候会被try catch捕获到; 而上面那段代码的settimeout里面的函数是一个另外事件循环里面回调的,新的事件循环都是一个全新的调用栈,所throw new Error就会被抛到nodejs全局,所以在settimeout异步任务里throw new Error是不会被外面的try catch捕获到,所以错误也需要callback传送出去;

function interview(callback) {
  setTimeout(() => {
    if (Math.random() < 0.8) {
      callback('success');
    } else {
      callback(new Error('fail'));
    }
  }, 500);
}
interview(function (res) {
  if (res instanceof Error) {
    return console.log('car');
  }
  console.log(res);
});
复制代码

但是nodejs回调函数有很多参数,所以约定第一个参数为error。第二个第三个为回调的结果

事件循环(event loop)

事件循环例子 比如在一个餐厅,一个客人点了一个鱼香茄子,服务生告诉厨房做一个鱼香茄子,建立一个鱼香茄子的线程,这时候另外一个客人点了一个小炒肉,服务生告诉厨房做一个小炒肉,又来一个客人点了一个番茄炒蛋,服务生告诉厨房做一个番茄炒蛋;假设说鱼香茄子做好了,服务生就把鱼香茄子端到你面前,结束了这个线程,这时候番茄炒蛋也好了,服务生也把这个番茄炒蛋端出去,最后小炒肉做好了,餐厅的任务就完成了; 这就是事件循环的例子,是一个处理事件的循环,每当有事件不断进来的时候,一旦其中有事件做完了就会把事件所需要的数据回调出去;

const eventLoop = {
  queue: [], //空队列
  loop() {
    while (this.queue.length) {
      var callback = this.queue.shift();
      callback();//调用栈的底部,这个回调之前的的代码全部都是c++
    }
    setTimeout(this.loop.bind(this), 50);
  },
  add(callback) {
    this.queue.push(callback);
  },
};
eventLoop.loop();
setTimeout(() => {
  eventLoop.add(function () {
    console.log(1);
  });
}, 500);

setTimeout(() => {
  eventLoop.add(function () {
    console.log(2);
  });
}, 800);
复制代码

每个事件循环都是一个全新的调用栈,调用栈的地步就是一个event loop的触发事件,可以理解为第一个callback()

Promise

当前的事件循环得不到的结果,在未来的事件循环会得到结果,它是一个状态机

  • pending
  • fulfilled/resolved(正确)
  • rejected(错误)
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    // resolve(2);
    reject(new Error());
  }, 200);
})
  .then((res) => {
    // console.log(res);
  })
  .catch((error) => {
    console.log(error);
  });

console.log(promise);
复制代码

.then 和 .catch

  • resolved状态的Promise会回调后面的第一个.then
  • rejected状态的Promise会回调后面的第一个.catch
  • 任何一个rejected状态后面没有.catch的Promise,都会造成浏览器/node环境的全局错误

Promise可以解决很多异步流程控制问题,上面callback的代码改写为promise

var promise = interview();
function interview() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < 0.8) {
        resolve('success');
      } else {
        reject(new Error());
      }
    }, 500);
  });
}
promise
  .then((res) => {
    console.log(res);
  })
  .catch((error) => {
    console.log(error);
  });
复制代码

执行then和catch会返回一个新的Promise,该Promise最终状态根据then和catch的回调函数的执行结果决定

  • 如果回调函数最终是throw,该promise是rejected状态
var promise = interview();
var promise2 = promise.then((res) => {
  throw new Error();
});

setTimeout(() => {
  console.log(promise);
  console.log(promise2);
}, 800);
function interview() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < 0.8) {
        resolve('success');
      } else {
        reject(new Error());
      }
    }, 500);
  });
}
复制代码

image.png

  • 如果回调函数最终是return,该promise是resolved状态,即便在catch中return也是resolved

image.png

  • 如果回调函数最终return了一个Promise,该Promise会和回调函数return的Promise保持一致,这样就可以在Promise的链式调用中串行的执行任务
var promise = interview();
var promise2 = promise.then((res) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('我接受');
    }, 400);
  });
});

//在800ms和1000ms的时候打印
setTimeout(() => {
  console.log(promise);
  console.log(promise2);
}, 800);
setTimeout(() => {
  console.log(promise);
  console.log(promise2);
}, 1000);


function interview() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < 1) {
        resolve('success');
      } else {
        reject(new Error());
      }
    }, 500);
  });
}
复制代码

image.png

800ms时promise2的状态还在pending,打印的promise2的状态和reutn new promise的状态一致;

所以链式调用就可以改写为

var promise = interview(1)
  .then((res) => {
    //then里面return一个promise,后续的操作会等promise完成之后再继续
    return interview(2);
  })
  .then((res) => {
    return interview(3);
  })
  .then((res) => {
    console.log('我笑了');
  })
  .catch((error) => {
    console.log('我哭了' + error.round);
  });

function interview(round) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.3) {
        resolve('success');
      } else {
        var error = new Error('fail');
        error.round = round;
        reject(error);
      }
    }, 500);
  });
}
复制代码

并发异步Promise.all([]) Promise.all([])接收一个数组,当数组里的promise全部是resolved状态时调用.then,只要有一个rejected就调用.catch

Promise.all([interview('geekbang'), interview('tencent')])
  .then((res) => {
    console.log('我笑了');
  })
  .catch((error) => {
    console.log('我在' + error.name + '挂了');
  });
function interview(name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.3) {
        resolve('success');
      } else {
        var error = new Error('fail');
        error.name = name;
        reject(error);
      }
    }, 500);
  });
}
复制代码
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改