webpack 的快

现在啥都离不开性能。性能其实就是讲快的问题。一般 webpack 的快,都是讲它的构建(打包)的速度,但其实也可以有辅助运行时候的快的。
简单记录一下。

构建速度

一、缓存

基本上只要讲到快的肯定都有缓存。

  1. webpack5 的持久化缓存

  2. 各类 loader 自带的缓存功能(比如 babel-loader的 cacheDirectory: true)

  3. cache-loader

  4. hard-source-webpack-plugin

  5. DllPlugin 和 DllReferencePlugin

前面四个是编译一次后产生缓存,也就是用以第二次缓存。
最后一个 Dll 是把一些不常变化的先打包好,然后引进来就可以了,不要每次都花时间构建。另类缓存。

二、加速文件查找

  1. 对文件进行编译,那文件多少,找文件也是个耗时的工作。webpack 配置的 resolve 字段,就是用来告诉查找规则。
module.exports = {
  resolve: { // 配置一系列搜索资源的规则
    alias: { // 起别名,相当于转换成绝对路径了,所以比相对路径快
      @src: path.resolve(process.cwd(), 'src')
    },
    // 引用的文件可以不加后缀,会找这些后缀的文件,引用文件详细到后缀名最快了
    // 但是一般也习惯不写后缀名,但这字段不要太多吧
    extensions: ['.wasm', '.mjs', '.js', '.json'],
    // 明确告诉 webpack 搜索哪个依赖文件,可以用绝对路径写死
    modules: ['node_modules']
  }
}
  1. 以及 loader 配置里的 includeexclude 来告诉 webpack 哪些需要编译哪些不需要,不干活当然最省时间。

三、多进程打包

  1. happypack(作者都推荐用 thread-loader 了。。。)

  2. thread-loader

运行速度

一、缓存

还是避不开缓存,这个结合《关于缓存的几个关键词》最下面内容来看。就是跟浏览器的缓存策略搭配起来。

二、花样减少代码

  1. 压缩代码,现在 prodcution 模式,webpack 会自定开启混淆压缩功能了。(当然也可以自己配置压缩功能

  2. 减去不被触及的代码,Tree Shaking,但我想还是尽量不要写这种代码吧,Tree Shaking 也不是百分百靠谱。

  3. 按需加载,配合 ES6 的 import export,webpack就会处理了。没有用到的模块就不打包进来。
    注意:这里提一下,require 也可以使用,但并不是浏览器支持 require,而是 webpack 对其做了方法处理。
    无论是 import 还是 require,最终都会变成 __webpack_require__。使用 require 就没有按需这种功效了。代码都会全部打进去。
    而且因为 import 语法原因,所以需要放在最上面声明,是静态解析,所以才能分析出哪些要不要。(其实这部分是 babel 干的活)
    当然 require 也有用处,代码虽然都打进来,但是不会一开始就运行(调用 require 了才会),也就是省去一开始的解析时间,最关键的还是,可以条件引入:

function getModule(flag) {
  if (flag) {
    return require('./a.js')
  }
  return require('./b.js')
}
  1. 抽出公共js,虽然大部分工程都会打成一个 js,但有些还是会有多份,就可能存在重复代码,然后这个相当复杂的 SplitChunksPluginCommonsChunkPlugin的升级版,不过这家伙已经是时代的眼泪了)就出场了。可以抽出公共组件,可以很精细地配置,当然官网说他们的默认配置最优了,没什么事就不要瞎改了。

  2. 异步加载,这个就跟上面那个有千丝的关系。我们运行性能讲究一个最小展示,可以看下JS性能优化探讨里的加快响应。
    有些功能不一定会被点到,那这部分功能就可以延后初始化。那相关的 js 就可以延迟加载。也即是动态加载,对应上面说的 import 是静态加载。使用 webpack 的语法 import()。这个跟 require 不同,不会被打包进去,而是抽多一份 js 出来。延迟去下载。

  3. 合并js文件,这个就跟上面那两个有万缕的关系。抽的 js 太多就会增多 http 请求,这是最耗性能的,走多一次 http 的消耗可能比一次请求多点内容更消耗,当然 http2 的多路复用另说。
    LimitChunkCountPluginMinChunkSizePlugin(LimitChunkCountPlugin功能多一点,没具体使用过) 用于合并小 js 文件。实际场景中,我们就曾在使用 monaco-editor(vscode的网页版)的时候,打包出来有很多细碎的 js,然后用了 MinChunkSizePlugin 把小 js 文件给合成一份。

