2026年最新版Web前端面试30万字(附PDF答案版)
涵盖JS、React/Vue、Node、TS、工程化、性能优化、项目场景题、手写编程题等
前端面试知识体系(GitHub仓库)
按技术栈分类,系统列出了JS、CSS、React、Vue、Node.js、算法等领域的高频真题
《2026年Web前端大厂面试高频100题》
聚焦大厂最高频的面试真题,附有答案和解析题目经典且热门,包含大量引发讨论的原理题、框架题和手写代码题
JavaScript相比于npm和yarn,pnpm的优势是什么?
如果使用Math.random()计算中奖概率会有什么问题吗?
怎么使用js 实现拖拽功能?
举例说明你对尾递归的理解,以及有哪些应用场景
说说你对 lterator, Generator 和 Async/Await 的理解
说说你对模块化方案的理解,比如CommonJS、AMD、CMD、ESModule分别是什么?
前端跨页面通信,你知道哪些方法?
Javascript脚本延迟加载的方式有哪些?
怎么理解ES6中 Generator的?使用场景有哪些?
导致页面加载白屏时间长的原因有哪些,怎么进行优化?
【Promise第38题】下面代码的输出是什么?
【Promise第40题】下面代码的输出是什么?
微前端中的应用隔离是什么,一般是怎么实现的?
JavaScript 对象的底层数据结构是什么?
浏览器和 Node 中的事件循环有什么区别?
版本号排序
哪些原因会导致js里this指向混乱?**
**
相比于npm和yarn,pnpm的优势是什么?
**
**
pnpm对比npm/yarn的优点:
- 更快速的依赖下载
- 更高效的利用磁盘空间
- 更优秀的依赖管理
我们按照包管理工具的发展历史,从 npm2 开始讲起:
npm2
用 node 版本管理工具把 node 版本降到 4,那 npm 版本就是 2.x 了。
然后找个目录,执行下 npm init -y,快速创建个 package.json。
然后执行 npm install express,那么 express 包和它的依赖都会被下载下来:
展开 express,它也有 node_modules:
再展开几层,每个依赖都有自己的 node_modules:
也就是说 npm2 的 node_modules 是嵌套的。
这很正常呀?有什么不对么?
这样其实是有问题的,多个包之间难免会有公共的依赖,这样嵌套的话,同样的依赖会复制很多次,会占据比较大的磁盘空间。
这个还不是最大的问题,致命问题是 windows 的文件路径最长是 260 多个字符,这样嵌套是会超过 windows 路径的长度限制的。
当时 npm 还没解决,社区就出来新的解决方案了,就是 yarn:
yarn
yarn 是怎么解决依赖重复很多次,嵌套路径过长的问题的呢?
铺平。所有的依赖不再一层层嵌套了,而是全部在同一层,这样也就没有依赖重复多次的问题了,也就没有路径过长的问题了。
我们把 node_modules 删了,用 yarn 再重新安装下,执行 yarn add express:
这时候 node_modules 就是这样了:
全部铺平在了一层,展开下面的包大部分是没有二层 node_modules 的:
当然也有的包还是有 node_modules 的,比如这样:
为什么还有嵌套呢?
因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。
npm 后来升级到 3 之后,也是采用这种铺平的方案了,和 yarn 很类似:
当然,yarn 还实现了 yarn.lock 来锁定依赖版本的功能,不过这个 npm 也实现了。
yarn 和 npm 都采用了铺平的方案,这种方案就没有问题了么?
并不是,扁平化的方案也有相应的问题。
最主要的一个问题是幽灵依赖,也就是你明明没有声明在 dependencies 里的依赖,但在代码里却可以 require 进来。
这个也很容易理解,因为都铺平了嘛,那依赖的依赖也是可以找到的。
但是这样是有隐患的,因为没有显式依赖,万一有一天别的包不依赖这个包了,那你的代码也就不能跑了,因为你依赖这个包,但是现在不会被安装了。
这就是幽灵依赖的问题。
而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题。
那社区有没有解决这俩问题的思路呢?
当然有,这不是 pnpm 就出来了嘛。
那 pnpm 是怎么解决这俩问题的呢?
pnpm
回想下 npm3 和 yarn 为什么要做 node_modules 扁平化?不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?
那如果不复制呢,比如通过 link。
首先介绍下 link,也就是软硬连接,这是操作系统提供的机制,硬连接就是同一个文件的不同引用,而软链接是新建一个文件,文件内容指向另一个路径。当然,这俩链接使用起来是差不多的。
如果不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去呢?
这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制。
没错,pnpm 就是通过这种思路来实现的。
再把 node_modules 删掉,然后用 pnpm 重新装一遍,执行 pnpm install。
你会发现它打印了这样一句话:
包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node_modules/.pnpm。
我们打开 node_modules 看一下:
确实不是扁平化的了,依赖了 express,那 node_modules 下就只有 express,没有幽灵依赖。
展开 .pnpm 看一下:
所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之间的依赖关系是通过软链接组织的。
比如 .pnpm 下的 expresss,这些都是软链接,
也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互依赖。
官方给了一张原理图,配合着看一下就明白了:
这就是 pnpm 的实现原理。
那么回过头来看一下,pnpm 为什么优秀呢?
首先,最大的优点是节省磁盘空间呀,一个包全局只保存一份,剩下的都是软硬连接,这得节省多少磁盘空间呀。
其次就是快,因为通过链接的方式而不是复制,自然会快。
这也是它所标榜的优点:
相比 npm2 的优点就是不会进行同样依赖的多次复制。
相比 yarn 和 npm3+ 呢,那就是没有幽灵依赖,也不会有没有被提升的依赖依然复制多份的问题。
这就已经足够优秀了,对 yarn 和 npm 可以说是降维打击。
总结
pnpm 最近经常会听到,可以说是爆火。本文我们梳理了下它爆火的原因:
npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。
npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。
pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。
这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。
pnpm 就是凭借这个对 npm 和 yarn 降维打击的。
**
**
如果使用 Math.random() 计算中奖概率会有什么问题吗?
**
**
一、引言
我们日常开发经常会用到随机数,基本上我接触下来,都是使用 Math.random() 生成的。
例如生成随机ID:
document.body.id = ('_' + Math.random()).replace('0.', '');
``
请问这样实现有没有问题?
回答:没有问题。
例如随机排序:
```js
[1, 2, 3, 4, 5].sort(_ => Math.random() - .5);
请问这样实现有没有问题?
回答:没有问题。
但是,如果你希望实现加密操作,例如生成密钥,尤其是在 Node.js 服务层,则 Math.random() 就有问题了,会有潜在的安全风险,需要使用 crypto.getRandomValues() 方法。
二、Math.random的安全风险
提到 Math.random() 的安全风险,有开发人员会说因为 Math.random() 返回的是伪随机数。
这个解释似是而非,和伪随机数没有关系,getRandomValues() 方法返回的也是伪随机数。
还有人说因为 Math.random() 返回的随机值范围不是均匀的,这个回答就不是似是而非了,而是大错特错。
那究竟为何是不安全的呢?
这个就要讲讲 Math.random() 方法的底层实现了,这里有一篇文章有深入介绍,我简述下其中的要点。
Math.random() 函数返回一个范围0-1的伪随机浮点数,其在 V8 中的实现原理是这样的:
为了保证足够的性能,Math.random() 随机数并不是实时生成的,而是直接生成一组随机数(64个),并放在缓存中。
当这一组随机数取完之后再重新生成一批,放在缓存中。
由于 Math.random() 的底层算法是公开的(xorshift128+ 算法),V8 源码可见,因此,是可以使用其他语言模拟的,这就导致,如果攻击者知道了当前随机生成器的状态,那就可以知道缓存中的所有随机数,那就很容易匹配与破解。
例如抽奖活动,使用 Math.random() 进行随机,那么就可以估算出一段时间内所有的中奖结果,从而带来非常严重且致命的损失。
此时应该使用 getRandomValues() 方法。
三、了解getRandomValues方法
Crypto.getRandomValues() 方法返回的也是伪随机数,不是真随机,按照 MDN 的说法,是为了性能考虑,没有使用真随机。
实际上,按照我的认识,所有可以使用算法生成的随机数都可以看成是伪随机数,真随机数应该是存在自然界,例如粒子的起伏,声音的噪点,分子的分布等。
和 Math.random() 方法的区别在于,getRandomValues() 方法的随机种子生成器更加的无序,例如系统层面的无序源(有些硬件自带随机种子)。
然后不同浏览器下 getRandomValues() 方法生成的随机数可能是有区别的。
以及 getRandomValues() 方法的底层实现是没有缓存的,随机数都是实时生成的,因此,性能上是要比 Math.random() 差的,因此,如果是高并发的场景,同时随机数仅仅是用做随机,与安全和金钱不相关,请使用 Math.random() 而不是 getRandomValues()。
就 Web 前端而言,必须要使用 getRandomValues() 方法的场景很少,不过由于纯前端几乎不存在所谓的高并发,因此,你使用 getRandomValues() 方法也是可以的,有装逼的作用。
语法和使用
let randNumber = self.crypto.getRandomValues(new Uint32Array(1))[0];
// 一串随机整数,通常10位
console.log(randNumber);
语法为:
crypto.getRandomValues(typedArray)
支持的参数 typedArray 表示整数型的类型数组,包括:Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array 或者 Uint32Array。
返回值回是所有被替换为随机数的新的数组。
不过 getRandomValues() 方法名称有些长,不利于记忆和敏捷使用,我们可以改造下,例如:
Math.randomValue = function () {
return self.crypto.getRandomValues(new Uint32Array(1))[0];
};
这样我们就可以使用 Math.randomValue() 方法返回足够安全的随机值了。
**
**
React.js 面试题(源码笔记等)
****fiber架构的工作原理?
React Reconciler 为何要采用 fiber 架构?
useState是如何实现的?
React Fiber是什么?
简单介绍下React中的 diff 算法
如何让 useEffect 支持 async/await?
React 中怎么实现状态自动保存(KeepAlive)?
React Fiber 是如何实现更新过程可控?
react中懒加载的实现原理是什么?
React有哪些性能优化的方法?
不同版本的 React 都做过哪些优化?
React19新特性
说说你对 React Hook的闭包陷阱的理解有哪些解决方案?
React 中,怎么给 children 添加额外的属性?
Fiber 为什么是 React 性能的一个飞跃?
**
**
fiber 架构的工作原理?
**
**
React 中的 Fiber 架构是一种新的协调算法,旨在提高 React 的性能和用户体验。它通过引入新的数据结构和机制,使得 React 能够更高效地处理 UI 更新。以下是 Fiber 架构的工作原理:
1. Fiber 数据结构
- Fiber 节点:Fiber 是一个表示组件的内部数据结构,每个 Fiber 节点对应一个 React 组件。它包含了组件的状态、更新信息和子组件的引用等。
- Fiber 树:Fiber 节点形成了一棵 Fiber 树,类似于旧版的虚拟 DOM 树。每个 Fiber 节点指向其父节点、子节点和兄弟节点。
2. 工作单元和增量渲染
- 工作单元:渲染过程被分解为多个工作单元,每个单元代表一个小的渲染任务。这样可以将渲染过程拆分成可中断的任务,以避免长时间的阻塞。
- 增量渲染:Fiber 允许将渲染任务拆分为增量的操作,逐步完成整个渲染过程。每次渲染会处理 Fiber 树的一部分,允许在任务之间插入中断点,从而提高了渲染的响应性。
3. 调度优先级
- 优先级调度:Fiber 引入了任务调度机制,允许根据任务的优先级来决定渲染的顺序。高优先级的任务(如用户输入、动画)会优先处理,而低优先级的任务(如数据加载)会在空闲时间处理。
- 任务中断和恢复:Fiber 支持在渲染过程中中断并恢复任务。当重要任务需要处理时,当前的渲染任务可以被中断,待重要任务完成后再恢复继续。
4. 更新和协调
- 更新队列:每个 Fiber 节点都有一个更新队列,用于存储与组件相关的更新信息。更新队列可以包含多个更新,React 会根据更新的优先级和顺序进行协调。
- 协调过程:Fiber 通过对比新旧 Fiber 树来决定哪些部分需要更新。这一过程称为协调(Reconciliation),它会检查节点的变更,生成更新的补丁。
5. 渲染阶段和提交阶段
- 渲染阶段:在渲染阶段,Fiber 架构会计算出需要更新的部分,但不会立即更新 DOM。这一阶段主要用于计算新的 Fiber 树,并生成更新任务。
- 提交阶段:在提交阶段,Fiber 会将渲染阶段计算出的更新应用到实际的 DOM 上。这个阶段是同步的,确保所有的更改都被正确地应用。
6. 错误处理
- 错误边界:Fiber 提供了更好的错误处理机制,可以局部地处理渲染中的错误。即使在渲染过程中发生错误,也能保证 UI 的部分更新和恢复。
**
**
React Reconciler 为何要采用 fiber 架构?
**
**
React Reconciler 采用 Fiber 架构主要是为了提升性能和用户体验。Fiber 是 React 16 引入的一种新的协调算法,它相对于旧版的 Reconciler 具备以下优势:
1. 增量渲染
- 旧版 Reconciler:一次性计算并更新整个 UI 树,可能会导致性能瓶颈,尤其是在大型应用中。
- Fiber 架构:支持增量渲染,将渲染任务拆分为小的单元,分批执行。这样可以在长时间运行的任务中插入中断点,使得 UI 更响应式。
2. 中断和优先级
- 旧版 Reconciler:一旦开始更新,渲染过程无法中断,可能会阻塞用户交互。
- Fiber 架构:允许中断和恢复工作,可以根据任务的优先级来调整渲染顺序。低优先级的任务可以在高优先级任务完成后再继续执行,提高了用户交互的流畅性。
3. 任务调度
- 旧版 Reconciler:没有任务调度机制,所有更新都按顺序执行。
- Fiber 架构:使用任务调度机制(Scheduler)来管理和调度不同优先级的更新任务,确保重要任务(如用户输入、动画)优先处理。
4. 异常处理
- 旧版 Reconciler:异常处理能力有限,无法优雅地处理渲染过程中的错误。
- Fiber 架构:允许局部错误处理,确保在渲染过程中即使发生异常,也能保证 UI 的部分更新和恢复。
5. 渲染中断与恢复
- 旧版 Reconciler:无法中断和恢复渲染。
- Fiber 架构:支持在渲染过程中中断并恢复,能够平滑处理长时间运行的任务。
6. 事务管理
- 旧版 Reconciler:处理复杂的事务和操作较为困难。
- Fiber 架构:将渲染任务分解为独立的事务,每个事务可以独立地管理和控制,简化了复杂操作的管理。
**
**
Vue.js 面试题(源码笔记等)
Vue 模板是如何编译的
vue3 相比较于 vue2,在编译阶段有哪些改进?
说说Vue 页面渲染流程
Vue 项目中,你做过哪些性能优化?
如果使用Vue3.0实现一个 Modal,你会怎么进行设计?
Vue3.0里为什么要用 Proxy API替代defineProperty APl ?
Vue 有了数据响应式,为何还要 diff ?
说说 vue3 中的响应式设计原理
说说 Vue 中 css scoped 的原理
vue3 的响应式库是独立出来的,如果单独使用是什么样的效果?
手写 vue 的双向绑定
什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路
说下Vite的原理
**
**
Vue 模板是如何编译的
**
**
new Vue({
render: h => h(App)
})
这个大家都熟悉,调用 render 就会得到传入的模板(.vue文件)对应的虚拟 DOM,那么这个 render 是哪来的呢?它是怎么把 .vue 文件转成浏览器可识别的代码的呢?
render 函数是怎么来的有两种方式
- 第一种就是经过模板编译生成 render 函数
- 第二种是我们自己在组件里定义了 render 函数,这种会跳过模板编译的过程
本文将为大家分别介绍这两种,以及详细的编译过程原理
认识模板编译
我们知道 <template></template> 这个是模板,不是真实的 HTML,浏览器是不认识模板的,所以我们需要把它编译成浏览器认识的原生的 HTML
这一块的主要流程就是
- 提取出模板中的原生 HTML 和非原生 HTML,比如绑定的属性、事件、指令等等
- 经过一些处理生成 render 函数
- render 函数再将模板内容生成对应的 vnode
- 再经过 patch 过程( Diff )得到要渲染到视图中的 vnode
- 最后根据 vnode 创建真实的 DOM 节点,也就是原生 HTML 插入到视图中,完成渲染
上面的 1、2、3 条就是模板编译的过程了
那它是怎么编译,最终生成 render 函数的呢?
模板编译详解——源码
baseCompile()
这就是模板编译的入口函数,它接收两个参数
template:就是要转换的模板字符串options:就是转换时需要的参数
编译的流程,主要有三步:
- 模板解析:通过正则等方式提取出
<template></template>模板里的标签元素、属性、变量等信息,并解析成抽象语法树AST - 优化:遍历
AST找出其中的静态节点和静态根节点,并添加标记 - 代码生成:根据
AST生成渲染函数render
这三步分别对应三个函数,后面会一一下介绍,先看一下 baseCompile 源码中是在哪里调用的
源码地址:src/complier/index.js - 11行
export const createCompiler = createCompilerCreator(function baseCompile (
template: string, // 就是要转换的模板字符串
options: CompilerOptions //就是转换时需要的参数
): CompiledResult {
// 1. 进行模板解析,并将结果保存为 AST
const ast = parse(template.trim(), options)
// 没有禁用静态优化的话
if (options.optimize !== false) {
// 2. 就遍历 AST,并找出静态节点并标记
optimize(ast, options)
}
// 3. 生成渲染函数
const code = generate(ast, options)
return {
ast,
render: code.render, // 返回渲染函数 render
staticRenderFns: code.staticRenderFns
}
})
就这么几行代码,三步,调用了三个方法很清晰
我们先看一下最后 return 出去的是个啥,再来深入上面这三步分别调用的方法源码,也好更清楚的知道这三步分别是要做哪些处理
编译结果
比如有这样的模板
<template>
<div id="app">{{name}}</div>
</template>
打印一下编译后的结果,也就是上面源码 return 出去的结果,看看是啥
{
ast: {
type: 1,
tag: 'div',
attrsList: [ { name: 'id', value: 'app' } ],
attrsMap: { id: 'app' },
rawAttrsMap: {},
parent: undefined,
children: [
{
type: 2,
expression: '_s(name)',
tokens: [ { '@binding': 'name' } ],
text: '{{name}}',
static: false
}
],
plain: false,
attrs: [ { name: 'id', value: '"app"', dynamic: undefined } ],
static: false,
staticRoot: false
},
render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`,
staticRenderFns: [],
errors: [],
tips: []
}
看不明白也没有关系,注意看上面提到的三步都干了啥
ast字段,就是第一步生成的static字段,就是标记,是在第二步中根据ast里的type加上去的render字段,就是第三步生成的
有个大概的印象了,然后再来看源码
1. parse()
源码地址:src/complier/parser/index.js - 79行
就是这个方法就是解析器的主函数,就是它通过正则等方法提取出 <template></template> 模板字符串里所有的 tag、props、children 信息,生成一个对应结构的 ast 对象
parse 接收两个参数
template:就是要转换的模板字符串options:就是转换时需要的参数。它包含有四个钩子函数,就是用来把parseHTML解析出来的字符串提取出来,并生成对应的AST
核心步骤是这样的:
调用 parseHTML 函数对模板字符串进行解析
- 解析到开始标签、结束标签、文本、注释分别进行不同的处理
- 解析过程中遇到文本信息就调用文本解析器
parseText函数进行文本解析 - 解析过程中遇到包含过滤器,就调用过滤器解析器
parseFilters函数进行解析
每一步解析的结果都合并到一个对象上(就是最后的 AST)
这个地方的源码实在是太长了,有大几百行代码,我就只贴个大概吧,有兴趣的自己去看一下
export function parse (
template: string, // 要转换的模板字符串
options: CompilerOptions // 转换时需要的参数
): ASTElement | void {
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
// 解析到开始标签时调用,如 <div>
start (tag, attrs, unary, start, end) {
// unary 是否是自闭合标签,如 <img />
...
},
// 解析到结束标签时调用,如 </div>
end (tag, start, end) {
...
},
// 解析到文本时调用
chars (text: string, start: number, end: number) {
// 这里会判断判断很多东西,来看它是不是带变量的动态文本
// 然后创建动态文本或静态文本对应的 AST 节点
...
},
// 解析到注释时调用
comment (text: string, start, end) {
// 注释是这么找的
const comment = /^<!--/
if (comment.test(html)) {
// 如果是注释,就继续找 '-->'
const commentEnd = html.indexOf('-->')
...
}
})
// 返回的这个就是 AST
return root
}
上面解析文本时调用的 chars() 会根据不同类型节点加上不同 type,来标记 AST 节点类型,这个属性在下一步标记的时候会用到
| type | AST 节点类型 |
|---|---|
| 1 | 元素节点 |
| 2 | 包含变量的动态文本节点 |
| 3 | 没有变量的纯文本节点 |
2. optimize()
这个函数就是在 AST 里找出静态节点和静态根节点,并添加标记,为了后面 patch 过程中就会跳过静态节点的对比,直接克隆一份过去,从而优化了 patch 的性能
函数里面调用的外部函数就不贴代码了,大致过程是这样的
- 标记静态节点(markStatic) 。就是判断 type,上面介绍了值为 1、2、3的三种类型
-
- type 值为1:就是包含子元素的节点,设置 static 为 false 并递归标记子节点,直到标记完所有子节点
- type 值为 2:设置 static 为 false
- type 值为 3:就是不包含子节点和动态属性的纯文本节点,把它的 static = true,patch 的时候就会跳过这个,直接克隆一份去
- 标记静态根节点(markStaticRoots) ,这里的原理和标记静态节点基本相同,只是需要满足下面条件的节点才能算作是静态根节点
-
- 节点本身必须是静态节点
- 必须有子节点
- 子节点不能只有一个文本节点
源码地址:src/complier/optimizer.js - 21行
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// 标记静态节点
markStatic(root)
// 标记静态根节点
markStaticRoots(root, false)
}
3. generate()
这个就是生成 render 的函数,就是说最终会返回下面这样的东西
// 比如有这么个模板
<template>
<div id="app">{{name}}</div>
</template>
// 上面模板编译后返回的 render 字段 就是这样的
render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`
// 把内容格式化一下,容易理解一点
with(this){
return _c(
'div',
{ attrs:{"id":"app"} },
[ _v(_s(name)) ]
)
}
这个结构是不是有点熟悉?
了解虚拟 DOM 就可以看出来,上面的 render 正是虚拟 DOM 的结构,就是把一个标签分为 tag、props、children,没有错
在看 generate 源码之前,我们要先了解一下上面这最后返回的 render 字段是什么意思,再来看 generate 源码,就会轻松得多,不然连函数返回的东西是干嘛的都不知道怎么可能看得懂这个函数呢
render
我们来翻译一下上面编译出来的 render
这个 with 在 《你不知道的JavaScript》上卷里介绍的是,用来欺骗词法作用域的关键字,它可以让我们更快的引用一个对象上的多个属性
看个例子
const name = '掘金'
const obj = { name:'沐华', age: 18 }
with(obj){
console.log(name) // 沐华 不需要写 obj.name 了
console.log(age) // 18 不需要写 obj.age 了
}
上面的 with(this){} 里的 this 就是当前组件实例。因为通过 with 改变了词法作用域中属性的指向,所以标签里使用 name 直接用就是了,而不需要 this.name 这样
那 _c、 _v 和 _s 是什么呢?
在源码里是这样定义的,格式是: _c(缩写) = createElement(函数名)
源码地址:src/core/instance/render-helpers/index.js - 15行
// 其实不止这几个,由于本文例子中没有用到就没都复制过来占位了
export function installRenderHelpers (target: any) {
target._s = toString // 转字符串函数
target._l = renderList // 生成列表函数
target._v = createTextVNode // 创建文本节点函数
target._e = createEmptyVNode // 创建空节点函数
}
// 补充
_c = createElement // 创建虚拟节点函数
再来看是不是就清楚多了呢
with(this){ // 欺骗词法作用域,将该作用域里所有属姓和方法都指向当前组件
return _c( // 创建一个虚拟节点
'div', // 标签为 div
{ attrs:{"id":"app"} }, // 有一个属性 id 为 'app'
[ _v(_s(name)) ] // 是一个文本节点,所以把获取到的动态属性 name 转成字符串
)
}
接下来我们再来看 generate() 源码
generate
源码地址:src/complier/codegen/index.js - 43行
这个流程很简单,只有几行代码,就是先判断 AST 是不是为空,不为空就根据 AST 创建 vnode,否则就创建一个空div 的 vnode
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// 就是先判断 AST 是不是为空,不为空就根据 AST 创建 vnode,否则就创建一个空div的 vnode
const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
可以看出这里面主要就是通过 genElement() 方法来创建 vnode 的,所以我们来看一下它的源码,看是怎么创建的
genElement()
源码地址:src/complier/codegen/index.js - 56行
这里的逻辑还是很清晰的,就是一堆 if/else 判断传进来的 AST 元素节点的属性来执行不同的生成函数
这里还可以发现另一个知识点 v-for 的优先级要高于 v-if,因为先判断 for 的
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) { // v-once
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // v-for
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // v-if
return genIf(el, state)
// template 节点 && 没有插槽 && 没有 pre 标签
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') { // v-slot
return genSlot(el, state)
} else {
// component or element
let code
// 如果有子组件
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
// 获取元素属性 props
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
// 获取元素子节点
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
// 返回上面作为 with 作用域执行的内容
return code
}
}
每一种类型调用的生成函数就不一一列举了,总的来说最后创建出来的 vnode 节点类型无非就三种,元素节点、文本节点、注释节点
自定义的 render
先举个例子吧,三种情况如下
// 1. test.vue
<template>
<h1>我是沐华</h1>
</template>
<script>
export default {}
</script>
// 2. test.vue
<script>
export default {
render(h){
return h('h1',{},'我是沐华')
}
}
</script>
// 3. test.js
export default {
render(h){
return h('h1',{},'我是沐华')
}
}
上面三种,最后渲染的出来的就是完全一模一样的,因为这个 h 就是上面模板编译后的那个 _c
这时有人可能就会问,为什么要自己写呢,不是有模板编译自动生成吗?
这个问题问得好!自己写肯定是有好处的
- 自己把 vnode 给写了,就会直接跳过了模板编译,不用去解析模板里的动态属性、事件、指令等等了,所以性能上会有那么一丢丢提升。这一点在下面的渲染的优先级上就有体现
- 还有一些情况,能让我们代码写法的更加灵活,更加方便简洁,不会冗余
比如 Element-UI 里面的组件源码里就有大量直接写 render 函数
接下来分别看下这两点是如何体现的
1. 渲染优先级
先看一下在官网的生命周期里,关于模板编译的部分
如图可以知道,如果有 template,就不会管 el 了,所以 template 比 el 的优先级更高,比如
那我们自己写了 render 呢?
<div id='app'>
<p>{{ name }}</p>
</div>
<script>
new Vue({
el:'#app',
data:{ name:'沐华' },
template:'<div>掘金</div>',
render(h){
return h('div', {}, '好好学习,天天向上')
}
})
</script>
这个代码执行后页面渲染出来只有 <div>好好学习,天天向上</div>
可以得出 render 函数的优先级更高
因为不管是 el 挂载的,还是 template 最后都会被编译成 render 函数,而如果已经有了 render 函数了,就跳过前面的编译了
这一点在源码里也有体现
在源码中找到答案:dist/vue.js - 11927行
Vue.prototype.$mount = function ( el, hydrating ) {
el = el && query(el);
var options = this.$options;
// 如果没有 render
if (!options.render) {
var template = options.template;
// 再判断,如果有 template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template);
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
return this
}
// 再判断,如果有 el
} else if (el) {
template = getOuterHTML(el);
}
}
return mount.call(this, el, hydrating)
};
2. 更灵活的写法
比如说我们需要写很多 if 判断的时候
<template>
<h1 v-if="level === 1">
<a href="xxx">
<slot></slot>
</a>
</h1>
<h2 v-else-if="level === 2">
<a href="xxx">
<slot></slot>
</a>
</h2>
<h3 v-else-if="level === 3">
<a href="xxx">
<slot></slot>
</a>
</h3>
</template>
<script>
export default {
props:['level']
}
</script>
不知道你有没有写过类似上面这样的代码呢?
我们换一种方式来写出和上面一模一样的代码看看,直接写 render
<script>
export default {
props:['level'],
render(h){
return h('h' + this.level, this.$slots.default())
}
}
</script>
搞定!就这!就这?
没错,就这!
或者下面这样,多次调用的时候就很方便
<script>
export default {
props:['level'],
render(h){
const tag = 'h' + this.level
return (<tag>{this.$slots.default()}</tag>)
}
}
</script>
**
**
Node.js 面试题
浏览器和 Node 中的事件循环有什么区别?
如何实现jwt鉴权机制?说说你的思路
怎么进行 Node 服务的内存优化?
为什么Node在使用es module时必须加上文件扩展名?
说说Node中的EventEmitter?如何实现一—个EventEmitter?
说说Node文件查找的优先级以及Require方法的文件查找策略?
两个 Node.js 进程如何通信?
pm2守护进程的原理是什么?
单线程的 nodejs 是如何充分利用计算机CPU 资源的呢?
body-parser 这个中间件是做什么用的?
说说对中间件概念的理解,如何封装 node中间件?
**
**
Typescript 面试题
****什么是TypeScript Declare关键字?
Typescript 中的 getter/setter 是什么?你如何使用它们?
unknown 是什么类型?
never 是什么类型,详细讲一下
如何在TypeScript中实现继承?
说-说Typescript中的类及其特性。
请实现下面的 sleep 方法
Typescript中的方法重写是什么?
typescript 中的 is 关键字有什么用?
什么是Typescript映射文件?
Typescript中的类型有哪些?
Typescript中interface 和 type 的差别是什么?
编程 面试题
实现深拷贝
请手写“堆排序”
虚拟 dom 原理是什么,手写一个简单的虚拟 dom 实现
请在不使用 setTimeout 的前提下,实现setinterval
实现JSONP
实现一个类,其实例可以链式调用,它有一个sleep 方法,可以sleep一段时间后再...
编写一个vue组件,组件内部使用插槽接收外部内容,v-model双向绑定,实现折叠...
版本号排序
Promise 的 finally 怎么实现的?
实现 Promise
字符串解析问题
**
**
工程化 面试题
说说你对前端工程化的理解
webpack loader 和 plugin 实现原理
为什么 webpack 可以通过文件打包,让浏览器可以支持 CommonJs 规范?
webpack tree-shaking 在什么情况下会失效?
微前端中的路由加载流程是怎么样的?
说下Vite的原理
说说你对 source Map 的了解
说说webpack的构建流程?
ES6 代码转成 ES5 代码的实现思路是什么?
webpack的module、bundle、chunk分别指的是什么?
webpack treeshaking机制的原理是什么?
为什么 SPA 应用都会提供一个 hash 路由好处是什么?
前端性能优化(大厂专题篇)
**
**
1. script标签放在header里和放在body底部里有什么区别?
2.前端性能优化指标有哪些?怎么进行性能检测?
3.SPA(单页应用)首屏加载速度慢怎么解决?
4.如果使用CSS提高页面性能?
5.怎么进行站点内的图片性能优化?
6.虚拟DOM一定更快吗?
7.有些框架不用虚拟dom,但是他们的性能也不错是为什么?
8,如果某个页面有几百个函数需要执行,可以怎么优化页面的性能?
9.讲一下png8、png16、png32的区别,并简单讲讲png的压缩原理
10.页面加载的过程中,JS 文件是不是一定会阻塞DOM和CSSOM的构建?
11. React.memo()和useMemo(的用法是什么,有哪些区别?
12.导致页面加载白屏时间长的原因有哪些,怎么进行优化?
13.如果一个列表有100000个数据,这个该怎么进行展示?
14.DNS预解析是什么?怎么实现?
15. 在React中可以做哪些性能优化?
16.浏览器为什么要请求并发数限制?
17. 如何确定页面的可用性时间,什么是PerformanceAPI?
18.谈谈对window.requestAnimationFrame 的理解
19. css加载会造成阻塞吗?
20.什么是内存泄漏?什么原因会导致呢?
21.如何用webpack来优化前端性能
22.说说常规的前端性能优化手段
23. 什么是CSS Sprites?
24. CSS优化、提高性能的方法有哪些?
25.script标签中,async和defer两个属性有什么用途和区别?**
**
九、项目场景题(26年面试必考)
**
**
1.如何判断用户设备
2.将多次提交压缩成一次提交
3.介绍下navigator.sendBeacon方法
4.混动跟随导航(电梯导航)该如何实现
5退出浏览器之前,发送积压的埋点数据请求,该如何做?
6.如何统计页面的long task(长任务)[热度:140】
7.PerfoemanceObserver如何测量页面性能
移动端如何实现下拉滚动加载(顶部加载)
9.判断页签是否为活跃状态
10.在网络带宽一定的情况下,切片上传感觉和整体上传消费的时间应该是差不多的这种说法正确吗?
11.大文件切片上传的时候,确定切片数量的时候,有那些考量因素
12.页面关闭时执行方法,该如何做
13.如何统计用户pv访问的发起请求数量
14.长文本溢出,展开/收起如何实现
15.如何实现鼠标拖拽
16统计全站每一个静态资源加载耗时,该如何做
17.防止前端页面重复请求
18.ResizeObserver作用是什么
19.要实时统计用户浏览器窗口大小,该如何做
20.当项目报错,你想定位是哪个commit引l入的错误的时,该怎么做**
**
第二部分
怎么在前端页面中添加水印?
如何封装一个请求,让其多次调用的时候,实际只发起一个请求的时候,返回同一份结果
web网页如何禁止别人移除水印
react中怎么实现下拉菜单场景,要求点击区域外能关闭下拉组件
如何搭建一套灰度系统?
React 如何实现vue 中 keep-alive 的功能?
如何监控前端页面的崩溃?
如何在前端团队快速落地代码规范
前端如何实现即时通讯?
用户访问页面白屏了,原因是啥,如何排查?
如何给自己团队的大型前端项目设计单元测试?
如何做一个前端项目工程的自动化部署,有哪些规范和流程设计?
你参与过哪些前端基建方面的建设?
假如让你负责一个商城系统的开发,现在需要统计商品的点击量,你有什么样设计与...
前端怎么做错误监控?
token过期后,页面如何实现无感刷新?
如何解决页面请求接口大规模并发问题
web应用中如何对静态资源加载失败的场景做降级处理?
什么是单点登录,以及如何进行实现?
**
**
前端项目难点亮点
1.如何防止重复提交
一般使用的是防抖和节流,节流函数通过控制每次时间执行的时间间隔,控制短时间多次执行方法。防抖函数是推迟每次事件执行的时间减少不必要的查询。但是网络慢的时候,还是会重复提交,没有显示状态,用户不知道有没有真的提交。所以就给按钮添加一个加载状态,查了发现el-button自带了loading属性,传参的时候传一个submit函数,是一个Promise,promise状态改变的时候把loading状态改成false。然后点击按钮会有加载动画,加载的时候,按钮是禁用的。
2.控制ajax执行先后顺序
一个按钮发送2个ajax请求,不会按顺序,因为是异步请求,浏览器可以并行执行,执行快慢看的是响应数据量大小和后台逻辑的复杂程度,为了保证顺序,就是是最后的结果,需要改成同步,ajax请求后台数据,获取数据之后渲染到页面,就需要同步。请求加一个async:false,就可以让ajax会同步执行。但是请求时间比较长需要loading层显示等待状态,但是浏览器渲染线程和js线程互斥,执行js的时候页面渲染会阻塞掉,让ajax函数后面的代码还有渲染线程停止,就算dom操作语句是发起请求的前一句,也会被阻塞,出现假死卡顿现象,所以可以引入JQuery中的对象deferred进行封装异步函数,对多个deferred对象进行并行化操作,当所有deferred对象都得到解决就执行后面添加的回调。
3.解析数据
填写信息的页面,返回的时候填写信息需要留存,要用vue的keep-alive实现
4.axios用post请求数据会被拦截,传不到后端
ajax请求可以拿到数据,axios就拿不到,因为axios的post默认参数格式是字符串,传给后端的数据需要用请求拦截器做处理。可以引入qs库,对data进行处理:qs.stringifyfy。Qs装axios的时候自动安装了。
5.路由懒加载
需要的时候进行加载,把不同路由对应的组件分成不同代码块,路由被访问才加载对应组件。路由会定义很多页面,页面打包后放到单独的js文件会导致非常大,懒加载把页面进行划分,需要时才加载,减少首页加载速度,懒加载主要就是把对应组件打包成js代码块进入首屏不用加载过度的资源,从而减少首屏加载速度。就是用import,在路由配置的router.js,import设置好的组件,from后面写的是组件的路径
6.实现从详情页返回列表页保存上次加载的数据和自动还原上次的浏览位置。
vue2中提供了keep-alive。keep-alive是Vue提供的一个抽象组件,用来对组件进行缓存,从而节省性能,由于是一个抽象组件,所以在v页面渲染完毕后不会被渲染成一个DOM元素,当组件在keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行被包裹在keep-alive中的组件的状态将会被保留,例如我们将某个列表类组件内容滑动到第100条位置,那么我们在切换到一个组件后再次切换回到该组件,该组件的位置状态依旧会保持在第100条列表处。如果要每次进入组件时页面初始位置都是顶部,可以用路由提供的基础功能scrollBehavior