由于JavaScript是单线程的一门脚本语言(主线程是单线程)
所以异步问题是个让人常头疼的问题
我们来看一下常见的传统解决方案
1.回调函数
回调函数是一种最常见 最传统的方式 类似的这种
// node 的文件读取
let fs = require('fs');
fs.readFile('./test1.js','utf8',function(err,data){
console.log(data)
})
这样我们可以在回调函数里拿到文件的内容,然而这样有一个问题, 要是我要读取多个文件,每一个读取的文件都要依赖前一个读取文件的内容
比如 我test1.js的内容是test2的路径
那么就要这样写
let fs = require('fs');
fs.readFile('./test1.js','utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data)
})
})
要是100个 1000个文件呢 ? 由于异步调用无法用try{}catch 捕获 万一中间读取失败了一次又该怎么做? 难道每个函数体内都if(err) 一下? 这种方式难以维护 也就是我们常说的回调地狱
2订阅发布模式
我现在有这样一种需求 我需要在不同的文件里读取不同的内容, 等多个文件的内容都读取完毕再一起输出
let fs = require('fs');
let result = {};
fs.readFile('./test1.js','utf8',function(err,data){
result.test1 = data
fs.readFile('./test2','utf8',function(err,data){
result.test2 = data
console.log(result)
})
})
用回调方式会带来什么问题? 需求: 这些异步请求没有依赖关系 我需要同时发起 而不是等待上一次读取的结果
现在我们来聊聊 订阅发布模式
订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。 通俗点就事说, 我把我要操作的事放入一个待执行的队列里, 等达到某一个条件,待执行队列依次执行,那么上代码
let fs = require('fs');
let result = {};
class Publish {
constructor() {
this.list = []
};
on(fn){
this.list.push(fn)
};
emit(string){
alert(string)
if (Object.keys(result).length == 2) {
this.list.forEach(fn => {
fn()
})
}
}
}
let p = new Publish()
p.on(function () {
console.log(result)
})
fs.readFile('./test1.js', 'utf8', function (err, data) {
result.test1 = data
p.emit('已经读取到test1的文件')
})
fs.readFile('./test2', 'utf8', function (err, data) {
result.test2 = data
p.emit('已经读取到test2的文件')
})
原理其实也就是回调函数
问题:发布订阅跟观察者模式有什么区别??
3 Promise
好在我们有了Promise这个类 关于Promise的文章有很多 大家自行可以搜索一下
我们来看下Promise A+ 规范
那根据这个规范我们简单的写一遍promise的源码吧
我们来定义2个文件
Promise.js和require.js
//require.js
let Promise = require('./promise.js')
let p = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve(100)
},100)
})
p.then(function(data){
console.log(data)
},function(e){
console.log(e)
})
//promise.js
class Promise {
constructor(executor){
// promise的三个状态
this.state = 'pending'
this.value = undefined
this.reason = undefined
// 有可能调用then的时候 并没有resolve或者reject 所以这里用来存放then之后要做的事
this.onResolvedCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
// 我们需要判断resolve出来的值是否还是一个promise
if(value instanceof Promise){
return value.then(resolve,reject)
}
// promiseA+ 规范要求这么写
setTimeout(()=>{
if (this.state === 'pending') {
this.state = 'resolved'
this.value = value
// 把保存起来的函数一一执行然后结果传给下一个
this.onResolvedCallbacks.forEach( fn => {
return fn(value)
})
}
})
}
const reject = (reason) => {
setTimeout(()=>{
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.onRejectedCallbacks.forEach(fn => {
return fn(reason)
})
}
})
}
try {
executor(resolve,reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled,onRejected){
// new 的时候马上执行executor ----> 就是(resolve,reject)=>{ }() 拿到resolve跟reject 然后做状态判断该调用哪个
onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : function (value) {
return value
};
onRejected = typeof onRejected == 'function' ? onRejected : function (value) {
throw value
};
let promise2 = new Promise((resolve,reject)=>{
if (this.state === 'resolved'){
//onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
// 规范上要这么做 防止直接resolve 同步调用then 这个时候promise2不存在报错
// 执行顺序参考浏览器事件环 哪天有空单独写一篇
setTimeout(()=>{
// 因为onFulfilled都是异步调用 所以不能在new Promise的时候捕获到
try {
let x = onFulfilled(this.value) // then成功的回调
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
if (this.state === 'rejected'){
setTimeout(() => {
try {
let x = onRejected(this.reason) // 失败的回调
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
if (this.state === 'pending'){ // 如果executor是个异步方法 那么会先调用then 所以这里把成功回调跟失败的回调都存起来
this.onResolvedCallbacks.push((value)=>{
try {
let x = onFulfilled(value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
console.log(error)
reject(error)
}
})
this.onRejectedCallbacks.push((reason)=>{
try {
let x = onRejected(reason)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
})
// then返回一个promise
return promise2
}
catch(onRejected){
return this.then(null, onRejected);
}
static all(promises){
return new Promise(function (resolve, reject) {
let result = [];
let count = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(function (data) {
result[i] = data;
if (++count == promises.length) {
resolve(result);
}
}, function (err) {
reject(err);
});
}
});
}
}
const resolvePromise = (promise2, x, resolve, reject)=>{
// promise2 跟 then的成功回调返回值有可能是同一个值
if(promise2 === x){
return reject(new TypeError('报错 循环引用了'))
}
let then,called;
// 要么对象要么函数
if(x !== null&&((typeof x === 'object' || typeof x === 'function')) ){
try {
then = x.then // 有可能是getter定义的会报错
// then 有可能是个函数或者普通值
if(typeof then === 'function'){
// 如果then是个函数的话 就认为它是个promise
then.call(x,function(){
if(called) return
called = true
resolvePromise(promise2, y, resolve, reject);
},function(error){
if (called) return
called = true
reject(error)
})
}else{
resolve(x)
}
} catch (error) {
if (called) return
called = true
reject(error)
}
}else{
// x是个普通值
resolve(x)
}
}
module.exports = Promise
执行require.js的结果是
这样我们就实现了一个promise 是不是很棒棒? 现在我们可以promise.then 链式调用了 然后用catch做统一错误处理 解决了上面错误捕获的问题 还有没有更好的方法? 当然有!
4 生成器 迭代器
在讲async await 之前 我们先讲一下 生成器
**当你在执行一个函数的时候,你可以在某个点暂停函数的执行,并且做一些其他工作,然后再返回这个函数继续执行, 甚至是携带一些新的值,然后继续执行。
上面描述的场景正是JavaScript生成器函数所致力于解决的问题。当我们调用一个生成器函数的时候,它并不会立即执行, 而是需要我们手动的去执行迭代操作(next方法)。也就是说,你调用生成器函数,它会返回给你一个迭代器。迭代器会遍历每个中断点。
next 方法返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数
function* foo () {
var index = 0;
while (index < 2) {
yield index++; //暂停函数执行,并执行yield后的操作
}
}
var bar = foo(); // 返回的其实是一个迭代器
console.log(bar.next()); // { value: 0, done: false }
console.log(bar.next()); // { value: 1, done: false }
console.log(bar.next()); // { value: undefined, done: true }
Generator函数的标志就是function关键词后连缀一个'*' 配合yield 暂停函数 返回的是一个迭代器 每次执行next的时候 停在yield
我们都见过类数组结构吧
let likeArray = { 0: 1, 1: 2, 2: 3, length: 3 }
let arr = [...likeArray]
//执行这段代码会报错 报错信息likeArray is not iterable likeArray是不可枚举的 那么我们如果想实现这样的类数组转为数组 怎么办呢
我们先看一下函数里的argument跟类数组有什么区别
function(){
console.log(argument)}
我们改下一下类数组结构
let likeArray = { 0: 1, 1: 2, 2: 3, length: 3, [Symbol.iterator](){
return {
next() {
return {
value: 1,
done: false
}
}
}
}
}
//在执行
let arr = [...likeArray]
控制台报错FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
感觉是不是有点像那么回事了
我们再改写
let likeArray = { 0: 1, 1: 2, 2: 3, length: 3, [Symbol.iterator](){
let index = 0
let self = this
return {
next() {
return {
done: self.length === index
value: self[index++],
}
}
}
}
}
// 输出[1,2,3] 只有在done是false的时候表示迭代完成 就不再继续执行了 value是每次迭代返回的值
再改写一下
let likeArray = {
0: 1, 1: 2, 2: 3, length: 3, [Symbol.iterator]:function*() {
let index = 0;
while (index !== this.length) {
yield this[index++]
}
}
}
console.log([...likeArray]) //[1,2,3] 调用返回一个迭代器 ... 每次调用迭代器的next方法 返回{value,done}
生成器可以配合node.js中的co, 借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。
例子:
let fs = require('fs');
function readFile(filename) {
return new Promise(function (resolve, reject) {
fs.readFile(filename, function (err, data) {
if (err)
reject(err);
else
resolve(data);
})
})
}
function *read() {
let template = yield readFile('./template.txt');
let data = yield readFile('./data.txt');
return template + '+' + data;
}
co(read).then(function (data) {
console.log(data);
}, function (err) {
console.log(err);
});
5 async/await
有了上面的基础 async/await 更加容易明白了
async/await的优点有
1.内置执行器
2.更好的语义
3.更广的适用性
let fs = require('fs');
function readFile(filename) {
return new Promise(function (resolve, reject) {
fs.readFile(filename, 'utf8', function (err, data) {
if (err)
reject(err);
else
resolve(data);
})
})
}
async function read() {
let template = await readFile('./template.txt');
let data = await readFile('./data.txt');
return template + '+' + data;
}
let result = read();
result.then(data=>console.log(data));
可以直接await 一个promise 使得异步代码执行看起来像同步一样 更优雅
async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function read() {
let template = await readFile('./template.txt');
let data = await readFile('./data.txt');
return template + '+' + data;
}
// 等同于
function read(){
return co(function*() {
let template = yield readFile('./template.txt');
let data = yield readFile('./data.txt');
return template + '+' + data;
});
}
**总结: 异步解决方案还有其他的一些方法 不过都不重要 我们只要掌握了async/await 用async/await写异步代码 更方便维护
第一次写文章 写的不好多多包涵 毕竟很多东西都是站在前任人的肩膀上直接拿过来的**