ES6模块-速记版本总结

381 阅读44分钟

Event Loop/线程/堆栈

一 JavaScript 执行机制

同步任务 - 主线程
异步任务(宏任务、微任务)- Event Table到Event Queue

  • 同步任务进入主线程异步任务进入Event Table并注册函数。
  • 指定事情完成时,Event Table会将这个函数移入Event Queue
  • 主线程内任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
  • 宏任务: setTimeout,setInterval
  • 微任务: Promise,process.nextTick

执行顺序: 宏任务 ->执行结束 -> 有无可执行的微任务 -> 执行所有的微任务 -> 开始新的宏任务

面试例子:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
复制代码

输出为1,7,6,8,2,4,3,5,9,11,10,12。

console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)
new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})
console.log(3)

1 new Promise 3 then 2
流程如下:
// 遇到 console.log(1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后面执行
// 遇到 new Promise,这个是直接执行的,打印 'new Promise'
// .then 属于微任务,放入微任务队列,后面再执行
// 遇到 console.log(3) 直接打印 3
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2
setTimeout(function () {
  console.log("1");
}, 0);
async function async1() {
  console.log("2");
  const data = await async2();
  console.log("3");
  return data;
}
async function async2() {
  return new Promise((resolve) => {
    console.log("4");
    resolve("async2的结果");
  }).then((data) => {
    console.log("5");
    return data;
  });
}
async1().then((data) => {
  console.log("6");
  console.log(data);
});
new Promise(function (resolve) {
  console.log("7");
  //   resolve()
}).then(function () {
  console.log("8");
});

输出结果:247536 async2 的结果 1

注意!我在最后一个 Promise 埋了个坑 我没有调用 resolve 方法 这个是在面试美团的时候遇到了 当时自己没看清楚 以为都是一样的套路 最后面试官说不对 找了半天才发现是这个坑 哈哈

setTimeout运行机制

定义:用来指定某个函数或某段代码在多少毫秒之后执行。它返回一个整数,表示定时器timer的编号,可以用来取消该定时器。
setTimeout 和 setInterval的运行机制,其实就是将指定的代码移出本次执行,等到下一轮 Event Loop 时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮 Event Loop 时重新判断。

意味着,setTimeout指定的代码,必须等到本次执行的所有同步代码都执行完,才会执行。被称为宏任务。

console.log(1); 
setTimeout(function () { 
    console.log(2); 
}, 0); 
console.log(3);

正确答案:1 3 2

setTimeout 为什么不能保证能够及时执行?

setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤还是空闲。 浏览器的JS引擎遇到setTimeout,指定代码会在设定的时间后加⼊到异步任务队列中。当js引擎发现同步队列中没有要执行的东西了,即运行栈空了就从异步队列中读取,然后放到运行栈中执行。所以setTimeout可能会多了等待线程的时间

三 JavaScript 单线程

JavaScript引擎是基于事件驱动和单线程执行.
所有任务可以分成两种:
同步任务(synchronous),另一种是异步任务(asynchronous)。

单线程指所有任务需要排队,前一个任务结束,才会执行后一个任务。
异步任务指不进入主线程、进入"任务队列",主线程执行完毕,通知"任务队列",某异步任务可执行了,该任务会进入主线程执行。

所以js的运行机制如下:

    1. 所有同步任务都在主线程上执行,形成一个执行栈(Call Stack)
    1. 主线程之外,存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
    1. 一旦"执行栈"中所有同步任务执行完毕,系统就会读取"任务队列",异步任务结束等待,进入执行栈,开始执行。

为什么JavaScript是单线程?

假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准? 为了利用多核CPU的计算能力,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

四 async与await

async

async函数返回一个promise对象,下面两种方法是等效的

function f() {
  return Promise.resolve('TEST');
}

// asyncF is equivalent to f!
async function asyncF() {
  return 'TEST';
}

await

await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值

async function f(){
  // 等同于
  // return 123
  return await 123
}
f().then(v => console.log(v)) // 123

不管await后面跟着的是什么,await都会阻塞后面的代码

async function fn1 (){
  console.log(1)
  await fn2()
  console.log(2) // 阻塞,此块阻塞代码会加入微队列任务中
}

async function fn2 (){
  console.log('fn2')
}

fn1()
console.log(3)

await 会阻塞下面的代码(即加入微任务队列),先执行 async 外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码

所以上述输出结果为:1fn232

流程分析

async function async1() {
  console.log('async1 start') 2
  await async2()
  console.log('async1 end') // 被阻塞,放到微任务队列 6
}
async function async2() {
  console.log('async2') 3
}
console.log('script start') 1
setTimeout(function () {
  console.log('settimeout') 8
})
async1()
new Promise(function (resolve) {
  console.log('promise1') 4
  resolve()
}).then(function () {
  console.log('promise2') 7
})
console.log('script end') 5

所以最后的结果是:script startasync1 startasync2promise1script endasync1 endpromise2settimeout

五 谈谈你对浏览器中进程和线程的理解

浏览器是多进程的

进程
Browser 进程浏览器主进程,负责创建和销毁其它进程、浏览器界面的展示、前进后退网络资源的下载与管理等
GPU 进程用于 3D 绘制等,最多一个
第三方插件进程每种类型的插件对应一个进程,仅当使用该插件时才创建。
浏览器渲染进程 内部是多线程的,每打开一个新网页就会创建一个进程,主要用于页面渲染,脚本执行,事件处理等。

浏览器渲染进程(浏览器内核)

浏览器的渲染进程是多线程的,页面的渲染,JavaScript 的执行,事件的循环,都在这个进程内进行:

浏览器渲染进程
GUI 渲染线程渲染界面,当界面需重绘或因某种操作引发回流时,该线程就会执行。
JS引擎线程负责处理js脚本程序、解析、运行代码等。
事件触发线程控制浏览器事件循环
定时器线程setInterval、setTimeout 所在线程
异步http 请求线程在 XMLHttpRequest连接后通过浏览器新开一个请求线程,检测到状态变更时,如设置有回调函数,将回调再放入事件队列中。由 JavaScript 引擎执行。

注意:
GUI 渲染线程与 JS引擎线程互斥,JS引擎执行时GUI 线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。如果JS执行时间过长,会造成页面渲染不连贯,加载阻塞。

html文档渲染过程,css文件和js文件的下载,是否会阻塞渲染?

浏览器内有多个进程
其中渲染进程被称为浏览器内核,负责页面渲染和执行JS脚本等。
渲染进程负责浏览器的解析和渲染,内部有 JS引擎线程、 GUI 渲染线程、事件循环管理线程、定时器线程、HTTP 线程。
JS引擎线程负责执行 JS 脚本,GUI 渲染线程负责页面的解析和渲染,两者是互斥的,也就是执行 JS 的时候页面是停止解析和渲染的。
浏览器的 HTML/CSS 的解析和渲染都属于 GUI渲染线程,所以和 JS 引擎线程是互斥。下面从代码实际运行的角度分析浏览器解析和渲染的顺序,以及互相间的阻塞关系。

CSS 阻塞

  • css 文件下载和解析不会影响 DOM 的解析,但是会阻塞 DOM 的渲染。因为 CSSOM Tree 要和 DOM Tree 合成 Render Tree 才能绘制页面。下面的test1在css下载并解析完成前是默认样式,test2 在 css 下载并解析完成之前不会显示

