前端面试js下

43. 数据是如何存储的?

基本数据类型用 栈 存储,引用数据类型用 堆 存储。

看起来没有错误,但实际上是有问题的。可以考虑一下闭包的情况,如果变量存在栈中,那函数调用完栈顶空间销毁 ,闭包变量不就没了吗?

其实还是需要补充一句:

闭包变量是存在堆内存中的。

存储在栈中

boolean
null
const deepClone = (target, map = new WeakMap()) => {

    if(!isObject(target))

        return target;

        let type = getType(target);

        let cloneTarget;

    if(!canTraverse[type]) {

        // 处理不能遍历的对象

        return handleNotTraverse(target, type);

    }else {

        // 这波操作相当关键,可以保证对象的原型不丢失!

        let ctor = target.constructor;

        cloneTarget = new ctor();

    }

    if(map.get(target))

        return target;

        map.set(target, true);

    if(type === mapTag) {

        //处理Map

        target.forEach((item, key) => {

            cloneTarget.set(deepClone(key, map), deepClone(item, map));

        })

    }

    if(type === setTag) {

        //处理Set

        target.forEach(item => {

            cloneTarget.add(deepClone(item, map));

        })

    }

    // 处理数组和对象

    for (let prop in target) {

        if (target.hasOwnProperty(prop)) {

            cloneTarget[prop] = deepClone(target[prop], map);

        }

    }

    return cloneTarget;

}

存放在堆中

undefined
number
string
symbol
bigint
所有的对象数据类型

值得注意的是,对于 赋值 操作,原始类型的数据直接完整地复制变量值,对象数据类型的数据则是复制

引用地址。

因此会有下面的情况:

let obj = { a: 1 };

let newObj = obj;

newObj.a = 2;

console.log(obj.a);//变成了2

之所以会这样,是因为 obj 和 newObj 是同一份堆空间的地址,改变newObj,等于改变了共同的堆内

存,这时候通过 obj 来获取这块内存的值当然会改变。

为什么不全部用栈来保存呢?

首先,对于系统栈来说,它的功能除了保存变量之外,还有创建并切换函数执行上下文的功能。举个例

子:

function f(a) {

console.log(a);

}

function func(a) {

f(a);

}

func(1);

假设用ESP指针来保存当前的执行状态,在系统栈中会产生如下的过程:

1)调用func, 将 func 函数的上下文压栈,ESP指向栈顶。

2)执行func,又调用f函数,将 f 函数的上下文压栈,ESP 指针上移。

3)执行完 f 函数,将ESP 下移,f函数对应的栈顶空间被回收。

4)执行完 func,ESP 下移,func对应的空间被回收。

图示如下:因此你也看到了,如果采用栈来存储相对基本类型更加复杂的对象数据,那么切换上下文的开销将变得

巨大!

不过堆内存虽然空间大,能存放大量的数据,但与此同时垃圾内存的回收会带来更大的开销。

44. V8 引擎如何进行垃圾内存的回收?

JS 语言不像 C/C++, 让程序员自己去开辟或者释放内存,而是类似Java,采用自己的一套垃圾回收算法进行自动的内存管理。作为一名资深的前端工程师,对于JS内存回收的机制是需要非常清楚, 以便于在极端的环境下能够分析出系统性能的瓶颈,另一方面,学习这其中的机制,也对我们深入理解JS的闭包特性、以及对内存的高效使用,都有很大的帮助。

V8 内存限制

在其他的后端语言中,如Java/Go, 对于内存的使用没有什么限制,但是JS不一样,V8只能使用系统的一部分内存,具体来说,在 64 位系统下,V8最多只能分配 1.4G , 在 32 位系统中,最多只能分配 0.7G 。

你想想在前端这样的大内存需求其实并不大,但对于后端而言,nodejs如果遇到一个2G多的文件,那么将无法全部将其读入内存进行各种操作了。

我们知道对于栈内存而言,当ESP指针下移,也就是上下文切换之后,栈顶的空间会自动被回收。但对于堆内存而言就比较复杂了,我们下面着重分析堆内存的垃圾回收。

所有的对象类型的数据在JS中都是通过堆进行空间分配的。当我们构造一个对象进行赋值操作的时候,其实相应的内存已经分配到了堆上。你可以不断的这样创建对象,让 V8 为它分配空间,直到堆的大小达到上限。

那么问题来了,V8 为什么要给它设置内存上限?明明我的机器大几十G的内存,只能让我用这么一点?究其根本,是由两个因素所共同决定的,一个是JS单线程的执行机制,另一个是JS垃圾回收机制的限制。

首先JS是单线程运行的,这意味着一旦进入到垃圾回收,那么其它的各种运行逻辑都要暂停; 另一方面垃圾回收其实是非常耗时间的操作,V8 官方是这样形容的:

以 1.5GB 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要50ms 以上,做一次非增量式(ps:

后面会解释)的垃圾回收甚至要 1s 以上。

可见其耗时之久,而且在这么长的时间内,我们的JS代码执行会一直没有响应,造成应用卡顿,导致应用性能和响应能力直线下降。因此,V8 做了一个简单粗暴的选择,那就是限制堆内存,也算是一种权衡的手段,因为大部分情况是不会遇到操作几个G内存这样的场景的。

不过,如果你想调整这个内存的限制也不是不行。配置命令如下:

// 这是调整老生代这部分的内存,单位是MB。后面会详细介绍新生代和老生代内存

node --max-old-space-size=2048 xxx.js

或者

// 这是调整新生代这部分的内存,单位是 KB。

node --max-new-space-size=2048 xxx.js

新生代内存的回收

V8 把堆内存分成了两部分进行处理——新生代内存和老生代内存。顾名思义,新生代就是临时分配的内存,存活时间短, 老生代是常驻内存,存活的时间长。V8 的堆内存,也就是两个内存之和。

根据这两种不同种类的堆内存,V8 采用了不同的回收策略,来根据不同的场景做针对性的优化。

首先是新生代的内存,刚刚已经介绍了调整新生代内存的方法,那它的内存默认限制是多少?在 64 位和 32 位系统下分别为 32MB 和 16MB。够小吧,不过也很好理解,新生代中的变量存活时间短,来了马上就走,不容易产生太大的内存负担,因此可以将它设的足够小。

那好了,新生代的垃圾回收是怎么做的呢?

首先将新生代内存空间一分为二:

其中From部分表示正在使用的内存,To 是目前闲置的内存。

当进行垃圾回收时,V8 将From部分的对象检查一遍,如果是存活对象那么复制到To内存中(在To内存中按照顺序从头放置的),如果是非存活对象直接回收即可。当所有的From中的存活对象按照顺序进入到To内存之后,From 和 To 两者的角色 对调 ,From现在被

闲置,To为正在使用,如此循环。

那你很可能会问了,直接将非存活对象回收了不就万事大吉了嘛,为什么还要后面的一系列操作?

注意,我刚刚特别说明了,在To内存中按照顺序从头放置的,这是为了应对这样的场景:

深色的小方块代表存活对象,白色部分表示待分配的内存,由于堆内存是连续分配的,这样零零散散的

空间可能会导致稍微大一点的对象没有办法进行空间分配,这种零散的空间也叫做内存碎片。刚刚介绍

的新生代垃圾回收算法也叫Scavenge算法

Scavenge 算法主要就是解决内存碎片的问题,在进行一顿复制之后,To空间变成了这个样子:

是不是整齐了许多?这样就大大方便了后续连续空间的分配。

不过Scavenge 算法的劣势也非常明显,就是内存只能使用新生代内存的一半,但是它只存放生命周期

短的对象,这种对象 一般很少 ,因此 时间 性能非常优秀。

老生代内存的回收

刚刚介绍了新生代的回收方式,那么新生代中的变量如果经过多次回收后依然存在,那么就会被放入到

老生代内存 中,这种现象就叫 晋升 。

发生晋升其实不只是这一种原因,我们来梳理一下会有那些情况触发晋升:

已经经历过一次 Scavenge 回收。

To(闲置)空间的内存占用超过25%。

现在进入到老生代的垃圾回收机制当中,老生代中累积的变量空间一般都是很大的,当然不能用

Scavenge 算法啦,浪费一半空间不说,对庞大的内存空间进行复制岂不是劳民伤财?

那么对于老生代而言,究竟是采取怎样的策略进行垃圾回收的呢?

第一步,进行标记-清除。这个过程在《JavaScript高级程序设计(第三版)》中有过详细的介绍,主要分成两个阶段,即标记阶段和清除阶段。首先会遍历堆中的所有对象,对它们做上标记,然后对于代码环境中 使用的变量 以及被 强引用 的变量取消标记,剩下的就是要删除的变量了,在随后的 清除阶段 对其进行空间的回收。

当然这又会引发内存碎片的问题,存活对象的空间不连续对后续的空间分配造成障碍。老生代又是如何处理这个问题的呢?第二步,整理内存碎片。V8 的解决方式非常简单粗暴,在清除阶段结束后,把存活的对象全部往一端靠拢。

由于是移动对象,它的执行速度不可能很快,事实上也是整个过程中最耗时间的部分。

增量标记

由于JS的单线程机制,V8 在进行垃圾回收的时候,不可避免地会阻塞业务逻辑的执行,倘若老生代的垃圾回收任务很重,那么耗时会非常可怕,严重影响应用的性能。那这个时候为了避免这样问题,V8 采取了增量标记的方案,即将一口气完成的标记任务分为很多小的部分完成,每做完一个小的部分就"歇"一下,就js应用逻辑执行一会儿,然后再执行下面的部分,如果循环,直到标记阶段完成才进入内存碎片的整理上面来。其实这个过程跟React Fiber的思路有点像,这里就不展开了。

