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的运行机制如下:
-
- 所有同步任务都在主线程上执行,形成一个执行栈(Call Stack)
-
- 主线程之外,存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
-
- 一旦"执行栈"中所有同步任务执行完毕,系统就会读取"任务队列",异步任务结束等待,进入执行栈,开始执行。
为什么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 函数中,再执行之前阻塞的代码
所以上述输出结果为:1,fn2,3,2
流程分析
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 start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout
五 谈谈你对浏览器中进程和线程的理解
浏览器是多进程的
| 进程 | |
|---|---|
| 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 下载并解析完成之前不会显示:
css 文件没下载并解析完成之前,**后续的** js 脚本不能执行。下面的 alert('ok') 在 css 下载并解析完成之前不会弹出来:
- css 文件的下载
不会阻塞 **前面的** js 脚本执行。下面的 alert('ok') 会在 css 下载完成前弹出:
所以在需要提前执行不操作 dom 元素的 js 时,不妨把 js 放到 css 文件之前。
js 阻塞
js 文件的下载和解析会阻塞 GUI 渲染进程,也就是会阻塞 DOM 和 CSS 的解析和渲染。
js 文件没下载并解析完成之前,后续的 HTML 和 CSS 无法解析:
- js 文件的下载
不会阻塞前面 HTML 和 CSS 的解析:
需要注意的点
- 第一,GUI 渲染线程会尽可能早的将内容呈现到屏幕上,并不会等到所有的 HTML 都解析完成之后再去构建和布局 Render Tree,而是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。下面 test1 会在 js 文件下载完成前渲染完成,而 test2 则会在 js 文件下载并执行完之后渲染:
- 第二,文件的下载是不会被阻塞的,不管是 css 还是 js 文件,浏览器的主进程会在页面解析前开启下载,所以就算在外部脚本执行前删除脚本,脚本也还是会下载。
堆与栈有什么区别?
Promise手写&&结合Event Loop输出顺序训练题&&如何中断promise&& promise相关题目
PromiseA+规范
术语
- promise 是一个有then方法的对象或者是函数,行为遵循本规范
- thenable 是一个有then方法的对象或者是函数
- value 是promise状态成功时的值,也就是resolve的参数, 包括各种数据类型, 也包括undefined/thenable或者是 promise
- reason 是promise状态失败时的值, 也就是reject的参数, 表示拒绝的原因
- exception 是一个使用throw抛出的异常值
规范
Promise States
promise应该有三种状态. 要注意他们之间的流转关系.
-
pending
1.1 初始的状态, 可改变.
1.2 一个promise在resolve或者reject前都处于这个状态。
1.3 可以通过 resolve -> fulfilled 状态;
1.4 可以通过 reject -> rejected 状态; -
fulfilled
2.1 最终态, 不可变.
2.2 一个promise被resolve后会变成这个状态.
2.3 必须拥有一个value值 -
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 onFulfilled 必须是函数类型, 如果不是函数, 应该被忽略. 1.2 onRejected 必须是函数类型, 如果不是函数, 应该被忽略.
-
onFulfilled 特性
2.1 在promise变成 fulfilled 时,应该调用 onFulfilled, 参数是value 2.2 在promise变成 fulfilled 之前, 不应该被调用. 2.3 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数)
-
onRejected 特性
3.1 在promise变成 rejected 时,应该调用 onRejected, 参数是reason 3.2 在promise变成 rejected 之前, 不应该被调用. 3.3 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数)
-
onFulfilled 和 onRejected 应该是微任务
这里用queueMicrotask来实现微任务的调用.
-
then方法可以被调用多次
5.1 promise状态变成 fulfilled 后,所有的 onFulfilled 回调都需要按照then的顺序执行, 也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多个onFulfilled的回调) 5.2 promise状态变成 rejected 后,所有的 onRejected 回调都需要按照then的顺序执行, 也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多个onRejected的回调)
-
返回值
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
-
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 输出顺序训练题链接
如何中断Promise?
Promise有个缺点,那就是一旦创建就无法取消,所以本质上promise是无法终止的。但是开发过程中会碰到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回调中返回了一个状态为rejected的Promise实例,那么.catch返回的Promise实例的状态也将变成rejected。 - 如果
.catch(onRejected)的onRejected回调中抛出了异常,那么.catch返回的Promise实例的状态也将变成rejected。 - 其他情况下,
.catch返回的Promise实例的状态将是fulfilled。
.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 只支持同步代码。
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. 回调函数
回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数
回调函数最大的问题是容易形成回调地狱,即多个回调函数嵌套,降低代码可读性,增加逻辑的复杂性,容易出错。
2. Promise
为解决回调函数的不足,社区创造出 Promise。
Promise 实际上是利用编程技巧将回调函数的横向加载,改成纵向加载,达到链式调用的效果,避免回调地狱。最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。
3. async、await
为了解决 Promise 的问题,async、await 在 ES7 中被提了出来,是目前为止最好的解决方案
async、await 函数写起来跟同步函数一样,条件是需要接收 Promise 或原始类型的值。是最容易理解的形式。
async、await 原理
1. generator
generator是‘多个线程协助方式完成异步任务’在ES6中的实现。
整个 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,像这样
多个异步操作,如果都充斥着 try/catch很不优雅,既然是 promise 那么就可以使用 then 函数。
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),就不会再变,任何时候都可以得到这个结果
流程
二、用法
Promise对象是一个构造函数,用来生成Promise实例
const promise = new Promise(function(resolve, reject) {});
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject
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的状态由p1、p2、p3决定,分为两种:
- 只有
p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数 - 只要
p1、p2、p3之中有一个被rejected,p的状态就变成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]);
只要p1、p2、p3之中有一个实例率先改变状态,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的区别
拓展运算符
数组新增的拓展方法
对象新增的拓展方法
函数新增的拓展方法
set数据结构
Map数据结构
Map 和 Set 的用法以及区别
首先了解一下 Map
Map 是一组键值对的结构,和 JSON 对象类似。
(1) Map数据结构如下
这里我们可以看到的是Map的数据结构是一个键值对的结构
(2) key 不仅可以是字符串还可以是对象
(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)
打印结果如下
再来了解一下 Set
Set 对象类似于数组,且成员的值都是唯一的 (1) 打印出的数据结构如下
(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)
打印结果
//首先初始化数据
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])
}
打印结果
// 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
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
ES6的Decorator
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 的遍历过程:
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
- 不断调用指针对象的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 参数的写法更自然简洁。
-
arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组 -
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 参数代替
ES6的拓展语法补充
SSR是什么?Vue中怎么实现?CSR和SSR分别是什么?
什么是微前端?
微前端可以解决什么问题?
实现微前端有哪些技术方案?
微前端中的应用隔离是什么,一般是怎么实现的?
什么是 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 控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。