image.png

  • css 文件没下载并解析完成之前,**后续的** js 脚本不能执行。下面的 alert('ok') 在 css 下载并解析完成之前不会弹出来:

image.png

  • css 文件的下载不会阻塞 **前面的** js 脚本执行。下面的 alert('ok') 会在 css 下载完成前弹出:

image.png

所以在需要提前执行不操作 dom 元素的 js 时,不妨把 js 放到 css 文件之前。

js 阻塞

js 文件的下载和解析会阻塞 GUI 渲染进程,也就是会阻塞 DOM 和 CSS 的解析和渲染。 js 文件没下载并解析完成之前,后续的 HTML 和 CSS 无法解析

image.png

  • js 文件的下载不会阻塞前面 HTML 和 CSS 的解析image.png

需要注意的点

  • 第一,GUI 渲染线程会尽可能早的将内容呈现到屏幕上,并不会等到所有的 HTML 都解析完成之后再去构建和布局 Render Tree,而是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。下面 test1 会在 js 文件下载完成前渲染完成,而 test2 则会在 js 文件下载并执行完之后渲染:

image.png

  • 第二,文件的下载是不会被阻塞的,不管是 css 还是 js 文件,浏览器的主进程会在页面解析前开启下载,所以就算在外部脚本执行前删除脚本,脚本也还是会下载。

image.png

堆与栈有什么区别?

image.png

Promise手写&&结合Event Loop输出顺序训练题&&如何中断promise&& promise相关题目

PromiseA+规范

术语

  1. promise 是一个有then方法的对象或者是函数,行为遵循本规范
  2. thenable 是一个有then方法的对象或者是函数
  3. value 是promise状态成功时的值,也就是resolve的参数, 包括各种数据类型, 也包括undefined/thenable或者是 promise
  4. reason 是promise状态失败时的值, 也就是reject的参数, 表示拒绝的原因
  5. exception 是一个使用throw抛出的异常值

规范

Promise States

promise应该有三种状态. 要注意他们之间的流转关系.

  1. pending

    1.1 初始的状态, 可改变.
    1.2 一个promise在resolve或者reject前都处于这个状态。
    1.3 可以通过 resolve -> fulfilled 状态;
    1.4 可以通过 reject -> rejected 状态;

  2. fulfilled

    2.1 最终态, 不可变.
    2.2 一个promise被resolve后会变成这个状态.
    2.3 必须拥有一个value值

  3. rejected

    3.1 最终态, 不可变.
    3.2 一个promise被reject后会变成这个状态
    3.3 必须拥有一个reason

Tips: 总结一下, 就是promise的状态流转是这样的

pending -> resolve(value) -> fulfilled
pending -> reject(reason) -> rejected

then

promise应该提供一个then方法, 用来访问最终的结果, 无论是value还是reason.

promise.then(onFulfilled, onRejected)
  1. 参数要求

    1.1 onFulfilled 必须是函数类型, 如果不是函数, 应该被忽略. 1.2 onRejected 必须是函数类型, 如果不是函数, 应该被忽略.

  2. onFulfilled 特性

    2.1 在promise变成 fulfilled 时,应该调用 onFulfilled, 参数是value 2.2 在promise变成 fulfilled 之前, 不应该被调用. 2.3 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数)

  3. onRejected 特性

    3.1 在promise变成 rejected 时,应该调用 onRejected, 参数是reason 3.2 在promise变成 rejected 之前, 不应该被调用. 3.3 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数)

  4. onFulfilled 和 onRejected 应该是微任务

    这里用queueMicrotask来实现微任务的调用.

  5. then方法可以被调用多次

    5.1 promise状态变成 fulfilled 后,所有的 onFulfilled 回调都需要按照then的顺序执行, 也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多个onFulfilled的回调) 5.2 promise状态变成 rejected 后,所有的 onRejected 回调都需要按照then的顺序执行, 也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多个onRejected的回调)

  6. 返回值

    then 应该返回一个promise

    promise2 = promise1.then(onFulfilled, onRejected);
    

    6.1 onFulfilled 或 onRejected 执行的结果为x, 调用 resolvePromise( 这里大家可能难以理解, 可以先保留疑问, 下面详细讲一下resolvePromise是什么东西 ) 6.2 如果 onFulfilled 或者 onRejected 执行时抛出异 常e, promise2需要被reject 6.3 如果 onFulfilled 不是一个函数, promise2 以promise1的value 触发fulfilled 6.4 如果 onRejected 不是一个函数, promise2 以promise1的reason 触发rejected

  7. resolvePromise

    resolvePromise(promise2, x, resolve, reject)
    

    7.1 如果 promise2 和 x 相等,那么 reject TypeError

    7.2 如果 x 是一个 promsie

     如果x是pending态,那么promise必须要在pending,直到 x 变成 fulfilled or rejected.
     如果 x 被 fulfilled, fulfill promise with the same value.
     如果 x 被 rejected, reject promise with the same reason.
    

    7.3 如果 x 是一个 object 或者 是一个 function

     let then = x.then.
     如果 x.then 这步出错,那么 reject promise with e as the reason.
     如果 then 是一个函数,then.call(x, resolvePromiseFn, rejectPromise)
         resolvePromiseFn 的 入参是 y, 执行 resolvePromise(promise2, y, resolve, reject);
         rejectPromise 的 入参是 r, reject promise with r.
     如果 resolvePromise 和 rejectPromise 都调用了,那么第一个调用优先,后面的调用忽略。
     如果调用then抛出异常e 
     如果 resolvePromise 或 rejectPromise 已经被调用,那么忽略
     则,reject promise with e as the reason
     如果 then 不是一个function. fulfill promise with x.
    

手写一个Promise

const PENDING = 'pending';
const FULFULLED = 'fulfilled';
const REJECTED = 'rejected';
class MPromise {
    constructor(fn) {
        this.status = PENDING;
        this.value = '';
        this.reason = '';
        this.resolveMicroQueueTaskList = [];
        this.rejectMicroQueueTaskList = [];
        fn(this.resolve.bind(this), this.reject.bind(this));
    }
    resolve(value) {
        if (this.status === PENDING) {
            this.value = value;
            this.status = FULFULLED;
        }
    }
    reject(reason) {
        if (this.status === PENDING) {
            this.reason = reason;
            this.status = REJECTED;
        }
    }
    get status() {
        return this._status;
    }

    set status(newStatus) {
        this._status = newStatus;
        if (newStatus === FULFULLED) {
            this.resolveMicroQueueTaskList.forEach(cb => {
                cb()
            });
        } else if (newStatus === REJECTED) {
            this.rejectMicroQueueTaskList.forEach(cb => {
                cb()
            });
        }

    }