经过增量标记之后,垃圾回收过程对JS应用的阻塞时间减少到原来了1 / 6, 可以看到,这是一个非常成功的改进。

45. 描述一下 V8 执行一段JS代码的过程?

前端相对来说是一个比较新兴的领域,因此各种前端框架和工具层出不穷,让人眼花缭乱,尤其是各大厂商推出 小程序 之后 各自制定标准 ,让前端开发的工作更加繁琐,在此背景下为了抹平平台之间的差异,诞生的各种 编译工具/框架 也数不胜数。但无论如何,想要赶上这些框架和工具的更新速度是非常难的,即使赶上了也很难产生自己的 技术积淀 ,一个更好的方式便是学习那些 本质的知识 ,抓住上层应用中不变的 底层机制 ,这样我们便能轻松理解上层的框架而不仅仅是被动地使用,甚至能够在适当的场景

下自己造出轮子,以满足开发效率的需求。

站在 V8 的角度,理解其中的执行机制,也能够帮助我们理解很多的上层应用,包括Babel、Eslint、前端框架的底层机制。那么,一段 JavaScript 代码放在 V8 当中究竟是如何执行的呢?

首先需要明白的是,机器是读不懂 JS 代码,机器只能理解特定的机器码,那如果要让 JS 的逻辑在机器上运行起来,就必须将 JS 的代码翻译成机器码,然后让机器识别。JS属于解释型语言,对于解释型的语言说,解释器会对源代码做如下分析:

通过词法分析和语法分析生成 AST(抽象语法树)生成字节码

然后解释器根据字节码来执行程序。但 JS 整个执行的过程其实会比这个更加复杂,接下来就来一一地拆解。

生成 AST

生成 AST 分为两步——词法分析和语法分析。

词法分析即分词,它的工作就是将一行行的代码分解成一个个token。 比如下面一行代码:

let name = 'sanyuan'其中会把句子分解成四个部分:

即解析成了四个token,这就是词法分析的作用。

接下来语法分析阶段,将生成的这些 token 数据,根据一定的语法规则转化为AST。举个例子:

let name = 'sanyuan'

console.log(name)

最后生成的 AST 是这样的:当生成了 AST 之后,编译器/解释器后续的工作都要依靠 AST 而不是源代码。顺便补充一句,babel 的

工作原理就是将 ES6 的代码解析生成 ES6的AST ,然后将 ES6 的 AST 转换为 ES5 的AST ,最后才将 ES5

的 AST 转化为具体的 ES5 代码。

生成 AST 后,接下来会生成执行上下文

生成字节码

开头就已经提到过了,生成 AST 之后,直接通过 V8 的解释器(也叫Ignition)来生成字节码。但是字节码并不能让机器直接运行,那你可能就会说了,不能执行还转成字节码干嘛,直接把 AST 转换成机器码不就得了,让机器直接执行。确实,在 V8 的早期是这么做的,但后来因为机器码的体积太大,引发了严重的内存占用问题。

给一张对比图让大家直观地感受以下三者代码量的差异:很容易得出,字节码是比机器码轻量得多的代码。那 V8 为什么要使用字节码,字节码到底是个什么东西?

字节码是介于AST 和 机器码之间的一种代码,但是与特定类型的机器码无关,字节码需要通过解

释器将其转换为机器码然后执行。

字节码仍然需要转换为机器码,但和原来不同的是,现在不用一次性将全部的字节码都转换成机器码,而是通过解释器来逐行执行字节码,省去了生成二进制文件的操作,这样就大大降低了内存的压力。

执行代码

在执行字节码的过程中,如果发现某一部分代码重复出现,那么 V8 将它记做 热点代码 (HotSpot),然后将这么代码编译成 机器码 保存起来,这个用来编译的工具就是V8的 编译器 (也叫做 TurboFan ) , 因此在这样的机制下,代码执行的时间越久,那么执行效率会越来越高,因为有越来越多的字节码被标记为 热点代码 ,遇到它们时直接执行相应的机器码,不用再次将转换为机器码。其实当你听到有人说 JS 就是一门解释器语言的时候,其实这个说法是有问题的。因为字节码不仅配合了解释器,而且还和编译器打交道,所以 JS 并不是完全的解释型语言。而编译器和解释器的 根本区别在于前者会编译生成二进制文件但后者不会。

并且,这种字节码跟编译器和解释器结合的技术,我们称之为 即时编译 , 也就是我们经常听到的 JIT 。

这就是 V8 中执行一段JS代码的整个过程,梳理一下:

1)首先通过词法分析和语法分析生成 AST

2)将 AST 转换为字节码

3)由解释器逐行执行字节码,遇到热点代码启动编译器进行编译,生成对应的机器码, 以优化执行效率

46. 宏任务(MacroTask)

在 JS 中,大部分的任务都是在主线程上执行,常见的任务有:

1)渲染事件

2)用户交互事件

3)js脚本执行

4)网络请求、文件读写完成事件等等。

为了让这些事件有条不紊地进行,JS引擎需要对之执行的顺序做一定的安排,V8 其实采用的是一种 队列

的方式来存储这些任务, 即先进来的先执行。模拟如下:

bool keep_running = true;

void MainTherad(){

for(;;){这里用到了一个 for 循环,将队列中的任务一一取出,然后执行,这个很好理解。但是其中包含了两种任务队列,除了上述提到的任务队列, 还有一个延迟队列,它专门处理诸如setTimeout/setInterval这样的定时器回调任务。

上述提到的,普通任务队列和延迟队列中的任务,都属于宏任务

47. 微任务(MicroTask)

对于每个宏任务而言,其内部都有一个微任务队列。那为什么要引入微任务?微任务在什么时候执行呢?

其实引入微任务的初衷是为了解决异步回调的问题。想一想,对于异步回调的处理,有多少种方式?总

结起来有两点:

1)将异步回调进行宏任务队列的入队操作。

2)将异步回调放到当前宏任务的末尾。

如果采用第一种方式,那么执行回调的时机应该是在前面 所有的宏任务 完成之后,倘若现在的任务队列非常长,那么回调迟迟得不到执行,造成 应用卡顿 。

为了规避这样的问题,V8 引入了第二种方式,这就是 微任务 的解决方式。在每一个宏任务中定义一个微 任务队列,当该宏任务执行完成,会检查其中的微任务队列,如果为空则直接执行下一个宏任务,如果不为空,则 依次执行微任务 ,执行完成才去执行下一个宏任务。

常见的微任务有MutationObserver、Promise.then(或.reject) 以及以 Promise 为基础开发的其他技术(比如fetch API), 还包括 V8 的垃圾回收过程。

Ok, 这便是 宏任务 和 微任务 的概念,接下来正式介绍JS非常重要的运行机制——EventLoop。

48. 理解EventLoop:浏览器

例子:

//执行队列中的任务

Task task = task_queue.takeTask();

ProcessTask(task);

//执行延迟队列中的任务

ProcessDelayTask()

if(!keep_running) //如果设置了退出标志,那么直接退出线程循环

break;

}

}

console.log('start');

setTimeout(() => {

console.log('timeout');

});

Promise.resolve().then(() => {

console.log('resolve');

});

console.log('end');分析一下:

1)刚开始整个脚本作为一个宏任务来执行,因此先打印start和end

2)setTimeout 作为一个宏任务放入宏任务队列

3)Promise.then作为一个为微任务放入到微任务队列

4)当本次宏任务执行完,检查微任务队列,发现一个Promise.then, 执行

5)接下来进入到下一个宏任务——setTimeout, 执行

因此最后的顺序是:

start

end

resolve

timeout

这样就带大家直观地感受到了浏览器环境下 EventLoop 的执行流程。不过,这只是其中的一部分情况,

接下来我们来做一个更完整的总结。

1)一开始整段脚本作为第一个宏任务执行

2)执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列

3)当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空

4)执行浏览器 UI 线程的渲染工作

5)检查是否有Web worker任务,有则执行

6)执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

最后留一道题目练习:

Promise.resolve().then(()=>{

console.log('Promise1')

setTimeout(()=>{

console.log('setTimeout2')

},0)

});

setTimeout(()=>{

console.log('setTimeout1')

Promise.resolve().then(()=>{

console.log('Promise2')

})

},0);

console.log('start');

// start

// Promise1

// setTimeout1

// Promise2

// setTimeout2

49. 理解EventLoopnodejs

nodejs 和 浏览器的 eventLoop 还是有很大差别的,值得单独拿出来说一说。

不知你是否看过关于 nodejs 中 eventLoop 的一些文章, 是否被这些流程图搞得眼花缭乱、一头雾水:看到这你不用紧张,这里会抛开这些晦涩的流程图,以最清晰浅显的方式来一步步拆解 nodejs 的事循环机制。

三大关键阶段

首先,梳理一下 nodejs 三个非常重要的执行阶段:

1)执行 定时器回调 的阶段。检查定时器,如果到了时间,就执行回调。这些定时器就是setTimeout、setInterval。这个阶段暂且叫它 timer 。

2)轮询(英文叫 poll )阶段。因为在node代码中难免会有异步操作,比如文件I/O,网络I/O等等,那么当这些异步操作做完了,就会来通知JS主线程,怎么通知呢?就是通过'data'、'connect'等事件使得事件循环到达 poll 阶段。到达了这个阶段后:

如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到timer阶段。

如果没有定时器, 会去看回调函数队列。

如果队列不为空,拿出队列中的方法依次执行

如果队列为空,检查是否有setImmdiate的回调

有则前往check阶段

没有则继续等待,相当于阻塞了一段时间(阻塞时间是有上限的), 等待 callback 函数加入队

列,加入后会立刻执行。一段时间后自动进入 check 阶段。