几个相似(splice slice,substr substring)方法的区别

数组有 splice,slice 这两个方法,字符串也有类似的,长得很像,经常混了,这里记录下。

arrayObject.splice(index, howmany, item1, ….., itemX)

splice:绞接,捻接(两段绳子)的意思。该方法向/从数组中添加/删除项目,然后返回被删除的项目。该方法会直接对数组进行修改。

index:必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
howmany:必需。要删除的项目数量。如果设置为 0,则不会删除项目。
item1, …, itemX:可选。向数组添加的新项目。

const arr = [1, 2, 3, 4]
arr.splice(2, 0, 7, 7, 7) // 返回[],arr = [1, 2, 7, 7, 7, 3, 4]
arr.splice(2, 1, 7, 7, 7) // 返回[3],arr = [1, 2, 7, 7, 7, 4]
arr.splice(0, 1) // 返回[1],arr = [2, 3, 4]
arr.splice(-1, 1, 7, 7, 7) // 返回[4],arr = [1, 2, 3, 7, 7, 7]

arrayObject.slice(start, end)

slice:切片的意思。该方法可从已有的数组中返回选定的元素。返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。不会改变原数组。

start:必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。
end:可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。

const arr = [1, 2, 3, 4]
arr.slice(0, 0) // []
arr.slice(0, 1) // [1]
arr.slice(2, 3) // [3]
arr.slice(0) // [1, 2, 3, 4]
arr.slice(-2) // [3, 4]
arr.slice(-2, -1) // [3]

字符串的 slice,substr,substring 方法

stringObject.slice(start, end)

该方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。跟数组一样不赘述

'1234567'.slice(1, 3) // 23
'1234567'.slice(-4, -1) // 456

stringObject.substr(start, length)

该方法可在字符串中抽取从 start 下标开始的指定数目的字符。
注意:ECMAscript 没有对该方法进行标准化,因此反对使用它。

start:必需。要抽取的子串的起始下标。必须是数值。如果是负数,那么该参数声明从字符串的尾部开始算起的位置。也就是说,-1 指字符串中最后一个字符,-2 指倒数第二个字符,以此类推。
length:可选。子串中的字符数。必须是数值。如果省略了该参数,那么返回从 stringObject 的开始位置到结尾的字串。

'1234567'.substr(1, 3) // 234
'1234567'.substr(0) // 1234567
'1234567'.substr(-4, 2) // 45

stringObject.substring(start, stop)

该方法用于提取字符串中介于两个指定下标之间的字符。

start:必需。一个非负的整数,规定要提取的子串的第一个字符在 stringObject 中的位置。
stop:可选。一个非负的整数,比要提取的子串的最后一个字符在 stringObject 中的位置多 1。如果省略该参数,那么返回的子串会一直到字符串的结尾。

注意:与 slice 和 substr 方法不同的是,substring 不接受负的参数。substring 功能与 slice 一样。

'1234567'.substring(1, 3) // 23
'1234567'.substring(3, 1) // 23,如果 start 比 stop 大,那么该方法在提取子串之前会先交换这两个参数。
'1234567'.substring(3) // 4567
'1234567'.substring(-1, 1) // 1,参数 -1 被解析成 0

webpack的配置

webpack也用了好久,想来短期内前端暂时也离不开它。虽然现在很多框架都集成了,但难免还是会遇到需要自己配置的时候。在这里记录下一些基础配置。
以下基于 webpack5,不过一般差别不大