    then(resolve, reject) {
        const resolveFunction = resolve ? resolve : (value) =>  value;
        const rejectFunction = reject ? reject : (reason) =>  reason;
        const nextPromse = new MPromise((resolve, reject) => {
            const resolveMicroQueueTask = () => {
                queueMicrotask(() => {
                    const x = resolveFunction(this.value);
                    this.resolveNextPromise(x, resolve);
                })
            }
            const rejectMicroQueueTask = () => {
                queueMicrotask(() => {
                    const y = rejectFunction(this.reason)
                    this.resolveNextPromise(y, resolve);
                })
            }
            switch (this.status) {
                case PENDING: {
                    this.resolveMicroQueueTaskList.push(resolveMicroQueueTask);
                    this.rejectMicroQueueTaskList.push(rejectMicroQueueTask);
                    break;
                }
                    
                case FULFULLED: {
                    resolveMicroQueueTask();
                    break;
                }
                    
                case REJECTED: {
                    rejectMicroQueueTask();
                }
            }
        })
        return nextPromse;
    }

    catch(reject) {
        this.then(null, reject);
    }

    resolveNextPromise(x, resolve) {
        resolve(x);
    }

    static resolve(value) {
        if(value instanceof MPromise) {
           return value; 
        } 
        return new MPromise((resolve, reject) => {
            resolve(value);
        })
        
    }
    static reject(value) {
        if(value instanceof MPromise) {
            return value;
        } else {
            return new MPromise((resolve, reject) => {
                reject(value);
            })
        }
    }
    static race (promiseList) {
        let promiseListLen = promiseList.length;
        
        return new MPromise((resolve, reject) => { 
            if(promiseListLen === 0) {
                resolve()
            }
            for(var i = 0; i< promiseList.length; i++){
                MPromise.resolve(promiseList[i]).then(res=> {
                    resolve(res)
                }).catch(err => {
                    reject(err)
                })
            }
        })
    }

    static all (promiseList) {
        let promiseListLen = promiseList.length;
        let j = 0;
        let promiseValList = [];
        return new MPromise((resolve, reject) => { 
            if(promiseListLen === 0) {
                resolve()
            }
            for(var i = 0; i< promiseList.length; i++){
                MPromise.resolve(promiseList[i]).then(res=> {
                    j++
                    promiseValList.push(res);
                    if(promiseListLen === j) {
                        resolve(promiseValList)
                    }
                }).catch(err => {
                    reject(err)
                })
            }
        })
    }
}

调用方式

链式调用

const promiseA = new MPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('reject promiseA')
    }, 1000);
})

promiseA.then(res => {
    console.log('then1 res', res);
    return 1
}).then(res=> {
    console.log('then2 res', res);
}).catch(err => {
    console.log('catch err', err)
})
// then1 res reject promiseA
// then2 res 1

static resolve | reject方法

MPromise.resolve(promiseA).then(res=> {
    console.log('res', res)
}).catch(err => {
    console.log('err', err)
})
MPromise.resolve('123').then((res)=> {
    console.log('res', res)
})
MPromise.reject('123').catch((res)=> {
    console.log('err', res)
})

race | all

const promiseA = new MPromise ((resolve, reject) => {
    setTimeout(function() {
        resolve(4000)
    }, 4000)
})
const promiseB = new MPromise ((resolve, reject) => {
    setTimeout(function() {
        resolve(3000)
    }, 3000)
})
const promiseC = new MPromise ((resolve, reject) => {
    setTimeout(function() {
        resolve(2000)
    }, 2000)
})

const promiseAsyncList = [promiseA, promiseB, promiseC]

#### race

MPromise.race(promiseAsyncList).then(res=> {
    console.log('res result is', res);
}).catch(err => {
    console.log('err result is', err);
})

#### all
MPromise.all(promiseAsyncList).then(res=> {
    console.log('res result is', res);
}).catch(err => {
    console.log('err result is', err);
})

Promise.all 和 Promise.allSettled 有什么区别?

最大不同:Promise.allSettled永远不会被reject

Promise.all的痛点

const promises = [
  delay(100).then(() => 1),
  delay(200).then(() => 2),
  Promise.reject(3)
  ]

Promise.all(promises).then(values=>console.log(values))
// 最终输出: Uncaught (in promise) 3

Promise.all(promises)
.then(values=>console.log(values))
.catch(err=>console.log(err))
// 加入catch语句后,最终输出:3

当需要处理多个Promise并行时,一旦有一个promise出现了异常,被reject了,情况就会变的麻烦。
尽管能用catch捕获其中的异常,但你会发现其他执行成功的Promise的消息都丢失了。所以要么全部成功,要么全部重来

Promise.allSettled

const promises = [
  delay(100).then(() => 1),
  delay(200).then(() => 2),
  Promise.reject(3)
  ]

Promise.allSettled(promises).then(values=>console.log(values))
// 最终输出: 
//    [
//      {status: "fulfilled", value: 1},
//      {status: "fulfilled", value: 2},
//      {status: "rejected", value: 3},
//    ]

Promise.allSettled情况下,当前promise的状态,没有任何一个promise的信息被丢失。

因此,当用Promise.allSettled时,我们只需专注在then语句里,当有promise被异常打断时,我们依然能妥善处理那些已经成功了的promise,不必全部重来。

面试练习题

## 1.
const test = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(111);
    }, 1000);
}).then((res) => {
    console.log(res); // 111
    return res; // -->如果没有return res,后面的test.value的值就是undefined
});

setTimeout(() => {
     console.log('catch1', test) // {<fulfilled>: 111}
}, 3000)

## 2. 
 const test = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(111);
        console.log('catch1', test) // catch1 Promise {<pending>}
    }, 1000);
}).catch((reason) => {
    console.log('报错' + reason); // 报错111
    console.log('catch2', test) // catch Promise {<pending>}
    // return reason -> 加了这个return, test后面的状态为 fulfilled: 111
});

setTimeout(() => {
    console.log('timeout', test);// timeout Promise {<fulfilled>: undefined}
}, 3000)

## 3
const test2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(111);
        console.log('catch1', test2) // catch1 Promise {<rejected>: 111}
    }, 1000);
})
setTimeout(() => {
    console.log('timeout', test2);// timeout Promise {<rejected>: 111}
}, 3000)

## 4
const test2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('catch1', test2) // catch1 Promise {<pending>}
        reject(111);
    }, 1000);
})
setTimeout(() => {
    console.log('timeout', test2);// timeout Promise {<rejected>: 111}
}, 3000)

40道promise && 事件循环机制 && await && async 输出顺序训练题链接

fe.ecool.fun/topic-list?…

如何中断Promise?

Promise有个缺点,那就是一旦创建就无法取消,所以本质上promise是无法终止的。但是开发过程中会碰到2个需求:

  1. 中断调用链
  2. 中断Promise

1. 中断调用链

在某个 then/catch 执行之后,不想让后续的链式调用继续执行了。
Promise的then方法接收两个参数:Promise.prototype.then(onFulfilled, onRejected)

若onFulfilled或onRejected是一个函数,当函数返回一个新Promise对象时,原Promise对象的状态将跟新对象保持一致,详见Promises/A+标准。

因此,当新对象保持“pending”状态时,原Promise链将会中止执行。

Promise.resolve().then(() => {
    console.log('then 1')
    return new Promise(() => {}) // 返回一个一直pending的promise,原promise对象会与此对象保持一致,就会中断不会向下执行
}).then(() => {
    console.log('then 2')
}).then(() => {
    console.log('then 3')
}).catch((err) => {
    console.log(err)
})

2. 中断Promise