3)check 阶段。这是一个比较简单的阶段,直接 执行 setImmdiate 的回调。

这三个阶段为一个循环过程。不过现在的eventLoop并不完整,我们现在就来一一地完善。

完善首先,当第 1 阶段结束后,可能并不会立即等待到异步事件的响应,这时候 nodejs 会进入到 I/O异常的

回调阶段 。比如说 TCP 连接遇到ECONNREFUSED,就会在这个时候执行回调。

并且在 check 阶段结束后还会进入到 关闭事件的回调阶段 。如果一个 socket 或句柄(handle)被突然

关闭,例如 socket.destroy(), 'close' 事件的回调就会在这个阶段执行。

梳理一下,nodejs 的 eventLoop 分为下面的几个阶段:

1)timer 阶段

2)I/O 异常回调阶段

空闲、预备状态(第2阶段结束,poll 未触发之前)

3)poll 阶段

4)check 阶段

5)关闭事件的回调阶段

实例演示

setTimeout(()=>{

console.log('timer1')

Promise.resolve().then(function() {

console.log('promise1')

})

}, 0)

setTimeout(()=>{

console.log('timer2')

Promise.resolve().then(function() {

console.log('promise2')

})

}, 0)

node版本 >= 11和在 11 以下的会有不同的表现。

首先说 node 版本 >= 11的,它会和浏览器表现一致,一个定时器运行完立即运行相应的微任务。

timer1

promise1

time2

promise2

而 node 版本小于 11 的情况下,对于定时器的处理是:

若第一个定时器任务出队并执行完,发现队首的任务仍然是一个定时器,那么就将微任务暂时保

存,直接去执行新的定时器任务,当新的定时器任务执行完后,再一一执行中途产生的微任务。

因此会打印出这样的结果:

timer1

timer2

promise1

promise2

50. nodejs 和 浏览器关于eventLoop的主要区别

两者最主要的区别在于浏览器中的微任务是在 每个相应的宏任务 中执行的,而nodejs中的微任务是在 不同阶段之间 执行的。

51. 关于process.nextTick的一点说明

process.nextTick 是一个独立于 eventLoop 的任务队列。

在每一个 eventLoop 阶段完成后会去检查这个队列,如果里面有任务,会让这部分任务 优先于微任务 执行。

52. nodejs中的异步、非阻塞I/O是如何实现的?

在听到 nodejs 相关的特性时,经常会对 异步I/O 、 非阻塞I/O 有所耳闻,听起来好像是差不多的意思,但其实是两码事,下面我们就以原理的角度来剖析一下对 nodejs 来说,这两种技术底层是如何实现的?

什么是I/O

I/O 即Input/Output, 输入和输出的意思。在浏览器端,只有一种 I/O,那就是利用 Ajax 发送网络请求,然后读取返回的内容,这属于 网络I/O 。回到 nodejs 中,其实这种的 I/O 的场景就更加广泛了,主要分为两种:

文件 I/O。比如用 fs 模块对文件进行读写操作。

网络 I/O。比如 http 模块发起网络请求。

阻塞和非阻塞I/O

阻塞 和 非阻塞 I/O 其实是针对操作系统内核而言的,而不是 nodejs 本身。阻塞 I/O 的特点就是一定要等到操作系统完成所有操作后才表示调用结束,而非阻塞 I/O 是调用后立马返回,不用等操作系统内核完成操作。

对前者而言,在操作系统进行 I/O 的操作的过程中,我们的应用程序其实是一直处于等待状态的,什么都做不了。那如果换成 非阻塞I/O ,调用返回后我们的 nodejs 应用程序可以完成其他的事情,而操作系统同时也在进行 I/O。这样就把等待的时间充分利用了起来,提高了执行效率,但是同时又会产生一个问题,nodejs 应用程序怎么知道操作系统已经完成了 I/O 操作呢?

为了让 nodejs 知道操作系统已经做完 I/O 操作,需要重复地去操作系统那里判断一下是否完成,这种重复判断的方式就是 轮询 。对于轮询而言,有以下这么几种方案:

1)一直轮询检查I/O状态,直到 I/O 完成。这是最原始的方式,也是性能最低的,会让 CPU 一直耗用在等待上面。其实跟阻塞 I/O 的效果是一样的。

2)遍历文件描述符(即 文件I/O 时操作系统和 nodejs 之间的文件凭证)的方式来确定 I/O 是否完成,I/O完成则文件描述符的状态改变。但 CPU 轮询消耗还是很大。

3)epoll模式。即在进入轮询的时候如果I/O未完成CPU就休眠,完成之后唤醒CPU。

总之,CPU要么重复检查I/O,要么重复检查文件描述符,要么休眠,都得不到很好的利用,我们希望的是:nodejs 应用程序发起 I/O 调用后可以直接去执行别的逻辑,操作系统默默地做完 I/O 之后给nodejs 发一个完成信号,nodejs 执行回调操作。

这是理想的情况,也是异步 I/O 的效果,那如何实现这样的效果呢?

异步 I/O 的本质

Linux 原生存在这样的一种方式,即(AIO), 但两个致命的缺陷:

1)只有 Linux 下存在,在其他系统中没有异步 I/O 支持。

2)无法利用系统缓存。

nodejs中的异步 I/O 方案

是不是没有办法了呢?在单线程的情况下确实是这样,但是如果把思路放开一点,利用多线程来考虑这个问题,就变得轻松多了。我们可以让一个进程进行计算操作,另外一些进行 I/O 调用,I/O 完成后把信号传给计算的线程,进而执行回调,这不就好了吗?没错,异步 I/O 就是使用这样的线程池来实现的。

只不过在不同的系统下面表现会有所差异,在 Linux 下可以直接使用线程池来完成,在Window系统下则采用 IOCP 这个系统API(其内部还是用线程池完成的)。

有了操作系统的支持,那 nodejs 如何来对接这些操作系统从而实现异步 I/O 呢?

以文件为 I/O 我们以一段代码为例:

let fs = require('fs');

fs.readFile('/test.txt', function (err, data) {

console.log(data);

});

执行流程

执行代码的过程中大概发生了这些事情:

1)首先,fs.readFile调用Node的核心模块fs.js ;

2)接下来,Node的核心模块调用内建模块node_file.cc,创建对应的文件I/O观察者对象(这个对象后面

有大用!) ;

3)最后,根据不同平台(Linux或者window),内建模块通过libuv中间层进行系统调用

libuv调用过程拆解

重点来了!libuv 中是如何来进行进行系统调用的呢?也就是 uv_fs_open() 中做了些什么?

1)创建请求对象

以Windows系统为例来说,在这个函数的调用过程中,我们创建了一个文件I/O的请求对象,并往里面

注入了回调函数。

req_wrap->object_->Set(oncomplete_sym, callback);req_wrap 便是这个请求对象,req_wrap 中 object_ 的 oncomplete_sym 属性对应的值便是我们

nodejs 应用程序代码中传入的回调函数。

2)推入线程池,调用返回

在这个对象包装完成后,QueueUserWorkItem() 方法将这个对象推进线程池中等待执行。

好,至此现在js的调用就直接返回了,我们的 js 应用程序代码可以 继续往下执行 ,当然,当前的 I/O 操

作同时也在线程池中将被执行,这不就完成了异步么:)

等等,别高兴太早,回调都还没执行呢!接下来便是执行回调通知的环节。

3)回调通知

事实上现在线程池中的 I/O 无论是阻塞还是非阻塞都已经无所谓了,因为异步的目的已经达成。重要的

是 I/O 完成后会发生什么。

在介绍后续的故事之前,给大家介绍两个重要的方法: GetQueuedCompletionStatus 和

PostQueuedCompletionStatus 。

1)还记得之前讲过的 eventLoop 吗?在每一个Tick当中会调用 GetQueuedCompletionStatus 检查线

程池中是否有执行完的请求,如果有则表示时机已经成熟,可以执行回调了。

2) PostQueuedCompletionStatus 方法则是向 IOCP 提交状态,告诉它当前I/O完成了。

把后面的过程串联起来。

当对应线程中的 I/O 完成后,会将获得的结果 存储 起来,保存到 相应的请求对象 中,然后调用

PostQueuedCompletionStatus() 向 IOCP 提交执行完成的状态,并且将线程还给操作系统。一旦

EventLoop 的轮询操作中,调用 GetQueuedCompletionStatus 检测到了完成的状态,就会把 请求对象

塞给I/O观察者(之前埋下伏笔,如今终于闪亮登场)。

I/O 观察者现在的行为就是取出 请求对象 的 存储结果 ,同时也取出它的 oncomplete_sym 属性,即回调

函数(不懂这个属性的回看第1步的操作)。将前者作为函数参数传入后者,并执行后者。 这里,回调函数

就成功执行啦!

总结 :

1) 阻塞 和 非阻塞 I/O 其实是针对操作系统内核而言的。阻塞 I/O 的特点就是一定要等到操作系统完成 所有操作后才表示调用结束,而非阻塞 I/O 是调用后立马返回,不用等操作系统内核完成操作。

2)nodejs中的异步 I/O 采用多线程的方式,由 EventLoop 、 I/O 观察者 , 请求对象 、 线程池 四大要素相互配合,共同实现。

53. JS异步编程有哪些方案?为什么会出现这些方案?

关于 JS 单线程 、 EventLoop 以及 异步 I/O 这些底层的特性,我们之前做过了详细的拆解,不在赘述。在探究了底层机制之后,我们还需要对代码的组织方式有所理解,这是离我们最日常开发最接近的部分,异步代码的组织方式直接决定了 开发 和 维护 的 效率 ,其重要性也不可小觑。尽管底层机制没变,但异步代码的组织方式却随着 ES 标准的发展,一步步发生了巨大的 变革 。接着让我们来一探究竟吧!

