简答题
一、谈谈你是如何理解JS异步编程的,EcentLoop、消息队列都是做什么的,什么是宏任务,什么是微任务?
- JS 异步编程:
- 解答:
JavaScript语言的执行环境是单线程,单线程是指一次只能完成一个任务,如果有多个任务,则需要排队,等待前一个任务完成后,才能开始后一个任务。基于这种原因而产生了两种执行任务的模式:同步模式和异步模式,且随着JavaScript面临的需求要来越多,它可以运行在浏览器、服务器上等,为了满足这些需求,使得JavaScript的规模和复杂性也在持续增长,所以JavaScriptd中的异步编程也在不断地调整,往更友好的方向发展,JavaScript异步编程经历了回调函数、Promise、生成器函数Generator、以及现在Async/Await等几个发展阶段。
在第一阶段使用的是回调函数,它是最基本的异步操作方式,以Ajax请求最为常见,它是最简单、容易理解和实现的异步编程;但是不利于代码的阅读和维护,各部分之间高度耦合,使得程序结构混乱,流程难以追踪,每个任务只能指定一个回调函数,不能使用try catch捕获错误,不能直接return,且极易出现回调地狱导致的调式困难,以及控制反转导致的一系列信任问题
- 解答:
ajax(urlA, () => {
// 处理逻辑
ajax(urlB, () => {
// 处理逻辑
ajax(urlC, () => {
// 处理逻辑
})
})
})
在第二阶段引入了Promise,它是ES6推出的一种异步编程的解决方案。Promise是承诺的意思,这个承诺在未来某个时刻会有一个确定的答复,该承诺有三种状态:等待中(pending)、完成(resolved)、拒绝(rejected)。Promise是个构造函数,接受一个函数作为参数。作为参数的函数有两个参数:resolve和reject,分别对应完成和拒绝两种状态。我们可以选择在不同时候执行resolve或reject去触发下一个动作,执行then方法里的函数。Promise实现了链式调用,每次调用then之后返回的都是一个Promise对象,如果在then使用了return,return返回的值会被Promise.resolve()包装。它很好的解决了回调函数中的回调地狱,解决了控制反转导致的信任问题,将代码的执行主动权拿了回来;但是Promise一旦状态从等待改变为其他状态就不再可变了,还有如果不设置回调函数,Promise内部抛出的错误。
new Promise((resolve, reject) => {
console.log('new Promise');
resolve('success'); })
console.log('end');
第三阶段使用生成器函数Generator,它是一种特殊的函数,其最大的特点是控制函数的执行。可以让我们用同步的方式写代码,可以分步执行并得到异步操作的结果,能够知晓异步操作的过程,以及切入修改异步操作的过程。但是需要手动去控制next(...),将回调成功的返回数据送回JavaScript的主流程中;
function *foo(x) {
let y = 2 * (yield (x + 1));
let z = yield (y / 3);
return (x + y + z);
}
let it = foo(5);
console.log(it.next()); // => {value: 6, done: false}
console.log(it.next(12)); // => {value: 8, done: false}
console.log(it.next(13));// => {value: 42, done: true}
最新阶段的Async/Await异步处理编程,其中async函数返回一个 Promise 对象,就是将函数返回使用Promise.resolve(),和then处理返回值一样,可以使用then方法添加回调函数。await后边一般跟Promise对象,async函数执行遇到await后,等待后面的Promise对象的状态从pending变成resolve后,将resolve的参数返回并自动往下执行直到下一个await或结束。它解决了Generator需要手动控制next(...)执行的问题。但它存在一个缺陷是如果多个异步代码没有依赖性却使用了await会导致性能降低。
async function test() {
console.log('1')
}
console.log(test) // Promise {<resolve>: "1"}
整个异步过程都是通过内部的消息队列和事件循环实现的
每个阶段的突破都是为了解决现有阶段的技术问题,异步编程的发展也是一个循序渐进的过程。
- EventLoop、消息队列:
- 解答:
事件循环机制和消息队列的维护是由事件触发线程控制的,事件触发线程是由浏览器渲染引擎提供的,它会维护一个消息队列
EventLoop是事件循环,主要负责监听调用栈(执行栈)和消息队列(任务队列),一旦调用栈中所有的任务都结束了,事件循环就会从消息队列中取出第一个回调函数,然后压入到调用栈中,一旦消息队列中发生了变化,事件循环就会监听到
消息队列:消息队列是类似队列的数据结构,遵循先入先出(FIFO)的规则。如果把调用栈理解为正在执行的工作表,那么消息队列则可以理解成待办的工作表,JS引擎会先做完调用栈中所有的任务,然后通过事件循环从消息队列中再取一个任务出来继续执行,以此类推,整个过程随时可以往消息队列中再去放任务,这些任务在消息队列中会排队等待事件循环
事件循环机制:
1.JS引擎线程会维护一个执行栈,同步代码会依次加入执行栈中,然后依次执行并出栈
2.JS引擎线程遇到异步函数,会将异步函数交给相应的WebApi,并继续执行后面的任务
3.WebApi会在条件满足的时候,将异步对应的回调加入到消息队列中,等待执行
4.执行栈为空时,JS引擎线程会去取消息队列中的回调函数(如果有的话),并加入到执行栈中执行
5.完成后出栈,继续执行4的操作,直至消息队列中的回调函数为空,以上便是事件循环的机制
- 解答:
- 宏任务、微任务
- 解答:
在JS中,有两类任务队列:宏任务队列(macrotask)和微任务队列(microtask),宏任务可以由多个,微任务只有一个- macrotask:主代码块、setTimeout、setInterval、setImmediate、I/O、UI rendering等(可以看到,事件队列中的每一个事件都是一个 macrotask,现在称之为宏任务队列)
- microtask:Promise、process.nextTick、Object.observer等
- 解答:
每次执行栈执行的代码即是一个宏任务,包括任务队列(宏任务队列)中的,因为执行栈中的宏任务执行完后会去取任务队列(宏任务队列)中的任务加入执行栈中
在执行宏任务时遇到Promise等,会创建微任务(.then()里面的回调),并加入到微任务队列队尾;微任务必然是在某个宏任务执行的时候创建的,而在下一个宏任务开始之前,浏览器会对页面重新渲染。同时,在上一个宏任务执行完成后,渲染页面之前,会执行当前微任务队列中的所有微任务。
执行机制
1.执行一个宏任务(执行栈中没有就从消息队列中获取)
2.执行过程中如果遇到微任务,就将微任务添加刀微任务的任务队列中
3.宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
4.当前宏任务执行完毕,开始检查渲染,然后GUI接管渲染
5.喧渲染完毕后,JS引擎线程继续,开始下一个宏任务(从宏任务队列中获取)
代码题
一、将下面异步代码使用Promise的方式改进
解答:使用Promise的方式改进实现如下:
// 使用Promise改进setTimeout异步代码
new Promise((resolve, reject) => {
resolve('hello')
}).then(value => {
return value + ' ' + 'lagou' // value = hello
})
.then(value => {
return value + ' ' + 'I ♥ U' // value = hello lagou
})
.then(value => {
console.log(value) // hello lagou I ♥ U
})
二、基于以下代码完成四个练习
练习1:使用组合函数fp.flowRight()重新实现下面这个函数
解答:fp.flowRight()重新实现如下:
//练习1
let isLastInstock = fp.flowRight(fp.prop('in_stock') , fp.last)
console.log(isLastInstock(cars)) //false 即最后一条数据的in_stock属性值为fase
练习2:使用fp.flowRight(),fp.prop(),fp.first()获取第一个car的name
解答:获取第一个car的name实现如下:
// 练习2
let getFirstCarName = fp.flowRight(fp.props('name'), fp.first)
console.log(getFirstCarName(cars)) //[ 'Ferrari FF' ]
练习3:使用帮助函数_average()重构averageDollarValue,使用函数组合方式实现
解答:重构averageDollarValue实现如下:
let averageDollarValue = fp.flowRight(_average,fp.map(car => car.dollar_value))
console.log(averageDollarValue(cars)) //790700
练习4:使用flowRight写一个sanitizeName()函数,返回一个下划线连接的小写字符串,把数组中的name转换为这种形式,例如:sanitizeName(["Hello World"]) => ["hello_world"]
解答:使用flowRight写一个sanitizeName()函数实现如下:
// 练习4
let _underscore = fp.replace(/\W+/g, '_')
//先遍历数组把里面字符串转成小写,在遍历数组将非单字字符转换成下划线
let sanitizeName1 = fp.flowRight( fp.map(_underscore), fp.map(fp.toLower))
//遍历数组中使用fp.flowRight组合函数,先转换成小写,再将非单字字符转换成下划线
let sanitizeName2 = fp.flowRight( fp.map(fp.flowRight(_underscore, fp.toLower)))
console.log(sanitizeName2(["Hello World",'LaGou Study'])) //[ 'hello_world', 'lagou_study' ]
三、基于下面提供的四个代码,完成后续的四个练习
练习1:使用fp.add(x,y)和fp.map(f,x)创建一个能让functor里的值增加的函数ex1
解答:让functor里的值增加的函数实现如下:
// 练习1
let ex1 = () => {
return fp.map(fp.add(1), maybe.map(x => x)._value)
}
console.log(ex1()) //[ 6, 7, 2 ]
练习2:实现一个函数ex2,能够fp.first获取列表的第一个元素
解答:获取列表的第一个元素实现如下:
//练习2
let xs = Container.of(['do', 'ray', 'me', 'fa', 'so', 'la', 'ti', 'do'])
let ex2 = () => {return fp.first(xs.map(x => x)._value)}
console.log(ex2()) //do
练习3:实现一个函数ex3,使用safeProp和fp.first找到user的名字的首字母
解答:使用safeProp和fp.first找到user的名字的首字母实现如下:
// 练习3
let safeProp = fp.curry(function (x, o){
return Maybe.of(o[x])
})
let user = {id: 2, name: 'Albert'}
let ex3 = () => {
return fp.first(safeProp(Object.keys(user).indexOf('name'),Object.values(user))._value)
}
console.log(ex3()) //A
练习4:使用MayBe重写ex4,不要有if语句
// 练习4
let ex4 = function (n) {
if(n){
return(parseInt(n))
}
}
let ex5 = n =>{
return Maybe.of(n)._value
}
let ex6 = n =>{
return !!n? Maybe.of(n)._value : undefined
}
console.log(ex4(''))
console.log(ex5(''))
console.log(ex6(''))
注:ex5的写法会在空字符串和null的时候导致输出和ex4,ex6不一样
四、手写实现MyPromise源码
要求:尽可能还原Promise中的每一个API,并通过注释的方式描述思路和原理
MyPromise源码(myPromise.js)
/*由于这个状态频繁使用,为了使用这个常量时编辑器有代码提示并能够复用,故把它定义为常量*/
const PENDING = 'pending' //等待
const FULFILLED = 'fulfilled' //成功
const REJECTED = 'rejected' //失败
class MyPromise{
//构造函数 ==》 立即执行执行器 ==》 指的是传递过来的回调函数
constructor(executor) {
// 执行器错误处理 当执行器中代码在执行过程中发生错误的时候,这个时候就让promise状态变成失败
try{
executor(this.resolve, this.reject)
}catch (e) {
//捕获执行器的错误
this.reject(e)
}
}
/*状态是每个promise对象独有的,故因该把状态属性定义为实例属性*/
status = PENDING //promise状态 ===》 默认值为等待
//由于每个promise对象都有自己成功之后的值,都有自己失败的原因,故应该把这两个属性定义为实例的属性
value = undefined //成功之后的值
reason = undefined //失败后的原因
//由于then方法可能会被多次调用,联想到数组能够同时存储多个函数 故在这里将属性定义为数组
successCallback = [] //成功回调
failCallback = [] //失败回调
//定义为箭头函数的目的:将来在直接调用某个普通函数时,这个函数的this指向是window或者undefined,为了能让this的指向为类的实例对象即promise对象,故而使用箭头函数
resolve = value => {
//为了确保状态确定后就不可更改,需加以判断
//如果状态不是等待,阻止程序向下执行
if(this.status !== PENDING) return
//将状态更改为成功
this.status = FULFILLED
//将传递过来的值进行赋值 ==》 保存成功后的值
this.value = value
//判断成功回调是否存在 如果存在 则调用并传递相应的参数
//数组中存储了多个回调函数,我们需要循环遍历这个数组,并在循环的过程中调用这个回调函数
//当数组的长度不为0时,就继续执行循环体中的代码
//考虑到需要从前往后执行,故调用数组的shift方法,把前面的回调函数弹出来
while(this.successCallback.length) this.successCallback.shift()()
}
reject = reason => {
//为了确保状态确定后就不可更改,需加以判断
//如果状态不是等待,阻止程序向下执行
if(this.status !== PENDING) return
//将状态更改为失败
this.status = REJECTED
//将传递过来的值进行赋值 ==》 失败后的原因
this.reason = reason
//判断失败回调是否存在 如果存在 则调用并传递相应的参数
//数组中存储了多个回调函数,我们需要循环遍历这个数组,并在循环的过程中调用这个回调函数
//当数组的长度不为0时,就继续执行循环体中的代码
//考虑到需要从前往后执行,故调用数组的shift方法,把前面的回调函数弹出来
while(this.failCallback.length) this.failCallback.shift()()
}
//then要被定义在原型对象中,并接受成功回调和失败回调两个参数
then (successCallback, failCallback){
//在调用then方法不传递任何参数的时候,要将最先的成功状态依次传递给有回调函数的then方法
successCallback = successCallback? successCallback : value => value
failCallback = failCallback? failCallback : reason => {throw reason}
//then要实现链式调用,then方法必须返回promise对象,上一个回调的返回值传递给下一个then成功的回调
let promise2 = new MyPromise((resolve, reject)=>{
//判断状态并调用对应的回调函数并传递相应的参数
if(this.status === FULFILLED){
//由于promise2还没被实例完,在实例过程中获取不到,故使用异步实现,让同步代码先实现,再来执行异步代码,这时候就能获取到promise2
setTimeout(()=>{
//then方法中的回调函数在执行过程中报错,这个错误要在下一个then的reject中捕获到
try{
let x = successCallback(this.value)
resolvePromise( promise2, x, resolve, reject)
}catch (e) {
//捕获then回调的错误
reject(e)
}
}, 0)
}else if(this.status ===REJECTED){
//由于promise2还没被实例完,在实例过程中获取不到,故使用异步实现,让同步代码先实现,再来执行异步代码,这时候就能获取到promise2
setTimeout(()=>{
// then方法中的回调函数在执行过程中报错,这个错误要在下一个then的reject中捕获到
try{
let x = failCallback(this.reason)
resolvePromise( promise2, x, resolve, reject)
}catch (e) {
//捕获then回调的错误
reject(e)
}
}, 0)
}else{
//说明当前状态等待,但不知是调用成功还是失败的回调
//故将成功回调和是失败回调存储起来
this.successCallback.push(() => {
successCallback()
setTimeout(()=>{
try{
let x = successCallback(this.value)
resolvePromise( promise2, x, resolve, reject)
}catch (e) {
//捕获then回调的错误
reject(e)
}
}, 0)
})
this.failCallback.push(() => {
setTimeout(()=>{
try{
let x = failCallback(this.reason)
resolvePromise( promise2, x, resolve, reject)
}catch (e) {
//捕获then回调的错误
reject(e)
}
}, 0)
})
}
});
return promise2
}
//无论当前这个promise对象最终的状态是成功还是失败,finally中的这个回调函数始终都会执行一次e
//在finally方法的后面可以链式的调用then方法拿到当前这个promise对象返回的结果
finally (callBack){
//如何得到当前promise对象的状态 ==》 this.then方法可以
return this.then(value=>{
//在finally中return了一个promise对象
return MyPromise.resolve(callBack()).then(()=>value)
},reason=>{
return MyPromise.resolve(callBack()).then(()=> {throw reason})
})
}
//用于当前promise对象失败的情况,如果没有传递失败的回调函数,则会被catch捕获到
catch(failCallback){
return this.then(undefined, failCallback)
}
// all方法是用来解决异步并发问题的,它允许我们按照异步代码调用的顺序得到异步执行的结果
// all的返回值也是一个promise对象,在后面可以链式对用then方法
// 在all中的promise对象都是成功的,那么all最后的结果也是成功,如果有一个失败,那么结果就是失败的
// 通过类调用,故为静态方法,该方法接收一个数组作为参数
static all (array){
let results = []
let index = 0
return new MyPromise((resolve, reject) => {
function addData(key, value) {
results[key] = value
index++
// 针对异步情况下,判断index的值和数组的长度相等,说明数组中所有的内容都执行完了,从而而决定是否调用resolve回调函数
if(index === array.length){
resolve(results)
}
}
//循环该数组
for(let i = 0; i < array.length; i++){
let current = array[i]
if(current instanceof MyPromise){
//promise对象,若成功的话将成功的值添加到数组中,若有一个失败的话失败的话则将失败的原因通过reject回调
current.then(value => addData(i, value), reason => reject(reason))
}else{
//普通值 则直接添加到数组中
addData(i, array[i])
}
}
})
}
//如果是普通值,则创建一个promise对象,被将这个值包裹在promise对象中,然后把创建出来的promise对象作为resolve方法的返回值,才能在后面链式调用then方法
//如果是promise对象,则原封不动的将这个promise对象再作为resolve方法的返回值,所以才能在后面调用then方法,通过then方法的成功回调拿到这个promise对象的返回值
static resolve (value){
if(value instanceof MyPromise) return value
return new MyPromise(resolve => resolve(value))
}
}
//判断 x 值时普通值还是promise对象
//如果是普通值,直接调用resolve
//如果是promise对象 查看promise对象返回的结果
//再根据promise对象返回的结果 决定调用resolve 还是调用reject
function resolvePromise(promise2, x, resolve, reject){
//相等表示自己返回自己(Promise对象自返回),这时应该调用reject回调函数
if(promise2 === x){
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
if( x instanceof MyPromise){
//promise对象
x.then(resolve,reject)
}else{
//普通值
resolve(x)
}
}
//导出当前类
module.exports = MyPromise;
测试代码(index.js)
/**
* 了解promise原理及原理代码实现
* 1.Promise 就是一个类 在执行这个类的时候 需要传递一个执行器进去 执行器会立即执行 故放在promise类的构造器中
* 2.Promise 中有三种状态 分别为 成功(fulfilled) 失败(rejected) 等待(pending)
* pending -> fulfilled
* pending -> rejected
** 一旦状态确定就不可更改
* 3.resolve和reject函数是用来更改状态
* resolve:fulfilled
* reject:rejected
* 4.then方法内部做的事情就是判断状态 如果状态成功 调用成功的回调函数 如果状态失败 调用失败的回调函数 then被定义为原型对象当中
* 5.then成功回调有一个参数 表示成功之后的值 then失败回调有一个参数 表示失败的原因
**/
//导向MyPromise
const MyPromise = require('./myPromise')
//创建promise对象 resolve reject 两个函数参数
let promise = new Promise((resolve, reject) => {
//把状态变成成功 参数为成功后的值
resolve()
//把状态编程失败 参数为失败的原因
reject()
})
//需实现promise的then方法,要先了解其中传递两个回调函数及其含义
//调用then方法时,首先要去判断promise状态,成功的话则调用成功回调函数,失败的话则调用失败回调函数
//由于then能被任意一个promise对象调用,故应该将它定义在原型对象中
//then成功回调有一个参数(value) 表示成功之后的值 then失败回调有一个参数(reason) 表示失败的原因
promise.then(value=>{},reason=>{})
/** 1.类核心逻辑实现调用 **/
/*let promise1 = new MyPromise((resolve, reject) => {
resolve('成功')
// reject('失败')
})
promise1.then(value => {
console.log(value) // 成功
}, reason => {
console.log(reason) // 失败
})*/
/** 2.在Promise类中加入异步逻辑 **/
/*let promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('成功')
},2000)
// reject('失败')
})
promise2.then(value => {
console.log(value) // 等待2s后输出成功
}, reason => {
console.log(reason) // 等待2s后输出失败
})*/
/** 3.实现then方法多次调用添加多个处理函数 **/
/*let promise3 = new MyPromise((resolve, reject) => {
resolve('成功')
// reject('失败')
})
promise3.then(value => {
console.log(1) // 1
console.log(value) // 成功
}, reason => {
console.log(reason) // 失败
})
promise3.then(value => {
console.log(2) // 2
console.log(value) // 成功
}, reason => {
console.log(reason) // 失败
})
promise3.then(value => {
console.log(3) // 3
console.log(value) // 成功
}, reason => {
console.log(reason) // 失败
})*/
/** 4./5.实现then方法的链式调用 **/
//后面then方法拿到的值实际上是上一个回调函数的返回值
/*let promise4 = new MyPromise((resolve, reject) => {
resolve('成功')
// reject('失败')
})
//测试4 then中返回值的情况
promise4.then(value => {
console.log(value) // 成功
return 100
}).then(value => {
console.log(value)
})
//测试5 then中返回promise情况
function other(){
return new MyPromise((resolve, reject) => {
resolve('other')
})
}
promise4.then(value => {
console.log(value) // 成功
return other()
}).then(value => {
console.log(value) // other
})*/
/** 6.then方法链式调用识别Promise对象自返回 **/
/*let promise6 = new MyPromise((resolve, reject) => {
resolve('成功')
// reject('失败')
})
let p6 = promise6.then(value => {
console.log(value)
return p6
})
p6.then(value => {
console.log(value) //成功
},reason => {
console.log(reason.message) //Chaining cycle detected for promise #<Promise>
})*/
/** 7.捕获错误及then链式调用其他状态代码补充 **/
// 执行器错误 then方法中的回调函数在执行过程中报错,这个错误要在下一个then的reject中捕获到
/*let promise7 = new MyPromise((resolve, reject) => {
// throw new Error('executor error')
// resolve('成功')
reject('失败')
})
promise7.then(value => {
console.log(value) //成功
},reason => {
console.log(reason.message) //executor error
})
promise7.then(value => {
console.log(value) //成功
throw new Error('then error')
},reason => {
console.log(reason.message) //executor error
return 10000
}).then(value => {
console.log(value) // 成功 10000
// throw new Error('then error')
},reason => {
console.log('xxx') //xxx
console.log(reason.message) //then error
})*/
/** 8.then中方法编程可选参数 **/
/*let promise8 = new MyPromise((resolve, reject) => {
resolve('成功')
// reject('失败')
})
promise8.then().then().then(value =>
console.log(value) //成功 失败
)*/
function p1() {
return new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('p1')
}, 2000)
})
}
function p2() {
return new MyPromise((resolve, reject) => {
// resolve('p2')
reject('失败')
})
}
/** 9.promise.all方法的实现 **/
/*MyPromise.all(['a', 'b', p1(), p2(), 'c']).then(result =>
console.log(result) // [ 'a', 'b', 'p1', 'p2', 'c' ]
)*/
/** 10.promise.resolve方法的实现 **/
/*MyPromise.resolve(100).then(value => console.log(value))
MyPromise.resolve(p1()).then(value => console.log(value))*/
/** 10.promise.finally方法的实现 **/
/*p2().finally(()=>{
console.log('finally')
return p1()
}).then(value=>{
console.log(value) // p2
},reason=>{
console.log(reason)
})*/
/** 11.promise.catch方法的实现 **/
/*p2().then(value => console.log(value))
.catch(reason => console.log(reason))*/