注意这里是中断而不是终止,因为 Promise 无法终止,这个中断的意思是:在合适的时候,把 pending 状态的 promise 给 reject 掉。例如一个常见的应用场景就是希望给网络请求设置超时时间,一旦超时就就中断,我们这里用定时器模拟一个网络请求,随机 3 秒之内返回。(race及reject)

function timeoutWrapper(p, timeout = 2000) {
    const wait = new Promise((resolve, reject) => {
        setTimeout(() => {
        reject('请求超时')
        }, timeout)
    })
    return Promise.race([p, wait])
}

Promise中,resolve后面的语句是否还会执行?

会被执行。如果不需要执行,需要在 resolve 语句前加上 return。

let promise1 = new Promise(function(resolve, reject){
    resolve(111);
    console.log(222);
})
promise1.then(res=> {
    console.log(res)
})
// 222 111 Promise{<fulfilled>: undefined}

let promise1 = new Promise(function(resolve, reject){
    return resolve(111);
    console.log(222);
})
promise1.then(res=> {
    console.log(res)
})
// 111 Promise{<fulfilled>: undefined}

Promise中的值穿透是什么?

.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透 当then中传入的不是函数,则这个then传入的值无效,返回的promise的data将会保存上一个的promise.data。这就是发生值穿透的原因。而且每一个无效的then所返回的promise的状态都为resolved。

Promise.resolve(1) 
    .then(2) // 注意这里 返回的promise的状态都为resolved
    .then(Promise.resolve(3)) 
    .then(console.log)

上面代码的输出是
1
Promise {<fulfilled>: undefined}

reduce() 方法

reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。 reduce() 可以作为一个高阶函数,用于函数的 compose。 reduce() 对于空数组是不会执行回调函数的。

array.reduce(function(total必->初始值, currentValue必, currentIndex选, arr选), initialValue选)
var numbers = [65, 44, 12, 4]; 
function getSum(total, num) { 
    return total + num; 
} 
function myFunction(item) { 
    document.getElementById("demo").innerHTML = numbers.reduce(getSum); 
}
// 125

使用Promise实现每隔1秒输出1,2,3

const arr = [1, 2, 3]
arr.reduce((p, x) => {
  return p.then(() => {
    return new Promise(r => {
      setTimeout(() => r(console.log(x)), 1000)
    })
  })
}, Promise.resolve())

实现mergePromise函数

把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组data中。

const time = (timer) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve()
    }, timer)
  })
}
const ajax1 = () => time(2000).then(() => {
  console.log(1);
  return 1
})
const ajax2 = () => time(1000).then(() => {
  console.log(2);
  return 2
})
const ajax3 = () => time(1000).then(() => {
  console.log(3);
  return 3
})

function mergePromise () {
  // 在这里写代码
}

mergePromise([ajax1, ajax2, ajax3]).then(data => {
  console.log("done");
  console.log(data); // data 为 [1, 2, 3]
});

// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]

这道题有点类似于Promise.all(),不过.all()不需要管执行顺序,只需要并发执行就行了。但是这里需要等上一个执行完毕之后才能执行下一个。 解题思路:

  • 定义一个数组data用于保存所有异步操作的结果
  • 初始化一个const promise = Promise.resolve(),然后循环遍历数组,在promise后面添加执行ajax任务,同时要将添加的结果重新赋值到promise上。
function mergePromise (ajaxArray) {
  // 存放每个ajax的结果
  const data = [];
  let promise = Promise.resolve();
  ajaxArray.forEach(ajax => {
  	// 第一次的then为了用来调用ajax
  	// 第二次的then是为了获取ajax的结果
    promise = promise.then(ajax).then(res => {
      data.push(res);
      return data; // 把每次的结果返回
    })
  })
  // 最后得到的promise它的值就是data
  return promise;
}

使用Promise封装一个异步加载图片的方法

这个比较简单,只需要在图片的onload函数中,使用resolve返回一下就可以了。

function loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = function() {
      resolve(img);
    };
    img.onerror = function() {
    	reject(new Error('Could not load image at' + url));
    };
    img.src = url;
  });
}

使用Promise实现:限制异步操作的并发个数,并尽可能快的完成全部

有8个图片资源的url,已经存储在数组urls中。 urls类似于['https://image1.png', 'https://image2.png', ....] 而且已经有一个函数function loadImg,输入一个url链接,返回一个Promise,该Promise在图片下载完成的时候resolve,下载失败则reject。

但有一个要求,任何时刻同时下载的链接数量不可以超过3个。

请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。

var urls = [
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting5.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn6.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn7.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn8.png",
];
function loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = function() {
      console.log("一张图片加载完成");
      resolve(img);
    };
    img.onerror = function() {
    	reject(new Error('Could not load image at' + url));
    };
    img.src = url;
  });
}



// 答案
function limitLoad(urls, handler, limit) {
  let sequence = [].concat(urls); // 复制urls
  // 这一步是为了初始化 promises 这个"容器"
  let promises = sequence.splice(0, limit).map((url, index) => {
    return handler(url).then(() => {
      // 返回下标是为了知道数组中是哪一项最先完成
      return index;
    });
  });
  // 注意这里要将整个变量过程返回,这样得到的就是一个Promise,可以在外面链式调用
  return sequence
    .reduce((pCollect, url) => {
      return pCollect
        .then(() => {
          return Promise.race(promises); // 返回已经完成的下标
        })
        .then(fastestIndex => { // 获取到已经完成的下标
        	// 将"容器"内已经完成的那一项替换
          promises[fastestIndex] = handler(url).then(
            () => {
              return fastestIndex; // 要继续将这个下标返回,以便下一次变量
            }
          );
        })
        .catch(err => {
          console.error(err);
        });
    }, Promise.resolve()) // 初始化传入
    .then(() => { // 最后三个用.all来调用
      return Promise.all(promises);
    });
}
limitLoad(urls, loadImg, 3)
  .then(res => {
    console.log("图片全部加载完毕");
    console.log(res);
  })
  .catch(err => {
    console.error(err);
  });

promise.catch后面的.then还会执行吗?

会继续执行.then.catch.finally都可以链式调用,其本质上是因为返回了一个新的Promise实例。

.catch只会处理rejected的情况,并且也会返回一个新的Promise实例。

.catch(onRejected)then(undefined, onRejected)在表现上是一致的。

事实上,catch(onRejected)从内部调用了then(undefined, onRejected)。

  • 如果.catch(onRejected)onRejected回调中返回了一个状态为rejectedPromise实例,那么.catch返回的Promise实例的状态也将变成rejected
  • 如果.catch(onRejected)onRejected回调中抛出了异常,那么.catch返回的Promise实例的状态也将变成rejected
  • 其他情况下,.catch返回的Promise实例的状态将是fulfilled

image.png

image.png

image.png .then或.catch后正常返回的时候,Promise的状态都为fullfilled

async和await用法以及与promise的关系

async和await的用法

async

声明异步函数,返回值为一个 Promise 对象,它以类似 同步 的方式来写异步方法

async function fn() {
    console.log('Hello world!');
}

console.log(fn().constructor); // Promise()
// 这里证明其返回值为一个 Promise 对象;

返回值