回调函数时代相信很多 nodejs 的初学者都或多或少踩过这样的坑,node 中很多原生的 api 就是诸如这样的:

fs.readFile('xxx', (err, data) => {

});

典型的高阶函数,将回调函数作为函数参数传给了readFile。但久而久之,就会发现,这种传入回调的

方式也存在大坑, 比如下面这样:

fs.readFile('1.json', (err, data) => {

fs.readFile('2.json', (err, data) => {

fs.readFile('3.json', (err, data) => {

fs.readFile('4.json', (err, data) => {

});

});

});

});

回调当中嵌套回调,也称 回调地狱 。这种代码的可读性和可维护性都是非常差的,因为嵌套的层级太

多。而且还有一个严重的问题,就是每次任务可能会失败,需要在回调里面对每个任务的失败情况进行

处理,增加了代码的混乱程度。

Promise 时代

ES6 中新增的 Promise 就很好了解决了 回调地狱 的问题,同时了合并了错误处理。写出来的代码类似于

下面这样:

readFilePromise('1.json').then(data => {

return readFilePromise('2.json')

}).then(data => {

return readFilePromise('3.json')

}).then(data => {

return readFilePromise('4.json')

});

以链式调用的方式避免了大量的嵌套,也符合人的线性思维方式,大大方便了异步编程。

co + Generator 方式

利用协程完成 Generator 函数,用 co 库让代码依次执行完,同时以同步的方式书写,也让异步操作按

顺序执行。

co(function* () {

const r1 = yield readFilePromise('1.json');

const r2 = yield readFilePromise('2.json');

const r3 = yield readFilePromise('3.json');

const r4 = yield readFilePromise('4.json');

})

async + await方式

这是 ES7 中新增的关键字,凡是加上 async 的函数都默认返回一个 Promise 对象,而更重要的是 async+ await 也能让异步代码以同步的方式来书写,而不需要借助第三方库的支持。

54. 能不能简单实现一下 node 中回调函数的机制?

回调函数 的方式其实内部利用了 发布-订阅 模式,在这里我们以模拟实现 node 中的 Event 模块为例来写实现回调函数的机制。

这个 EventEmitter 一共需要实现这些方法: addListener , removeListener , once ,

removeAllListener , emit 。

首先是addListener:

removeLisener 的实现如下:

const readFileAsync = async function () {

const f1 = await readFilePromise('1.json')

const f2 = await readFilePromise('2.json')

const f3 = await readFilePromise('3.json')

const f4 = await readFilePromise('4.json')

}

function EventEmitter() {

this.events = new Map();

}

// once 参数表示是否只是触发一次

const wrapCallback = (fn, once = false) => ({ callback: fn, once });

EventEmitter.prototype.addListener = function (type, fn, once = false) {

let handler = this.events.get(type);

if (!handler) {

// 为 type 事件绑定回调

this.events.set(type, wrapCallback(fn, once));

} else if (handler && typeof handler.callback === 'function') {

// 目前 type 事件只有一个回调

this.events.set(type, [handler, wrapCallback(fn, once)]);

} else {

// 目前 type 事件回调数 >= 2

handler.push(wrapCallback(fn, once));

}

}

EventEmitter.prototype.removeListener = function (type, listener) {

let handler = this.events.get(type);

if (!handler) return;

if (!Array.isArray(handler)) {

if (handler.callback === listener.callback) this.events.delete(type);

else return;

}

for (let i = 0; i < handler.length; i++) {

let item = handler[i];

if (item.callback === listener.callback) {// 删除该回调,注意数组塌陷的问题,即后面的元素会往前挪一位。i 要 --

handler.splice(i, 1);

i--;

if (handler.length === 1) {

// 长度为 1 就不用数组存了

this.events.set(type, handler[0]);

}

}

}

}

once 实现思路很简单,先调用 addListener 添加上了once标记的回调对象, 然后在 emit 的时候遍历回

调列表,将标记了once: true的项remove掉即可。

EventEmitter.prototype.once = function (type, fn) {

this.addListener(type, fn, true);

}

EventEmitter.prototype.emit = function (type, ...args) {

let handler = this.events.get(type);

if (!handler) return;

if (Array.isArray(handler)) {

// 遍历列表,执行回调

handler.map(item => {

item.callback.apply(this, args);

// 标记的 once: true 的项直接移除

if (item.once) this.removeListener(type, item);

})

} else {

// 只有一个回调则直接执行

handler.callback.apply(this, args);

}

return true;

}

最后是 removeAllListener:

EventEmitter.prototype.removeAllListener = function (type) {

let handler = this.events.get(type);

if (!handler) return;

else this.events.delete(type);

}

现在我们测试一下:

let e = new EventEmitter();

e.addListener('type', () => {

console.log("type事件触发!");

})

e.addListener('type', () => {

console.log("WOW!type事件又触发了!");

})

function f() {

console.log("type事件我只触发一次");

}一个简易的 Event 就这样实现完成了,为什么说它简易呢?因为还有很多细节的部分没有考虑:

1)在 参数少 的情况下,call 的性能优于 apply,反之 apply 的性能更好。因此在执行回调时候可以根据

情况调用 call 或者 apply。

2)考虑到内存容量,应该设置 回调列表的最大值 ,当超过最大值的时候,应该选择部分回调进行删除操

作。

3)鲁棒性有待提高。对于参数的校验很多地方直接忽略掉了。

55. Promise 凭借什么消灭了回调地狱?

问题

首先,什么是回调地狱:

1)多层嵌套的问题。

2)每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。

这两种问题在回调函数时代尤为突出。Promise 的诞生就是为了解决这两个问题。

解决方法

Promise 利用了三大技术手段来解决回调地狱:

回调函数延迟绑定

返回值穿透

错误冒泡

首先来举个例子:

e.once('type', f)

e.emit('type');

e.emit('type');

e.removeAllListener('type');

e.emit('type');

// type事件触发!

// WOW!type事件又触发了!

// type事件我只触发一次

// type事件触发!

// WOW!type事件又触发了!let readFilePromise = (filename) => {

fs.readFile(filename, (err, data) => {

if(err) {

reject(err);

}else {

resolve(data);

}

})

}

readFilePromise('1.json').then(data => {

return readFilePromise('2.json')

});

看到没有,回调函数不是直接声明的,而是在通过后面的 then 方法传入的,即延迟传入。这就是回调

函数延迟绑定。

然后我们做以下微调:

let x = readFilePromise('1.json').then(data => {

return readFilePromise('2.json')//这是返回的Promise

});

x.then(/* 内部逻辑省略 */)

我们会根据 then 中回调函数的传入值创建不同类型的Promise, 然后把返回的 Promise 穿透到外层, 以

供后续的调用。这里的 x 指的就是内部返回的 Promise,然后在 x 后面可以依次完成链式调用。

这便是返回值穿透的效果。

这两种技术一起作用便可以将深层的嵌套回调写成下面的形式:

readFilePromise('1.json').then(data => {

return readFilePromise('2.json');

}).then(data => {

return readFilePromise('3.json');

}).then(data => {

return readFilePromise('4.json');

});

这样就显得清爽了许多,更重要的是,它更符合人的线性思维模式,开发体验也更好。

两种技术结合产生了 链式调用 的效果。

这解决的是多层嵌套的问题,那另一个问题,即每次任务执行结束后 分别处理成功和失败 的情况怎么解决

的呢?

Promise 采用了 错误冒泡 的方式。其实很简单理解,我们来看看效果:

readFilePromise('1.json').then(data => {

return readFilePromise('2.json');

}).then(data => {

return readFilePromise('3.json');

}).then(data => {

return readFilePromise('4.json');

}).catch(err => {

// xxx

})这样前面产生的错误会一直向后传递,被 catch 接收到,就不用频繁地检查错误了。

解决效果

实现链式调用,解决多层嵌套问题

实现错误冒泡后一站式处理,解决每次任务中判断错误、增加代码混乱度的问题

56. 为什么Promise要引入微任务?

Promise 中的执行函数是同步进行的,但是里面存在着异步操作,在异步操作结束后会调用 resolve 方法,或者中途遇到错误调用 reject 方法,这两者都是作为微任务进入到 EventLoop 中。但是你有没有想过,Promise 为什么要引入微任务的方式来进行回调操作?

解决方式

回到问题本身,其实就是如何处理回调的问题。总结起来有三种方式:

1)使用同步回调,直到异步任务进行完,再进行后面的任务。

2)使用异步回调,将回调函数放在进行 宏任务队列 的队尾。

3)使用异步回调,将回调函数放到 当前宏任务中 的最后面。

优劣对比

第一种方式显然不可取,因为同步的问题非常明显,会让整个脚本阻塞住,当前任务等待,后面的任务都无法得到执行,而这部分等待的时间是可以拿来完成其他事情的,导致 CPU 的利用率非常低,而且还有另外一个致命的问题,就是无法实现延迟绑定的效果。

如果采用第二种方式,那么执行回调(resolve/reject)的时机应该是在前面 所有的宏任务 完成之后,倘若现在的任务队列非常长,那么回调迟迟得不到执行,造成 应用卡顿 。

为了解决上述方案的问题,另外也考虑到延迟绑定的需求,Promise 采取第三种方式, 即引入微任务, 即把 resolve(reject) 回调的执行放在当前宏任务的末尾。

这样,利用微任务解决了两大痛点:

采用异步回调替代同步回调解决了浪费 CPU 性能的问题。

放到当前宏任务最后执行,解决了回调执行的实时性问题。

