本文接上篇博客面试篇(1)
问题来源: 阿里盒马前端五面问题总结
eventloop
宏任务(Macrotasks):js同步执行的代码块,setTimeout、setInterval、XMLHttprequest、setImmediate、I/O、UI rendering等。
微任务(Microtasks):promise、process.nextTick(node环境)、Object.observe, MutationObserver等。
微任务比宏任务要牛逼一点 浏览器执行的顺序: (1)执行主代码块,这个主代码块也是宏任务 (2)若遇到Promise,把then之后的内容放进微任务队列 (3)遇到setTimeout,把他放到宏任务里面 (4)一次宏任务执行完成,检查微任务队列有无任务 (5)有的话执行所有微任务 (6)执行完毕后,开始下一次宏任务。
举例:去窗口办理业务
正常 task
- 同志你好,这是我的资料
- 好的,我现在处理。处理完了,请下一位
Promise
- 同志你好,这是我资料
- 你这资料这没填对,你这样,你填完后重新排到队尾好吧,咱们别阻碍别人的业务
- 好的好的,谢谢
setTimeout
- 同志你好,这是我资料
- 你的证件带了吗?
- 哎!我忘家了,我现在回去拿!
- 哎 …,你回去拿哈,回来了重新叫号重新排队
- 好的好的,不好意思同志
所以 Promise 的优先级会高于 setTimeout。micro-tasks 列队结束后会(通常)会触发一次 update rendering。整个流程差不多是:
- 有没有 macrotask 任务?有就来一个
- 有没有 microtask 任务?有就把这个列队的任务全弄完
- 弄完了,需要更新视图(update rendering)吗?需要就更新
上代码!
上面的执行结果是2,1。
从规范上来讲,setTimeout有一个4ms的最短时间,也就是说不管你设定多少,反正最少都要间隔4ms才运行里面的回调。而Promise的异步没有这个问题。
从具体实现上来说,这两个的异步队列不一样,Promise所在的那个异步队列优先级要高一些。具体讲解看第二个例子:
执行结果1,2,3,5,4
为什么执行这样的结果?
1、创建Promise实例是同步执行的。所以先输出1,2,3,这三行代码都是同步执行。
2、promise.then和setTimeout都是异步执行,会先执行谁呢?
setTimeout异步会放到异步队列中等待执行。
promise.then异步会放到microtask queue中。microtask队列中的内容经常是为了需要直接在当前脚本执行完后立即发生的事,所以当同步脚本执行完之后,就调用microtask队列中的内容,然后把异步队列中的setTimeout放入执行栈中执行,所以最终结果是先执行promise.then异步,然后再执行setTimeout异步。
这是由于:
Promise 的回调函数属于异步任务,会在同步任务之后执行。
但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。
注意:目前microtask队列中常用的就是promise.then。
1 | setTimeout(() => { |
执行结果3,4,5,6,7
ES5和ES6对象继承有几种方式,都是如何实现的呢?
1.构造继承(当然构造继承又有三种实现方式,如下)
借用构造函数(constructor stealing)的技术(有时候也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用 apply()和 call()方法也可以在(将来)新创建的对象上执行构造函数,如下所示
- 冒充继承
1 | function Person(name,age){ |
- 绑定this方式实现
1 | function Person(name,age){ |
- call的方式实现
1 | function Person(name,age){ |
- apply方式实现
1 | function Person(name,age){ |
2.原型继承
1 | function Person(name,age){ |
3.组合继承(组合构造和原型方式实现继承)
组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。下面来看一个例子
1 | function SuperType(name){ |
4.寄生继承
寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,并且同样也是由克罗克福德推而广之的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。以下代码示范了寄生式继承模式。
在这个例子中,createAnother()函数接收了一个参数,也就是将要作为新对象基础的对象。然后,把这个对象(original)传递给 object()函数,将返回的结果赋值给 clone。再为 clone 对象添加一个新方法 sayHi(),最后返回 clone 对象。可以像下面这样来使用 createAnother()函数:
1 | function createAnother(original){ |
5.寄生组合继承
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示
1 | function inheritPrototype(subType, superType){ |
6.es6继承
代码量少,易懂
1 | //class 相当于es5中构造函数 |
ES5继承和ES6继承的区别
es5继承首先是在子类中创建自己的this指向,最后将方法添加到this中
Child.prototype=new Parent() || Parent.apply(this) || Parent.call(this)
es6继承是使用关键字先创建父类的实例对象this,最后在子类class中修改this
Element-ui按需引入
可以看我之前写的博客
webpack优化
缩小文件搜索范围
Webpack从Entry出发递归解析导入语句寻找相应的依赖,在项目庞大的时候,文件量大增,递归解析速度明显的下降。
- 减少被loader匹配到的文件数,通过include缩小搜索范围。
1 | include:path.resolve(_dirname,'src') //只对src目录下的文件进行处理 |
- reolve.modules 指定第三方模块的目录,默认情况下,webpack会先从./node_modules,如果未找到则在从../node_modules,依次沿上级目录寻找。当第三方模块固定在./node_modules时就不需要递归寻找。
1 | resolve:{ |
- resolve.alias 对于某些庞大的第三方库,替换其导入路径,直接使用打包完成最小的min文件,例如React
1 | resolve:{ |
使用该方法的缺点是会影响Tree-Shaking,一般对于整体性较强的库来使用。 * resolve.extensions配置 指定默认的匹配的后缀,减少不必要的尝试
1 | resolve:{ |
- module.noParse配置 让webpack忽略没有采用模块化标准的文件,不用去递归解析这一部分,如JQuery
1 | noParse:[/react\.min\.js%/], //忽略react.min.js |
DllPlugin 动态链接库
动态链接库的思想
- 将网页依赖的基础模块抽离,打包到一个个单独的动态链接中。(一个链接可包括多个模块)
- 当导入的模块存在于动态链接库中时,模块不用再次被打包,直接从动态链接库中获取
- 页面依赖的动态链接库都需要被加载
原理
可以把DLL看做一个代码仓库,这个代码仓库里面拷贝了程序所需要的库函数,并且把这些函数存储在一个独立的文件里。项目编译时的时候并不会把其加入编译之后,而是当程序运行时需要调用到里面的函数时,动态的将函数地址传给调用程序
构建速度的对性能提升很大,因为动态链接库只用编译一次,之后的构建都不会再编译动态链接库中的模块了。
使用DllPlugin需要两个webpack的内部插件
- DllPlugin插件
用于打包出一个个单独的动态链接库文件
- DllReferencePlugin插件
引入Dllplugin打包好的动态链接库文件
步骤 * 新建一个webpack配置文件用于打包输出Dll文件
1 | module.export = { |
打包过后的会生成两个文件文件
react_manifest.json
1 | { |
react.dll.js
其中react_manifest.json描述了对应的dll.js文件包含了哪些模块,每个模块的路径,id。当进行编译的时候,main.js入口文件遇到dll.js中的模块时,能够通过dll.js文件暴露的全局变量_dll_react
直接获取。
- 使用动态链接库文件
1 | module.exports = { |
web-dev-server的优化
自动刷新的原理
- 向网页中注入代理客户端代码,代理客户端和webpack通过Websocket通信控制刷新整个网页
- 将要开发的网页放进iframe中,通过刷新iframe来刷新整个网页
代理客户端刷新
当项目输出多个chunk的时候,devserver并不知道网页依赖于哪个chunk,于是简单的粗暴向所有的chunk都注入代理客户端,导致问题就是当输出的chunk变多的时候,编译速度明显的下降。有两种方式可以优化其构建速度 - 关闭不是很好用的inline模式,--inline false
,这样devser会采用第二种方式刷新网页方式,将网页放入iframe中,通过访问localhost.cn:8080/webpack-dev-server就能够访问到原来的网页。 - 手动向网页注入代理客户端脚本,向index.html插入<script src="localhost.cn:8080/webpack-dev-server"></script>
模块热替换
模块热替换与自动刷新原理相似,都是在网页中注入一个代理客户端来连接webpack和网页。
原理
模块热替换会对源码进行相应的处理 假设现在有两个js文件,一个main.js,一个appcomponent.js,main.js是appcomponent.js的父组件。当开启hmr的时候,hotmodule会去改变main.js的源码如下
1 | //加入了module.hot,accepet第一个参数是代表要接收哪些子模块的替换,第二个参数是子模块发生变化的回调函数 |
当修改了appcomponent.js的时候,appcomponent的模块更新就会一层一层的往上传,直到某层文件接收当前文件的变化,例如上面的main.js接收appcomponent.js的更新之后,就会调用回调函数,将APPComponent重新替换掉,来实现不刷新使网页变化。但是如果修改的是最外层的main.js就会刷新整个网页。当模块发生变化后,事件一层一层的上传,但是到最后也没有相应的文件来接收变化,就会刷新整个网页。
作者:Mondo
链接:https://zhuanlan.zhihu.com/p/88602013
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
happypack
多线程执行
webpack
执行预处理文件时单线程的,我们可以使用happypack来多线程处理文件。
- 安装
1 | npm i happypack -D |
- 使用
修改webpack.base.js
文件
1 | const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); |
富文本编辑器内部要显示脚本?
转义?禁止一些tag和属性值?
(具体不太了解)
async和await的es5实现
(先空着)
- 本文作者: Raphael_Li
- 本文链接: https://lifei-2019.github.io/interview5/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!