异步结果是通过 .then() 或者 .catch() 方法来获取并进行进一步处理

// 使用 .then() 的情况
async function fn1() {
    return 'Hello world!';
}

fn1().then(function(res) {
    console.log(res);
});
// Hello world!

// 使用 .catch() 的情况
async function fn3(){
    console.log(aaa); // aaa 依然未定义;
    return 'Hello world!';
}

fn3().then(function(res){
    console.log(res);
}).catch(function(error){
    console.log(error);
});
// ReferenceError: aaa is not defined

await

等待 的意思

var value = await myPromise();

暂停当前 async function 内部执行,等后面的 myPromise() 处理完返结果后,继续执行 async function 函数内部的剩余语句;
myPromise() 是一个 Promise对象,而自定义的变量 value 则用于获取 Promise 对象返回的 resolve 状态值;

用法

await 必须在 async function,否则会提示语法错误;
如果 await 后面跟的是其他值,则直接返回该值。

async function fn() {
    console.log(1);
    var result = await new Promise(function(resolve, reject) {
        setTimeout(function(){
            resolve(2); 
        }, 2000);
    });
    console.log(result); // 
    console.log(3);
    console.log(await 4); // 4 会被直接返回
}
fn();
// 1
// 2 (2 秒后输出)
// 3
// 4
  • await 会等到后面的 Promise 返回结果 后才会执行 async 函数后面剩下的语句,也就是说如果 Promise 不返回结果,后面的代码就不会执行
async function fn() {
    console.log(1);
    await new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log(2);
        }, 2000);
    });
    console.log(3);
}
fn();
// 1
// 2 (2 秒后输出,并且后面不会继续输出 3)
  • await 后面的 Promise 返回一个 reject 状态的结果的话,则会被当成错误在后台抛出
async function fn() {
    console.log(1);
    var result = await new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(2);
        }, 2000);
    });
    console.log(3);
}
fn();
// 1
// Uncaught (in promise) 2 (2 秒后输出)
  • 2 秒后会抛出出错误,并且 3 这个数并没有被输出,说明如果await后的promise内是reject状态,await下面几行的执行也被忽略了

匿名函数

async 也可以用于申明匿名函数用于不同场景,或者嵌套使用 async 函数,如 await async 的形式,只是要在 await 后面使用 async 形式的函数的话,需要这个函数立即执行且有返回值;

let fn = async function() {
    let a = await (async function() {
        console.log(1);
        return 2;
    })();
    console.log(a);

    async function fn2() {
        return 3;
    }
    console.log(await fn2());
}
fn();
// 1
// 2
// 3

await 后面的 Promise 返回的 reject, 也可以被该 async 函数返回的 Promise 对象以 reject 状态获取

async function fn() {
    console.log(1);
    var result = await new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(2);
        }, 2000);
    });
    console.log(3);
}
fn().catch(function(error) {
    console.log(error);
});
// 1
// 2 (2 秒后输出)

这种情况就不会以错误抛出,直接对异常值进行了处理,并且最后同样没有输出数字 3,即后面的代码依然被忽略了

注意事项

async/await 函数以同步的方式书写异步函数确实方便了不少场景,如定义所讲,函数内部遇到 await 会等到返回结果再继续执行下去,也就是说,非 await 部分仍然会以正常的异步或同步方式执行,例如遇到 setTimeout() 就会放入任务队列等待同步语句执行完后再执行

async function fn() {
    console.log(0);
    
    await new Promise(resolve => {
        setTimeout(() => {
            console.log(1);
            resolve();
        }, 1000);
    });

    setTimeout(() => {
        console.log(2);
    }, 0);

    console.log(3);
}

fn();
// 0
// 1(2 秒后)
// 3
// 2

await 内部

虽然说函数会等待 await 返回结果在继续执行,但是 await 内部的代码也依然按正常的同步和异步执行,例如:

async function fn() {
    console.log(0); 
    
    setTimeout(() => {
        console.log(1); 
    }, 0);

    await new Promise(resolve => {
        setTimeout(() => {
            console.log(2); 
        }, 0);

        console.log(3); 

        setTimeout(() => {
            console.log(4); 
            resolve();
        }, 1000);

        setTimeout(() => {
            console.log(5); 
        }, 0);
    });

    setTimeout(() => {
        console.log(6); 
    }, 0);
    console.log(7);
}

fn();
// 0
// 3
// 1
// 2
// 5
// 4(2 秒后)
// 7
// 6

但是假如 await 代码内返回结果的函数(resolve() 或 reject())是在 同步任务 中执行的话,情况就有些不一样了,例如:

async function fn() {
    console.log(0);

    setTimeout(() => {
        console.log(1);
    }, 0);

    await new Promise(resolve => {
        setTimeout(() => {
            console.log(2);
        }, 0);

        console.log(3);
        resolve();
        console.log(4);

        setTimeout(() => {
            console.log(5);
        }, 0);
    });

    setTimeout(() => {
        console.log(6);
    }, 0);
    console.log(7);
}

fn();
// 0 
// 3
// 4
// 7
// 1
// 2
// 5
// 6

由于同步任务 先于 异步任务执行的机理,在同步任务执行过程中依次输出了 0、3 后,就立即执行了 resolve() 使得 await 得到了返回结果,再往后就继续同步的输出了 4,但是输出 5 的代码是异步任务,与输出 1、2 的代码一并放入任务队列,此时由于 await 返回了结果,所以可以执行 await 以外的代码了,输出 6 是异步任务,于是先输出了同步任务的 7,同步任务都执行完了,最后执行任务队列中的异步任务,按之前进入队列的顺序,就是依次输出 1、2、5、6,所有代码运行结束;

函数嵌套2

当 async 函数中嵌套着其他 async 函数时,执行过程可能又有些和预想的不一样

async function fn() {
    console.log(0);

    setTimeout(() => {
        console.log(1);
    }, 0);

    (async function() {
        console.log(2);
    
        setTimeout(() => {
            console.log(3);
        }, 0);

        await new Promise(res => setTimeout(res, 1000))

        setTimeout(() => {
            console.log(4);
        }, 1000);

        console.log(5);
    })()

    console.log(6)
}

fn();
// 0
// 2
// 6
// 1
// 3
// 5(1 秒后)
// 4(再等 1 秒后)

也许会疑惑,不是说 async 函数会等到 await 返回结果后再继续执行吗,为何就先输出 6 了?其实不要混淆概念,确实 async 函数内部是这样干的(3 后 1秒输出 5、4),但 async 函数它自身执行时依然是正常的同步任务执行,也就是虽然内部的 async 函数会等待其 await 返回结果才继续执行后面的代码,但外部的 async 函数可不会等待内部的那个 await,会照常执行(你不是我的菜,天涯何处无芳草╮(╯▽╰)╭);

如果确实需要等待这个嵌套的 async 函数执行完再执行剩下的代码,那么前面加个 await 就行了,原理是也是可行的,因为 async 函数就是返回的一个 Promise 函数,代码如下:

async function fn() {
    console.log(0);

    setTimeout(() => {
        console.log(1);
    }, 0);

    await (async function() {
        console.log(2);
    
        setTimeout(() => {
            console.log(3);
        }, 0);

        await new Promise(res => setTimeout(res, 1000))

        setTimeout(() => {
            console.log(4);
        }, 1000);

        console.log(5);
    })()

    console.log(6)
}