const path = require('path')
const { execSync } = require('child_process')
const webpack = require('webpack')
const Customplugin = require('./custom-plugin')

// 默认也会覆盖,不过我习惯删了,windows不支持命令的话,可以改用 fs 模块
execSync(`rm -rf build`)

module.exports = {
  context: process.cwd(), // 当前上下文,一般也不用配置
  mode: 'development', // 打包模式,production模式自动压缩混淆
   // source-map 类型 
   // https://webpack.js.org/configuration/devtool/#root
  devtool: false,
   // 编译信息,一般也不需要那么多,可以精细配置 
   // https://webpack.js.org/configuration/stats/#root
  stats: 'none',
  entry:  { 
    a: __dirname + '/a.js',
    b: __dirname + '/b.js',
    c: __dirname + '/c.js'
  }, // 指定文件入口
  output: {
    path: __dirname + '/build',// 打包后的文件存放的地方
    filename: '[name].js'// 打包后输出文件的文件名,比如上面打出来就会是 a.js b.js c.js
  },
  // externals:不打包某些库 https://webpack.js.org/configuration/externals/#externals
  optimization: {
    minimize: false, // 不要压缩
     // 抽出公共js,贼复杂,一般用默认的就可以了 
     // https://webpack.js.org/plugins/split-chunks-plugin/#root
    splitChunks: {
      chunks: 'all',
      // minChunks: 3,
      // minSize: 0,
      cacheGroups: {
        default: {
          priority: 1,
          reuseExistingChunk: true,
          enforce: true
        }
      }
    }
  },
  module: {
    rules: [
      { // 添加自定义 loader
        loader: './custom-loader.js',
        options: {
          param: 1
        }
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
        options: {
          cacheDirectory: true,
          presets: ['@babel/preset-env'],
          plugins: ['@babel/transform-runtime']
        }
      },
      {
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1
            }
          },
          {
            loader: 'less-loader',
            options: {
              noIeCompat: true
            }
          }
        ]
      }
    ]
  }, 
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': '"development"',
    }),
    new Customplugin({ params: 1 }) // 添加自定义 plugin
  ],
  resolve: { // 配置一系列搜索资源的规则
    alias: { // 起别名,方便资源引用
      @src: path.resolve(process.cwd(), 'src')
    },
    // 引用的文件可以不加后缀,会找这些后缀的文件
    extensions: ['.wasm', '.mjs', '.js', '.json'],
    // 告诉 webpack 搜索哪些文件夹,可以用绝对路径写死
    modules: ['node_modules'],
    // 从npm包导入时,例如import * as D3 from 'd3',此选项将确定package.json选中其中的哪些字段
    mainFields: ['browser', 'module', 'main'],
    // 解析目录时要使用的文件名,一般都是 index 了,不要瞎改
    mainFiles: ['index']
  }
}

自定义loader:

const loaderUtils = require('loader-utils')

// https://webpack.js.org/api/loaders/
module.exports = function (content, map, meta) {
  console.log(this.data.value) // 123,具体场景看文档
  // 用 loaderUtils 解析这个 loader 的参数
  const options = loaderUtils.getOptions(this)

  // 最后要以一个模块的导出形式
  content = `module.exports = function () { ${content} }`

  // 同步
  this.callback(null, content, map, meta)
  return// 当调用 callback() 时总是返回 undefined

  // 同步
  // return content

  // 异步
  // const callback = this.async()
  // callback(null, result, map, meta)
}

// 可以定义一个 pitch 方法,要写在 loader 方法下面,但是会先于 loader 方法执行
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  data.value = 123
}

自定义plugin:

const pluginName = 'CustomPlugin'

// https://webpack.js.org/api/plugins/
// 写成 ES5 prototype 形式也可以,一定要有 apply 方法
class CustomPlugin {
  constructor (options) {
    this.options = options
  }
  apply(compiler) {
    // compiler 和 compilation 有很多生命周期钩子
    compiler.hooks.run.tap(pluginName, compilation => {
      console.log('The webpack build process is starting!!!')
    })
  }
}

