2023.04.09 更新前端口试题目总结(10道题)_20230
2023.04.09 更新前端面试问题(10道题)
2023.04.05 - 2023.04.09 更新前端面试问题(10道题)
获取更多面试问题可以访问
github 地址: http://github./pro-collection/intervie-question/issues
gitee 地址: http://gitee./yanleeb/intervie-question/issues
目录
- 中级开发者相关问题【共计 6 道题】
- 260.介绍下 BFC、IFC、GFC 和 FFC?【CSS】【出题公司: 百度】
- 262.数组里面有10万个数据,取第一个元素和第10万个元素的时间相差多少?【JavaScript】
- 263.算法题之「移动零」【JavaScript】
- 264.请实现一个 add 函数,满足以下功能【JavaScript】
- 265.react-router 里的 标签和 标签有什么区别【JavaScript】
- 266.实现 convert 方法,把原始 list 转换成树形结构,要求尽可能降低时间复杂度【JavaScript】
- 高级开发者相关问题【共计 3 道题】
- 257.[React] 为什么不能在循环、条件或嵌套函数中调用 Hooks?【eb框架】
- 259.为什么普通 for 循环的性能远远高于 forEach 的性能?【JavaScript】
- 261.[Vue] 使用Proxy实现简易的vue双向数据绑定【eb框架】【出题公司: 腾讯】
- 资深开发者相关问题【共计 1 道题】
- 267.[Webpack] ebpack热更新原理是什么?【工程化】
中级开发者相关问题【共计 6 道题】
260.介绍下 BFC、IFC、GFC 和 FFC?【CSS】【出题公司: 百度】
BFC(Block Formatting Contexts)块级格式化上下文
什么是BFC?
BFC 全称Block Formatting Context, 名为 块级格式化上下文。
W3C官方解释为BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。
如何触发BFC?
- 根元素或其它包含它的元素
- 浮动 float: left/right/inherit
- 绝对定位元素 position: absolute/fixed
- 行内块display: inline-block
- 表格单元格 display: table-cell
- 表格标题 display: table-caption
- 溢出元素 overflo: hidden/scroll/auto/inherit
- 弹性盒子 display: flex/inline-flex
BFC布局规则
- 内部的Box会在垂直方向,一个接一个地放置。
- Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠。
- 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
- BFC的区域不会与float box重叠。
- BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
- 计算BFC的高度时,浮动元素也参与计算
BFC应用场景
解决块级元素垂直方向margin重叠
我们来看下面这种情况
box1
box2
按我们习惯性思维,上面这个box的margin-bottom是60px,下面这个box的margin-也是60px,那他们垂直的间距按道理来说应该是120px才对。(可事实并非如此,我们可以来具体看一下)
这种情况下的margin边距为两者的最大值,而不是两者相加,那么我们可以使用BFC来解决这种margin塌陷的问题。
nanjiu
南玖
解决高度塌陷问题
我们再来看这种情况,内部box使用float脱离了普通文档流,导致外层容器没办法撑起高度,使得背景颜色没有显示出来。
nanjiu
南玖
我们可以看到此时的外层容器的高度为0,导致背景颜色没有渲染出来,这种情况我们同样可以使用BFC来解决,可以直接为外层容器触发BFC,我们来看看效果
nanjiu
南玖
清除浮动
在早期前端页面大多喜欢用浮动来布局,但浮动元素脱离普通文档流,会覆盖旁边内容
nanjiu
南玖
我们可以通过触发后面这个元素形成BFC,从而来清楚浮动元素对其布局造成的影响
nanjiu
南玖
什么是IFC?
IFC全称Inline Formatting Context,名为行级格式化上下文
如何触发IFC?
- 块级元素中仅包含内联级别元素
形成条件非常简单,需要注意的是当IFC中有块级元素插入时,会产生两个匿名块将父元素分割开来,产生两个IFC。
IFC布局规则
- 在一个IFC内,子元素是水平方向横向排列的,并且垂直方向起点为元素顶部。
- 子元素只会计算横向样式空间,【padding、border、margin】,垂直方向样式空间不会被计算,【padding、border、margin】。
- 在垂直方向上,子元素会以不同形式来对齐(vertical-align)
- 能把在一行上的框都完全包含进去的一个矩形区域,被称为该行的行框(line box)。行框的宽度是由包含块(containing box)和与其中的浮动来决定。
- IFC中的line box一般左右边贴紧其包含块,但float元素会优先排列。
- IFC中的line box高度由 CSS 行高计算规则来确定,同个IFC下的多个line box高度可能会不同。
- 当 inline boxes的总宽度少于包含它们的line box时,其水平渲染规则由 text-align 属性值来决定。
- 当一个inline box超过父元素的宽度时,它会被分割成多个boxes,这些boxes分布在多个line box中。如果子元素未设置强制换行的情况下,inline box将不可被分割,将会溢出父元素。
IFC应用场景
元素水平居中
当一个块要在环境中水平居中时,设置其为inline-block则会在外层产生IFC,通过text-align则可以使其水平居中。
string 1
string 2
多行文本水平垂直居中
创建一个IFC,然后设置其vertical-align:middle,其他行内元素则可以在此父元素下垂直居中。
string 1
string 2
GFC(Grid Formatting Contexts)栅格格式化上下文
什么是GFC?
GFC全称Grids Formatting Contexts,名为网格格式上下文
简介 CSS3引入的一种新的布局模型——Grids网格布局,目前暂未推广使用,使用频率较低,简单了解即可。 Grid 布局与 Flex 布局有一定的相似性,都可以指定容器内部多个项目的位置。,它们也存在重大区别。 Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。
如何触发GFC?
当为一个元素设置display值为grid或者inline-grid的时候,此元素将会获得一个独立的渲染区域。
GFC布局规则
通过在网格容器(grid container)上定义网格定义行(grid definition ros)和网格定义列(grid definition columns)属性各在网格项目(grid item)上定义网格行(grid ro)和网格列(grid columns)为每一个网格项目(grid item)定义位置和空间(具体可以在MDN上查看)
GFC应用场景
任意魔方布局
这个布局使用用GFC可以轻松实现自由拼接效果,换成其他方法,一般会使用相对/绝对定位,或者flex来实现自由拼接效果,复杂程度将会提升好几个等级。
1
2
3
4
5
6
7
FFC(Flex Formatting Contexts)弹性格式化上下文
什么是FFC?
FFC全称Flex Formatting Contexts,名为弹性格式上下文
简介 CSS3引入了一种新的布局模型——flex布局。 flex是flexible box的缩写,一般称之为弹性盒模型。和CSS3其他属性不一样,flexbox并不是一个属性,而是一个模块,包括多个CSS3属性。flex布局提供一种更加有效的方式来进行容器内的项目布局,以适应各种类型的显示设备和各种尺寸的屏幕,使用Flex box布局实际上就是声明创建了FFC(自适应格式上下文)
如何触发FFC?
当 display 的值为 flex 或 inline-flex 时,将生成弹性容器(Flex Containers), 一个弹性容器为其内容建立了一个新的弹性格式化上下文环境(FFC)
FFC布局规则
- 设置为 flex 的容器被渲染为一个块级元素
- 设置为 inline-flex 的容器被渲染为一个行内元素
- 弹性容器中的每一个子元素都是一个弹性项目。弹性项目可以是任意数量的。弹性容器外和弹性项目内的一切元素都不受影响。简单地说,Flexbox 定义了弹性容器内弹性项目该如何布局
注意FFC布局中,float、clear、vertical-align属性不会生效。
Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。
FFC应用场景
这里只介绍它对于其它布局所相对来说更方便的特点,其实flex布局现在是非常普遍的,很多前端人员都喜欢用flex来写页面布局,操作方便且灵活,兼容性好。
自动撑开剩余高度/宽度
看一个经典两栏布局左边为侧边导航栏,右边为内容区域,用我们之前的常规布局,可能就需要使用到css的calc方法来动态计算剩余填充宽度了,但如果使用flex布局的话,只需要一个属性就能解决这个问题
calc动态计算方法
box1
box2
使用FFC
box1
box2
参考文档
- http://juejin./post/7072174649735381029
262.数组里面有10万个数据,取第一个元素和第10万个元素的时间相差多少?【JavaScript】
数组可以直接根据索引取的对应的元素,所以不管取哪个位置的元素的时间复杂度都是 O(1)
得出结论消耗时间几乎一致,差异可以忽略不计
原因:
JavaScript 没有真正意义上的数组,所有的数组其实是对象,其“索引”看起来是数字,其实会被转换成字符串,作为属性名(对象的 key)来使用。所以无论是取第 1 个还是取第 10 万个元素,都是用 key 精确查找哈希表的过程,其消耗时间大致相同。
Chrome 浏览器JS引擎 V8中,数组有两种存储模式,一种是类似C语言中的线性结构存储(索引值连续,且都是正整数的情况下),一种是采用Hash结构存储(索引值为负数,数组稀疏,间隔比较大)
263.算法题之「移动零」【JavaScript】
题目如下
给定一个数组 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;
}
264.请实现一个 add 函数,满足以下功能【JavaScript】
题目如下
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 剩余参数的长度
注释 2currying 包裹之后返回一个新函数,接收参数为 ...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
}
265.react-router 里的标签和标签有什么区别【JavaScript】
从最终渲染的 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 broser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks ith 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件事情
- 有onclick那就执行onclick
- click的时候阻止a标签默认事件(这样子点击123就不会跳转和刷新页面)
- 再取得跳转href(即是to),用history(前端路由两种方式之一,history & hash)跳转,此时只是链接变了,并没有刷新页面
266.实现 convert 方法,把原始 list 转换成树形结构,要求尽可能降低时间复杂度【JavaScript】
题目如下
以下数据结构中,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.hasOnProperty(item.parentId)) {
const parent = map[item.parentId]
parent.children = parent.children || []
parent.children.push(item)
}
}
return res
}
高级开发者相关问题【共计 3 道题】
257.[React] 为什么不能在循环、条件或嵌套函数中调用 Hooks?【eb框架】
如果在条件语句中使用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') {
// $FloFixMe: Flo 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 (orkInProgressHook === null) {
// 这行代码每个 React 版本不太一样,但做的都是同一件事将 hook 作为链表的头节点处理
firstWorkInProgressHook = orkInProgressHook = hook;
} else {
// 若链表不为空,则将 hook 追加到链表尾部
orkInProgressHook = orkInProgressHook.next = hook;
}
// 返回当前的 hook
return orkInProgressHook;
}
到这里可以看出,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 并不知道这个状态名字叫啥,所以需要通过顺序来索引到对应的状态值
259.为什么普通 for 循环的性能远远高于 forEach 的性能?【JavaScript】
问题说"for循环优于forEach"并不完全正确
循环次数不够多的时候, forEach 性能优于 for
// 循环十万次
let arrs = ne 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 = ne 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 , 主要原因如下
- foreach相对于for循环,代码减少了,foreach依赖IEnumerable。在运行的时候效率低于for循环。
- for循环没有额外的函数调用栈和上下文,所以它的实现最为简单。forEach对于forEach来说,它的函数签名中包含了参数和上下文,所以性能会低于 for 循环。
参考文档
- http://zhuanlan.zhihu./p/461523927
- javascript 中 for 的性能比 forEach 的性能要好,为何还要使用 forEach? - 李十三的回答 - 知乎
- http://juejin./post/6844904159938887687
261.[Vue] 使用Proxy实现简易的vue双向数据绑定【eb框架】【出题公司: 腾讯】
proxy 的基本使用
可以直接看这个链接 #8
使用proxy实现数据劫持
let data = {
name: YoLinDeng,
height: '176cm'
}
const p = ne Proxy(data, {
get(target, prop) {
return Reflect.get(...arguments)
},
set(target, prop, neValue) {
return Reflect.set(...arguments)
}
})
关于vue中数据响应式的原理
对数据进行侦测
- 在vue2.X中,实现一个observe类,对于对象数据,通过Object.defineProperty来劫持对象的属性,实现getter和setter方法,这样就可以在getter的时候知道谁(订阅者)读取了数据,即谁依赖了当前的数据,将它通过Dep类(订阅器)收集统一管理,在setter的时候调用Dep类中的notify方法通知所以相关的订阅者进行更新视图。如果对象的属性也是一个对象的话,则需要递归调用observe进行处理。
- 对于数组则需要处理,通过实现一个拦截器类,并将它挂载到数组数据的原型上,当调用push/pop/shift/unshift/splice/sort/reverse修改数组数据时候,相当于调用的是拦截器中重新定义的方法,这样在拦截器中就可以侦测到数据改变了,并通知订阅者更新视图。
- vue3中使用Proxy替代了Object.defineProperty,优点在于可以直接监听对象而非属性、可以直接监听数组的变化、多达13种拦截方法。缺点是兼容性还不够好。Proxy作为新标准将受到浏览器厂商重点持续的性能优化。
对模板字符串进行编译
- 实现Compile解析器类,将template中的模板字符串通过正则等方式进行处理生成对应的ast(抽象语法树),通过调用定义的不同钩子函数进行处理,包括开始标签(start)并判断是否自闭和以及解析属性、结束标签(end)、文本(chars)、注释(ment)
- 将通过html解析与文本解析的ast进行优化处理,在静态节点上打标记,为后面dom-diff算法中性能优化使用,即在对比前后vnode的时候会跳过静态节点不作对比。
- 根据处理好的ast生产render函数,在组件挂载的时候调用render函数就可以得到虚拟dom。
虚拟dom
- vnode的类型包括注释节点、文本节点、元素节点、组件节点、函数式组件节点、克隆节点,VNode可以描述的多种节点类型,它们本质上都是VNode类的实例,只是在实例化的时候传入的属性参数不同而已。
- 通过将模板字符串编译生成虚拟dom并缓存起来,当数据发生变化时,通过对比变化前后虚拟dom,以变化后的虚拟dom为基准,更新旧的虚拟dom,使它和新的一样。把dom-diff过程叫做patch的过程,其主要做了三件事,分别是创建/删除/更新节点。
- 对于子节点的更新策略,vue中为了避免双重循环数据量大时候造成时间复杂度高带来的性能问题,而选择先从子节点数组中4个特殊位置进行对比,分别是新前与旧前,新后与旧后,新后与旧前,新前与旧后。如果四种情况都没有找到相同的节点,则再通过循环方式查找。
实现简易的vue双向数据绑定
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.pileNode(this.el)
this.observe(this._data)
}
// 实现监听器方法
observe(data) {
const context = this
// 使用proxy代理,劫持数据
this._data = ne Proxy(data, {
set(target, prop, neValue) {
// 自定义事件
let event = ne CustomEvent(prop, {
detail: neValue
})
// 发布自定义事件
context.dispatchEvent(event)
return Reflect.set(...arguments)
}
})
}
// 实现解析器方法,解析模板
pileNode(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.hasOnProperty('v-model')) {
let keyName = attr['v-model'].nodeValue
node.value = this._data[keyName]
node.addEventListener('input', e => {
this._data[keyName] = node.value
})
}
// 递归调用解析器方法
this.pileNode(node)
}
})
}
}
资深开发者相关问题【共计 1 道题】
267.[Webpack] ebpack热更新原理是什么?【工程化】
Hot Module Replacement,简称HMR,无需完全刷新整个页面的,更新模块。HMR的好处,在日常开发工作中体会颇深节省宝贵的开发时间、提升开发体验。
刷新我们一般分为两种
- 一种是页面刷新,不保留页面状态,就是简单粗暴,直接indo.location.reload()。
- 另一种是基于WDS (Webpack-dev-server)的模块热替换,只需要局部刷新页面上发生变化的模块,可以保留当前的页面状态,比如复选框的选中状态、输入框的输入等。
HMR作为一个Webpack内置的功能,可以通过HotModuleReplacementPlugin或--hot开启。那么,HMR到底是怎么实现热更新的呢?下面让我们来了解一下吧!
1. ebpack-dev-server启动本地服务
我们根据ebpack-dev-server的package.json中的bin命令,可以找到命令的入口文件bin/ebpack-dev-server.js。
// node_modules/ebpack-dev-server/bin/ebpack-dev-server.js
// 生成ebpack编译主引擎 piler
let piler = ebpack(config);
// 启动本地服务
let server = ne Server(piler, options, log);
server.listen(options.port, options.host, (err) => {
if (err) {thro err};
});
本地服务代码
// node_modules/ebpack-dev-server/lib/Server.js
class Server {
constructor() {
this.setupApp();
this.createServer();
}
setupApp() {
// 依赖了express
this.app = ne express();
}
createServer() {
this.listeningApp = http.createServer(this.app);
}
listen(port, hostname, fn) {
return this.listeningApp.listen(port, hostname, (err) => {
// 启动express服务后,启动ebsocket服务
this.createSocketServer();
}
}
}
这一小节代码主要做了三件事
- 启动ebpack,生成piler实例。piler上有很多方法,比如可以启动 ebpack 所有编译工作,以及监听本地文件的变化。
- 使用express框架启动本地server,让浏览器可以请求本地的静态资源。
- 本地server启动之后,再去启动ebsocket服务,如果不了解ebsocket,建议简单了解一下ebsocket速成。通过ebsocket,可以建立本地服务和浏览器的双向通信。这样就可以实现当本地文件发生变化,立马告知浏览器可以热更新代码啦!
上述代码主要干了三件事,源码在启动服务前又做了很多事,接下来便看看ebpack-dev-server/lib/Server.js还做了哪些事?
2. 修改ebpack.config.js的entry配置
启动本地服务前,调用了updateCompiler(this.piler)方法。这个方法中有 2 段关键性代码。一个是获取ebsocket客户端代码路径,另一个是根据配置获取ebpack热更新代码路径。
// 获取ebsocket客户端代码
const clientEntry = `${require.resolve(
'../../client/'
)}?${domain}${sockHost}${sockPath}${sockPort}`;
// 根据配置获取热更新代码
let hotEntry;
if (options.hotOnly) {
hotEntry = require.resolve('ebpack/hot/only-dev-server');
} else if (options.hot) {
hotEntry = require.resolve('ebpack/hot/dev-server');
}
修改后的ebpack入口配置如下
// 修改后的entry入口
{ entry:
{ index:
[
// 上面获取的clientEntry
'xxx/node_modules/ebpack-dev-server/client/index.js?http://localhost:8080',
// 上面获取的hotEntry
'xxx/node_modules/ebpack/hot/dev-server.js',
// 开发配置的入口
'./src/index.js'
],
},
}
为什么要新增了 2 个文件?在入口默默增加了 2 个文件,那就意味会一同打包到bundle文件中去,也就是线上运行时。
(1)ebpack-dev-server/client/index.js
这个文件用于ebsocket的,因为ebsoket是双向通信,如果不了解ebsocket,建议简单了解一下ebsocket速成。我们在第 1 步 ebpack-dev-server初始化 的过程中,启动的是本地服务端的ebsocket。那客户端也就是我们的浏览器,浏览器还没有和服务端通信的代码呢?总不能让开发者去写吧hhhhhh。我们需要把ebsocket客户端通信代码偷偷塞到我们的代码中。客户端具体的代码后面会在合适的时机细讲哦。
(2)ebpack/hot/dev-server.js
这个文件主要是用于检查更新逻辑的,这里大家知道就好,代码后面会在合适的时机(第5步)细讲。
3. 监听ebpack编译结束
修改好入口配置后,又调用了setupHooks方法。这个方法是用来注册监听事件的,监听每次ebpack编译完成。
// node_modules/ebpack-dev-server/lib/Server.js
// 绑定监听事件
setupHooks() {
const {done} = piler.hooks;
// 监听ebpack的done钩子,tapable提供的监听方法
done.tap('ebpack-dev-server', (stats) => {
this._sendStats(this.sockets, this.getStats(stats));
this._stats = stats;
});
};
当监听到一次ebpack编译结束,就会调用_sendStats方法通过ebsocket给浏览器发送通知,ok和hash事件,这样浏览器就可以拿到最新的hash值了,做检查更新逻辑。
// 通过ebsoket给客户端发消息
_sendStats() {
this.sockWrite(sockets, 'hash', stats.hash);
this.sockWrite(sockets, 'ok');
}
4. ebpack监听文件变化
每次修改代码,就会触发编译。说明我们还需要监听本地代码的变化,主要是通过setupDevMiddleare方法实现的。
这个方法主要执行了ebpack-dev-middleare库。很多人分不清ebpack-dev-middleare和ebpack-dev-server的区别。其实就是因为ebpack-dev-server只负责启动服务和前置准备工作,所有文件相关的操作都抽离到ebpack-dev-middleare库了,主要是本地文件的编译和输出以及监听,无非就是职责的划分更清晰了。
那我们来看下ebpack-dev-middleare源码里做了什么事:
// node_modules/ebpack-dev-middleare/index.js
piler.atch(options.atchOptions, (err) => {
if (err) { /错误处理/ }
});
// 通过“memory-fs”库将打包后的文件写入内存
setFs(context, piler);
(1)调用了piler.atch方法,在第 1 步中也提到过,piler的强大。这个方法主要就做了 2 件事
- 对本地文件代码进行编译打包,也就是ebpack的一系列编译流程。
- 编译结束后,开启对本地文件的监听,当文件发生变化,重新编译,编译完成之后继续监听。
为什么代码的改动保存会自动编译,重新打包?这一系列的重新检测编译就归功于piler.atch这个方法了。监听本地文件的变化主要是通过文件的生成时间是否有变化,这里就不细讲了。
(2)执行setFs方法,这个方法主要目的就是将编译后的文件打包到内存。这就是为什么在开发的过程中,你会发现dist目录没有打包后的代码,因为都在内存中。原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销,这一切都归功于memory-fs。
5. 浏览器接收到热更新的通知
我们已经可以监听到文件的变化了,当文件发生变化,就触发重新编译。还监听了每次编译结束的事件。当监听到一次ebpack编译结束,_sendStats方法就通过ebsoket给浏览器发送通知,检查下是否需要热更新。下面重点讲的就是_sendStats方法中的ok和hash事件都做了什么。
那浏览器是如何接收到ebsocket的消息呢?回忆下第 2 步骤增加的入口文件,也就是ebsocket客户端代码。
'xxx/node_modules/ebpack-dev-server/client/index.js?http://localhost:8080'
这个文件的代码会被打包到bundle.js中,运行在浏览器中。来看下这个文件的核心代码吧。
// ebpack-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('ebpack/hot/emitter');
hotEmitter.emit('ebpackHotUpdate', currentHash);
}
}
socket方法建立了ebsocket和服务端的连接,并注册了 2 个监听事件。
- hash事件,更新最新一次打包后的hash值。
- ok事件,进行热更新检查。
热更新检查事件是调用reloadApp方法。比较奇怪的是,这个方法又利用node.js的EventEmitter,发出ebpackHotUpdate消息。这是为什么?为什么不直接进行检查更新呢?
个人理解就是为了更好的维护代码,以及职责划分的更明确。ebsocket仅仅用于客户端(浏览器)和服务端进行通信。而真正做事情的活还是交回给了ebpack。
那ebpack怎么做的呢?再来回忆下第 2 步。入口文件还有一个文件没有讲到,就是
'xxx/node_modules/ebpack/hot/dev-server.js'
这个文件的代码同样会被打包到bundle.js中,运行在浏览器中。这个文件做了什么就显而易见了吧!先瞄一眼代码
// node_modules/ebpack/hot/dev-server.js
var check = function check() {
module.hot.check(true)
.then(function(updatedModules) {
// 容错,直接刷新页面
if (!updatedModules) {
indo.location.reload();
return;
}
// 热更新结束,打印信息
if (upToDate()) {
log("info", "[HMR] App is up to date.");
}
})
.catch(function(err) {
indo.location.reload();
});
};
var hotEmitter = require("./emitter");
hotEmitter.on("ebpackHotUpdate", function(currentHash) {
lastHash = currentHash;
check();
});
这里ebpack监听到了ebpackHotUpdate事件,并获取最新了最新的hash值,然后终于进行检查更新了。检查更新呢调用的是module.hot.check方法。那么问题又来了,module.hot.check又是哪里冒出来了的!答案是HotModuleReplacementPlugin搞得鬼。这里留个疑问,继续往下看。
6. HotModuleReplacementPlugin
前面好像一直是ebpack-dev-server做的事,那HotModuleReplacementPlugin在热更新过程中又做了什么伟大的事业呢?
你可以对比下,配置热更新和不配置时bundle.js的区别。内存中看不到?直接执行ebpack命令就可以看到生成的bundle.js文件啦。不要用ebpack-dev-server启动就好了。
(1)没有配置的。
(2)配置了HotModuleReplacementPlugin或--hot的。
哦~ 我们发现moudle新增了一个属性为hot,再看hotCreateModule方法。 这不就找到module.hot.check是哪里冒出来的。
经过对比打包后的文件,__ebpack_require__中的moudle以及代码行数的不同。我们都可以发现HotModuleReplacementPlugin原来也是默默的塞了很多代码到bundle.js中呀。这和第 2 步骤很是相似哦!为什么,因为检查更新是在浏览器中操作呀。这些代码必须在运行时的环境。
你也可以直接看浏览器Sources下的代码,会发现ebpack和plugin偷偷加的代码都在哦。在这里调试也很方便。
HotModuleReplacementPlugin如何做到的?这里我就不讲了,因为这需要你对tapable以及plugin机制有一定了解,可以看下我写的文章Webpack插件机制之Tapable-源码解析。你也可以选择跳过,只关心热更新机制即可,毕竟信息量太大。
7. moudle.hot.check 开始热更新
通过第 6 步,我们就可以知道moudle.hot.check方法是如何来的啦。那都做了什么?之后的源码都是HotModuleReplacementPlugin塞入到bundle.js中的哦,我就不写文件路径了。
- 利用上一次保存的hash值,调用hotDonloadManifest发送xxx/hash.hot-update.json的ajax请求;
- 请求结果获取热更新模块,以及下次热更新的Hash 标识,并进入热更新准备阶段。
hotAvailableFilesMap = update.c; // 需要更新的文件
hotUpdateNeHash = update.h; // 更新下次热更新hash值
hotSetStatus("prepare"); // 进入热更新准备状态
- 调用hotDonloadUpdateChunk发送xxx/hash.hot-update.js 请求,通过JSONP方式。
function hotDonloadUpdateChunk(chunkId) {
var script = document.createElement("script");
script.charset = "utf-8";
script.src = __ebpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
if (null) script.crossOrigin = null;
document.head.appendChild(script);
}
这个函数体为什么要单独拿出来,因为这里要解释下为什么使用JSONP获取最新代码?主要是因为JSONP获取的代码可以直接执行。为什么要直接执行?我们来回忆下/hash.hot-update.js的代码格式是怎么样的。
可以发现,新编译后的代码是在一个ebpackHotUpdate函数体内部的。也就是要立即执行ebpackHotUpdate这个方法。
再看下ebpackHotUpdate这个方法。
indo["ebpackHotUpdate"] = function (chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules);
} ;
- hotAddUpdateChunk方法会把更新的模块moreModules赋值给全局全量hotUpdate。
- hotUpdateDonloaded方法会调用hotApply进行代码的替换。
function hotAddUpdateChunk(chunkId, moreModules) {
// 更新的模块moreModules赋值给全局全量hotUpdate
for (var moduleId in moreModules) {
if (Object.prototype.hasOnProperty.call(moreModules, moduleId)) {
hotUpdate[moduleId] = moreModules[moduleId];
}
}
// 调用hotApply进行模块的替换
hotUpdateDonloaded();
}
8. hotApply 热更新模块替换
热更新的核心逻辑就在hotApply方法了。 hotApply代码有将近 400 行,还是挑重点讲了,看哭
①删除过期的模块,就是需要替换的模块
通过hotUpdate可以找到旧模块
var queue = outdatedModules.slice();
hile (queue.length > 0) {
moduleId = queue.pop();
// 从缓存中删除过期的模块
module = installedModules[moduleId];
// 删除过期的依赖
delete outdatedDependencies[moduleId];
// 存储了被删掉的模块id,便于更新代码
outdatedSelfAeptedModules.push({
module: moduleId
});
}
②将新的模块添加到 modules 中
appliedUpdate[moduleId] = hotUpdate[moduleId];
for (moduleId in appliedUpdate) {
if (Object.prototype.hasOnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
③通过__ebpack_require__执行相关模块的代码
for (i = 0; i < outdatedSelfAeptedModules.length; i++) {
var item = outdatedSelfAeptedModules[i];
moduleId = item.module;
try {
// 执行最新的代码
__ebpack_require__(moduleId);
} catch (err) {
// ...容错处理
}
}
hotApply的确比较复杂,知道大概流程就好了,这一小节,要求你对ebpack打包后的文件如何执行的有一些了解,大家可以自去看下。
还是以阅读源码的形式画的图,①-④的小标记,是文件发生变化的一个流程。
参考文档
- 轻松理解ebpack热更新原理
- ebsocket基础知识了解
- tapable: Webpack插件机制之Tapable-源码解析
- Webpack Hot Module Replacement 的原理解析
- 看完这篇,面试再也不怕被问 Webpack 热更新