fn();
// 0
// 2
// 1
// 3
// 5(1 秒后)
// 6
// 4(再等 1 秒后)

async/await 和 Promise 有什么关系

es2017的新语法,async/await就是generator+promsie的语法糖

await必须在async方法中使用,async返回的也是一个promise对象

如果方法内无await节点:

return 一个字面量则会得到一个{PromiseStatus: resolved}的Promise

throw 一个Error则会得到一个{PromiseStatus: rejected}的Promise。

如果方法内有await节点

async会返回一个{PromiseStatus: pending}的Promise(发生切换,异步等待Promise的执行结果)

Promise的resolve会使得await的代码节点获得相应的返回结果,并继续向下执行。

Promise的reject 会使得await的代码节点自动抛出相应的异常,终止向下继续执行。

forEach 中使用 await

function test() {
  let arr = [3, 2, 1];
  arr.forEach(async (item) => {
    const res = await fetch(item);
    console.log(res);
  });
  console.log("end");
}

function fetch(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x);
    }, 500 * x);
  });
}
test();

执行结果: end 1 2 3

为什么

forEach 只支持同步代码。

image.png forEach 只是简单的执行了下回调函数而已,并不会去处理异步的情况。 并且即使你在 callback 中使用 break 也并不能结束遍历。

map 中使用 await

function test() {
  let arr = [3, 2, 1];
  arr.map(async (item) => {
    const res = await fetch(item);
    console.log(res);
  });
  console.log("end");
}

function fetch(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x);
    }, 500 * x);
  });
}
test();

执行结果: end 1 2 3

怎么解决

for...of循环

因为 for...of 内部处理的机制和 forEach 不同,forEach 是直接调用回调函数,for...of 是通过迭代器的方式去遍历。

async function test() {
  let arr = [3, 2, 1];
  for (let item of arr) {
    let res = await fetch(item);
    console.log(res);
  }
  console.log("end");
}

function fetch(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x);
    }, 500 * x);
  });
}
test();

执行结果: Promise {<pending>} 3 2 1 end

for...in循环

async function test() {
  let arr = [{
      key: 3
  },{
      key: 2
  },{
      key: 1
  }];
  for (let i in arr) {
    let res = await fetch(arr[i].key);
    console.log(res);
  }
  console.log("end");
}

function fetch(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x);
    }, 500 * x);
  });
}
test();

执行结果: Promise {<pending>} 3 2 1 end

for循环

async function test() {
  let arr = [3, 2, 1];
  for (var i = 0; i < arr.length; i++) {
    const res = await fetch(arr[i]);
    console.log(res);
  }
  console.log("end");
}

function fetch(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x);
    }, 500 * x);
  });
}

test();

执行结果: Promise {<pending>} 3 2 1 end

async、await 实现原理

JavaScript 异步编程回顾

由于 JavaScript 是单线程执行模型,因此必须支持异步编程才能提高运行效率。异步编程的语法目标是让异步过程写起来像同步过程。

1. 回调函数

回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数

image.png

回调函数最大的问题是容易形成回调地狱,即多个回调函数嵌套,降低代码可读性,增加逻辑的复杂性,容易出错。

image.png

2. Promise

为解决回调函数的不足,社区创造出 Promise。

image.png Promise 实际上是利用编程技巧将回调函数的横向加载,改成纵向加载,达到链式调用的效果,避免回调地狱。最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。

3. async、await

为了解决 Promise 的问题,async、await 在 ES7 中被提了出来,是目前为止最好的解决方案

image.png async、await 函数写起来跟同步函数一样,条件是需要接收 Promise 或原始类型的值。是最容易理解的形式。

async、await 原理

1. generator

generator是‘多个线程协助方式完成异步任务’在ES6中的实现。

image.png 整个 generator 函数就是一个封装的异步任务,异步操作需要暂停的地方,都用 yield 语句注明。generator 函数的执行方法如下:

function* gen(x) {
    console.log('start')
    const y = yield x * 2
    return y
}
const g = gen(1)
g.next() // start {value: 2; done: false}
g.next(4) // {value: 4; done: true}

gen() 不会立即执行,而是一上来就暂停,返回一个 Iterator 对象(具体可以参考 Iterator遍历器 )

  • 每次 g.next() 都会打破暂停状态去执行,直到遇到下一个 yield 或者 return
  • 遇到 yield 时,会执行 yield 后面的表达式,并返回执行之后的值,然后再次进入暂停状态,此时 done: false 。
  • next 函数可以接受参数,作为上个阶段异步任务的返回结果,被函数体内的变量接收
  • 遇到 return 时,会返回值,执行结束,即 done: true
  • 每次 g.next() 的返回值永远都是 {value: ... , done: ...} 的形式

async/await 怎么进行错误处理?

一般情况下 async/await 在错误处理方面,主要使用 try/catch,像这样

image.png 多个异步操作,如果都充斥着 try/catch很不优雅,既然是 promise 那么就可以使用 then 函数。 image.png

2.generator自执行器

3. thunk函数

4. CO函数库

你是怎么理解ES6中 Promise的?使用场景有哪些?

Promise ,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大 在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码

doSomething(function(result) {
    doSomethingElse(result, function(newResult) {
      doThirdThing(newResult, function(finalResult) {
        console.log('得到最终结果: ' + finalResult);
      }, failureCallback);
    }, failureCallback);
}, failureCallback);

阅读上面代码,是不是很难受,上述形成了经典的回调地狱,现在通过Promise的改写上面的代码

doSomething().then(function(result) {
    return doSomethingElse(result);
})
.then(function(newResult) {
    return doThirdThing(newResult);
})
.then(function(finalResult) {
    console.log('得到最终结果: ' + finalResult);
})
.catch(failureCallback);

瞬间感受到promise解决异步操作的优点:

  • 链式操作减低了编码难度
  • 代码可读性明显增强 下面我们正式来认识promise

状态

promise对象仅有三种状态

  • pending(进行中)
  • fulfilled(已成功)
  • rejected(已失败)

特点

  • 对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态
  • 一旦状态改变(从pending变为fulfilled和从pending变为rejected),就不会再变,任何时候都可以得到这个结果

流程

image.png

二、用法

Promise对象是一个构造函数,用来生成Promise实例
const promise = new Promise(function(resolve, reject) {});
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

  • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”
  • reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”

实例方法

Promise构建出来的实例存在以下方法:

  • then()
  • catch()
  • finally()

then()

then是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数

then方法返回的是一个新的Promise实例,也就是promise能链式书写的原因

getJSON("/posts.json").then(function(json) {
    return json.post;
}).then(function(post) {
    // ...
});

catch

catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数

getJSON('/posts.json').then(function(posts) {
    // ...
}).catch(function(error) {
    // 处理 getJSON 和 前一个回调函数运行时发生的错误console.log('发生错误!', error);
});

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止

getJSON('/post/1.json').then(function(post) {
    return getJSON(post.commentURL);
  }).then(function(comments) {
    // some code
  }).catch(function(error) {
    // 处理前面三个Promise产生的错误
  });

一般来说,使用catch方法代替then()第二个参数 Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应

