Promise是前端绕不过去的一道坎,用起来很方便:
return new Promise((resolve,reject)=>{})
它能够生成一个新的Promise,以便对异步回掉的值进行读取和近一步操作(防止回调地狱),也方便于错误处理。如以下例子:
const fn = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const n: number = Math.random() * 6 + 1;
if (n > 3) {
resolve(n);
} else {
reject(n)
}
}, 0);
})
}
执行fn()
,会反回一个promise实例,它会产生一个1-6的随机数,如果产生的随机数比3大,那么就是成功的,他会作为resolve函数的参数,若是失败,它则为reject函数的参数。
那么要获取的n很简单,只要通过promise的.then()
方法就可以:
fn().then(
(result) => {
console.log(`n is bigger than 3 ,it's value is ${result}`)
},
(reason) => {
console.log(`error: n is smaller than 3 ,it's value is ${reason}`)
})
可以复制前面这两坨代码到浏览器的控制台试试,这是一个很简单的异步回调与Promise的应用。
但是它的原理到底是什么,却让很多新手百思不得其解。
- 搞懂js前端代码的运行顺序
如果你写的整个代码都是同步的,那么很好理解,他会按照你想要的顺序。
如果加上异步代码呢(setTimeout
就是一个常见的异步方程):
const a = () => {
console.log("a")
}
const b = () => {
setTimeout(() => {
console.log("b")
}, 0);
}
a();
b();
a();
只时候调用栈会分为两个,一个是主任务的调用栈,一个是宏任务的调用栈(异步任务又分为宏任务和微任务,setTimeout是一个宏任务):
main:[a(),a()]
宏任务:[b()]
只有当主线任务的调用栈清空才会开始执行宏任务的调用栈里面的内容。
为什么Promise难以理解,就是因为里面涉及了微任务,它的resolve()
和reject()
里面的内容都是异步调用的(值得一提的是都是以微任务的形式。宏任务与微任务的区别可以简单理解为微任务和宏任务的调用栈不一样,当执行完主线任务时,优先执行微任务调用栈,在执行宏任务调用栈)。
有了上面这些简单的概念后,就可以开始写Promise了。
- 写一个无法链式调用的Promise
所谓的无法链式调用,就是当Promise.then()之后,无法继续.then(),即不会返回一个新的Promise实例
代码如下:
class MyPromise {
state: string = "pending";
callback = [];
constructor(fn: Function) {
fn(this.resolve, this.reject)
};
resolve = (result) => {
if (this.state !== "pending") return;
this.state = "fulfilled";
setTimeout(() => {
this.callback.forEach(handle => {
handle[0].call(undefined, result);
})
}, 0)
};
reject = (reason) => {
if (this.state !== "pending") return;
this.state = "rejected";
setTimeout(() => {
this.callback.forEach(handle => {
handle[1].call(undefined, reason);
})
}, 0)
};
then = (succeed: Function, fail: Function) => {
const handle = [];
if (succeed) {
handle[0] = succeed;
};
if (fail) {
handle[1] = fail;
}
this.callback.push(handle);
}
}
这里先用setTimeout模仿了微任务,后面再进行优化。
先看他的构造函数
constructor(fn: Function) {
fn(this.resolve, this.reject)
};
他必须传一个函数作为该构造函数的参数,并且该函数有两个参数 ,一个是resolve,一个是reject,也都是函数。重点是:这个函数会被立即执行,且this.resolve和this.reject会作为两个参数传进去
可能这里会看的比较乱,结合例子来看会好一点,还是前面那个例子:
const fn = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const n: number = Math.random() * 6 + 1;
if (n > 3) {
resolve(n);
} else {
reject(n)
}
}, 0);
})
}
fn().then(
(result) => {
console.log(`n is bigger than 3 ,it's value is ${result}`)
},
(reason) => {
console.log(`error: n is smaller than 3 ,it's value is ${reason}`)
})
这时候它的参数是匿名函数:
(resolve, reject) => {
setTimeout(() => {
const n: number = Math.random() * 6 + 1;
if (n > 3) {
resolve(n);
} else {
reject(n)
}
}, 0);
}
不如给该函数取名为a。那么代码可以转化为
const a=(resolve, reject) => {
setTimeout(() => {
const n: number = Math.random() * 6 + 1;
if (n > 3) {
resolve(n);
} else {
reject(n)
}
}, 0);
}
const fn = () => {
return new Promise(a)
}
fn().then(
(result) => {
console.log(`n is bigger than 3 ,it's value is ${result}`)
},
(reason) => {
console.log(`error: n is smaller than 3 ,it's value is ${reason}`)
})
那么当执行上面代码时,首先会走到
fn(),那么他会return new Promise(a)
,这时候a为Promise的参数,会马上执行a,
这行a的时候会发现a里面是一个宏任务(setTimeout),那么会把他放到宏任务的栈中,继续执行后面的内容。
这时候会执行.then()
函数,看一下它的实现代码:
then = (succeed: Function, fail?: Function) => {
const handle = [];
if (succeed) {
handle[0] = succeed;
};
if (fail) {
handle[1] = fail;
}
this.callback.push(handle);
}
很容易就能看出来,它也是个函数,并且接受两个函数succeed和fail作为参数。
那它到底做了什么呢?
首先它声明了一个常量handle,它的类型是一个数组
其次他会检查是否传入了succeed和faile函数,如果传入了的话,就分别把它们作为handle的第零项和第一项
最后再把handle给push到callback里面,而callback的类型是一个二维数组。
由于这里面没有异步代码,那么这个函数会被马上调用执行。
这时候callback就多了一个项,留着后面被resolve和reject引用。
这时候假设后面已经没有其他代码了,那么主线任务的调用栈已经全部走完,要开始走微任务的调用栈。
这时候并没有微任务的调用栈,开始走宏任务的调用栈。
宏任务的调用栈只有一个,就是刚刚传入的匿名setTimeout函数。
当开始走宏任务的调用栈的时候要注意一点,这时候的主任务栈就是宏任务了,那么意味着他可以继续有异步调用栈(宏任务与微任务)。
现在走setTimeout函数,为了方便阅读,我又把它写了一遍:
setTimeout(() => {
const n: number = Math.random() * 6 + 1;
if (n > 3) {
resolve(n);
} else {
reject(n)
}
}, 0);
它的逻辑非常的简单,就是生成一个1-6的随机数n,当n大于3的时候,认定为成功,把n当作参数,调用resolve函数,小于3时即调用reject函数。
不妨先假设n>3,这时候会这行resolve(n)。
而这里的resolve是什么呢???
它的类型是一个函数,他是a函数(Promise构造函数所需的参数)的第一个参数参数,那么只有当a被调用的时候才知道它具体是什么,在上面这段代码里面,他只是一个形式参数。
那么a在那里被调用呢?
刚刚已经说过a是一个立即执行函数,所以它再Promise实例构造的时候就被调用了:
fn(this.resolve, this.reject)
这下子清楚了,resolve是类里面的的resolve函数this.resolve
:
resolve = (result) => {
if (this.state !== "pending") return;
this.state = "fulfilled";
setTimeout(() => {
this.callback.forEach(handle => {
handle[0].call(undefined, result);
})
}, 0)
};
那么接下来就是这行上面那个函数了
首先要判断当前promise的壮体是否为pending,为的是让resolve函数只能调用一次
然后把状态改为fulfilled
这时候又出现了一个异步任务(为了方便,用setTimeout模范了微任务,后面在做改进)那么直接把它push到微任务的调用栈中,执行后面的内容
resolve()函数已经执行完了
setTimeout函数也执行完了,因此当前相对的主线任务的stack已经全部清空,开始执行微任务的stack
此时开始执行当前相对的微任务,即用setTimeout模仿的那个任务:
setTimeout(() => {
this.callback.forEach(handle => {
handle[0].call(undefined, result);
})
}, 0)
这个函数很简单,就是对callback进行遍历,然后把所拿到的handle的第一项进行调用,参数为resolve的参数。
到此,所有的任务都运行完了,所有的stack也清空了。
所以由这个例子可见:
resolve函数在空间上应该比then()函数现运行,但实际上确实then函数现运行,把succeed函数push到callback里面,然后resolve或reject再去遍历callback。
- 实现Promise的链式调用
实现这个功能其实就是让.then()
返回一个new Promise
因此对then进行修改:
then = (succeed: Function, fail?: Function) => {
const handle = [];
if (succeed) {
handle[0] = succeed;
};
if (fail) {
handle[1] = fail;
}
this.callback.push(handle);
return new MyPromise(()=>{})
}
这时候会遇到一个问题,就是构造函数的参数,要传入什么呢?好像写不下去了,因为该函数是要被立即执行的。
因此,我们不如现传入一个空函数,他会被立即执行,什么事也没发生,但是它的本质作用其实就是要当promise成功的时候调用resolve函数,失败的时候调用reject函数,我们在内部帮他调用就好了。因此我们要把它保存起来,把它作为callback的第三项,所以代码进一步改写为:
then = (success: Function, fail?: Function) => {
const handle = [];
if (success) {
handle[0] = success;
}
if (fail) {
handle[1] = fail;
}
+ handle[2] = new Promise2(() => {
});
this.callback.push(handle);
+ return handle[2];
}
我们不妨把then的返回值叫做promise2,那我们要如何帮助它调用resolve和reject呢,这就要再promise的resolve函数里面做文章了,改写resolve:
resolve = (result) => {
if (this.state !== "pending") return;
this.state = "fulfilled";
nextTick(() => {
this.callback.forEach((handle) => {
if (handle[0] !== null) {
+ const x = handle[0].call(undefined, result);
+ handle[2].resolveWith(x);
}
})
});
}
我们把promise1的then函数所传的succeed函数的返回值声明为x,然后把它作为参数,调用handle[2] (promise2)的resolveWith
函数。
接下来完成resolveWith
:
resolveWith(x: any) {
if (x instanceof MyPromise) {
x.then((result) => {
this.resolve(result);
}, (reason) => {
this.reject(reason)
})
} else {
this.resolve(x);
}
};
要根据x的类型,进行不同的处理。这个过程其实非常的繁琐复杂,但我这里只做简单的分类,即x为promise或者不是,如果不是的话,那么直接把x作为参数,调用this.resolve(x)
(此时的this指的是promise2)
如果x是promise,那么x就会有可能成功或者失败,会有两种不用的状态,那么我们直接在他的.then()
里面调用promise2的resolve和reject
x.then((result) => {
this.resolve(result);
}, (reason) => {
this.reject(reason)
})
至此,就可以实现一个简单的可以链式调用的Promise了。
- 实现简单的微进程函数nextTick
其实如果是用node.js进行开发的话,它的process.nextTick()就可以满足创建微进程的作用,但是我们的代码不仅要跑在我们的开发环境上,还要跑在浏览器上,而浏览器并没有这个函数,因此我们要利用浏览器上面的另一个函数MutationObserver,具体的内容可以自己查阅,这里只进行实现。把两者结合起来:
const nextTick = (fn) => {
if (process === undefined || process.nextTick === undefined || !(process.nextTick instanceof Function)) {
let counter = 1;
let observer = new MutationObserver(fn);
const testNode = document.createTextNode(String(observer));
observer.observe(testNode, {
characterData: true
})
counter += 1;
testNode.data = String(counter);
} else {
process.nextTick(fn);
}
}
export default nextTick;
- 完整代码
class MyPromise {
state: string = "pending";
callback: any[][] = [];
resolve = (result) => {
if (this.state !== "pending") return;
this.state = "fulfilled";
nextTick(() => {
this.callback.forEach((handle) => {
if (handle[0] !== null) {
const x = handle[0].call(undefined, result);
handle[2].resolveWith(x);
}
})
});
}
reject = (reason) => {
if (this.state !== "pending") return;
this.state = "rejected";
nextTick(() => {
this.callback.forEach((handle) => {
if (handle[1] !== null) {
const x = handle[1].call(undefined, reason);
handle[2].resolveWith(x);
}
})
});
}
constructor(fn: Function) {
fn(this.resolve, this.reject);
};
then = (success: Function, fail?: Function) => {
const handle = [];
if (success) {
handle[0] = success;
}
if (fail) {
handle[1] = fail;
}
handle[2] = new MyPromise(() => {
});
this.callback.push(handle);
return handle[2];
}
resolveWith(x: any): any {
if (this === x) {
return new TypeError();
} else if (x instanceof Promise2) {
x.then((result) => {
this.resolve(result);
}, (reason) => {
this.reject(reason)
})
} else {
this.resolve(x);
}
};
}