一、前言
传统的解决代码单线程执行的方案是回调函数和事件
。这是个解决问题的方案,但是会造成回调地狱。
异步编程是优化代码逻辑提高代码易读性的关键。
目前通用的异步编程方法有三种:
- Promise
- generator+co
- async+await
这三种方法我都经常在用,但是对它们的原理却一知半解。于是想炒个冷饭从头到尾理一遍,梳理一下它们之间的关系。
二、Promise
2.1 原理
Promise对象是一个构造函数,用来生成Promise实例。
Promise对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。
Promise函数的两个参数分别是resolve
和reject
。它们是Promise中定义的两个函数,在运行自定义函数时返回。
resolve
函数将Promise对象的状态从 pending
变为resolved
,reject
将Promise对象的状态从 pending
变为rejected
Promise的原型链上定义了then方法,提供两个回调函数分别捕获resolve、reject返回的值。
2.2 静态方法
方法 | 描述 |
---|---|
Promise.resolve(promise); | 返回 promise(仅当 promise.constructor == Promise 时) |
Promise.resolve(thenable); | 从 thenable 中生成一个新 promise。thenable 是具有 then() 方法的类似于 promise 的对象。 |
Promise.resolve(obj); | 在此情况下,生成一个 promise 并在执行时返回 obj。 |
Promise.reject(obj); | 生成一个 promise 并在拒绝时返回 obj。为保持一致和调试之目的(例如堆叠追踪), obj 应为 instanceof Error。 |
Promise.all(array); | 生成一个 promise,该 promise 在数组中各项执行时执行,在任意一项拒绝时拒绝。 |
Promise.race(array); | 生成一个 Promise,该 Promise 在任意项执行时执行,或在任意项拒绝时拒绝,以最先发生的为准。 |
sample 1
let p1 = new Promise((resolve,reject)=>{
console.log('hello')
setTimeout(function () {
reject('1212')
},1000)
})
p1.then(data=> {
console.log('success'+data)
},err=>{
console.log('err'+err)
})
p1.then(data=> {
console.log('success'+data)
},err=>{
console.log('err'+err)
})
terminal:
hello
err1212
err1212
sample 1 中新建了一个Promise实例,定时1S后使用reject方法,将Promise实例的状态从pending变成rejected,触发then的err捕捉回调函数。
在sample 1 中调用then方法,并不会马上执行回调。是等待实例中状态改变后才会执行。这一点和发布订阅
模式很类似。
sample 2
let fs = require('fs')
let event = {
arr:[],
result:[],
on(fn){
this.arr.push(fn)
},
emit(data){
this.result.push(data)
this.arr.forEach(fn=>fn(this.result))
}
}
event.on(function (data) {
if(data.length === 2){
console.log(data)
}
})
fs.readFile('1.txt','utf8',function (err,data) {
event.emit(data)
})
fs.readFile('2.txt','utf8',function (err,data) {
event.emit(data)
})
smaple2 中将结果data放入暂存数组中,在执行接听函数的时候返回。
2.3 简写Promise源码
通过之前的例子和对发布订阅模式的理解,我们可以大概写出Promise实例的基本功能:
code 1:
function Promise(executor) {
let self = this
self.value = undefined
self.reason = undefined
self.status = 'pending'
self.onResovedCallbacks = []
self.onRejectedCallbacks = []
function resolve(data) {
if(self.status === 'pending'){
self.value = data
self.status = 'resolved'
self.onResovedCallbacks.forEach(fn=>fn())
}
}
function reject(reason) {
if(self.status === 'pending') {
self.reason = reason
self.status = 'reject'
self.onRejectedCallbacks.forEach(fn=>fn())
}
}
//如果函数执行时发生异常
try{
executor(resolve,reject)
}catch (e){
reject(e)
}
}
Promise.prototype.then = function (onFulfilled,onRejected) {
let self = this
if(self.status === 'pending'){
self.onResovedCallbacks.push(()=>{
onFulfilled(self.value)
})
self.onRejectedCallbacks.push(()=>{
onRejected(self.reason)
})
}else if(self.status === 'resolved'){
onFulfilled(self.value)
}else if(self.status === 'reject'){
onRejected(self.reason)
}
}
module.exports = Promise
- 函数内部变量
- status:储存Promise的状态
- onResovedCallbacks:储存Promise pending状态下成功回调函数
- onRejectedCallbacks:储存Promise pending状态下失败回调函数
- resolve函数
- reject函数
- Promise.prototype.then
- 根据实例状态执行响应的回调
- status == pending使用发布订阅模式储存回调函数。
2.4 Promise用法简述
- 如果一个promise执行完后,返回的还是一个Promise对象,会把这个promise的执行结果,传递给下一个then中。
let fs = require('fs')
function read(filePath,encoding) {
return new Promise((resolve,reject)=>{
fs.readFile(filePath,encoding,(err,data)=> {
if(err) reject(err)
resolve(data)
})
})
}
read('1.txt','utf8').then(
f1=>read(f1,'utf8') // 1
).then(
data=> console.log('resolved:',comments)
err=> console.log('rejected: ',err)
)
- 如果then中返回的不是promise,是一个普通值,会将这个普通值作为下一个then的返回结果。
......
read('1.txt','utf8').then(
f1=>read(f1,'utf8')
).then(
return 123 //2
).then(
data=> console.log('resolved:',comments)
err=> console.log('rejected: ',err)
)
- 如果当前then中失败了会走下一个then的失败。
......
read('1.txt','utf8').then(
f1=>read(f1,'utf8')
).then(
return 123
).then(
throw new Error('出错') //3
).then(
data=> console.log('resolved:',comments)
err=> console.log('rejected: ',err)
)
- 如果返回的是undefined不管当前是失败还是成功,都会走下一次成功。
- catch是错误没有处理的情况下会走。
- then中可以不写。
......
read('1.txt','utf8').then(
f1=>read(f1,'utf8')
).then(
return 123
).then(
throw new Error('出错')
).then() //6
.then(
data=> console.log('resolved:',comments)
err=> console.log('rejected: ',err)
)
这些用法中最重要的是promise的then链式调用。 可以大致猜到,旧Promise的then方法返回的是一个新的Promise对象。
参考Promises/A+规范,可以完善手写的Promise源码使其支持promise的静态方法和调用规则。
code 2:
function Promise(executor) {
let self = this
self.value = undefined
self.reason = undefined
self.status = 'pending'
self.onResovedCallbacks = []
self.onRejectedCallbacks = []
function resolve(value) {
if (self.status === 'pending') {
self.value = value
self.status = 'resolved'
self.onResovedCallbacks.forEach(fn=>fn())
}
}
function reject(reason) {
if (self.status === 'pending') {
self.reason = reason
self.status = 'rejected'
self.onRejectedCallbacks.forEach(fn=>fn())
}
}
//如果函数执行时发生异常
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
function resolvePromise(promise2, x, resolve, reject) {
//If promise and x refer to the same object, reject promise with a TypeError as the reason.
if (promise2 === x) {
return reject(new TypeError('chaining cycle'))
}
let called
//2.3.3.Otherwise, if x is an object or function,
if (x !== null && (typeof x == 'object' || typeof x === 'function')) {
try {
let then = x.then
//2.3.3.3.If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where:
//2.3.3.3.3.If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
if (typeof then === 'function') {
then.call(x, y=> {
if (called) return;
called = true;
//递归直到解析成普通值为止
//2.3.3.1.If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
resolvePromise(promise2, y, resolve, reject)
}, err=> {
if (called) return;
called = true;
reject(err)
})
} else {
resolve(x)
}
} catch (e) {
if (called) return;
called = true;
//2.3.3.3.If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
reject(e)
}
} else {
//If x is not an object or function, fulfill promise with x.
resolve(x)
}
}
//then调用的时候 都是异步调用 (原生的then的成功或者失败 是一个微任务)
Promise.prototype.then = function (onFulfilled, onRejected) {
//成功和失败的函数 是可选参数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val=>val;
onRejected = typeof onRejected === 'function' ? onRejected : (e)=> {throw e};
let self = this
let promise2;
promise2 = new Promise((resolve, reject)=> {
if (self.status === 'resolved') {
setTimeout(()=> {
try {
let x = onFulfilled(self.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
} else if (self.status === 'rejected') {
setTimeout(()=> {
try {
let x = onRejected(self.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
} else if (self.status === 'pending') {
self.onResovedCallbacks.push(()=> {
setTimeout(()=> {
try {
let x = onFulfilled(self.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
//当执行成功回调的时候,可能会出现异常,那就用这个异常作为promise2的错误结果
reject(e)
}
}, 0)
})
self.onRejectedCallbacks.push(()=> {
setTimeout(()=> {
try {
let x = onRejected(self.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
}
})
return promise2
}
//setTimeout (规范要求)
Promise.reject = function (reason) {
return new Promise((resolve,reject)=>{
reject(reason)
})
}
Promise.resolve = function (value) {
return new Promise((resolve,reject)=>{
resolve(value)
})
}
Promise.prototype.catch = function (onReject) {
return this.then(null,onReject)
}
Promise.defer = Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve, reject)=> {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd;
}
module.exports = Promise
- 为了支持then的链式调用,Promise.then.prototype中返回一个新的Promise对象
return p2 = new Promise()
2.增加resolvePromise方法,处理旧Promise的回调函数的结果x,根据x的类型,分别调用新promise对象的resolve/reject方法。
- 是普通值用resolve方法返回
- 是函数或者对象就继续用resolvePromise方法迭代(解决回调函数是Promise对象)
- 出错就用reject方法返回
三、bluebird
1: NodeJS 中的 fs.readFile 方法的基本使用方式
const fs = require('fs'),path = require('path');
fs.readFile(path.join(__dirname, '1.txt'), 'utf-8', (err, data) => {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
2:使用Promise封装
let fs = require('fs')
function read(filePath, encoding) {
return new Promise((resolve, reject)=> {
fs.readFile(filePath, encoding, (err, data)=> {
if (err) reject(err)
resolve(data)
})
})
}
read('1.txt', 'utf8').then( data=> data)
把fs.readFile方法用Promise封装一下就能使用Promise api。但是每次手动封装比较麻烦,bluebird可以帮我们简化这个步骤。
3:在 NodeJS 环境中,通过 const bluebird = require('bluebird') 就可以开始使用 Bluebird 提供的 Promise 对象。
Promise.promisify 将单个方法转换成Promise对象。
const bluebird = require('bluebird')
let read = bluebird.promisify(fs.readFile)
read('1.txt', 'utf-8').then(data=> {
console.log('data promisify', data)
})
使用bluebird.promisify
方法,就能将fs.readFile直接封装成一个promise对象,它的原理很简单,return new Promise
是它的核心:
function promisify(fn) {
return function () {
return new Promise((resolve, reject)=> {
fn(...arguments, function (err, data) {
if (err) reject(err)
resolve(data)
})
})
}
}
4.使用 Promise.promisifyAll 把一个对象的所有方法都自动转换成使用 Promise。
const bluebird = require('bluebird'),
fs = require('fs'),
path = require('path');
Promise.promisifyAll(fs);
fs.readFileAsync(path.join(__dirname, 'sample.txt'), 'utf-8')
.then(data => console.log(data))
.catch(err => console.error(err));
promisifyAll核心是遍历对象,生成些新创建方法的名称在已有方法的名称后加上"Async"后缀。
function promisifyAll(obj) {
Object.keys(obj).forEach(key=>{
if(typeof obj[key] === 'function'){
obj[key+'Async'] = promisify(obj[key])
}
})
}
四、generator+co
4.1 简介
generator函数最大的特点是可以用yield
暂停执行,为了区别普通函数在函数名前加*号。
function *say() {
let a = yield "test1"
let b = yield "test2"
}
let it = say();
console.log(1, it.next()) //1 { value: 'test1', done: false }
console.log(2, it.next()) //2 { value: 'test2', done: false }
console.log(3, it.next()) //3 { value: undefined, done: true }
执行say()方法返回的是指针对象,不会返回函数执行结果。it 就是iterator 迭代器
需要调用指针对象的next()方法,让函数指针不断移动并返回一个对象。({value:xxx,done:xxx})
value是yield后面的值,done表示函数是否执行完成。
我们可以用generator函数实现结果的产出,但是也需要它支持输入。
generator函数的运行顺序如下:
使用it.next()执行函数,结果并不会返回给定义的变量a。next方法可以接受参数,这是向 Generator 函数体内输入数据。 第二个next的时候传入参数,就能被变量a接收到。
terminal 返回:
1 { value: 'test1', done: false }
aaa
2 { value: 'test2', done: false }
bbb
3 { value: undefined, done: true }
4.2 使用
example:使用generator异步执行函数,使函数的返回作为下一个函数的入参执行。
let bluebird = require('bluebird')
let fs = require('fs')
let read = bluebird.promisify(fs.readFile)
function *r() {
let r1 = yield read('1.txt', 'utf-8')
console.log('r1',r1); // r1 2.txt
let r2 = yield read(r1, 'utf-8')
console.log('r2',r2); // r2 3.txt
let r3 = yield read(r2, 'utf-8')
console.log('r3',r3); // r3 hello
return r3
}
拿读取文件的例子:使用bluebird将fs.readFile变成promise对象,将读取到的文件内容作为入参传入下一个要执行的函数。
突然发现,要拿到结果会是个复杂的过程,但还是硬着头皮下下去:
const it_r = r()
it_r.next().value.then(d1=>{
return it_r.next(d1).value
}).then(d2=>{
return it_r.next(d2).value
}).then(d3=>{
return it_r.next(d3).value
}).then(data=>{
console.log(data) // hello
})
it.next().value 返回的是一个promise,使用then方法,拿到它成功回调的值,并传入下一个next。
这样能成功拿到我们要的值,但是太麻烦了。于是就有了generator+co的组合!
安装:
$ npm install co
使用:
co(r()).then(data=> {
console.log(data)
})
co会迭代执行it.next()方法,直到done的布尔值为true就返回generator函数的运行结果。
大致执行代码如下:
function co(it) {
return new Promise((resolve, reject)=> {
function next(data) {
let {value, done} = it.next(data)
if(done){
resolve(value)
}else{
value.then(data=> {
next(data)
},reject)
}
}
next()
})
}
五、async+await
async 函数是Generator 函数的语法糖。
比Generator函数用起来简单
- 可以让代码像同步
- 可以try+catch
- 可以使用promise api
async function r() {
try{
let r1 = await read('1.txt','utf8')
let r2 = await read(r1,'utf8')
let r3 = await read(r2,'utf8')
return r3
}catch(e){
console.log('e',e)
}
}
r().then(data=> {
console.log(data)
},err=>{
console.log('err',err)
})
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。遇到await就会先返回,等待函数执行。
参考
招聘贴
字节跳动招人啦!
职位描述:前端开发(高级)ToB方向—视频云(Base: 上海、北京)
1、负责音视频点播/直播/实时通信等多媒体服务产品化以及业务云平台建设;
2、负责多媒体质量体系、运维体系建设及系统开发工作;
3、擅长抽象设计、工程化思维,专注交互、打造极致用户体验。
职位要求
1、计算机、通信和电子信息科学等相关专业优先;
2、熟练掌握各种前端技术,包括 HTML/CSS/JavaScript/Node.js 等;
3、深入了解 JavaScript 语言,使用过 React 或 Vue.js 等主流开发框架;
4、熟悉 Node.js,了解 Express/KOA 等框架,有大型服务端程序开发经验者优先;
5、对用户体验、交互操作及用户需求分析等有一定了解,有产品或界面设计经验者优先;
6、有自己的技术产品、开源作品或活跃的开源社区贡献者优先。
职位亮点
视频云团队依托抖音、西瓜视频等产品的音视频技术积累和基础资源,为客户提供极致的一站式音视频多媒体服务,包括音视频点播、直播、实时通信、图片处理等。对内作为视频技术中台,服务内部业务;对外打造产品化的音视频多媒体服务解决方案,服务企业级用户。
团队具备规范的项目迭代流程、完善的项目角色配置;技术氛围浓厚,拥抱开源社区,定期分享,让大家能够伴随业务快速成长,用技术改变世界!
投递方式
可直接发送简历至:yuanyuan.wallace@bytedance.com
也可以扫描内推二维码在线投递,期待你的加入!~