module.exports = CustomPlugin

深度优先遍历(DFS)-栈和广度优先遍历(BFS)-队列的理解

JS版本的,概念什么的就不赘述了。参考了这位大佬的文章(JS算法之深度优先遍历(DFS)和广度优先遍历(BFS))
在这里记录下自己的理解,为啥一个是栈,一个是队列。看码:

<div id="box">
  <ul>
    <li><div><p></p></div></li>
    <li><img /></li>
    <li><a></a></li>
  </ul>
  <div>
    <p>1</p>
    <p><span><strong></strong></span></p>
    <p>3</p>
  </div>
  <button><strong></strong></button>
</div>
// 深度优先,用的是栈,同一边进出,后入先出
function deepFirstSearch(node) {
  var nodes = []
  if (node != null) {
    var stack = []
    stack.push(node) // 第一个元素无论 push 还是 unshift 都一样,这里只是为了相呼应。
    while(stack.length != 0) {
      // 最核心的差别在这里了
      // 第一次进来:是[$box],弹出来后都是为空
      // 第二次进来: [button, div, ul],然后 ul 被 pop 出去,收进结果数组 nodes 里
      var item = stack.pop()
      nodes.push(item)
      var children = item.children
      for (var i = children.length - 1; i >= 0; i--) {
        // 这一步也是一样,把每个子节点放进去待遍历数组
        // 把 $box 的子节点放进待选数组,此时是 [button, div, ul]
        // 第一次 while 进来都是一样
        // 这里先收集左边还是右边的子级意义都是一样的
        // 这里是从先把 button 放进来,ul 最后,等下 pop 出去就是 ul 最先
        stack.push(children[i])
        // 点出 ul 的子级,收进 stack 的最后,下次 while 进来,pop 就会是 ul 的子级,也即是 li
        // 然后再继续下去,还是会把 ul 的子级的子级,也就是 div,收进 stack 最后
        // 也就是不断的子级优先,达成深挖
      }
    }
  }
  return nodes
}

// 广度优先,用的是队列,一边进另一边出,先入先出
function breadthFirstSearch(node) {
  var nodes = []
  if (node != null) {
    var queue = []
    // 第一次进来:是[$box],弹出来后都是为空
    // 第二次进来:[button, div, ul],然后 button 被 unshift 出去,收进结果数组 nodes 里
    // 第三次进来:[div, ul, strong],然后 div 被 unshift 出去,收进结果数组 nodes 里
    // 第四次进来:[ul, strong, p, p, p],然后 ul 被 unshift 出去,收进结果数组 nodes 里
    // 可以看出跟深度的差别,点出子级都是排进待选数组的最后
    // 但是深度是栈pop,加塞在数组最后的子级优先
    // 广度是队列unshift,一起放进数组的兄弟级优先
    queue.unshift(node)
    while(queue.length != 0) {
      var item = queue.shift()
      nodes.push(item)
      var children = item.children
      for (var i = children.length - 1; i >= 0; i--) {
        queue.push(children[i])
        // 对应上面第二次进来:点出 button 的子级,也就是 strong,收进 queue 的最后
        // 但对 unshift 的结果没影响,下次 while 进来,unshift 依然是 div
        // 对应上面第三次进来:点出 div 的子级,也就是 p,收进 queue 的最后
      }
    }
  }
  return nodes
}

可以看出,就是在遍历时候暂存的数据结构不一样。
深度优先,就是一直深挖,孩子的孩子的孩子…。
广度优先,就是兄弟兄弟兄弟孩子兄弟…。需要注意的是,同级的就算兄弟。意思是相对于根级为孙子辈,就算不是同个父级,也算是兄弟级。

2022-10-18 21:31:18
更新了一篇用 js 做遍历的《深广度优先遍历》