好,Promise 的基本实现思想已经讲清楚了,相信大家已经知道了它 为什么这么设计 ,接下来就让我们

一步步弄清楚它内部到底是 怎么设计的 。

57. Promise 如何实现链式调用?

简易版实现

首先写出第一版的代码:

//定义三种状态

const PENDING = "pending";

const FULFILLED = "fulfilled";

const REJECTED = "rejected";function MyPromise(executor) {

let self = this; // 缓存当前promise实例

self.value = null;

self.error = null;

self.status = PENDING;

self.onFulfilled = null; //成功的回调函数

self.onRejected = null; //失败的回调函数

const resolve = (value) => {

if(self.status !== PENDING) return;

setTimeout(() => {

self.status = FULFILLED;

self.value = value;

self.onFulfilled(self.value);//resolve时执行成功回调

});

};

const reject = (error) => {

if(self.status !== PENDING) return;

setTimeout(() => {

self.status = REJECTED;

self.error = error;

self.onRejected(self.error);//resolve时执行成功回调

});

};

executor(resolve, reject);

}

MyPromise.prototype.then = function(onFulfilled, onRejected) {

if (this.status === PENDING) {

this.onFulfilled = onFulfilled;

this.onRejected = onRejected;

} else if (this.status === FULFILLED) {

//如果状态是fulfilled,直接执行成功回调,并将成功值传入

onFulfilled(this.value)

} else {

//如果状态是rejected,直接执行失败回调,并将失败原因传入

onRejected(this.error)

}

return this;

}

可以看到,Promise 的本质是一个有限状态机,存在三种状态:

PENDING(等待)

FULFILLED(成功)

REJECTED(失败)

状态改变规则如下图:对于 Promise 而言,状态的改变 不可逆 ,即由等待态变为其他的状态后,就无法再改变了。

不过,回到目前这一版的 Promise, 还是存在一些问题的。

设置回调数组

首先只能执行一个回调函数,对于多个回调的绑定就无能为力,比如下面这样:

let promise1 = new MyPromise((resolve, reject) => {

fs.readFile('./001.txt', (err, data) => {

if(!err){

resolve(data);

}else {

reject(err);

}

})

});

let x1 = promise1.then(data => {

console.log("第一次展示", data.toString());

});

let x2 = promise1.then(data => {

console.log("第二次展示", data.toString());

});

let x3 = promise1.then(data => {

console.log("第三次展示", data.toString());

});

这里绑定了三个回调,想要在 resolve() 之后一起执行,那怎么办呢?

需要将 onFulfilled 和 onRejected 改为数组,调用 resolve 时将其中的方法拿出来一一执行即可。

self.onFulfilledCallbacks = [];

self.onRejectedCallbacks = [];

MyPromise.prototype.then = function(onFulfilled, onRejected) {

if (this.status === PENDING) {

this.onFulfilledCallbacks.push(onFulfilled);

this.onRejectedCallbacks.push(onRejected);

} else if (this.status === FULFILLED) {onFulfilled(this.value);

} else {

onRejected(this.error);

}

return this;

}

接下来将 resolve 和 reject 方法中执行回调的部分进行修改:

// resolve 中

self.onFulfilledCallbacks.forEach((callback) => callback(self.value));

//reject 中

self.onRejectedCallbacks.forEach((callback) => callback(self.error));

链式调用完成

我们采用目前的代码来进行测试:

let fs = require('fs');

let readFilePromise = (filename) => {

return new MyPromise((resolve, reject) => {

fs.readFile(filename, (err, data) => {

if(!err){

resolve(data);

}else {

reject(err);

}

})

})

}

readFilePromise('./001.txt').then(data => {

console.log(data.toString());

return readFilePromise('./002.txt');

}).then(data => {

console.log(data.toString());

})

// 001.txt的内容

// 001.txt的内容

咦?怎么打印了两个 001 ,第二次不是读的 002 文件吗?

问题出在这里:

MyPromise.prototype.then = function(onFulfilled, onRejected) {

//...

return this;

}

这么写每次返回的都是第一个 Promise。then 函数当中返回的第二个 Promise 直接被无视了!

说明 then 当中的实现还需要改进, 我们现在需要对 then 中返回值重视起来。

MyPromise.prototype.then = function (onFulfilled, onRejected) {

let bridgePromise;let self = this;

if (self.status === PENDING) {

return bridgePromise = new MyPromise((resolve, reject) => {

self.onFulfilledCallbacks.push((value) => {

try {

// 看到了吗?要拿到 then 中回调返回的结果。

let x = onFulfilled(value);

resolve(x);

} catch (e) {

reject(e);

}

});

self.onRejectedCallbacks.push((error) => {

try {

let x = onRejected(error);

resolve(x);

} catch (e) {

reject(e);

}

});

});

}

//...

}

假若当前状态为 PENDING,将回调数组中添加如上的函数,当 Promise 状态变化后,会遍历相应回调

数组并执行回调。

但是这段程度还是存在一些问题:

1)首先 then 中的两个参数不传的情况并没有处理,

2)假如 then 中的回调执行后返回的结果(也就是上面的 x )是一个 Promise, 直接给 resolve 了,这是我

们不希望看到的。

怎么来解决这两个问题呢?

先对参数不传的情况做判断:

// 成功回调不传给它一个默认函数

onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;

// 对于失败回调直接抛错

onRejected = typeof onRejected === "function" ? onRejected : error => { throw

error };

然后对返回Promise的情况进行处理:

function resolvePromise(bridgePromise, x, resolve, reject) {

//如果x是一个promise

if (x instanceof MyPromise) {

// 拆解这个 promise ,直到返回值不为 promise 为止

if (x.status === PENDING) {

x.then(y => {

resolvePromise(bridgePromise, y, resolve, reject);

}, error => {

reject(error);

});

} else {x.then(resolve, reject);

}

} else {

// 非 Promise 的话直接 resolve 即可

resolve(x);

}

}

然后在 then 的方法实现中作如下修改:

resolve(x) -> resolvePromise(bridgePromise, x, resolve, reject);

在这里大家好好体会一下拆解 Promise 的过程,其实不难理解,要强调的是其中的递归调用始终传入的

resolve 和 reject 这两个参数是什么含义,其实他们控制的是最开始传入的 bridgePromise 的状态,

这一点非常重要。

紧接着,我们实现一下当 Promise 状态不为 PENDING 时的逻辑。

成功状态下调用then:

if (self.status === FULFILLED) {

return bridgePromise = new MyPromise((resolve, reject) => {

try {

// 状态变为成功,会有相应的 self.value

let x = onFulfilled(self.value);

// 暂时可以理解为 resolve(x),后面具体实现中有拆解的过程

resolvePromise(bridgePromise, x, resolve, reject);

} catch (e) {

reject(e);

}

})

}

失败状态下调用then:

if (self.status === REJECTED) {

return bridgePromise = new MyPromise((resolve, reject) => {

try {

// 状态变为失败,会有相应的 self.error

let x = onRejected(self.error);

resolvePromise(bridgePromise, x, resolve, reject);

} catch (e) {

reject(e);

}

});

}

Promise A+中规定成功和失败的回调都是微任务,由于浏览器中 JS 触碰不到底层微任务的分配,可以

直接拿 setTimeout (属于宏任务的范畴) 来模拟,用 setTimeout 将需要执行的任务包裹 ,当然,上面

的 resolve 实现也是同理, 大家注意一下即可,其实并不是真正的微任务。if (self.status === FULFILLED) {

return bridgePromise = new MyPromise((resolve, reject) => {

setTimeout(() => {

//...

})

}

if (self.status === REJECTED) {

return bridgePromise = new MyPromise((resolve, reject) => {

setTimeout(() => {

//...

})

}

好了,到这里, 我们基本实现了 then 方法,现在我们拿刚刚的测试代码做一下测试, 依次打印如下:

001.txt的内容

002.txt的内容

可以看到,已经可以顺利地完成链式调用。

错误捕获及冒泡机制分析

现在来实现 catch 方法:

Promise.prototype.catch = function (onRejected) {

return this.then(null, onRejected);

}

对,就是这么几行,catch 原本就是 then 方法的语法糖。

相比于实现来讲,更重要的是理解其中错误冒泡的机制,即中途一旦发生错误,可以在最后用 catch 捕

获错误。

我们回顾一下 Promise 的运作流程也不难理解,贴上一行关键的代码:

// then 的实现中

onRejected = typeof onRejected === "function" ? onRejected : error => { throw

error };

一旦其中有一个PENDING状态的 Promise 出现错误后状态必然会变为失败, 然后执行 onRejected函

数,而这个 onRejected 执行又会抛错,把新的 Promise 状态变为失败,新的 Promise 状态变为失败后

又会执行onRejected......就这样一直抛下去,直到用catch 捕获到这个错误,才停止往下抛。

这就是 Promise 的错误冒泡机制。

至此,Promise 三大法宝: 回调函数延迟绑定、回调返回值穿透 和 错误冒泡。

58. 实现Promise resolvereject finally

实现 Promise.resolve

实现 resolve 静态方法有三个要点:

传参为一个 Promise, 则直接返回它。

传参为一个 thenable 对象,返回的 Promise 会跟随这个对象, 采用它的最终状态 作为 自己的状

态 。

其他情况,直接返回以该值为成功状态的promise对象。

具体实现如下:

Promise.resolve = (param) => {
    if(param instanceof Promise) return param;
        return new Promise((resolve, reject) => {
    if(param && param.then && typeof param.then === 'function') {
        // param 状态变为成功会调用resolve,将新 Promise 的状态变为成功,反之亦然
        param.then(resolve, reject);
    }else {
        resolve(param);
    }
    })
}

实现 Promise.reject

Promise.reject 中传入的参数会作为一个 reason 原封不动地往下传, 实现如下:

Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason);
    });
}

