1.4-前端知识点
Create by fall on 18 Oct 2021
Recently revised in 11 Oct 2023
简单
cookie 可以实现不同域共享吗
浏览器同源策略,Cookie 不能直接在不同域间共享
不同域共享Cookie
同主域下子域共享Cookie
实现原理:设置 Cookie 的Domain
属性,若将其设为主域,该Cookie在主域及其所有子域都可用。例如,主域example.com
,将Cookie的Domain
设为.example.com
,则在sub1.example.com
、sub2.example.com
等子域都能访问使用此Cookie。
注意事项:
- 安全性考量:虽方便子域交互,但存在安全风险,任一子域遭攻击,攻击者可能利用Cookie访问其他子域资源。因此,设置Cookie共享时,要保证各子域安全,避免在Cookie存储敏感信息。
- 浏览器兼容性:多数现代浏览器支持通过设置
Domain
属性实现子域共享Cookie,但旧版本浏览器或特殊配置环境可能存在兼容性问题,实际应用需充分测试。
富文本里面, 是如何做到划词的
事件监听
监听鼠标按下、鼠标移动和鼠标松开这三个主要的鼠标事件。当鼠标按下时,标记选择的开始;在鼠标移动过程中,根据鼠标的位置更新选择范围;鼠标松开时,确定最终的选择。
选择范围计算
使用浏览器提供的 Selection
对象来获取和管理选择的范围。在鼠标移动过程中,不断更新 Selection
对象的范围。
操作处理
一旦选择完成,可以根据具体的需求对选中的字符进行操作。例如,修改样式(如加粗、变色)、获取选中的文本内容、执行复制粘贴等操作。
document.addEventListener("mouseup", function () {
const selection = window.getSelection();
if (selection) {
const selectedText = selection.toString();
console.log("选中的文本: ", selectedText);
}
});
如何减少项目中的 if else
- 策略模式:创建一组策略对象,每个对象对应一种条件和处理逻辑。根据不同的条件选择相应的策略对象来执行操作。
- 表驱动法:建立一个数据结构(如对象或数组),将条件与对应的处理函数或值关联起来,通过查找表来获取相应的处理方式。
- 多态:如果条件判断基于不同的对象类型,可以使用多态性,让每个对象类型实现自己的处理方法。
- 提取函数:将每个
if-else
分支中的复杂逻辑提取为独立的函数,以提高代码的可读性和可维护性。 - 状态模式:当条件判断反映的是对象的不同状态时,可以使用状态模式来处理。
axios 请求的底层依赖是什么
底层依赖会根据运行环境而有所不同,
- 浏览器中,使用 XMLHttpRequest 来发送请求,也可以使用
fetch
。 - node 中,依赖于 Nodejs 的 http 或 https 模块发送请求
babel-runtime 是什么
在使用 babel
进行代码转换时,有时会注入一些在多个文件中相同且可能被重复使用的代码。例如,使用类转换(无松散模式)时,每个包含类的文件都会重复出现类似 _classcallcheck
这样的函数。
babel-runtime
的主要作用就是将这些可能被重用的代码抽取成单独的模块,以避免在每个文件中重复出现相同的代码。它通过模块导入的方式引入这些功能,从而避免了对全局作用域的修改或污染。
使用 babel-runtime
通常需要配合 babel-plugin-transform-runtime
插件一起使用。babel-plugin-transform-runtime
插件会进行一些处理,例如自动导入 babel-runtime/core-js
,并将全局静态方法、全局内置对象映射到对应的模块;将内联的工具函数移除,改成通过 babel-runtime/helpers
模块进行导入;如果使用了 async/generator
函数,则自动导入 babel-runtime/regenerator
模块等。
总的来说,babel-runtime 是一种按需加载的实现方式,可避免对全局作用域的污染,同时减少重复代码。
前端打包后,有无办法将请求的调用源码地址包括代码行数也上报上去?
在使用了代码混淆的前端代码中,依然可以通过下面的方法获取足够的上下文信息
源码映射(Source Maps):在构建过程中生成源码映射(Source Maps)文件是标准做法。Source Maps 主要用于将混淆、压缩后的 JavaScript 代码映射回到其原始版本,允许在浏览器调试工具中查看原始代码和追踪错误。
- 在生产版本中生成 Source Map 文件
.map
。的并确保它们正常处理(通常是将它们放置在服务器上的一个公开但安全的位置)。 - 反映在 Source Maps 中的映射: Source Maps 文件应将原始的源文件路径和行号映射到构建后的代码中对应的位置。
使用错误跟踪(Error Monitoring)集成系统,比如 Sentry、LogRocket、Bugsnag
- 自动和源码追踪: 漏洞和崩溃报告将自动包含被未混淆的源码引用,您只需确保生产版本的 Source Maps 配置正确。
- 代码行号报告: 用户报告的堆栈跟踪信息将包括对应底层源文件,而非混淆后的行号。
确保 Source Maps 不公开到客户端以避免潜在的安全风险。应该将它们存放于受控的服务器环境,以避免源码泄露或不当使用。
前端倒计时有误差怎么办
使用高精度时间戳 performance.now()
同步服务器时间
let severTimeOffset = 0
async function getServerTime(){
const nowTime = Date.now()
const serverTime = fetch('/api/serverTime').json()
const requestTime = Date.now() - nowTime
severTimeOffset = Date.now() - serverTime + requestTime
}
后台倒计时 web worker
// main.js
const worker = new Worker("worker.js");
worker.onmessage = (e) => {
if (e.data.type === "update") {
console.log(`剩余时间:${e.data.seconds}秒`);
}
};
// worker.js
let targetTime;
self.onmessage = (e) => {
if (e.data.type === "start") {
targetTime = e.data.targetTime;
startCountdown();
}
};
function startCountdown() {
function update() {
const remainingMs = Math.max(0, targetTime - Date.now());
const seconds = Math.floor(remainingMs / 1000);
self.postMessage({ type: "update", seconds });
if (remainingMs > 0) {
setTimeout(update, 1000);
}
}
update();
}
使用 requestAnimationFrame 关键帧
function smoothCountdown(targetTime) {
function update() {
const remainingMs = Math.max(0, targetTime - Date.now());
const seconds = Math.floor(remainingMs / 1000);
// 更新UI
console.log(`剩余时间:${seconds}秒`);
if (remainingMs > 0) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
前端有几十个请求,如何去控制并发
如果同时进行几十个请求,使用Promise.allSettled
和分批处理:将请求按并发限制分成小批次,
如果请求之间有依赖,使用 async
浏览器缓存策略是怎样的(强缓存 协商缓存)具体是什么过程?
前端浏览器缓存知识梳理:https://juejin.cn/post/6947936223126093861
304 是什么意思一般什么场景出现 ,命中强缓存返回什么状态码
协商缓存命中返回 304
这种方式使用到了 headers 请求头里的两个字段,Last-Modified & If-Modified-Since 。服务器通过响应头 Last-Modified 告知浏览器,资源最后被修改的时间:
Last-Modified: Thu, 20 Jun 2019 15:58:05 GMT
当再次请求该资源时,浏览器需要再次向服务器确认,资源是否过期,其中的凭证就是请求头 If-Modified-Since 字段,值为上次请求中响应头 Last-Modified 字段的值:
If-Modified-Since: Thu, 20 Jun 2019 15:58:05 GMT
浏览器在发送请求的时候服务器会检查请求头 request header 里面的 If-modified-Since,如果最后修改时间相同则返回 304,否则给返回头(response header)添加 last-Modified 并且返回数据(response body)。
另外,浏览器在发送请求的时候服务器会检查请求头(request header)里面的 if-none-match 的值与当前文件的内容通过 hash 算法(例如 nodejs: cryto.createHash('sha1'))生成的内容摘要字符对比,相同则直接返回 304,否则给返回头(response header)添加 etag 属性为当前的内容摘要字符,并且返回内容。
综上总结为:
请求头last-modified的日期与响应头的last-modified一致
请求头if-none-match的hash与响应头的etag一致
这两种情况会返回Status Code: 304
强缓存命中返回 200
tree shaking 是什么,原理是什么
Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination
tree shaking 的原理是什么?
ES6 Module引入进行静态分析,故而编译的时候正确判断到底加载了那些模块
静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码
扩展:common.js 和 es6 中模块引入的区别?
CommonJS 是一种模块规范,最初被应用于 Nodejs,成为 Nodejs 的模块规范。运行在浏览器端的 JavaScript 由于也缺少类似的规范,在 ES6 出来之前,前端也实现了一套相同的模块规范 (例如: AMD),用来对前端模块进行管理。自 ES6 起,引入了一套新的 ES6 Module 规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。但目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 export 和 import,会经过 Babel 转换为 CommonJS 规范。在使用上的差别主要有:
1、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
2、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口(静态编译)。
3、CommonJs 是单个值导出,ES6 Module 可以导出多个
4、CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
5、CommonJs 的 this 是当前模块,ES6 Module 的 this 是 undefined
defer和async区别
在没有 defer 或者 async 的情况下:会立即执行脚本,所以通常建议把 script 放在 body 最后
<script src="script.js"></script>
async:有 async 的话,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。 但是多个 js 文件的加载顺序不会按照书写顺序进行
<script async src="script.js"></script>
derer:有 derer 的话,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成,并且多个defer会按照顺序进行加载。
<script defer src="script.js"></script>
为什么外链css为什么要放头部?
首先整个页面展示给用户会经过html 的解析与渲染过程。
而外链css无论放在html的任何位置都不影响html的解析,但是影响html的渲染。
如果将css放在尾部,html的内容可以第一时间显示出来,但是会阻塞html行内css的渲染。
浏览器的这个策略其实很明智的,想象一下,如果没有这个策略,页面首先会呈现出一个行内css样式,待CSS下载完之后又突然变了一个模样。用户体验可谓极差,而且渲染是有成本的。
如果将css放在头部,css的下载解析是可以和html的解析同步进行的,放到尾部,要花费额外时间来解析CSS,并且浏览器会先渲染出一个没有样式的页面,等CSS加载完后会再渲染成一个有样式的页面,页面会出现明显的闪动的现象。
为什么script要放在尾部?
因为当浏览器解析到script的时候,就会立即下载执行,中断html的解析过程,如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。
具体的流程是这样的:
-
浏览器一边下载HTML网页,一边开始解析。
-
解析过程中,发现script标签
-
暂停解析,网页渲染的控制权转交给JavaScript引擎
-
如果script标签引用了外部脚本,就下载该脚本,否则就直接执行
-
执行完毕,控制权交还渲染引擎,恢复往下解析HTML网页
外链的script包含async或者defer如何处理?
这两个属性只是script标签在header标签中使用的,如果你把它放在body后面是无效的。
script 这两个属性主要用于其js文件没有操作DOM
的情况,这时候就可以将该js脚本设置为异步加载,通过async或defer来标记代码。
async和defer的区别:
0、async和defer都仅对外部脚本有效,对于内置而不是连接外部脚本的script标签,以及动态生成的script标签不起作用。
1、async和defer虽然都是异步的,不过使用async标志的脚本文件一旦加载完成就会立即执行;而使用defer标记的脚本文件,会在 DOMContentLoaded 事件之前(也就是页面DOM加载完成时)执行。
2、如果有多个js脚本文件,async标记不保证按照书写的顺序执行,哪个脚本先下载结束,就先执行那个脚本。而defer标记则会按照js脚本书写顺序执行。
3、一般来说,如果脚本之间没有依赖关系,就使用async属性,如果脚本之间有依赖关系,就使用defer属性。如果同时使用async和defer属性,后者不起作用,浏览器行为由async属性决定。
对于async标记,浏览器的解析过程是这样的:
-
浏览器开始解析HTML网页
-
解析过程中,发现带有async属性的script标签
-
浏览器继续往下解析HTML网页,同时并行下载script标签中的外部脚本
-
脚本下载完成,浏览器暂停解析HTML网页,开始执行下载的脚本
-
脚本执行完毕,浏览器恢复解析HTML网页
对于defer标记,浏览器的解析过程是这样的:
-
浏览器开始解析HTML网页
-
解析过程中,发现带有defer属性的script标签
-
浏览器继续往下解析HTML网页,同时并行下载script标签中的外部脚本
-
浏览器完成解析HTML网页,此时再执行下载的脚本
由于使用了async或defer的script会放在header中,而header又会存在外链css,那么二者有顺序要求吗?
header中script和外链css的位置顺序
先说结论:
如果在html的header中同时有js脚本和外链css,js脚本最好放外链css前面。
其实js的执行是依赖css样式
的。即只有css样式全部下载完成后才会执行js。
因为如果脚本的内容是获取元素的样式,宽高等CSS控制的属性,浏览器是需要计算的,也就是依赖于CSS。浏览器无法感知脚本内容到底是什么,为避免样式获取错误,因而只好等前面所有的样式下载完后,再执行JS。
但是如果css下载事件很长的话,js也无法正常运行,导致html无法正常解析出来。如果css的内容下载更快的话,是没影响的,但反过来的话,JS就要等待了,然而这些等待的时间是完全不必要的。
实现异步的方法
回调函数 -> 时间监听 -> 发布订阅 -> Promise -> Generate -> async / await
回调函数(Callback)
回调函数是异步操作最基本的方法。以下代码就是一个回调函数的例子:
ajax(url, () => {
// 处理逻辑
})
但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误,不能直接 return。
事件监听
这种方式下,异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
下面是两个函数 f1 和 f2,编程的意图是f2必须等到f1执行完成,才能执行。首先,为f1绑定一个事件(这里采用的jQuery的写法)
f1.on('done', f2);
上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:
function f1() {
setTimeout(function () {
// ...
f1.trigger('done');
}, 1000);
}
上面代码中,f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
发布订阅
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
首先,f2向信号中心jQuery订阅done信号。
jQuery.subscribe('done', f2);
然后,f1进行如下改写:
function f1() {
setTimeout(function () {
// ...
jQuery.publish('done');
}, 1000);
}
上面代码中,jQuery.publish('done')的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。 f2完成执行后,可以取消订阅(unsubscribe)
jQuery.unsubscribe('done', f2);
这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
Promise
Promise 本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。 什么时候会用到过一段时间?答案是异步操作,异步是指可能比较长时间才有结果的才做,例如网络请求、读取本地文件等
Promise 的三种状态:
- Pending----Promise 对象实例创建时候的初始状态
- Fulfilled----可以理解为成功的状态
- Rejected----可以理解为失败的状态
一旦从等待状态变成为其他状态就永远不能更改状态了
生成器 Generators/ yield
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。
- 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
- Generator 函数除了状态机,还是一个遍历器对象生成函数。
- 可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。
- yield 表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
Async/Await
-
用async/await,你可以轻松地达成之前使用生成器和co函数所做到的工作,它有如下特点:
1. async/await是基于Promise实现的,它不能用于普通的回调函数。
2. async/await与Promise一样,是非阻塞的。
3. async/await使得异步代码看起来像同步代码,这正是它的魔力所在。
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async function async1() {
return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}
Generator函数依次调用三个文件那个例子用async/await写法,只需几句话便可实现
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
async function readResult(params) {
try {
let p1 = await read(params, 'utf8')//await后面跟的是一个Promise实例
let p2 = await read(p1, 'utf8')
let p3 = await read(p2, 'utf8')
console.log('p1', p1)
console.log('p2', p2)
console.log('p3', p3)
return p3
} catch (error) {
console.log(error)
}
}
readResult('1.txt').then( // async函数返回的也是个promise
data => {
console.log(data)
},
err => console.log(err)
)
// p1 2.txt
// p2 3.txt
// p3 结束
// 结束
5.2 Async/Await并发请求
如果请求两个文件,毫无关系,可以通过并发请求
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
function readAll() {
read1()
read2()//这个函数同步执行
}
async function read1() {
let r = await read('1.txt','utf8')
console.log(r)
}
async function read2() {
let r = await read('2.txt','utf8')
console.log(r)
}
readAll() // 2.txt 3.txt
中等
前端性能定位以及优化指标
前端性能优化手段从以下几个方面入手:加载优化、执行优化、渲染优化、样式优化、脚本优化
- 网络优化:减少 HTTP 请求、缓存资源、压缩代码、无阻塞、首屏加载、按需加载、预加载、压缩图像、减少Cookie、避免重定向、异步加载第三方资源,代码打包,treeshaking,gzip 压缩
- 渲染步骤优化:设置 viewport、减少 DOM 节点、优化动画、优化高频事件、GPU 加速,CSS 写在头部,JS 写在尾部并异步、避免img、iframe等的src为空、尽量避免重置图像大小、图像尽量避免使用DataURL
- 样式优化:避免在 HTML 中书写 style、避免 CSS 表达式、移除 CSS 空规则、正确使用 display、减少使用 float。
- 脚本优化:减少重绘和回流、缓存 DOM 选择与计算、缓存 .length 的值、尽量使用事件代理、尽量使用id选择器、touch事件优化
- 首次渲染时间:减少等待时间,让用户更快看到页面
- 我们可以从 前端性能监控-埋点以及 window.performance相关的 api 去回答
- 也可以从性能分析工具 Performance 和 Lighthouse
- 还可以从性能指标 LCP FCP FID CLS 等去着手
以下为性能相关的文章 大家可以去看看
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源而造成阻塞的现象,若无外力作用,它们都将无法继续执行
- 竞争资源引起进程死锁
- 可剥夺和非剥夺资源
- 竞争非剥夺资源
- 竞争临时性资源
- 进程推进顺序不当
产生条件
互斥条件:涉及的资源是非共享的
- 涉及的资源是非共享的,一段时间内某资源只由一个进程占用,如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放
不剥夺条件:不能强行剥夺进程拥有的资源
- 进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放
请求和保持条件:进程在等待一新资源时继续占有已分配的资源
- 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放
- 环路等待条件:存在一种进程的循环链,链中的每一个进程已获得的资源同时被链中的下一个进程所请求 在发生死锁时,必然存在一个进程——资源的环形链
解决办法
只要打破四个必要条件之一就能有效预防死锁的发生
暂时性死区
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
扩展:
let 、const与暂时性死区
let 或 const 声明的变量拥有暂时性死区(TDZ):当进入它的作用域,它不能被访问(获取或设置)直到执行到达声明。
首先看看不具有暂时性死区的var:
- 当进入var变量的作用域(包围它的函数),立即为它创建(绑定)存储空间。变量会立即被初始化并赋值为undefined。
- 当执行到变量声明的时候,如果变量定义了值则会被赋值。
通过let声明的变量拥有暂时性死区,生命周期如下:
- 当进入let变量的作用域(包围它的语法块),立即为它创建(绑定)存储空间。此时变量仍是未初始化的。
- 获取或设置未初始化的变量将抛出异常ReferenceError。
- 当执行到变量声明的时候,如果变量定义了值则会被赋值。如果没有定义值,则赋值为undefined。
const 工作方式与 let 类似,但是定义的时候必须赋值并且不能改变。
在 TDZ 内部,如果获取或设置变量将抛出异常:
if (true) { // enter new scope, TDZ starts
// Uninitialized binding for `tmp` is created
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ ends, `tmp` is initialized with `undefined`
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
下面的示例将演示死区(dead zone)是真正短暂的(基于时间)和不受空间条件限制(基于位置)
if (true) { // enter new scope, TDZ starts
const func = function () {
console.log(myVar); // OK!
};
// Here we are within the TDZ and
// accessing `myVar` would cause a `ReferenceError`
let myVar = 3; // TDZ ends
func(); // called outside TDZ
}
typeof 与暂时性死区
变量在暂时性死区无法被访问,所以无法对它使用 typeof:
if (true) {
console.log(typeof tmp); // ReferenceError
let tmp;
}
闭包的理解
闭包:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
闭包的特点:
让外部访问函数内部变量成为可能; 可以避免使用全局变量,防止全局变量污染; 可以让局部变量常驻在内存中; 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
应用场景
- 埋点(是网站分析的一种常用的数据采集方法)计数器
function count() {
var num = 0;
return function () {
return ++num
}
}
var getNum = count();
var getNewNum = count();
document.querySelectorAll('button')[0].onclick = function(){
console.log('点击加入购物车次数: '+getNum());
}
document.querySelectorAll('button')[1].onclick = function(){
console.log('点击付款次数: '+getNewNum());
}
事件+循环
按照以下方式添加事件,打印出来的i不是按照序号的
形成原因就是操作的是同一个词法环境,因为onclick后面的函数都是一个闭包,但是操作的是同一个词法环境
var lis = document.querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function () {
alert(i)
}
}
解决办法:
使用匿名函数之后,就形成一个闭包, 操作的就是不同的词法环境
var lis = document.querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
(function (j) {
lis[j].onclick = function () {
alert(j)
}
})(i)
}
v8 垃圾回收机制
首先js因为是单线程,垃圾回收会占用主线程,导致页面卡顿,所以需要一个算法或者说策略,而v8采用的是分代式回收,而垃圾回收在堆中分成了很多部分用作不同的作用(我在说什么啊!当时),回收主要表现在新老生代上,新生代就活得短一点的对象,老生代就活得长一点的对象。
在新生代里有一个算法,将新生代分成了两个区,一个FORM,一个TO,每次经过Scavenge会将FORM区中的没引用的销毁,然后活着的TO区调换位置,反复如此,当经过一次acavange后就会晋升的老生代还有个条件就是TO区的内存超过多少了也会晋升。
而老生代,采用标记清除和标记整理,但标记清除会造成内存不连续,所以会有标记整理取解决掉内存碎片,就是清理掉边界碎片
第一个问题是为了不影响后续 FORM 空间的分配,第二个问题你应该看过有关这方面的文章把,垃圾回收会构建一个根列表,从根节点去访问那些变量,可访问到就是活动,不可就是垃圾
资深
微前端框架
Qiankun(蚂蚁集团)
- 核心特性
- 基于 Single-SPA 封装,支持 React/Vue/Angular 等多框架共存
- 提供 JS 沙箱(Proxy/快照机制)和 CSS 沙箱(Shadow DOM/动态作用域)
- 完整的生命周期管理和路由劫持机制
- 优势
- 成熟稳定,经过蚂蚁金服大规模生产验证
- 开箱即用,配置简单,适合快速搭建微前端架构
- 完善的生态支持(如 Vue CLI 插件、Webpack 工具链)
- 劣势
- 对子应用侵入性较强,需改造入口文件和构建配置
- 沙箱机制在复杂场景下可能存在性能损耗
- 适用场景
- 大型企业级应用,技术栈多样且需长期维护
- 需统一路由管理和状态共享的中台项目
Single-SPA(独立开源)
- 核心特性
- 微前端底层框架,提供应用加载、路由调度和生命周期管理
- 高度灵活,可与任意框架结合
- 支持应用预加载和资源共享
- 优势
- 无框架绑定,适合自定义程度高的复杂场景
- 社区活跃,插件生态丰富(如 single-spa-react/single-spa-vue)
- 劣势
- 配置繁琐,需手动处理样式隔离和通信机制
- 对新手不够友好,学习曲线陡峭
- 适用场景
- 技术栈混合且需高度定制的项目
- 已有成熟路由和状态管理体系的应用改造
Wujie(腾讯)
- 核心特性
- 结合 Web Components 和 iframe,实现原生隔离
- 支持样式、JS、路由全隔离,安全性高
- 提供轻量级通信机制(postMessage/自定义事件)
- 优势
- 天然隔离性,适合金融、医疗等高安全场景
- 高性能按需加载,首屏时间优化显著
- 劣势
- iframe 的历史包袱(如滚动条、SEO 问题)
- Web Components 兼容性问题(IE11 不支持)
- 适用场景
- 对隔离性和安全性要求极高的场景
- 技术栈统一且现代浏览器占比高的项目
关键维度对比与选型建议
技术栈兼容性
- 多框架支持:Qiankun > Single-SPA > Garfish > ICestark
- 技术栈无关:MicroApp > Wujie > Module Federation
- 推荐场景:若存在
React/Vue/Angular
混合开发,优先选择 Qiankun 或 Single-SPA;若需完全技术栈无关,MicroApp 和 Wujie 更优。
隔离性与安全性
- 强隔离:Wujie(iframe+Shadow DOM)> MicroApp(Proxy 沙箱)> Qiankun(Proxy/快照沙箱)
- 弱隔离:Module Federation(依赖 Webpack 模块作用域)
- 推荐场景:金融、医疗等高安全场景选择 Wujie;普通业务场景 Qiankun 或 MicroApp 即可。
性能与资源管理
- 高性能:Module Federation(模块级按需加载)> Garfish(并行加载优化)> MicroApp(轻量级沙箱)
- 低性能:Qiankun(沙箱开销)> Single-SPA(手动优化要求高)
- 推荐场景:追求极致性能选择 Module Federation 或 Garfish;中大型应用可平衡 Qiankun 的成熟度与性能。
开发体验与学习成本
- 低学习成本:Qiankun > MicroApp > Wujie
- 高学习成本:Module Federation > Single-SPA > ICestark
- 推荐场景:新手或快速交付项目选择 Qiankun 或 MicroApp;复杂场景需深入学习 Single-SPA 或 Module Federation。
生态与社区支持
- 成熟生态:Qiankun > Single-SPA > Module Federation
- 新兴生态:MicroApp > Wujie > Garfish
- 推荐场景:长期维护项目选择 Qiankun 或 Single-SPA;创新项目可尝试 MicroApp 或 Garfish。
落地避坑指南
1. 样式隔离方案选择
- Shadow DOM:适合现代浏览器环境,需处理弹窗组件挂载问题
- 动态样式作用域:兼容性好,需监控动态插入样式
- 推荐实践:默认启用 Qiankun 的
experimentalStyleIsolation
,关键子应用逐步迁移至 Shadow DOM
通信机制设计
- 轻量级通信:使用框架内置事件总线(如 Qiankun 的
props
传递) - 复杂通信:结合状态管理库(如 Redux)或微服务 API
- 避坑点:避免直接操作全局变量,优先使用框架提供的通信接口
路由管理策略
- 主应用统一管理:适合单页应用模式,需处理子应用路由前缀
- 子应用自治:适合多页应用模式,需注意路由冲突
- 推荐实践:使用 Qiankun 的
activeRule
或 Single-SPA 的registerApplication
配置路由匹配规则
4 资源加载优化
- 预加载:Qiankun 的
preload
配置或 Webpack 的prefetch
注释 - 按需加载:Module Federation 的动态导入或 Garfish 的并行加载机制
- 避坑点:避免同时加载过多子应用,优先加载关键路径资源
框架选型决策树
- 技术栈多样 → Qiankun 或 Single-SPA
- 高隔离需求 → Wujie 或 MicroApp
- 模块共享优先 → Module Federation 或 EMP
- 快速集成 → MicroApp 或 Piral
- 企业级复杂场景 → ICestark 或 Garfish
如何标准化处理线上用户反馈的问题
首先,用户反馈一个问题,
- 反馈到产品经理,是否是产品需要解决的问题
- 产品与研发协商,然后答复客户
- 在解决问题时,要尝试复现问题
- 可复现:研发处理
- 不可复现,信息搜集后交给研发处理
通用的排查手段
- 缓存影响,cookies、本地缓存、indexDB 等等,开一个无痕浏览器如果功能正常,多半是此原因
- 插件影响
- 浏览器版本问题,升级或更换浏览器
- 网络影响
信息收集
收集用户操作路径,报错信息,运行日志等
- 用户复现路径
- 用户信息
- 用户操作系统和版本
- 用户使用浏览器类型和版本
- 报错信息
- 遇到问题时的截图或者录屏
- 问题是近期出现,还是一直存在
- 是否是某一特定群体出现的问题
- 如果是性能问题,提供 performance 文件
如何做好前端监控方案
谷歌面试题
- 什么是时间复杂度?为什么它很有用?
- 什么是 DOM?
- 什么是事件循环?
- 什么是闭包?
- 原型继承是如何工作的?它和类式继承有什么区别? (在我看来这不是一个有用的问题,但是很多人喜欢问)
- this 是如何工作的?
- 什么是事件冒泡?它是怎么工作的? (在我看来这也是一个不好的问题,但是很多人喜欢问)
- 讲一下服务端和客户端通信的方式有哪些,一些高层级的网络协议如何是工作的?(IP, TCP, HTTP/S/2, UDP, RTC, DNS, 等等)
- 什么是 REST?我们为什么要使用它?
- 如果一个网站速度很慢,如何诊断和修复?网站性能优化的方式有哪些?以及这些方式的适用场景。
- 你用过哪些框架?它们的优点和缺点是什么?为什么要使用它们?这些框架解决了哪些问题?