const someAsyncThing = function() {
    return new Promise(function(resolve, reject) {
        // 下面一行会报错,因为x没有声明
        resolve(x + 2);
    });
};

浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进程

catch()方法之中,还能再抛出错误,通过后面catch方法捕获到

finally()

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作

promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});

构造函数方法

Promise构造函数存在以下方法:

  • all()
  • race()
  • allSettled()
  • resolve()
  • reject()

all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例

const p = Promise.all([p1, p2, p3]);

接受一个数组(迭代对象)作为参数,数组成员都应为Promise实例

实例p的状态由p1p2p3决定,分为两种:

  • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数
  • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法

const p1 = new Promise((resolve, reject) => {
    resolve('hello');
  })
  .then(result => result)
  .catch(e => e);
  
  const p2 = new Promise((resolve, reject) => {
    throw new Error('报错了');
  })
  .then(result => result)
  .catch(e => 'e '+ e); // 这里返回的状态是fulfilled, value的值是'e '+ e。 所以后面只会进到then不会进到catch。原因请看promise手写代码相关文档.catch方法的返回值为resolve(e)
  
  Promise.all([p1, p2])
  .then(result => console.log(result)) // 不会走到下一步catch。状态都为fulfilled
  .catch(e => console.log(e));
  // ["hello", e Error: 报错了] <- 打印

如果p2没有自己的catch方法,就会调用Promise.all()catch方法

const p1 = new Promise((resolve, reject) => {
    resolve('hello');
  })
  .then(result => result);
  
  const p2 = new Promise((resolve, reject) => {
    throw new Error('报错了');
  })
  .then(result => result); // 进不到then方法,p2的值是rejected状态,值为Error:报错了
  
  Promise.all([p1, p2])
  .then(result => console.log(result))
  .catch(e => console.log(e)); //因为有一个是rejected,所以会走到catch不会走then
  // Error: 报错了

race()

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例

const p = Promise.race([p1, p2, p3]);

只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变

率先改变的 Promise 实例的返回值则传递给p的回调函数

const p = Promise.race([
    fetch('/resource-that-may-take-a-while'),
    new Promise(function (resolve, reject) {
      setTimeout(() => reject(new Error('request timeout')), 5000)
    })
  ]);
  
  p
  .then(console.log)
  .catch(console.error);

allSettled()

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例 只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束

const promises = [
    fetch('/api-1'),
    fetch('/api-2'),
    fetch('/api-3'),
  ];
  
  await Promise.allSettled(promises);
  removeLoadingIndicator();

resolve()

将现有对象转为 Promise 对象

Promise.resolve('foo') 
// 等价于 
new Promise(resolve => resolve('foo'))

参数可以分成四种情况,分别如下:

  • 参数是一个 Promise 实例,promise.resolve将不做任何修改、原封不动地返回这个实例
  • 参数是一个thenable对象,promise.resolve会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法
  • 参数不是具有then()方法的对象,或根本就不是对象,Promise.resolve()会返回一个新的 Promise 对象,状态为resolved
  • 没有参数时,直接返回一个resolved状态的 Promise 对象

reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
  console.log(s)
});
// 出错了

Promise.reject()方法的参数,会原封不动地变成后续方法的参数

Promise.reject('出错了')
.catch(e => {
  console.log(e === '出错了')
})
// true

三、使用场景

将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化

symbol

ES5 的对象属性名都是字符串,容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)

Symbol 值通过Symbol函数生成。

这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

箭头函数和普通函数有啥区别?箭头函数能当构造函数吗?

什么是箭头函数?

ES6中允许使用箭头=>来定义箭头函数。省去了function关键字,采用箭头=>来定义函数

箭头函数与普通函数的区别

1、语法更加简洁、清晰

2、箭头函数不会创建自己的this (它的this指向定义时外层执行环境的this,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。)

3、箭头函数继承而来的this指向永远不变

4、.call()/.apply()/.bind()无法改变箭头函数中this的指向

.call()/.apply()/.bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向,虽然这么做代码不会报错。

5、箭头函数不能作为构造函数使用

我们先了解一下构造函数的new都做了些什么?简单来说,分为四步:

① JS内部首先会先生成一个对象;

② 再把函数中的this指向该对象

③ 然后执行构造函数中的语句;

④ 最终返回该对象实例。

但是!!因为箭头函数没有自己的this,它的this其实是继承了外层执行环境中的this,且this指向永远不会随在哪里调用、被谁调用而改变,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new调用时会报错!

6、箭头函数没有自己的arguments

箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值

7、箭头函数没有原型prototype

let sayHi = () => {
    console.log('Hello World !') 
}; 
console.log(sayHi.prototype); // undefined

8、箭头函数不能用作Generator函数,不能使用yeild关键字

箭头函数的 this 指向哪⾥?

箭头函数并没有属于⾃⼰的this,捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。

// ES6 
const obj = { 
  getArrow() { 
    return () => { 
      console.log(this === obj); 
    }; 
  } 
}

ES6扩展语法

let和const的区别

image.png

拓展运算符

image.png

数组新增的拓展方法

image.png

对象新增的拓展方法

image.png

函数新增的拓展方法

image.png

set数据结构

image.png

Map数据结构

image.png

Map 和 Set 的用法以及区别

首先了解一下 Map

Map 是一组键值对的结构,和 JSON 对象类似。

(1) Map数据结构如下

这里我们可以看到的是Map的数据结构是一个键值对的结构

image.png (2) key 不仅可以是字符串还可以是对象

image.png

(3) Map常用语法如下

//初始化`Map`需要一个二维数组(请看 Map 数据结构),或者直接初始化一个空`Map` 
let map = new Map();

//添加key和value值
map.set('Amy','女')
map.set('liuQi','男')

//是否存在key,存在返回true,反之为false
map.has('Amy') //true
map.has('amy') //false

//根据key获取value
map.get('Amy') //女

//删除 key为Amy的value
map.delete('Amy')
map.get('Amy') //undefined  删除成功

(4) 一个key只能对应一个value,多次对一个key放入value,后面的值会把前面的值覆盖掉

var map =new Map
map.set('Amy',"女")
map.set('Amy',"男")
console.log(map) 

打印结果如下

image.png

再来了解一下 Set

Set 对象类似于数组,且成员的值都是唯一的 (1) 打印出的数据结构如下

image.png

(2) 最常用来去重使用,去重方法有很多但是都没有它运行的快。

var arr=[1,3,4,2,5,1,4]
// 这里原本是一个对象用了es6的语法 转化成了数组,就是转化数组之前已经过滤掉了重复的元素了
var arr2=[...new Set(arr)] //[1,3,4,2,5]

(3) Set常用语法如下

//初始化一个Set ,需要一个Array数组,要么空Set
var set = new Set([1,2,3,5,6]) 
console.log(set)  // {1, 2, 3, 5, 6}

//添加元素到Set中
set.add(7) //{1, 2, 3, 5, 6, 7}

//删除Set中的元素
set.delete(3) // {1, 2, 5, 6, 7}

//检测是否含有此元素,有为true,没有则为false
set.has(2) //true

总结Map和Set的区别

(1) 这两种方法具有极快的查找速度;那么下面我们来对比一下Map,Set,Array 的执行时间