实现 Promise.prototype.finally

无论当前 Promise 是成功还是失败,调用 finally 之后都会执行 finally 中传入的函数,并且将值原封不动的往下传。

Promise.prototype.finally = function(callback) {
    this.then(value => {
        return Promise.resolve(callback()).then(() => {
        return value;
    })
    }, error => {
        return Promise.resolve(callback()).then(() => {
        throw error;
    })
    })
}

59. Promise all race

实现 Promise.all

对于 all 方法而言,需要完成下面的核心功能:

1)传入参数为一个空的可迭代对象,则直接进行resolve。

2)如果参数中有一个promise失败,那么Promise.all返回的promise对象失败。

3)在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组

具体实现如下:

实现 Promise.race

race 的实现相比之下就简单一些,只要有一个 promise 执行完,直接 resolve 并停止执行。

Promise.all = function(promises) {

return new Promise((resolve, reject) => {

let result = [];

let index = 0;

let len = promises.length;

if(len === 0) {

resolve(result);

return;

}

for(let i = 0; i < len; i++) {

// 为什么不直接 promise[i].then, 因为promise[i]可能不是一个promise

Promise.resolve(promise[i]).then(data => {

result[i] = data;

index++;

if(index === len) resolve(result);

}).catch(err => {

reject(err);

})

}

})

}

Promise.race = function(promises) {

return new Promise((resolve, reject) => {

let len = promises.length;

if(len === 0) return;

for(let i = 0; i < len; i++) {

Promise.resolve(promise[i]).then(data => {

resolve(data);

return;

}).catch(err => {

reject(err);

return;

})

}

})

}

60. 谈谈你对生成器以及协程的理解

生成器(Generator)是 ES6 中的新语法,相对于之前的异步语法,上手的难度还是比较大的。因此这里我

们先来好好熟悉一下 Generator 语法。

生成器执行流程

上面是生成器函数?

生成器是一个带 星号 的"函数"(注意:它并不是真正的函数),可以通过 yield 关键字 暂停执行 和 恢复执

行 的

举个例子:

由此可以看到,生成器的执行有这样几个关键点:

1)调用 gen() 后,程序会阻塞住,不会执行任何语句。

2)调用 g.next() 后,程序继续执行,直到遇到 yield 程序暂停。

3)next 方法返回一个对象, 有两个属性: value 和 done 。value 为 当前 yield 后面的结果 ,done

表示 是否执行完 ,遇到了 return 后, done 会由 false 变为 true 。

yield*

当一个生成器要调用另一个生成器时,使用 yield* 就变得十分方便。比如下面的例子:

function* gen() {

console.log("enter");

let a = yield 1;

let b = yield (function () {return 2})();

return 3;

}

var g = gen() // 阻塞住,不会执行任何语句

console.log(typeof g) // object 看到了吗?不是"function"

console.log(g.next())

console.log(g.next())

console.log(g.next())

console.log(g.next())

// enter

// { value: 1, done: false }

// { value: 2, done: false }

// { value: 3, done: true }

// { value: undefined, done: true }function* gen1() {

yield 1;

yield 4;

}

function* gen2() {

yield 2;

yield 3;

}

我们想要按照1234的顺序执行,如何来做呢?

在 gen1 中,修改如下:

function* gen1() {

yield 1;

yield* gen2();

yield 4;

}

这样修改之后,之后依次调用 next 即可。

生成器实现机制——协程

可能你会比较好奇,生成器究竟是如何让函数暂停, 又会如何恢复的呢?接下来我们就来对其中的执行机

制—— 协程 一探究竟。

什么是协程?

协程是一种比线程更加轻量级的存在,协程处在线程的环境中, 一个线程可以存在多个协程 ,可以将协程

理解为线程中的一个个任务。不像进程和线程,协程并不受操作系统的管理,而是被具体的应用程序代

码所控制。

协程的运作过程

那你可能要问了,JS 不是单线程执行的吗,开这么多协程难道可以一起执行吗?

答案是:并不能。一个线程一次只能执行一个协程。比如当前执行 A 协程,另外还有一个 B 协程,如果

想要执行 B 的任务,就必须在 A 协程中将 JS 线程的控制权转交给 B协程 ,那么现在 B 执行,A 就相当于

处于暂停的状态。

举个具体的例子:

function* A() {

console.log("我是A");

yield B(); // A停住,在这里转交线程执行权给B

console.log("结束了");

}

function B() {

console.log("我是B");

return 100;// 返回,并且将线程执行权还给A

}

let gen = A();

gen.next();在这个过程中,A 将执行权交给 B,也就是 A 启动 B ,我们也称 A 是 B 的父协程。因此 B 当中最后

return 100 其实是将 100 传给了父协程。

需要强调的是,对于协程来说,它并不受操作系统的控制,完全由用户自定义切换,因此并没有进程/线

程 上下文切换 的开销,这是 高性能 的重要原因。

OK, 原理就说到这里。可能你还会有疑问: 这个生成器不就暂停-恢复、暂停-恢复这样执行的吗?它和异

步有什么关系?而且,每次执行都要调用next,能不能让它一次性执行完毕呢?下一节我们就来仔细拆

解这些问题。

61. 如何让 Generator 的异步代码按顺序执行完毕?

这里面其实有两个问题:

1) Generator 如何跟 异步 产生关系?

2)怎么把 Generator 按顺序执行完毕?

thunk 函数

要想知道 Generator 跟异步的关系,首先带大家搞清楚一个概念——thunk函数(即 偏函数 ),虽然这只

是实现两者关系的方式之一。(另一种方式是 Promise , 后面会讲到)

举个例子,比如我们现在要判断数据类型。可以写如下的判断逻辑:

可以看到,出现了非常多重复的逻辑。我们将它们做一下封装:

现在我们这样做即可:

gen.next();

// 我是A

// 我是B

// 结束了

let isString = (obj) => {

return Object.prototype.toString.call(obj) === '[object String]';

};

let isFunction = (obj) => {

return Object.prototype.toString.call(obj) === '[object Function]';

};

let isArray = (obj) => {

return Object.prototype.toString.call(obj) === '[object Array]';

};

let isSet = (obj) => {

return Object.prototype.toString.call(obj) === '[object Set]';

};

// ...

let isType = (type) => {

return (obj) => {

return Object.prototype.toString.call(obj) === `[object ${type}]`;

}

}let isString = isType('String');

let isFunction = isType('Function');

//...

相应的 isString 和 isFunction 是由 isType 生产出来的函数,但它们依然可以判断出参数是否为

String(Function),而且代码简洁了不少。

isString("123");

isFunction(val => val);

isType这样的函数我们称为thunk 函数。它的核心逻辑是接收一定的参数,生产出定制化的函数,然后

使用定制化的函数去完成功能。thunk函数的实现会比单个的判断函数复杂一点点,但就是这一点点的

复杂,大大方便了后续的操作。

Generator 和 异步

thunk 版本

以文件操作为例,我们来看看 异步操作 如何应用于Generator。

const readFileThunk = (filename) => {

return (callback) => {

fs.readFile(filename, callback);

}

}

readFileThunk 就是一个 thunk函数 。异步操作核心的一环就是绑定回调函数,而 thunk函数 可以帮我

们做到。首先传入文件名,然后生成一个针对某个文件的定制化函数。这个函数中传入回调,这个回调

就会成为异步操作的回调。这样就让 Generator 和 异步 关联起来了。

紧接者我们做如下的操作:

const gen = function* () {

const data1 = yield readFileThunk('001.txt')

console.log(data1.toString())

const data2 = yield readFileThunk('002.txt')

console.log(data2.toString)

}

接着我们让它执行完:

let g = gen();

// 第一步: 由于进场是暂停的,我们调用next,让它开始执行。

// next返回值中有一个value值,这个value是yield后面的结果,放在这里也就是是thunk函数生成的定

制化函数,里面需要传一个回调函数作为参数

g.next().value((err, data1) => {

// 第二步: 拿到上一次得到的结果,调用next, 将结果作为参数传入,程序继续执行。

// 同理,value传入回调

g.next(data1).value((err, data2) => {

g.next(data2);

})

})打印结果如下:

001.txt的内容

002.txt的内容

上面嵌套的情况还算简单,如果任务多起来,就会产生很多层的嵌套,可操作性不强,有必要把执行的

代码封装一下:

function run(gen){

const next = (err, data) => {

let res = gen.next(data);

if(res.done) return;

res.value(next);

}

next();

}

run(g);

再次执行,依然打印正确的结果。代码虽然就这么几行,但包含了递归的过程,好好体会一下。

这是通过 thunk 完成异步操作的情况。

Promise 版本

还是拿上面的例子,用 Promise 来实现就轻松一些:

const readFilePromise = (filename) => {

return new Promise((resolve, reject) => {

fs.readFile(filename, (err, data) => {

if(err) {

reject(err);

}else {

resolve(data);

}

})

}).then(res => res);

}

const gen = function* () {

const data1 = yield readFilePromise('001.txt')

console.log(data1.toString())

const data2 = yield readFilePromise('002.txt')

console.log(data2.toString)

}

执行的代码如下:let g = gen();

function getGenPromise(gen, data) {

return gen.next(data).value;

}

getGenPromise(g).then(data1 => {

return getGenPromise(g, data1);

}).then(data2 => {

return getGenPromise(g, data2)

})

打印结果如下:

001.txt的内容

002.txt的内容

同样,我们可以对执行 Generator 的代码加以封装:

function run(g) {

const next = (data) => {

let res = g.next();

if(res.done) return;

res.value.then(data => {

next(data);

})

}

next();

}

