框架相关
原生JS虽能实现绝大部分功能,但要么就是过于繁琐,要么就是存在缺陷,故绝大多数开发者都会首选框架开发方案。现阶段较热门是React、Vue两大框架,两者工作原理上存在共通点,也存在一些不同点,对于校招来说,不需要两个框架都学得特别熟,一般面试官会针对你简历中写的框架进行提问。
在框架方面,生命周期、钩子函数、虚拟DOM这些基本知识是必须要掌握的,在学习的过程可以结合框架的官方文档
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
Vue框架
知识要点:
1. vue-cli工程
2. vue核心知识点
3. vue-router
4. vuex
5. http请求
6. UI样式
7. 常用功能
8. MVVM设计模式
React框架
知识要点:
1. 基本知识
2. React 组件
3. React Redux
4. React 路由
qiankun
注意:子应用沙箱中的
proxy
对象(第 62 行
)可以简单理解为子应用的window
全局对象(代码如下),子应用对全局属性的操作就是对该proxy
对象属性的操作,带着这份理解继续往下看吧。
// 子应用脚本文件的执行过程:
eval(
// 这里将 proxy 作为 window 参数传入
// 子应用的全局对象就是该子应用沙箱的 proxy 对象
(function(window) {
/* 子应用脚本文件内容 */
})(proxy)
);
在 第 65~72 行中
,当调用 set
向子应用 proxy/window
对象设置属性时,所有的属性设置和更新都会先记录在 addedPropsMapInSandbox
或 modifiedPropsOriginalValueMapInSandbox
中,然后统一记录到currentUpdatedPropsValueMap
中。
在 第 73 行
中修改全局 window
的属性,完成值的设置。
当调用 get
从子应用 proxy/window
对象取值时,会直接从 window
对象中取值。对于非构造函数的取值将会对 this
指针绑定到 window
对象后,再返回函数。
LegacySandbox
的沙箱隔离是通过激活沙箱时还原子应用状态,卸载时还原主应用状态(子应用挂载前的全局状态)实现的,具体实现如下(见下图)。
qiankun
从上图可以看出:
-
第 37 行
:在激活沙箱时,沙箱会通过currentUpdatedPropsValueMap
查询到子应用的独立状态池(沙箱可能会激活多次,这里是沙箱曾经激活期间被修改的全局变量),然后还原子应用状态。 -
第 44~45 行
:在关闭沙箱时,通过addedPropsMapInSandbox
删除在沙箱运行期间新增的全局变量,通过modifiedPropsOriginalValueMapInSandbox
还原沙箱运行期间被修改的全局变量,从而还原到子应用挂载前的状态。
从上面的分析可以得知,LegacySandbox
的沙箱隔离机制利用快照模式实现,我们画一张图来帮助理解(见下图)
qiankun
多实例沙箱 - ProxySandbox
ProxySandbox
是一种新的沙箱模式,目前用于多实例模式的状态隔离。在稳定后以后可能会成为 单实例沙箱
,我们来看看 ProxySandbox
沙箱是怎么进行状态隔离的(见下图)
qiankun
我们来分析一下 ProxySandbox
类的几个属性:
| 字段 | 解释 |
| — | — |
| updateValueMap
| 记录沙箱中更新的值,也就是每个子应用中独立的状态池 |
| name
| 沙箱名称 |
| proxy
| 代理对象,可以理解为子应用的 global/window
对象 |
| sandboxRunning
| 当前沙箱是否在运行中 |
| active
| 激活沙箱,在子应用挂载时启动 |
| inactive
| 关闭沙箱,在子应用卸载时启动 |
| constructor
| 构造函数,创建沙箱环境 |
我们现在从 window.Proxy
的 set
和 get
属性来详细讲解 ProxySandbox
是如何实现沙箱运行环境的。(见下图)
qiankun
注意:子应用沙箱中的
proxy
对象可以简单理解为子应用的window
全局对象(代码如下),子应用对全局属性的操作就是对该proxy
对象属性的操作,带着这份理解继续往下看吧。
// 子应用脚本文件的执行过程:
eval(
// 这里将 proxy 作为 window 参数传入
// 子应用的全局对象就是该子应用沙箱的 proxy 对象
(function(window) {
/* 子应用脚本文件内容 */
})(proxy)
);
当调用 set
向子应用 proxy/window
对象设置属性时,所有的属性设置和更新都会命中 updateValueMap
,存储在 updateValueMap
集合中(第 38 行
),从而避免对 window
对象产生影响(旧版本则是通过 diff
算法还原 window
对象状态快照,子应用之间的状态是隔离的,而父子应用之间 window
对象会有污染)。
当调用 get
从子应用 proxy/window
对象取值时,会优先从子应用的沙箱状态池 updateValueMap
中取值,如果没有命中才从主应用的 window
对象中取值(第 49 行
)。对于非构造函数的取值将会对 this
指针绑定到 window
对象后,再返回函数。
如此一来,ProxySandbox
沙箱应用之间的隔离就完成了,所有子应用对 proxy/window
对象值的存取都受到了控制。设置值只会作用在沙箱内部的 updateValueMap
集合上,取值也是优先取子应用独立状态池(updateValueMap
)中的值,没有找到的话,再从 proxy/window
对象中取值。
相比较而言,ProxySandbox
是最完备的沙箱模式,完全隔离了对 window
对象的操作,也解决了快照模式中子应用运行期间仍然会对 window
造成污染的问题。
我们对 ProxySandbox
沙箱画一张图来加深理解(见下图)
qiankun
SnapshotSandbox
在不支持 window.Proxy
属性时,将会使用 SnapshotSandbox
沙箱,我们来看看其内部实现(见下图)
qiankun
我们来分析一下 SnapshotSandbox
类的几个属性:
| 字段 | 解释 |
| — | — |
| name
| 沙箱名称 |
| proxy
| 代理对象,此处为 window
对象 |
| sandboxRunning
| 当前沙箱是否激活 |
| windowSnapshot
| window
状态快照 |
| modifyPropsMap
| 沙箱运行期间被修改过的 window
属性 |
| constructor
| 构造函数,激活沙箱 |
| active
| 激活沙箱,在子应用挂载时启动 |
| inactive
| 关闭沙箱,在子应用卸载时启动 |
SnapshotSandbox
的沙箱环境主要是通过激活时记录 window
状态快照,在关闭时通过快照还原 window
对象来实现的。(见下图)
qiankun
我们先看 active
函数,在沙箱激活时,会先给当前 window
对象打一个快照,记录沙箱激活前的状态(第 38~40 行
)。打完快照后,函数内部将 window
状态通过 modifyPropsMap
记录还原到上次的沙箱运行环境,也就是还原沙箱激活期间(历史记录)修改过的 window
属性。
在沙箱关闭时,调用 inactive
函数,在沙箱关闭前通过遍历比较每一个属性,将被改变的 window
对象属性值(第 54 行
)记录在 modifyPropsMap
集合中。在记录了 modifyPropsMap
后,将 window
对象通过快照 windowSnapshot
还原到被沙箱激活前的状态(第 55 行
),相当于是将子应用运行期间对 window
造成的污染全部清除。
SnapshotSandbox
沙箱就是利用快照实现了对 window
对象状态隔离的管理。相比较 ProxySandbox
而言,在子应用激活期间,SnapshotSandbox
将会对 window
对象造成污染,属于一个对不支持 Proxy
属性的浏览器的向下兼容方案。
我们对 SnapshotSandbox
沙箱画一张图来加深理解(见下图)
qiankun
挂载沙箱 - mountSandbox
qiankun
我们继续回到这张图,genSandbox
函数不仅返回了一个 sandbox
沙箱,还返回了一个 mount
和 unmount
方法,分别在子应用挂载时和卸载时的时候调用。
我们先看看 mount
函数内部(见下图)
qiankun
首先,在 mount
内部先激活了子应用沙箱(第 26 行
),在沙箱启动后开始劫持各类全局监听(第 27 行
),我们这里重点看看 patchAtMounting
内部是怎么实现的。(见下图)
qiankun
patchAtMounting
内部调用了下面四个函数:
-
patchTimer(计时器劫持)
-
patchWindowListener(window 事件监听劫持)
-
patchHistoryListener(window.history 事件监听劫持)
-
patchDynamicAppend(动态添加 Head 元素事件劫持)
上面四个函数实现了对 window
指定对象的统一劫持,我们可以挑一些解析看看其内部实现。
计时器劫持 - patchTimer
我们先来看看 patchTimer
对计时器的劫持(见下图)
qiankun
从上图可以看出,patchTimer
内部将 setInterval
进行重载,将每个启用的定时器的 intervalId
都收集起来(第 23~24 行
),以便在子应用卸载时调用 free
函数将计时器全部清除(见下图)。
qiankun
我们来看看在子应用加载时的 setInterval
函数验证即可(见下图)
qiankun
从上图可以看出,在进入子应用时,setInterval
已经被替换成了劫持后的函数,防止全局计时器泄露污染。
动态添加样式表和脚本文件劫持 - patchDynamicAppend
patchWindowListener
和 patchHistoryListener
的实现都与 patchTimer
实现类似,这里就不作复述了。
我们需要重点对 patchDynamicAppend
函数进行解析,这个函数的作用是劫持对 head
元素的操作(见下图)
qiankun
从上图可以看出,patchDynamicAppend
主要是对动态添加的 style
样式表和 script
标签做了处理。
我们先看看对 style
样式表的处理(见下图)
qiankun
从上图可以看出,主要的处理逻辑在 第 68~74 行
,如果当前子应用处于激活状态(判断子应用的激活状态主要是因为:当主应用切换路由时可能会自动添加动态样式表,此时需要避免主应用的样式表被添加到子应用
head节点中导致出错
),那么动态 style
样式表就会被添加到子应用容器内(见下图),在子应用卸载时样式表也可以和子应用一起被卸载,从而避免样式污染。同时,动态样式表也会存储在 dynamicStyleSheetElements
数组中,在后面还会提到其用处。
qiankun
我们再来看看对 script
脚本文件的处理(见下图)
qiankun
对动态 script
脚本文件的处理较为复杂一些,我们也来解析一波:
在 第 83~101 行
处对外部引入的 script
脚本文件使用 fetch
获取,然后使用 execScripts
指定 proxy
对象(作为 window
对象)后执行脚本文件内容,同时也触发了 load
和 error
两个事件。
在 第 103~106 行
处将注释后的脚本文件内容以注释的形式添加到子应用容器内。
在 第 109~113 行
是对内嵌脚本文件的执行过程,就不作复述了。
我们可以看出,对动态添加的脚本进行劫持的主要目的就是为了将动态脚本运行时的 window
对象替换成 proxy
代理对象,使子应用动态添加的脚本文件的运行上下文也替换成子应用自身。
HTMLHeadElement.prototype.removeChild
的逻辑就是多加了个子应用容器判断,其他无异,就不展开说了。
最后我们来看看 free
函数(见下图)
qiankun
这个 free
函数与其他的 patches(劫持函数)
实现不太一样,这里缓存了一份 cssRules
,在重新挂载的时候会执行 rebuild
函数将其还原。这是因为样式元素 DOM
从文档中删除后,浏览器会自动清除样式元素表。如果不这么做的话,在重新挂载时会出现存在 style
标签,但是没有渲染样式的问题。
卸载沙箱 - unmountSandbox
我们再回到 mount
函数本身(见下图)
qiankun
从上图可以看出,在 patchAtMounting
函数中劫持了各类全局监听,并返回了解除劫持的 free
函数。在卸载应用时调用 free
函数解除这些全局监听的劫持行为(见下图)
qiankun
从上图可以看到 sideEffectsRebuilders
在 free
后被返回,在 mount
的时候又将被调用 rebuild
重建动态样式表。这块环环相扣,是稍微有点绕,没太看明白的同学可以翻上去再看一遍。
到这里,qiankun
的最核心部分-沙箱机制,我们就已经解析完毕了,接下来我们继续剖析别的部分。
在这里我们画一张图,对沙箱的创建过程进行一个总梳理(见下图)
qiankun
注册内部生命周期函数
在创建好了沙箱环境后,在 第 100~106 行
注册了一些内部生命周期函数(见下图)
qiankun
在上图中,第 106 行
的 mergeWith
方法的作用是将内置的生命周期函数与传入的 lifeCycles
生命周期函数。
这里的
lifeCycles
生命周期函数指的是全子应用共享的生命周期函数,可用于执行多个子应用间相同的逻辑操作,例如加载效果
之类的。(见下图)
qiankun
除了外部传入的生命周期函数外,我们还需要关注 qiankun
内置的生命周期函数做了些什么(见下图)
qiankun
我们对上图的代码进行逐一解析:
-
第 13~15 行
:在加载子应用前beforeLoad
(只会执行一次)时注入一个环境变量,指示了子应用的public
路径。 -
第 17~19 行
:在挂载子应用前beforeMount
(可能会多次执行)时可能也会注入该环境变量。 -
第 23~30 行
:在卸载子应用前beforeUnmount
时将环境变量还原到原始状态。
通过上面的分析我们可以得出一个结论,我们可以在子应用中获取该环境变量,将其设置为 __webpack_public_path__
的值,从而使子应用在主应用中运行时,可以匹配正确的资源路径。(见下图)
qiankun
触发 beforeLoad
生命周期钩子函数
在注册完了生命周期函数后,立即触发了 beforeLoad
生命周期钩子函数(见下图)
qiankun
从上图可以看出,在 第 108 行
中,触发了 beforeLoad
生命周期钩子函数。
随后,在 第 110 行
执行了 import-html-entry
的 execScripts
方法。指定了脚本文件的运行沙箱(jsSandbox
),执行完子应用的脚本文件后,返回了一个对象,对象包含了子应用的生命周期钩子函数(见下图)。
qiankun
在 第 112~121 行
对子应用的生命周期钩子函数做了个检测,如果在子应用的导出对象中没有发现生命周期钩子函数,会在沙箱对象中继续查找生命周期钩子函数。如果最后没有找到生命周期钩子函数则会抛出一个错误,所以我们的子应用一定要有 bootstrap, mount, unmount
这三个生命周期钩子函数才能被 qiankun
正确嵌入到主应用中。
这里我们画一张图,对子应用挂载前的初始化过程做一个总梳理(见下图)
qiankun
进入到 mount
挂载流程
在一些初始化配置(如 子应用资源、运行沙箱环境、生命周期钩子函数等等
)准备就绪后,qiankun
内部将其组装在一起,返回了三个函数作为 single-spa
内部的生命周期函数(见下图)
qiankun
single-spa
内部的逻辑我们后面再展开说,这里我们可以简单理解为 single-spa
内部的三个生命周期钩子函数:
-
bootstrap
:子应用初始化时调用,只会调用一次; -
mount
:子应用挂载时调用,可能会调用多次; -
unmount
:子应用卸载时调用,可能会调用多次;
我们可以看出,在 bootstrap
阶段调用了子应用暴露的 bootstrap
生命周期函数。
我们这里对 mount
阶段进行展开,看看在子应用 mount
阶段执行了哪些函数(见下图)
qiankun
我们进行逐行解析:
-
第 127~133 行
:对单实例模式进行检测。在单实例模式下,新的子应用挂载行为会在旧的子应用卸载之后才开始。(由于这里是串行顺序执行,所以如果某一处发生阻塞的话,会阻塞所有后续的函数执行) -
第 134 行
:执行注册子应用时传入的render
函数,将HTML Template
和loading
作为入参。这里一般是在发生了一次unmount
后,再次进行mount
挂载行为时将HTML
挂载在指定容器中(见下图)
由于初始化的时候已经调用过一次
render
,所以在首次调用mount
时可能已经执行过一次render
方法。
在下面的代码中也有对重复挂载的情况进行判断的语句 -
if (frame.querySelector("div") === null
,防止重复挂载子应用。
qiankun
-
第 135 行
:触发了beforeMount
全局生命周期钩子函数; -
第 136 行
:挂载沙箱,这一步中激活了对应的子应用沙箱,劫持了部分全局监听(如setInterval
)。此时开始子应用的代码将在沙箱中运行。(反推可知,在beforeMount
前的部分全局操作将会对主应用造成污染,如setInterval
) -
第 137 行
:触发子应用的mount
生命周期钩子函数,在这一步通常是执行对应的子应用的挂载操作(如ReactDOM.render、Vue.$mount
。(见下图)
qiankun
-
第 138 行
:再次调用render
函数,此时loading
参数为false
,代表子应用已经加载完成。 -
第 139 行
:触发了afterMount
全局生命周期钩子函数; -
第 140~144 行
:在单实例模式下设置prevAppUnmountedDeferred
的值,这个值是一个promise
,在当前子应用卸载时才会被resolve
,在该子应用运行期间会阻塞其他子应用的挂载动作(第 134 行
);
我们在上面很详细的剖析了整个子应用的 mount
挂载流程,如果你还没有搞懂的话,没关系,我们再画一个流程图来帮助理解。(见下图)
qiankun
进入到 unmount
卸载流程
我们刚才梳理了子应用的 mount
挂载流程,我们现在就进入到子应用的 unmount
卸载流程。在子应用激活阶段, activeRule
未命中时将会触发 unmount
卸载行为,具体的行为如下(见下图)
qiankun
从上图我们可以看出,unmount
卸载流程要比 mount
简单很多,我们直接来梳理一下:
-
第 148 行
:触发了beforeUnmount
全局生命周期钩子函数; -
第 149 行
:这里与mount
流程的顺序稍微有点不同,这里先执行了子应用的unmount
生命周期钩子函数,保证子应用仍然是运行在沙箱内,避免造成状态污染。在这里一般是对子应用的一些状态进行清理和卸载操作。(如下图,销毁了刚才创建的vue
实例)
qiankun
最后
小编的一位同事在校期间连续三年参加ACM-ICPC竞赛。从参赛开始,原计划每天刷一道算法题,实际上每天有时候不止一题,一年最终完成了 600+:
凭借三年刷题经验,他在校招中很快拿到了各大公司的offer。
入职前,他把他的刷题经验总结成1121页PDF书籍,作为礼物赠送给他的学弟学妹,希望同学们都能在最短时间内掌握校招常见的算法及解题思路。
整本书,我仔细看了一遍,作者非常细心地将常见核心算法题和汇总题拆分为4个章节。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
而对于有时间的同学,作者还给出了他结合众多数据结构算法书籍,挑选出的一千多道题的解题思路和方法,以供有需要的同学慢慢研究。
进行清理和卸载操作。(如下图,销毁了刚才创建的 vue
实例)
qiankun
最后
小编的一位同事在校期间连续三年参加ACM-ICPC竞赛。从参赛开始,原计划每天刷一道算法题,实际上每天有时候不止一题,一年最终完成了 600+:
凭借三年刷题经验,他在校招中很快拿到了各大公司的offer。
入职前,他把他的刷题经验总结成1121页PDF书籍,作为礼物赠送给他的学弟学妹,希望同学们都能在最短时间内掌握校招常见的算法及解题思路。
整本书,我仔细看了一遍,作者非常细心地将常见核心算法题和汇总题拆分为4个章节。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
而对于有时间的同学,作者还给出了他结合众多数据结构算法书籍,挑选出的一千多道题的解题思路和方法,以供有需要的同学慢慢研究。