//首先初始化数据
var lng=100
var arr =new Array(lng).fill(2)

打印结果

image.png

//首先初始化数据
var lng=100
var arr =new Array(lng).fill(2)
var set =new Set(arr)
let map =new Map()
for(var i=0;i<lng;i++){
arr[i]=i
map.set(i,arr[i])
}

打印结果

image.png

image.png

// Array
console.time()
for(var j=0;j<lng;j++){
arr.includes(j)
}
console.timeEnd()  //default: 0.01220703125 ms


// Set
console.time()
for(var j=0;j<lng;j++){
set.has(j)
}
console.timeEnd()  // default: 0.005859375 ms


// Map
console.time()
for(var j=0;j<lng;j++){
map.has(j)
}
console.timeEnd()
// default: 0.007080078125 ms

Set执行时间最短,那么查找速度最快,当然了Set 和 Map的查找速度都很快想差不大,所以说这两种方法具有极快的查找速度

(2) 初始化需要的值不一样,Map需要的是一个二维数组,而Set 需要的是一维 Array 数组

(3) Map 和 Set 都不允许键重复

(4) Map的键是不能修改,但是键对应的值是可以修改的;Set不能通过迭代器来改变Set的值,因为Set的值就是键。

(5) Map 是键值对的存在,值也不作为健;而 Set 没有 value 只有 key,value 就是 key;

怎样理解ES6的Generator

image.png

function* foo (x) {
    var y = 2 * (yield (x + 1))
    var z = yield (y / 3)
    return (x + y + z)
}
var a = foo(5);
a.next() // {value: 6, done: false}
a.next(6) // {value: 4, done: false}
a.next(4) // {value: 21, done: true} 5 + 12 + 4

怎样理解ES6的Proxy

image.png

ES6的Decorator

image.png

Iterator 迭代器

for循环、while循环等,遍历Array获取其中的值,那其他数据结构如何通过遍历获取呢?是否可以提供一个统一的访问机制?来访问Object、Map、Set等。

Iterator迭代器的出现就是为了迭代而生,为不同的集合:Object、Array、Map、Set,提供了一个统一的接口
遍历就是访问数据结构的所有元素,而迭代是遍历的一种形式。

// 模拟next方法返回值
var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true}
    }
  }
}

上面的makeIterator函数,它就是一个迭代器生成函数,作用就是返回一个迭代器对象。对数组执行这个函数,就会返回该数组的迭代器对象it。

Iterator 的遍历过程:

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。 调用next方法,会返回一个包含value和done这两个属性的对象。value为当前属性的值,done是一个Boolean值,表示遍历是否结束了。

Iterator规范

迭代器对象it包含一个next() 方法,调用next()方法,返回两个属性:布尔值done和值value,value的类型无限制

如何让一个对象成为一个可迭代对象呢?

要成为可迭代对象, 一个对象必须实现@@iterator方法, 这意味着对象(或者它原型链上的某个对象)必须有一个键为@@iterator的属性,可通过常量 Symbol.iterator 访问该属性。

let myIterable = {
    a: 1,
    b: 2,
    c: 3
}
myIterable[Symbol.iterator] = function() {
  let self = this;
  let arr = Object.keys(self);
  let index = 0;
  return {
    next() {
      return index < arr.length ? {value: self[arr[index++]], done: false} : {value: undefined, done: true};
    }
  }
}

var it = myIterable[Symbol.iterator]();

it.next();
// {value: 1, done: false} -> 再执行it.next() -> {value: 2, done: false} -> {value: 3, done: false} -> {value: undefined, done: true}

for(const i of myIterable) {
  console.log(i); // 1 2 3  
}

小结:Iterator规范————Iterator迭代器包含一个next()方法,方法调用返回返回两个属性:done和value;通过定义一个对象的Symbol.iterator属性,即可将此对象修改为迭代器对象,支持for...of遍历。

原生具备 Iterator 接口的数据结构有:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

object 不具备原生Iterator接口,需要在[Symbol.iterator]上布置。生成的每项形式为[key,val]

Reflect

Reflect 对象不是构造函数,所以创建时不是用 new 来进行创建。

在 ES6 中增加这个对象的目的:

  • 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。
  • 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc)则会返回 false。
  • 让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in obj 和 delete obj[name],而 Reflect.has(obj, name)和 Reflect.deleteProperty(obj, name)让它们变成了函数行为。
  • Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。

var loggedObj = new Proxy(obj, {
  get(target, name) {
    console.log("get", target, name);
    return Reflect.get(target, name);
  },
  deleteProperty(target, name) {
    console.log("delete" + name);
    return Reflect.deleteProperty(target, name);
  },
  has(target, name) {
    console.log("has" + name);
    return Reflect.has(target, name);
  },
});

上面代码中,每一个 Proxy 对象的拦截操作(get、delete、has),内部都调用对应的 Reflect 方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。 总结:
将 Object的一些明显属于语言内部的方法放到 Reflect 对象上
修改某些 Object 方法的返回结果,让其变得更合理
让 Object 操作都变成函数行为
Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法

ES6中rest参数

形式为...变量名

ES6 引入 rest 参数(...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function add(...values) {
  let sum = 0;

  for (var val of values) { // 这里的values可以作为数组进行遍历
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

上面代码的add函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。

下面是一个 rest 参数代替arguments变量的例子。

// arguments变量写法
function sortNumbers () {
    return Array.prototype.slice.call(arguments).sort();
}
// rest参数写法
const sortNumber = (...numbers) => numbers.sort();

两种写法,rest 参数的写法更自然简洁。

  1. arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组

  2. rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。

下面是一个利用 rest 参数改写数组push方法的例子。

function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
  console.log(array) // [1, 2, 3]
}

var a = [];
push(a, 1, 2, 3)

注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

// 报错
function f(a, ...b, c) {
  // ...
}

函数的length属性,不包括 rest 参数

(function(a,b,c) {}).length  // 3
(function(a,b,...c) {}).length  // 2
(function(...c) {}).length  // 0
(function(a,b=1,c) {}).length // 1

箭头函数不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替

image.png

image.png

ES6的拓展语法补充

fe.ecool.fun/topic/5e131…

fe.ecool.fun/topic/5e131…

SSR是什么?Vue中怎么实现?CSR和SSR分别是什么?

image.png

什么是微前端?

微前端可以解决什么问题?

实现微前端有哪些技术方案?

微前端中的应用隔离是什么,一般是怎么实现的?

image.png

什么是 PWA?

渐进式网页应用

PWA 相关的技术不断升级优化,在用户体验和用户留存两方面都提供了非常好的解决方案。PWA 可以将 Web 和 App 各自的优势融合在一起:渐进式、可响应、可离线、实现类似 App 的交互、即时更新、安全、可以被搜索引擎检索、可推送、可安装、可链接。

需要特别说明的是,PWA 不是特指某一项技术,而是应用了多项技术的 Web App。其核心技术包括 App Manifest、Service Worker、Web Push,等等。

Service worker是什么?

service worker是PWA的重要组成部分,主要用来做持久的离线缓存,也是Web Worker的升级版。

Service worker是一个注册在指定源和路径下的事件驱动 Worker。它采用 JavaScript 控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。