同样能输出正确的结果。代码非常精炼,希望能参照刚刚链式调用的例子,仔细体会一下递归调用的过

程。

采用 co

以上我们针对 thunk 函数 和 Promise 两种 Generator异步操作 的一次性执行完毕做了封装,但实际场

景中已经存在成熟的工具包了,如果大名鼎鼎的co库, 其实核心原理就是我们已经手写过了(就是刚刚

封装的Promise情况下的执行代码),只不过源码会各种边界情况做了处理。使用起来非常简单:

const co = require('co');

let g = gen();

co(g).then(res =>{

console.log(res);

})

打印结果如下:

001.txt的内容

002.txt的内容

100

简单几行代码就完成了 Generator 所有的操作,真不愧 co 和 Generator 天生一对啊!

62. 解释一下async/await的运行机制。

async/await被称为 JS 中异步终极解决方案。它既能够像 co + Generator 一样用同步的方式来书写异步代码,又得到底层的语法支持,无需借助任何第三方库。接下来,我们从原理的角度来重新审视这个语法糖背后究竟做了些什么。

async

什么是 async ?

MDN 的定义: async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。

注意重点: 返回结果为Promise

举个例子:

这就是隐式返回 Promise 的效果。

await

我们来看看 await 做了些什么事情。

以一段代码为例:

我们来分析一下这段程序。首先代码同步执行,打印出 0 ,然后将 test 压入执行栈,打印出 100 , 下面注意了,遇到了关键角色await

放个慢镜头:

被 JS 引擎转换成一个 Promise :

这里调用了 resolve,resolve的任务进入微任务队列。

async function func() {

return 100;

}

console.log(func());

// Promise {<resolved>: 100}

async function test() {

console.log(100)

let x = await 200

console.log(x)

console.log(200)

}

console.log(0)

test()

console.log(300)

await 100;

let promise = new Promise((resolve,reject) => {

resolve(100);

})然后,JS 引擎将暂停当前协程的运行,把线程的执行权交给 父协程 (父协程不懂是什么的,上上篇才讲,

回去补课)。

回到父协程中,父协程的第一件事情就是对 await 返回的 Promise 调用 then , 来监听这个 Promise 的

状态改变 。

然后往下执行,打印出 300 。

根据 EventLoop 机制,当前主线程的宏任务完成,现在检查 微任务队列 , 发现还有一个Promise的

resolve,执行,现在父协程在 then 中传入的回调执行。我们来看看这个回调具体做的是什么。

Ok, 现在执行权到了 test协程 手上,test 接收到 父协程 传来的200, 赋值给 a ,然后依次执行后面的语

句,打印 200 、 200 。

最后的输出为:

总结一下,async/await利用协程和Promise实现了同步方式编写异步代码的效果,其中Generator是对

协程的一种实现,虽然语法简单,但引擎在背后做了大量的工作,我们也对这些工作做了一一的拆解。

用async/await写出的代码也更加优雅、美观,相比于之前的Promise不断调用then的方式,语义化更加

明显,相比于co + Generator性能更高,上手成本也更低,不愧是JS异步终极解决方案!

63. forEach 中用 await 会产生什么问题?怎么解决这个问题?

问题

问题:对于异步代码,forEach 并不能保证按顺序执行。

举个例子:

promise.then(value => {

// 相关逻辑,在resolve 执行之后来调用

})

promise.then(value => {

// 1. 将线程的执行权交给test协程

// 2. 把 value 值传递给 test 协程

})

0

100

300

200

200

async function test() {

let arr = [4, 2, 1]

arr.forEach(async item => {

const res = await handle(item)

console.log(res)

})

console.log('结束')

}

function handle(x) {return new Promise((resolve, reject) => {

setTimeout(() => {

resolve(x)

}, 1000 * x)

})

}

test()

我们期望的结果是:

4

2

1

结束

但是实际上会输出:

结束

1

2

4

问题原因

这是为什么呢?我想我们有必要看看 forEach 底层怎么实现的。

// 核心逻辑

for (var i = 0; i < length; i++) {

if (i in array) {

var element = array[i];

callback(element, i, array);

}

}

可以看到,forEach 拿过来直接执行了,这就导致它无法保证异步任务的执行顺序。比如后面的任务用

时短,那么就又可能抢在前面的任务之前执行。

解决方案

如何来解决这个问题呢?

其实也很简单, 我们利用 for...of 就能轻松解决。

async function test() {

let arr = [4, 2, 1]

for(const item of arr) {

const res = await handle(item)

console.log(res)

}

console.log('结束')

}解决原理——Iterator

for...of并不像forEach那么简单粗暴的方式去遍历执行,而是采用一种特别的手段——迭代器去遍历。

首先,对于数组来讲,它是一种可迭代数据类型。那什么是可迭代数据类型呢?

原生具有[Symbol.iterator]属性数据类型为可迭代数据类型。如数组、类数组(如arguments、

NodeList)、Set和Map。

可迭代对象可以通过迭代器进行遍历。

let arr = [4, 2, 1];

// 这就是迭代器

let iterator = arr[Symbol.iterator]();

console.log(iterator.next());

console.log(iterator.next());

console.log(iterator.next());

console.log(iterator.next());

// {value: 4, done: false}

// {value: 2, done: false}

// {value: 1, done: false}

// {value: undefined, done: true}

因此,我们的代码可以这样来组织:

async function test() {

let arr = [4, 2, 1]

let iterator = arr[Symbol.iterator]();

let res = iterator.next();

while(!res.done) {

let value = res.value;

console.log(value);

await handle(value);

res = iterater.next();

}

console.log('结束')

}

// 4

// 2

// 1

// 结束

多个任务成功地按顺序执行!

重新认识生成器

回头再看看用iterator遍历[4,2,1]这个数组的代码。咦?返回值有value和done属性,生成器也可以调用 next,返回的也是这样的数据结构,这么巧?!

没错,生成器本身就是一个迭代器。

既然属于迭代器,那它就可以用for...of遍历了吧?

当然没错,不信来写一个简单的斐波那契数列(50以内):

64. 关于JS中一些重要的api实现

ES5实现数组的map方法

核心要点:

1)回调函数的参数有哪些,返回值如何处理。

2)不修改原来的数组。

let arr = [4, 2, 1];

// 迭代器

let iterator = arr[Symbol.iterator]();

console.log(iterator.next());

console.log(iterator.next());

console.log(iterator.next());

console.log(iterator.next());

// {value: 4, done: false}

// {value: 2, done: false}

// {value: 1, done: false}

// {value: undefined, done: true}

function* fibonacci(){

let [prev, cur] = [0, 1];

console.log(cur);

while(true) {

[prev, cur] = [cur, prev + cur];

yield cur;

}

}

for(let item of fibonacci()) {

if(item > 50) break;

console.log(item);

}

// 1

// 1

// 2

// 3

// 5

// 8

// 13

// 21

// 34Array.prototype.MyMap = function(fn, context){

var arr = Array.prototype.slice.call(this);//由于是ES5所以就不用...展开符了

var mappedArr = [];

for (var i = 0; i < arr.length; i++ ){

mappedArr.push(fn.call(context, arr[i], i, this));

}

return mappedArr;

}

ES5实现数组的reduce方法

核心要点:

1)初始值不传怎么处理

2)回调函数的参数有哪些,返回值如何处理。

Array.prototype.myReduce = function(fn, initialValue) {

var arr = Array.prototype.slice.call(this);

var res, startIndex;

res = initialValue ? initialValue : arr[0];

startIndex = initialValue ? 0 : 1;

for(var i = startIndex; i < arr.length; i++) {

res = fn.call(null, res, arr[i], i, this);

}

return res;

}

实现call/apply

思路: 利用this的上下文特性。

//实现apply只要把下一行中的...args换成args即可

Function.prototype.myCall = function(context = window, ...args) {

let func = this;

let fn = Symbol("fn");

context[fn] = func;

let res = context[fn](...args);//重点代码,利用this指向,相当于

context.caller(...args)

delete context[fn];

return res;

}

实现Object.create方法(常用)

function create(proto) {

function F() {};

F.prototype = proto;

F.prototype.constructor = F;

return new F();

}

实现bind方法

核心要点:

1)对于普通函数,绑定this指向

2)对于构造函数,要保证原函数的原型对象上的属性不能丢失

Function.prototype.bind = function(context, ...args) {

let self = this;//谨记this表示调用bind的函数

let fBound = function() {

//this instanceof fBound为true表示构造函数的情况。如new func.bind(obj)

return self.apply(this instanceof fBound ? this : context || window,

args.concat(Array.prototype.slice.call(arguments)));

}

fBound.prototype = Object.create(this.prototype);//保证原函数的原型对象上的属性不

丢失

return fBound;

}

实现new关键字

核心要点:

1)创建一个全新的对象,这个对象的proto要指向构造函数的原型对象

2)执行构造函数

3)返回值为object类型则作为new方法的返回值返回,否则返回上述全新对象

function myNew(fn, ...args) {

let instance = Object.create(fn.prototype);

let res = fn.apply(instance, args);

return typeof res === 'object' ? res: instance;

}

实现instanceof的作用

核心要点:原型链的向上查找。function myInstanceof(left, right) {

let proto = Object.getPrototypeOf(left);

while(true) {

if(proto == null) return false;

if(proto == right.prototype) return true;

proto = Object.getPrototypeof(proto);

}

}

实现单例模式

核心要点: 用闭包和Proxy属性拦截

function proxy(func) {

let instance;

let handler = {

construct(target, args) {

if(!instance) {

instance = Reflect.construct(func, args);

}

return instance;

}

}

return new Proxy(func, handler);

}

实现防抖功能

核心要点:

