2023.04.05 - 2023.04.09 更新前端面试问题总结(10道题)
获取更多面试问题可以访问
github 地址: https://github.com/pro-collection/interview-question/issues
gitee 地址: https://gitee.com/yanleweb/interview-question/issues
目录:
BFC 全称:Block Formatting Context, 名为 块级格式化上下文。
W3C官方解释为:BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。
我们来看下面这种情况:
box1
box2
按我们习惯性思维,上面这个box的margin-bottom是60px,下面这个box的margin-top也是60px,那他们垂直的间距按道理来说应该是120px才对。(可事实并非如此,我们可以来具体看一下)
这种情况下的margin边距为两者的最大值,而不是两者相加,那么我们可以使用BFC来解决这种margin塌陷的问题。
nanjiu
南玖
我们再来看这种情况,内部box使用float脱离了普通文档流,导致外层容器没办法撑起高度,使得背景颜色没有显示出来。
nanjiu
南玖
我们可以看到此时的外层容器的高度为0,导致背景颜色没有渲染出来,这种情况我们同样可以使用BFC来解决,可以直接为外层容器触发BFC,我们来看看效果:
nanjiu
南玖
在早期前端页面大多喜欢用浮动来布局,但浮动元素脱离普通文档流,会覆盖旁边内容:
nanjiu
南玖
我们可以通过触发后面这个元素形成BFC,从而来清楚浮动元素对其布局造成的影响
nanjiu
南玖
IFC全称:Inline Formatting Context,名为行级格式化上下文
形成条件非常简单,需要注意的是当IFC中有块级元素插入时,会产生两个匿名块将父元素分割开来,产生两个IFC。
当一个块要在环境中水平居中时,设置其为inline-block则会在外层产生IFC,通过text-align则可以使其水平居中。
string 1
string 2
创建一个IFC,然后设置其vertical-align:middle,其他行内元素则可以在此父元素下垂直居中。
string 1
string 2
GFC全称:Grids Formatting Contexts,名为网格格式上下文
简介: CSS3引入的一种新的布局模型——Grids网格布局,目前暂未推广使用,使用频率较低,简单了解即可。 Grid 布局与 Flex 布局有一定的相似性,都可以指定容器内部多个项目的位置。但是,它们也存在重大区别。 Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。
当为一个元素设置display值为grid或者inline-grid的时候,此元素将会获得一个独立的渲染区域。
通过在网格容器(grid container)上定义网格定义行(grid definition rows)和网格定义列(grid definition columns)属性各在网格项目(grid item)上定义网格行(grid row)和网格列(grid columns)为每一个网格项目(grid item)定义位置和空间(具体可以在MDN上查看)
这个布局使用用GFC可以轻松实现自由拼接效果,换成其他方法,一般会使用相对/绝对定位,或者flex来实现自由拼接效果,复杂程度将会提升好几个等级。
1
2
3
4
5
6
7
FFC全称:Flex Formatting Contexts,名为弹性格式上下文
简介: CSS3引入了一种新的布局模型——flex布局。 flex是flexible box的缩写,一般称之为弹性盒模型。和CSS3其他属性不一样,flexbox并不是一个属性,而是一个模块,包括多个CSS3属性。flex布局提供一种更加有效的方式来进行容器内的项目布局,以适应各种类型的显示设备和各种尺寸的屏幕,使用Flex box布局实际上就是声明创建了FFC(自适应格式上下文)
当 display 的值为 flex 或 inline-flex 时,将生成弹性容器(Flex Containers), 一个弹性容器为其内容建立了一个新的弹性格式化上下文环境(FFC)
**⚠️注意:**FFC布局中,float、clear、vertical-align属性不会生效。
Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。
这里只介绍它对于其它布局所相对来说更方便的特点,其实flex布局现在是非常普遍的,很多前端人员都喜欢用flex来写页面布局,操作方便且灵活,兼容性好。
看一个经典两栏布局:左边为侧边导航栏,右边为内容区域,用我们之前的常规布局,可能就需要使用到css的calc方法来动态计算剩余填充宽度了,但如果使用flex布局的话,只需要一个属性就能解决这个问题:
calc动态计算方法:
box1
box2
使用FFC:
box1
box2
数组可以直接根据索引取的对应的元素,所以不管取哪个位置的元素的时间复杂度都是 O(1)
得出结论:消耗时间几乎一致,差异可以忽略不计
原因:
JavaScript 没有真正意义上的数组,所有的数组其实是对象,其“索引”看起来是数字,其实会被转换成字符串,作为属性名(对象的 key)来使用。所以无论是取第 1 个还是取第 10 万个元素,都是用 key 精确查找哈希表的过程,其消耗时间大致相同。
Chrome 浏览器JS引擎 V8中,数组有两种存储模式,一种是类似C语言中的线性结构存储(索引值连续,且都是正整数的情况下),一种是采用Hash结构存储(索引值为负数,数组稀疏,间隔比较大)
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。
解法1:
function zeroMove(array) {
let len = array.length;
let j = 0;
for (let i = 0; i < len - j; i++) {
if (array[i] === 0) {
array.push(0);
array.splice(i, 1);
i--;
j++;
}
}
return array;
}
解法2:算法思路
function moveZeroToLast(arr) {
let index = 0;
for (let i = 0, length = arr.length; i < length; i++) {
if (arr[i] === 0) {
index++;
} else if (index !== 0) {
arr[i - index] = arr[i];
arr[i] = 0;
}
}
return arr;
}
add(1); // 1
add(1)(2); // 3
add(1)(2)(3); // 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6
解法1:
function currying(fn, length) {
length = length || fn.length; // 注释 1
return function(...args) { // 注释 2
return args.length >= length // 注释 3
? fn.apply(this, args) // 注释 4
: currying(fn.bind(this, ...args), length - args.length) // 注释 5
}
}
/**
注释 1:第一次调用获取函数 fn 参数的长度,后续调用获取 fn 剩余参数的长度
注释 2:currying 包裹之后返回一个新函数,接收参数为 ...args
注释 3:新函数接收的参数长度是否大于等于 fn 剩余参数需要接收的长度
注释 4:满足要求,执行 fn 函数,传入新函数的参数
注释 5:不满足要求,递归 currying 函数,新的 fn 为 bind 返回的新函数(bind 绑定了 ...args 参数,未执行),新的 length 为 fn 剩余参数的长度
*/
解法2
const currying = fn =>
judge = (...args) =>
args.length >= fn.length
? fn(...args)
: (...arg) => judge(...args, ...arg)
解法3:
function add() {
let args = [].slice.call(arguments);
let fn = function() {
let fn_args = [].slice.call(arguments)
return add.apply(null, args.concat(fn_args))
}
fn.toString = function() {
return args.reduce((a, b) => a + b)
}
return fn
}
从最终渲染的 DOM 来看,这两者都是链接,都是 标签,区别是:
是 react-router 里实现路由跳转的链接,一般配合
而 标签就是普通的超链接了,用于从当前页面跳转到 href 指向的另一个页面(非锚点情况)
源码层面
先看Link点击事件handleClick部分源码
if (_this.props.onClick) _this.props.onClick(event);
if (!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!_this.props.target && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
var history = _this.context.router.history;
var _this$props = _this.props,
replace = _this$props.replace,
to = _this$props.to;
if (replace) {
history.replace(to);
} else {
history.push(to);
}
}
Link做了3件事情:
以下数据结构中,id 代表部门编号,name 是部门名称,parentId 是父部门编号,为 0 代表一级部门,现在要求实现一个 convert 方法,把原始 list 转换成树形结构,parentId 为多少就挂载在该 id 的属性 children 数组下,结构如下:
// 原始 list 如下
let list = [
{ id: 1, name: '部门A', parentId: 0 },
{ id: 2, name: '部门B', parentId: 0 },
{ id: 3, name: '部门C', parentId: 1 },
{ id: 4, name: '部门D', parentId: 1 },
{ id: 5, name: '部门E', parentId: 2 },
{ id: 6, name: '部门F', parentId: 3 },
{ id: 7, name: '部门G', parentId: 2 },
{ id: 8, name: '部门H', parentId: 4 }
];
const result = convert(list);
// 转换后的结果如下
let result = [
{
id: 1,
name: '部门A',
parentId: 0,
children: [
{
id: 3,
name: '部门C',
parentId: 1,
children: [
{
id: 6,
name: '部门F',
parentId: 3
}, {
id: 16,
name: '部门L',
parentId: 3
}
]
},
{
id: 4,
name: '部门D',
parentId: 1,
children: [
{
id: 8,
name: '部门H',
parentId: 4
}
]
}
]
},
···
]
;
解法1:
大型找爹现场
时间复杂度O(n^2)
function convert(arr) {
return arr.filter((child) => {
child.children = arr.filter(item => item.parentId === child.id)
return child.parentId === 0
})
}
console.log(convert(list))
解法2:
先遍历出hash表O(n)
再遍历找爹O(n)
时间复杂度:O(2n)=O(n)
大型找爹现场,找到爹就把自己push到爹的房里,如果没有房间先造一个
function convert(arr) {
const res = []
const map = arr.reduce((obj, item) => (obj[item.id] = item, obj), {})
for (let item of arr) {
if (item.parentId === 0) {
res.push(item)
continue
}
if (map.hasOwnProperty(item.parentId)) {
const parent = map[item.parentId]
parent.children = parent.children || []
parent.children.push(item)
}
}
return res
}
如果在条件语句中使用hooks,React会抛出 error。
这与React Hooks的底层设计的数据结构相关,先抛出结论:react用链表来严格保证hooks的顺序。
一个典型的useState使用场景:
const [name,setName] = useState('leo');
......
setName('Lily');
那么hooks在这两条语句分别作了什么?
上图是 useState 首次渲染的路径,其中,跟我们问题相关的是 mountState 这个过程,简而言之,这个过程初始化了一个hooks,并且将其追加到链表结尾。
// 进入 mounState 逻辑
function mountState(initialState) {
// 将新的 hook 对象追加进链表尾部
var hook = mountWorkInProgressHook();
// initialState 可以是一个回调,若是回调,则取回调执行后的值
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
// 创建当前 hook 对象的更新队列,这一步主要是为了能够依序保留 dispatch
const queue = hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
// 将 initialState 作为一个“记忆值”存下来
hook.memoizedState = hook.baseState = initialState;
// dispatch 是由上下文中一个叫 dispatchAction 的方法创建的,这里不必纠结这个方法具体做了什么
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
// 返回目标数组,dispatch 其实就是示例中常常见到的 setXXX 这个函数,想不到吧?哈哈
return [hook.memoizedState, dispatch];
}
从这段源码中我们可以看出,mounState 的主要工作是初始化 Hooks。在整段源码中,最需要关注的是 mountWorkInProgressHook 方法,它为我们道出了 Hooks 背后的数据结构组织形式。以下是 mountWorkInProgressHook 方法的源码:
function mountWorkInProgressHook() {
// 注意,单个 hook 是以对象的形式存在的
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
// 这行代码每个 React 版本不太一样,但做的都是同一件事:将 hook 作为链表的头节点处理
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// 若链表不为空,则将 hook 追加到链表尾部
workInProgressHook = workInProgressHook.next = hook;
}
// 返回当前的 hook
return workInProgressHook;
}
到这里可以看出,hook 相关的所有信息收敛在一个 hook 对象里,而 hook 对象之间以单向链表的形式相互串联。
接着,我们来看更新过程
上图中,需要注意的是updateState的过程:按顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染。
我们把 mountState 和 updateState 做的事情放在一起来看:mountState(首次渲染)构建链表并渲染;updateState 依次遍历链表并渲染。
hooks 的渲染是通过“依次遍历”来定位每个 hooks 内容的。如果前后两次读到的链表在顺序上出现差异,那么渲染的结果自然是不可控的。
这个现象有点像我们构建了一个长度确定的数组,数组中的每个坑位都对应着一块确切的信息,后续每次从数组里取值的时候,只能够通过索引(也就是位置)来定位数据。也正因为如此,在许多文章里,都会直截了当地下这样的定义:Hooks 的本质就是数组。但读完这一课时的内容你就会知道,Hooks 的本质其实是链表。
我们举个例子:
let mounted = false;
if(!mounted){
// eslint-disable-next-line
const [name,setName] = useState('leo');
const [age,setAge] = useState(18);
mounted = true;
}
const [career,setCareer] = useState('码农');
console.log('career',career);
......
setName('Lily')}>
点我点我点我
点击p后,我们期望的输出是 "码农",然而事实上(尽管会error,但是打印还是执行)打印的为 "Lily"
原因是,三个useState在初始化的时候已经构建好了一个三个节点的链表结构,依次为: name('leo') --> age(18) --> career('码农')
每个节点都已经派发了一个与之对应的update操作,因此执行setName时候,三个节点就修改为了 name('Lily') --> age(18) --> career('码农')
然后执行update渲染操作,从链表依次取出值,此时,条件语句的不再执行,第一个取值操作会从链表的第一个,也就是name对应的hooks对象进行取值:此时取到的为 name:Lily
必须按照顺序调用从根本上来说是因为 useState 这个钩子在设计层面并没有“状态命名”这个动作,也就是说你每生成一个新的状态,React 并不知道这个状态名字叫啥,所以需要通过顺序来索引到对应的状态值
循环次数不够多的时候, forEach 性能优于 for
// 循环十万次
let arrs = new Array(100000);
console.time('for');
for (let i = 0; i < arrs.length; i++) {};
console.timeEnd('for'); // for: 2.36474609375 ms
console.time('forEach');
arrs.forEach((arr) => {});
console.timeEnd('forEach'); // forEach: 0.825927734375 ms
循环次数越大, for 的性能优势越明显
// 循环 1 亿次
let arrs = new Array(100000000);
console.time('for');
for (let i = 0; i < arrs.length; i++) {};
console.timeEnd('for'); // for: 72.7099609375 ms
console.time('forEach');
arrs.forEach((arr) => {});
console.timeEnd('forEach'); // forEach: 923.77392578125 ms
对比类型 | for | forEach |
遍历 | for循环按顺序遍历 | forEach 使用 iterator 迭代器遍历 |
数据结构 | for循环是随机访问元素 | forEach 是顺序链表访问元素 |
性能上 | 对于arraylist,是顺序表,使用for循环可以顺序访问,速度较快;使用foreach会比for循环稍慢一些 | 对于linkedlist,是单链表,使用for循环每次都要从第一个元素读取next域来读取,速度非常慢;使用foreach可以直接读取当前结点,数据较快 |
for 性能优于 forEach , 主要原因如下:
可以直接看这个链接: #8
使用proxy实现数据劫持
let data = {
name: YoLinDeng,
height: '176cm'
}
const p = new Proxy(data, {
get(target, prop) {
return Reflect.get(...arguments)
},
set(target, prop, newValue) {
return Reflect.set(...arguments)
}
})
vue的双向数据绑定主要是指,数据变化更新视图变化,视图变化更新数据。
实现代码如下
Document
{{name}}
{{message}}
{{test}}
class vue extends EventTarget {
constructor(option) {
super()
this.option = option
this._data = this.option.data
this.el = document.querySelector(this.option.el)
this.compileNode(this.el)
this.observe(this._data)
}
// 实现监听器方法
observe(data) {
const context = this
// 使用proxy代理,劫持数据
this._data = new Proxy(data, {
set(target, prop, newValue) {
// 自定义事件
let event = new CustomEvent(prop, {
detail: newValue
})
// 发布自定义事件
context.dispatchEvent(event)
return Reflect.set(...arguments)
}
})
}
// 实现解析器方法,解析模板
compileNode(el) {
let child = el.childNodes
let childArr = [...child]
childArr.forEach(node => {
if (node.nodeType === 3) {
let text = node.textContent
let reg = /{{s*([^s{}]+)s*}}/g
if (reg.test(text)) {
let $1 = RegExp.$1
this._data[$1] && (node.textContent = text.replace(reg, this._data[$1]))
// 监听数据更改事件
this.addEventListener($1, e => {
node.textContent = text.replace(reg, e.detail)
})
}
} else if (node.nodeType === 1) { // 如果是元素节点
let attr = node.attributes
// 判断属性中是否含有v-model
if (attr.hasOwnProperty('v-model')) {
let keyName = attr['v-model'].nodeValue
node.value = this._data[keyName]
node.addEventListener('input', e => {
this._data[keyName] = node.value
})
}
// 递归调用解析器方法
this.compileNode(node)
}
})
}
}
Hot Module Replacement,简称HMR,无需完全刷新整个页面的同时,更新模块。HMR的好处,在日常开发工作中体会颇深:节省宝贵的开发时间、提升开发体验。
刷新我们一般分为两种:
HMR作为一个Webpack内置的功能,可以通过HotModuleReplacementPlugin或--hot开启。那么,HMR到底是怎么实现热更新的呢?下面让我们来了解一下吧!
我们根据webpack-dev-server的package.json中的bin命令,可以找到命令的入口文件bin/webpack-dev-server.js。
// node_modules/webpack-dev-server/bin/webpack-dev-server.js
// 生成webpack编译主引擎 compiler
let compiler = webpack(config);
// 启动本地服务
let server = new Server(compiler, options, log);
server.listen(options.port, options.host, (err) => {
if (err) {throw err};
});
本地服务代码:
// node_modules/webpack-dev-server/lib/Server.js
class Server {
constructor() {
this.setupApp();
this.createServer();
}
setupApp() {
// 依赖了express
this.app = new express();
}
createServer() {
this.listeningApp = http.createServer(this.app);
}
listen(port, hostname, fn) {
return this.listeningApp.listen(port, hostname, (err) => {
// 启动express服务后,启动websocket服务
this.createSocketServer();
}
}
}
这一小节代码主要做了三件事:
上述代码主要干了三件事,但是源码在启动服务前又做了很多事,接下来便看看webpack-dev-server/lib/Server.js还做了哪些事?
启动本地服务前,调用了updateCompiler(this.compiler)方法。这个方法中有 2 段关键性代码。一个是获取websocket客户端代码路径,另一个是根据配置获取webpack热更新代码路径。
// 获取websocket客户端代码
const clientEntry = `${require.resolve(
'../../client/'
)}?${domain}${sockHost}${sockPath}${sockPort}`;
// 根据配置获取热更新代码
let hotEntry;
if (options.hotOnly) {
hotEntry = require.resolve('webpack/hot/only-dev-server');
} else if (options.hot) {
hotEntry = require.resolve('webpack/hot/dev-server');
}
修改后的webpack入口配置如下:
// 修改后的entry入口
{ entry:
{ index:
[
// 上面获取的clientEntry
'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080',
// 上面获取的hotEntry
'xxx/node_modules/webpack/hot/dev-server.js',
// 开发配置的入口
'./src/index.js'
],
},
}
为什么要新增了 2 个文件?在入口默默增加了 2 个文件,那就意味会一同打包到bundle文件中去,也就是线上运行时。
(1)webpack-dev-server/client/index.js
首先这个文件用于websocket的,因为websoket是双向通信,如果不了解websocket,建议简单了解一下websocket速成。我们在第 1 步 webpack-dev-server初始化 的过程中,启动的是本地服务端的websocket。那客户端也就是我们的浏览器,浏览器还没有和服务端通信的代码呢?总不能让开发者去写吧hhhhhh。因此我们需要把websocket客户端通信代码偷偷塞到我们的代码中。客户端具体的代码后面会在合适的时机细讲哦。
(2)webpack/hot/dev-server.js
这个文件主要是用于检查更新逻辑的,这里大家知道就好,代码后面会在合适的时机(第5步)细讲。
修改好入口配置后,又调用了setupHooks方法。这个方法是用来注册监听事件的,监听每次webpack编译完成。
// node_modules/webpack-dev-server/lib/Server.js
// 绑定监听事件
setupHooks() {
const {done} = compiler.hooks;
// 监听webpack的done钩子,tapable提供的监听方法
done.tap('webpack-dev-server', (stats) => {
this._sendStats(this.sockets, this.getStats(stats));
this._stats = stats;
});
};
当监听到一次webpack编译结束,就会调用_sendStats方法通过websocket给浏览器发送通知,ok和hash事件,这样浏览器就可以拿到最新的hash值了,做检查更新逻辑。
// 通过websoket给客户端发消息
_sendStats() {
this.sockWrite(sockets, 'hash', stats.hash);
this.sockWrite(sockets, 'ok');
}
每次修改代码,就会触发编译。说明我们还需要监听本地代码的变化,主要是通过setupDevMiddleware方法实现的。
这个方法主要执行了webpack-dev-middleware库。很多人分不清webpack-dev-middleware和webpack-dev-server的区别。其实就是因为webpack-dev-server只负责启动服务和前置准备工作,所有文件相关的操作都抽离到webpack-dev-middleware库了,主要是本地文件的编译和输出以及监听,无非就是职责的划分更清晰了。
那我们来看下webpack-dev-middleware源码里做了什么事:
// node_modules/webpack-dev-middleware/index.js
compiler.watch(options.watchOptions, (err) => {
if (err) { /*错误处理*/ }
});
// 通过“memory-fs”库将打包后的文件写入内存
setFs(context, compiler);
(1)调用了compiler.watch方法,在第 1 步中也提到过,compiler的强大。这个方法主要就做了 2 件事:
为什么代码的改动保存会自动编译,重新打包?这一系列的重新检测编译就归功于compiler.watch这个方法了。监听本地文件的变化主要是通过文件的生成时间是否有变化,这里就不细讲了。
(2)执行setFs方法,这个方法主要目的就是将编译后的文件打包到内存。这就是为什么在开发的过程中,你会发现dist目录没有打包后的代码,因为都在内存中。原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销,这一切都归功于memory-fs。
我们已经可以监听到文件的变化了,当文件发生变化,就触发重新编译。同时还监听了每次编译结束的事件。当监听到一次webpack编译结束,_sendStats方法就通过websoket给浏览器发送通知,检查下是否需要热更新。下面重点讲的就是_sendStats方法中的ok和hash事件都做了什么。
那浏览器是如何接收到websocket的消息呢?回忆下第 2 步骤增加的入口文件,也就是websocket客户端代码。
'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080'
这个文件的代码会被打包到bundle.js中,运行在浏览器中。来看下这个文件的核心代码吧。
// webpack-dev-server/client/index.js
var socket = require('./socket');
var onSocketMessage = {
hash: function hash(_hash) {
// 更新currentHash值
status.currentHash = _hash;
},
ok: function ok() {
sendMessage('Ok');
// 进行更新检查等操作
reloadApp(options, status);
},
};
// 连接服务地址socketUrl,?http://localhost:8080,本地服务地址
socket(socketUrl, onSocketMessage);
function reloadApp() {
if (hot) {
log.info('[WDS] App hot update...');
// hotEmitter其实就是EventEmitter的实例
var hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
}
}
socket方法建立了websocket和服务端的连接,并注册了 2 个监听事件。
热更新检查事件是调用reloadApp方法。比较奇怪的是,这个方法又利用node.js的EventEmitter,发出webpackHotUpdate消息。这是为什么?为什么不直接进行检查更新呢?
个人理解就是为了更好的维护代码,以及职责划分的更明确。websocket仅仅用于客户端(浏览器)和服务端进行通信。而真正做事情的活还是交回给了webpack。
那webpack怎么做的呢?再来回忆下第 2 步。入口文件还有一个文件没有讲到,就是:
'xxx/node_modules/webpack/hot/dev-server.js'
这个文件的代码同样会被打包到bundle.js中,运行在浏览器中。这个文件做了什么就显而易见了吧!先瞄一眼代码:
// node_modules/webpack/hot/dev-server.js
var check = function check() {
module.hot.check(true)
.then(function(updatedModules) {
// 容错,直接刷新页面
if (!updatedModules) {
window.location.reload();
return;
}
// 热更新结束,打印信息
if (upToDate()) {
log("info", "[HMR] App is up to date.");
}
})
.catch(function(err) {
window.location.reload();
});
};
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function(currentHash) {
lastHash = currentHash;
check();
});
这里webpack监听到了webpackHotUpdate事件,并获取最新了最新的hash值,然后终于进行检查更新了。检查更新呢调用的是module.hot.check方法。那么问题又来了,module.hot.check又是哪里冒出来了的!答案是HotModuleReplacementPlugin搞得鬼。这里留个疑问,继续往下看。
前面好像一直是webpack-dev-server做的事,那HotModuleReplacementPlugin在热更新过程中又做了什么伟大的事业呢?
首先你可以对比下,配置热更新和不配置时bundle.js的区别。内存中看不到?直接执行webpack命令就可以看到生成的bundle.js文件啦。不要用webpack-dev-server启动就好了。
(1)没有配置的。
(2)配置了HotModuleReplacementPlugin或--hot的。
哦~ 我们发现moudle新增了一个属性为hot,再看hotCreateModule方法。 这不就找到module.hot.check是哪里冒出来的。
经过对比打包后的文件,__webpack_require__中的moudle以及代码行数的不同。我们都可以发现HotModuleReplacementPlugin原来也是默默的塞了很多代码到bundle.js中呀。这和第 2 步骤很是相似哦!为什么,因为检查更新是在浏览器中操作呀。这些代码必须在运行时的环境。
你也可以直接看浏览器Sources下的代码,会发现webpack和plugin偷偷加的代码都在哦。在这里调试也很方便。
HotModuleReplacementPlugin如何做到的?这里我就不讲了,因为这需要你对tapable以及plugin机制有一定了解,可以看下我写的文章Webpack插件机制之Tapable-源码解析。当然你也可以选择跳过,只关心热更新机制即可,毕竟信息量太大。
通过第 6 步,我们就可以知道moudle.hot.check方法是如何来的啦。那都做了什么?之后的源码都是HotModuleReplacementPlugin塞入到bundle.js中的哦,我就不写文件路径了。
hotAvailableFilesMap = update.c; // 需要更新的文件
hotUpdateNewHash = update.h; // 更新下次热更新hash值
hotSetStatus("prepare"); // 进入热更新准备状态
function hotDownloadUpdateChunk(chunkId) {
var script = document.createElement("script");
script.charset = "utf-8";
script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
if (null) script.crossOrigin = null;
document.head.appendChild(script);
}
这个函数体为什么要单独拿出来,因为这里要解释下为什么使用JSONP获取最新代码?主要是因为JSONP获取的代码可以直接执行。为什么要直接执行?我们来回忆下/hash.hot-update.js的代码格式是怎么样的。
可以发现,新编译后的代码是在一个webpackHotUpdate函数体内部的。也就是要立即执行webpackHotUpdate这个方法。
再看下webpackHotUpdate这个方法。
window["webpackHotUpdate"] = function (chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules);
} ;
function hotAddUpdateChunk(chunkId, moreModules) {
// 更新的模块moreModules赋值给全局全量hotUpdate
for (var moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
hotUpdate[moduleId] = moreModules[moduleId];
}
}
// 调用hotApply进行模块的替换
hotUpdateDownloaded();
}
热更新的核心逻辑就在hotApply方法了。 hotApply代码有将近 400 行,还是挑重点讲了,看哭
通过hotUpdate可以找到旧模块
var queue = outdatedModules.slice();
while (queue.length > 0) {
moduleId = queue.pop();
// 从缓存中删除过期的模块
module = installedModules[moduleId];
// 删除过期的依赖
delete outdatedDependencies[moduleId];
// 存储了被删掉的模块id,便于更新代码
outdatedSelfAcceptedModules.push({
module: moduleId
});
}
appliedUpdate[moduleId] = hotUpdate[moduleId];
for (moduleId in appliedUpdate) {
if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
var item = outdatedSelfAcceptedModules[i];
moduleId = item.module;
try {
// 执行最新的代码
__webpack_require__(moduleId);
} catch (err) {
// ...容错处理
}
}
hotApply的确比较复杂,知道大概流程就好了,这一小节,要求你对webpack打包后的文件如何执行的有一些了解,大家可以自去看下。
还是以阅读源码的形式画的图,①-④的小标记,是文件发生变化的一个流程。
页面更新:2024-06-08
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号