如果在定时器的时间范围内再次触发,则重新计时。

const debounce = (fn, delay) => {

let timer = null;

return (...args) => {

clearTimeout(timer);

timer = setTimeout(() => {

fn.apply(this, args);

}, delay);

};

};

实现节流功能

核心要点:

如果在定时器的时间范围内再次触发,则不予理睬,等当前定时器完成,才能启动下一个定时器。const throttle = (fn, delay = 500) => {

let flag = true;

return (...args) => {

if (!flag) return;

flag = false;

setTimeout(() => {

fn.apply(this, args);

flag = true;

}, delay);

};

};

实现深拷贝

以下为简易版深拷贝,没有考虑循环引用的情况和Buffer、Promise、Set、Map的处理,如果一一实

现,过于复杂,面试短时间写出来不太现实

const clone = parent => {

// 判断类型

const isType = (target, type) => `[object ${type}]` ===

Object.prototype.toString.call(target)

// 处理正则

const getRegExp = re => {

let flags = "";

if (re.global) flags += "g";

if (re.ignoreCase) flags += "i";

if (re.multiline) flags += "m";

return flags;

};

const _clone = parent => {

if (parent === null) return null;

if (typeof parent !== "object") return parent;

let child, proto;

if (isType(parent, "Array")) {

// 对数组做特殊处理

child = [];

} else if (isType(parent, "RegExp")) {

// 对正则对象做特殊处理

child = new RegExp(parent.source, getRegExp(parent));

if (parent.lastIndex) child.lastIndex = parent.lastIndex;

} else if (isType(parent, "Date")) {

// 对Date对象做特殊处理

child = new Date(parent.getTime());

} else {

// 处理对象原型

proto = Object.getPrototypeOf(parent);

// 利用Object.create切断原型链

child = Object.create(proto);

}

for (let i in parent) {

// 递归child[i] = _clone(parent[i]);

}

return child;

};

return _clone(parent);

};

实现Promise

重点难点,比较复杂

用法

new Promise((resolve, reject) => {

//异步成功执行resolve,否则执行reject

}).then((res) => {

//resolve触发第一个回调函数执行

}, (err) => {

//reject触发第二个回调函数执行

}).then(res => {

//需要保证then方法返回的依然是promise

//这样才能实现链式调用

}).catch(reason => {

});

//等待所有的promise都成功执行then,

//反之只要有一个失败就会执行catch

Promise.all([promise1, ...]).then();

js

1)初步实现Promise

1)实现三种状态:‘pending’, 'fulfilled', 'rejected'

2)能够实现then方法两种回调函数的处理

//promise.js

class Promise{

//传一个异步函数进来

constructor(excutorCallBack){

this.status = 'pending';

this.value = undefined;

this.fulfillAry = [];

this.rejectedAry = [];

//=>执行Excutor

let resolveFn = result => {

if(this.status !== 'pending') return;

let timer = setTimeout(() => {

this.status = 'fulfilled';

this.value = result;

this.fulfillAry.forEach(item => item(this.value));

}, 0);

};

let rejectFn = reason => {

if(this.status !== 'pending')return;let timer = setTimeout(() => {

this.status = 'rejected';

this.value = reason;

this.rejectedAry.forEach(item => item(this.value))

})

};

try{

//执行这个异步函数

excutorCallBack(resolveFn, rejectFn);

} catch(err) {

//=>有异常信息按照rejected状态处理

rejectFn(err);

}

}

then(fulfilledCallBack, rejectedCallBack) {

//resolve和reject函数其实一个作为微任务

//因此他们不是立即执行,而是等then调用完成后执行

this.fulfillAry.push(fulfilledCallBack);

this.rejectedAry.push(rejectedCallBack);

//一顿push过后他们被执行

}

}

module.exports = Promise;

测试如下:

let Promise = require('./promise');

let p1 = new Promise((resolve, reject) => {

setTimeout(() => {

Math.random()<0.5?resolve(100):reject(-100);

}, 1000)

}).then(res => {

console.log(res);

}, err => {

console.log(err);

})

2)完成链式效果

最大的难点在于链式调用的实现,具体来说就是then方法的实现。

//then传进两个函数

then(fulfilledCallBack, rejectedCallBack) {

//保证两者为函数

typeof fulfilledCallBack !== 'function' ? fulfilledCallBack = result =>

result:null;

typeof rejectedCallBack !== 'function' ? rejectedCallBack = reason => {

throw new Error(reason instanceof Error? reason.message:reason);

} : null

//返回新的Promise对象,后面称它为“新Promise”

return new Promise((resolve, reject) => {

//注意这个this指向目前的Promise对象,而不是新的Promise

//再强调一遍,很重要://目前的Promise(不是这里return的新Promise)的resolve和reject函数其实一个作为微任务

//因此他们不是立即执行,而是等then调用完成后执行

this.fulfillAry.push(() => {

try {

//把then里面的方法拿过来执行

//执行的目的已经达到

let x = fulfilledCallBack(this.value);

//下面执行之后的下一步,也就是记录执行的状态,决定新Promise如何表现

//如果返回值x是一个Promise对象,就执行then操作

//如果不是Promise,直接调用新Promise的resolve函数,

//新Promise的fulfilAry现在为空,在新Promise的then操作后.新Promise的resolve执

x instanceof Promise ? x.then(resolve, reject):resolve(x);

}catch(err){

reject(err)

}

});

//以下同理

this.rejectedAry.push(() => {

try {

let x = this.rejectedCallBack(this.value);

x instanceof Promise ? x.then(resolve, reject):resolve(x);

}catch(err){

reject(err)

}

})

}) ;

}

测试用例:

let p1 = new Promise((resolve, reject) => {

setTimeout(() => {

Math.random()<0.5?resolve(100):reject(-100);

}, 1000)

})

let p2 = p1.then(result => {

//执行then返回的是一个新的Promise

return result + 100;

})

let p3 = p2.then(result => {

console.log(result);

}, reason => {

console.log(reason)

})

简单画图来模拟一下链式调用的内部流程:有了then方法,catch自然而然调用即可:

catch(rejectedCallBack) {

return this.then(null, rejectedCallBack);

}

3Promise.all()

接下来实现Promise.all()

//为类的静态方法,而不是在原型上

static all(promiseAry = []) {

let index = 0,

result = [];

return new Promise((resolve, reject) => {

for(let i = 0; i < promiseAry.length; i++){

promiseAry[i].then(val => {

index++;

result[i] = val;

if( index === promiseAry.length){

resolve(result)

}

}, reject);

}

})

}

4Promise.race()

接下来是race方法

static race(promises) {

return new Promise((resolve, reject) => {

if (promises.length === 0) {

return;

} else {

for(let i = 0; i < promises.length; i++){promises[i].then(val => {

resolve(result);

return;

}

}, reject);

}

}

});

}

5Promise.resolve()

static resolve (value) {

if (value instanceof Promise) return value

return new Promise(resolve => resolve(value))

}

6Promise.reject()

static reject (value) {

return new Promise((resolve, reject) => reject(value))

}

完整代码

现在手写一个简陋但是功能较为完备的Promise就大功告成了。

class Promise{

constructor(excutorCallBack){

this.status = 'pending';

this.value = undefined;

this.fulfillAry = [];

this.rejectedAry = [];

//=>执行Excutor

let resolveFn = result => {

if(this.status !== 'pending') return;

let timer = setTimeout(() => {

this.status = 'fulfilled';

this.value = result;

this.fulfillAry.forEach(item => item(this.value));

}, 0);

};

let rejectFn = reason => {

if(this.status !== 'pending')return;

let timer = setTimeout(() => {

this.status = 'rejected';

this.value = reason;

this.rejectedAry.forEach(item => item(this.value))

})

};

try{excutorCallBack(resolveFn, rejectFn);

} catch(err) {

//=>有异常信息按照rejected状态处理

rejectFn(err);

}

}

then(fulfilledCallBack, rejectedCallBack) {

typeof fulfilledCallBack !== 'function' ? fulfilledCallBack = result =>

result:null;

typeof rejectedCallBack !== 'function' ? rejectedCallBack = reason => {

throw new Error(reason instanceof Error? reason.message:reason);

} : null

return new Promise((resolve, reject) => {

this.fulfillAry.push(() => {

try {

let x = fulfilledCallBack(this.value);

x instanceof Promise ? x.then(resolve, reject ):resolve(x);

}catch(err){

reject(err)

}

});

this.rejectedAry.push(() => {

try {

let x = this.rejectedCallBack(this.value);

x instanceof Promise ? x.then(resolve, reject):resolve(x);

}catch(err){

reject(err)

}

})

}) ;

}

catch(rejectedCallBack) {

return this.then(null, rejectedCallBack);

}

static all(promiseAry = []) {

let index = 0,

result = [];

return new Promise((resolve, reject) => {

for(let i = 0; i < promiseAry.length; i++){

promiseAry[i].then(val => {

index++;

result[i] = val;

if( index === promiseAry.length){

resolve(result)

}

}, reject);

}

})

}

static race(promiseAry) {

return new Promise((resolve, reject) => {

if (promiseAry.length === 0) {

return;

}

for (let i = 0; i < promiseAry.length; i++) {

promiseAry[i].then(val => {

resolve(val);

65. 事件流向

(1)冒泡:子节点一层层冒泡到根节点

(2)捕获顺序与冒泡相反

(3)addEventListener 最后个参数 true 代表捕获反之代表冒泡

(4)阻止冒泡不停止父节点捕获

66. 事件委托

参考定义:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件 好处:给重复的节点添加相同操作,减少 dom 交互,提高性能 实现思路:给父组件添加事件,通过事件冒泡,排查元素是否为指定元素,并进行系列操作

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值