Giter Club home page Giter Club logo

hexo-files's People

Contributors

xfh0192 avatar

Watchers

 avatar  avatar

hexo-files's Issues

算法基础概念

作为一名前端,虽然在平常开发中很少写算法,但当我们需要深入前端框架、开发语言、开源库时,懂算法将大大提高我们看源码的能力。例如 react 的 diff 算法、webpack 中利用 tree-shaking 优化、v8 中的调用栈、消息队列等,这些就大量使用了算法,看懂了就能更好的了解它们的性能,更高效的解决问题。

一、概念

计算机执行命令,是需要消耗性能的。假如一段代码的计算过程漫长,则处理器执行的计算步骤就会更多,性能消耗更大,处理时间更长。而我们都希望计算机的响应越快越好,那么怎么衡量代码的执行效率呢?

好的数据结构与算法能够大大缩短代码的执行时间与存储空间

二、复杂度

如何表示复杂度,具体来讲就是代码执行的时间、执行消耗的存储空间。如:

function count(n) {
    for (let i = 0; i < n; i++) {
        console.log(i)
    }
}

从 CPU 的角度看,每段代码不过是读写数据或操作数据,尽管每次操作 CPU 执行的个数、执行的时间都不同,但我们粗咯把每次执行的时间都一致,称为 unit_time 。

那么上面代码的执行时间为 n*unit_time,记为 T(n)=(2n+2)*unit_time,可以写为:

T(n) = O(f(n))

其中:

n:表示数据规模的大小
f(n):表示每行代码执行的次数总和
O:表示代码的执行时间 T(n) 与 f(n) 表达式成正比

这就是大 O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度,简称时间复杂度

三、时间复杂度

当 n 无限大时,时间复杂度 T(n) 受 n 的最高数量级影响最大,与f(n) 中的常量、低阶、系数关系就不那么大了。所以我们分析代码的时间复杂度时,仅仅关注代码执行次数最多的那段就可以了。

例如:

function fun1(n) {
    let sum = 0,i = 0; 
    for(; i <= n; i++) {
        sum += i; 
    }
    return sum
}
function fun2(n) {
    let sum = 0, sum1 = 0, i = 0, j = 0; 
    for(; i <= n; i++) { // 循环1
        sum += i; 
    }
    for(i = 0; i <= n; i++) { // 循环2
        for(j = 0; j <= n; j++) { 
            sum += i * j; 
        }
    }
    return sum
}
function fun3(n) {
    let sum = 0, i = 0; 
    for(; i <= n; i++) { 
        sum += fun(i); 
    }
    return sum
}

fun1 中第1行都是常量,对 n 的影响不大,所以总的时间复杂度要看第2、3行的循环,即时间复杂度为: O(n)

fun2 中循环1的时间复杂度为 O(n) ,循环2的时间复杂度为 O(n^2),当 n 趋于无穷大时,总体的时间复杂度要趋于 O(n^2) ,即上面代码的时间复杂度是 O(n^2)。

fun3 的时间复杂度是 **O(n * T(fun)) = O(n*n) **,即 O(n^2) 。
所以:T(c+n)=O(n),T(m+n)=O(max(m, n)),T(n) = T1(n) T2(m) = O(nm),其中 c 为常量

常见复杂度(按数量阶递增)

多项式量级:

  • 常量阶: O(1):当算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)
  • 对数阶:O(logn): 简单介绍一下
let i=1;
while (i <= n)  {
  i = i * 2;
}

每次循环 i 都乘以 2 ,直至 i > n ,即执行过程是:20、21、22、…、2k、…、2x、 n
所以总执行次数 x ,可以写成 2x = n ,则时间复杂度为 O(log2n) 。这里是 2 ,也可以是其他常量 k ,时间复杂度也是: O(log3n) = O(log32 * log2n) = O(log2n)

  • 线性阶:O(n)
  • 线性对数阶:O(nlogn)
  • 平方阶、立方阶、….、k次方阶:O(n2)、O(n3)、…、O(nk)

四、空间复杂度

时间复杂度表示算法的执行时间与数据规模之间的增长关系。类比一下,空间复杂度表示算法的存储空间与数据规模之间的增长关系。例如:

function fun(n) {
    let a = [];
    for (let i = 0; i < n; i++) {
        a.push(i);
    }
    return a;
}

以上代码我们可以清晰的看出代码执行的空间为 O(1+n) = O(n),即为 i 及数组 a 占用的储存空间。

所以,空间复杂度分析比时间复杂度分析要简单很多。

五、平均时间复杂度

时间复杂度受数据本身影响,还分为:

  • 最好时间复杂度:在最理想的情况下,执行这段代码的时间复杂度
  • 最坏时间复杂度:在最糟糕的情况下,执行这段代码的时间复杂度
  • 平均时间复杂度:所有情况下,求一个平均值,可以省略掉系数、低阶、常量

refer: https://juejin.im/post/5e82045de51d4547134bc53d

浏览器渲染机制

浏览器渲染机制

浏览器的渲染机制,简单来说:

  1. 处理 HTML 并构建 DOM 树
  2. 处理 CSS 并构建 CSSOM 树
  3. 将 DOM 与 CSSOM 树合并成一个渲染树
  4. 根据渲染树来布局,计算每个节点的位置
  5. 调用 GPU 绘制,合成图层,显示在屏幕上

browser-render

注意:

  1. 由于 DOM 和 CSSOM 通常是并行构建的,所以 CSS加载不会阻塞DOM的解析
  2. 但是,Render Tree 是依赖于 DOM 树和 CSSOM 树的,因此 CSS加载会阻塞 DOM 的渲染

当页面上存在js脚本时,解析顺序会怎么变化呢?

假如有这样的一个页面,浏览器的渲染过程又是怎么样的呢?

<!DOCTYPE>
<html>
  <head>
    <meta charset="utf-8">
    <title>Document</title>
    <style rel="stylesheet" href="https://xxxxx.css">
  </head>

  <body>
    <div>hello</div>
    <script src="https://xxxx.js"></script>
  </body>
</html>

UI 渲染线程与 js 线程是互斥的

因为 js 的执行可能会修改已解析的 DOM 或 CSSOM,因此 UI 渲染线程与 js 线程是互斥的。

css 的加载是否会阻塞 js 的执行?

不会。因为最终的 CSSOM 树,将会按照css选择器的优先级计算出最后的渲染规则,css和js对于样式的设置没有时间先后的区别

Load 和 DOMContentLoaded 的区别

DOMContentLoaded

mdn定义:当纯 HTML 被完全加载以及解析时,DOMContentLoaded 事件会被触发,而不必等待样式表,图片或者子框架完成加载。

JavaScript的同步模式会导致DOM解析暂停。

说明 js 的加载和执行,将会阻塞 HTML 的解析,从而阻塞页面渲染

所以,JavaScript 的同步模式会导致 DOM 解析暂停。

Load

onload 事件触发时,页面上的所有 DOM,图片,css文件,脚本都加载完成了,也就是渲染完毕了


总结:

  1. UI 渲染线程和 js 执行线程是互斥的
  2. html 加载+解析;css加载+解析;js加载+执行
  3. css 加载不会阻塞 html 和 js 的解析,由加载线程加载css文件
  4. js 加载和执行,会阻塞 html 解析(当执行完js脚本后,html才会继续解析)
  5. 当html解析完成,浏览器会分发 DOMContentLoaded 事件
  6. 浏览器根据构建完成的 DOM 树和 CSSOM (样式)树,通过重绘/回流,计算出渲染树并交给 UI 线程绘制
  7. UI 线程根据渲染树,进行分层绘制,并将各图层交给 GPU 渲染,GPU 对各层进行合成,最终渲染出来
  8. 渲染后,等页面上所有的图片等资源加载完成,浏览器分发 onload 事件

refer:

  1. https://juejin.im/post/5e143104e51d45414a4715f7#heading-18
  2. https://segmentfault.com/a/1190000012925872
  3. https://developer.mozilla.org/zh-CN/docs/Web/API/Document/DOMContentLoaded_event

简单整理一些前端工程与webpack的内容

简单整理一些前端工程与webpack的内容

这篇文章,是在用了一段时间webapack之后,查找整理的一些内容,主要是作为自己的笔记,大概比较乱。

简单说下前端工程

现如今的web应用,功能完善,界面繁多,为用户提供了完整的产品体验,规模也越来越大。

从本质上讲,所有Web应用都是一种运行在网页浏览器中的软件,这些软件的图形用户界面(Graphical User Interface,简称GUI)即为前端。

但同时,就导致项目变得越来越复杂,对开发的速度和项目的质量要求更高了。

而且,前端发展到目前的阶段,开发工作必然离不开工程化。

前端是一种技术问题较少、工程问题较多的软件开发领域。

当我们要开发一款完整的Web应用时,前端将面临更多的工程问题,比如:

  • 大体量:多功能、多页面、多状态、多系统;
  • 大规模:多人甚至多团队合作开发;
  • 高性能:CDN部署、缓存控制、文件指纹、缓存复用、请求合并、按需加载、同步/异步加载、移动端首屏CSS内嵌、HTTP 2.0服务端资源推送。

经过业界大佬的尝试,我们只需做好两件事就能大幅提升前端开发效率,并且兼顾运行性能:

1. 组件化开发/模块化开发

工程化的开发**中,最基本的就是分而治之,也就是模块化的开发模式。

由于系统功能被分治到独立的模块或组件中,粒度比较精细,组织形式松散,开发者之间不会产生开发时序的依赖,大幅提升并行的开发效率,理论上允许随时加入新成员认领组件开发或维护工作,也更容易支持多个团队共同维护一个大型站点的开发。

1. 组件可组合,
2. 组件的JS可依赖其他JS模块,
3. CSS可依赖其他CSS单元

于是我们有了react、vue等框架,有AMD/CommonJS/UMD/ES6 Module等方案。

但是开发了这么多的模块,怎么样让页面正确加载呢?

2. 资源管理

可以顺便看看:大公司里怎样开发和部署前端代码?

前端应用没有安装过程,其所需程序资源都部署在远程服务器,用户使用浏览器访问不同的页面来加载不同的资源,随着页面访问的增加,渐进式的将整个程序下载到本地运行,“增量下载”是前端在工程上有别于客户端GUI软件的根本原因。

模块化/组件化开发之后,我们最终要解决的,就是模块/组件加载的技术问题。

没有这样的方案,很难将前端应用的规模发展到更高阶段,很难实现落地前面介绍的那种组件化开发方案,也很难让多方合作高效率的完成一项大型应用的开发,并保证其最终运行性能良好。

解决资源管理的方法其实并不复杂:

静态资源管理系统 = 一个通用的资源表生成工具 + 基于表的资源加载框架

https://webpack.docschina.org/concepts/manifest/

资源表 是一份数据文件(比如JSON),是项目中所有静态资源(主要是JS和CSS)的构建信息记录,通过构建工具扫描项目源码生成,是一种k-v结构的数据,以每个资源的id为key,记录了资源的类别、部署路径、依赖关系、打包合并等内容( webpack打包后的manifest代码 ),比如:

{
    "a.js": {
        "url": "/static/js/a.5f100fa.js",
        "dep": [ "b.js", "a.css" ]
    },
    "a.css": {
        "url": "/static/css/a.63cf374.css",
        "dep": [ "button.css" ]
    },
    "b.js": {
        "url": "/static/js/b.97193bf.js"
    },
    "button.css": {
        "url": "/static/css/button.de33108.css"
    }
}

资源加载框架 则提供一些资源引用的API,让开发者根据id来引用资源,替代静态的script/link标签来收集、去重、按需加载资源。调用这些接口时,框架通过查表来查找资源的各项信息,并递归查找其依赖的资源的信息,然后我们可以在这个过程中实现各种性能优化算法来“智能”加载资源。(webpack打包后的runtime代码 。类似webpack中的plugin)

实现这一步的答案,就是打包工具(webpack、rollup、glup等)。

webpack介绍、一些基本配置

webpack是什么?

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle。

webpack的配置、核心概念

const path = require('path');
module.exports = {
  entry: "./app/entry", // string | object | array
  // Webpack打包的入口
  
  // https://webpack.docschina.org/configuration/output#output-filename
  output: {  // 定义webpack如何输出的选项
  
    path: path.resolve(__dirname, "dist"), // string
    // 所有输出文件的目标路径
    
    filename: "[chunkhash].js", // string
    // 「入口(entry chunk)」文件命名模版
    
    publicPath: "/assets/", // string
    // 构建文件的输出目录
    
    /* 其它高级配置 */
  },
  
  module: {  // 模块相关配置
  
    rules: [ // 配置模块loaders,解析规则
      {
        test: /\.jsx?$/,  // RegExp | string
        
        include: [ // 和test一样,必须匹配选项
          path.resolve(__dirname, "app")
        ],
        
        exclude: [ // 必不匹配选项(优先级高于test和include)
          path.resolve(__dirname, "app/demo-files")
        ],
        
        loader: "babel-loader", // 模块上下文解析
        
        options: { // loader的可选项
          presets: ["es2015"]
        },
        
      },
  },
  
  // https://webpack.docschina.org/concepts/module-resolution/
  resolve: { //  解析模块的可选项
  
    modules: [ // 模块的查找目录
      "node_modules",
      path.resolve(__dirname, "app")
    ],
    
    extensions: [".js", ".json", ".jsx", ".css"], // 用到的文件的扩展
    
    alias: { // 模块别名列表
      "module": "new-module"
	 },
	 
  },

  devtool: "source-map", // enum
  // 为浏览器开发者工具添加元数据增强调试
  
  plugins: [ // 附加插件列表
    // ...
  ],

}
  • Entry:指定webpack开始构建的入口模块,从该模块开始构建并计算出直接或间接依赖的模块或者库
  • Output:告诉webpack如何命名输出的文件以及输出的目录
  • Loaders:由于webpack只能处理javascript,所以我们需要对一些非js文件处理成webpack能够处理的模块,比如sass文件
  • Plugins:Loaders将各类型的文件处理成webpack能够处理的模块,plugins有着很强的能力。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。但也是最复杂的一个。比如对js文件进行压缩优化的UglifyJsPlugin插件
  • Chunk:coding split的产物,我们可以对一些代码打包成一个单独的chunk,比如某些公共模块,去重,更好的利用缓存。或者按需加载某些功能模块,优化加载时间。在webpack3及以前我们都利用CommonsChunkPlugin将一些公共代码分割成一个chunk,实现单独加载。在webpack4 中CommonsChunkPlugin被废弃,使用SplitChunksPlugin

其他需要注意的配置

  • mode:提供 mode 配置选项,告知 webpack 使用相应环境的内置优化

https://webpack.docschina.org/concepts/mode/

  • optimization:优化。其中splitChunks指定提取代码chunk的规则,cacheGroups指定splitChunks之外的特定的提取chunk

--- demo

构建配置中,可以注意的优化点

tree shaking

https://webpack.docschina.org/guides/tree-shaking/

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。

通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯的 ES2015 模块,是有副作用的,不可删除)",这样就可以安全地删除文件中未使用的部分。

"side effect(副作用)" 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

通过 package.json 的 "sideEffects" 属性,来实现这种方式:

# package.json

{
  "name": "your-project",
  "sideEffects": false
}

如果所有代码都不包含 side effect,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export。

但是,如果你的代码确实有一些副作用,可以改为提供一个数组:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js",
    "*.css"
  ]
}

注意,所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并 import 一个 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除。

最后,还可以在 module.rules 配置选项 中设置 "sideEffects"。

代码分割、懒加载

https://webpack.docschina.org/guides/code-splitting/

常用的代码分离方法有三种:

1.入口起点:使用 entry 配置手动地分离代码。

# webpack.config.js

module.exports = {
	...
	entry: {
	            'output': resolveRoot(`src/${dir}/entry.js`),
	            'another-module': resolveRoot(`src/${dir}/another-module.js`),
	        },
	
	        // webpack 如何输出结果的相关选项
	        output: {
	            
	            path: resolveRoot(`dist/${dir}`),
	            
	            filename: '[name].js',  // 对于多个入口起点,filename 需要包含占位符以区分不同文件
	        },
	        

这种方式存在一些隐患:

  • 如果入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中。
  • 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来。

2.公共chunk:使用 SplitChunksPlugin 去重和分离 chunk。

// https://webpack.docschina.org/configuration/optimization/

  • 条件可配置,把控性高
  • 需要对项目资源有一定程度的了解

webpack3.x 中的 SplitChunksPlugin 基于以下条件,会自动 split chunks:

  • New chunk can be shared OR modules are from the node_modules folder
  • New chunk would be bigger than 30kb (before min+gz)
  • Maximum number of parallel requests when loading chunks on demand would be lower or equal to 5 (loading chunks 的并行请求数小于等于5)
  • Maximum number of parallel requests at initial page load would be lower or equal to 3(页面初始化时,loading chunks 的并行请求数小于等于3)
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',	// 分割策略,此处表示分割异步加载的chunk
      minSize: 30000,	// 某代码块至少有30000b,才会被分割
      maxSize: 0,
      minChunks: 1,		// 某代码块至少被其他模块依赖多少次,才会被分割
      maxAsyncRequests: 5,		// loading chunks 的并行请求数小于等于5
      maxInitialRequests: 3,	// 页面初始化时,loading chunks 的并行请求数小于等于3
      automaticNameDelimiter: '~',	// 文件名分隔符
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10		// 分割优先级。被多个模块依赖的module,优先被划分到更高优先级的chunk中(默认0)
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true	// 如果该chunk中包含有从主模块中分离出来的module,那么它会被重用而不是创建一个新的chunk(即使该chunk包含了主模块不需要的代码)
        }
      }
    }
  }
};

3.动态导入:通过模块中的内联函数调用来分离代码。

import(/* webpackChunkName: "lodash" */ 'lodash')

4.预取/预加载模块(prefetch/preload module)

// todo...

  • 缓存

https://webpack.docschina.org/guides/caching/


补充:webpack中容易接触到的其他配置

https://webpack.docschina.org/concepts/module-resolution/

模块解析 resolve

resolver 是一个库(library),用于帮助找到模块的绝对路径。 一个模块可以作为另一个模块的依赖模块,然后被后者引用,如下:

import foo from 'path/to/module';
// 或者
require('path/to/module');

所依赖的模块可以是来自应用程序代码或第三方的库(library)。 resolver 帮助 webpack 从每个如 require/import 语句中,找到需要引入到 bundle 中的模块代码。当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径。

webpack 中的解析规则

使用 enhanced-resolve,webpack 能够解析三种文件路径:

绝对路径
import '/home/me/file';

import 'C:\\Users\\me\\file';
由于我们已经取得文件的绝对路径,因此不需要进一步再做解析。
相对路径
import '../src/file1';
import './file2';

在这种情况下,使用 import 或 require 的资源文件所在的目录,被认为是上下文目录(context directory)。在 import/require 中给定的相对路径,会拼接此上下文路径(context path),以产生模块的绝对路径。

模块路径

模块将在 resolve.modules 中指定的所有目录内搜索。 你可以替换初始模块路径,此替换路径通过使用 resolve.alias 配置选项来创建一个别名。

一旦根据上述规则解析路径后,resolver 将检查路径是否指向文件或目录。如果路径指向一个文件:

  • 如果路径具有文件扩展名,则被直接将文件打包。
  • 否则,将使用 [resolve.extensions] 选项作为文件扩展名来解析,此选项告诉 resolver 在解析中能够接受哪些扩展名(例如 .js, .jsx)。

// todo...


refer:

【eggjs+sequelize】一些函数的小记

翻页逻辑的封装

提供给列表页的查询使用,默认查询的数据都必须是可用的(status === 1)

/**
     * @description 处理列表页的翻页计算
     * @param instance egg的this实例
     * @param fn 回调函数,每次发起查询时的执行函数
     * @param options 配置,提供控制current,per_page的对象
     */
    public async dealPagination<T>(fn: (arg1: any) => any, options = {}) {
        const {
            current = '1',
            per_page = '20',
        } = this.ctx.request.query;
        let current_page = parseInt(options['current'] || current, 10);
        let per_page_count = parseInt(options['per_page'] || per_page, 10);

        let res: {
            rows: Array<T & sequelize.Instance<T>>;
            count: number;
        } = await Promise.resolve(fn.call(null, {
            where: {
                status: {
                    [sequelize.Op.not]: this.config.constant.status.disable.v,
                },
            },
            order: [
                ['create_time', 'DESC'],
            ],
            limit: per_page_count,
            offset: (current_page - 1) * per_page_count,
        }));

        return Promise.resolve({
            list: res.rows,
            page: {
                per_page: per_page_count,
                current: current_page,
                total_page: Math.ceil(res.count / per_page_count),
            },
        });
    }

    /**
     * 对dealPagination进一步封装,保持返回status有效的数据
     * @param modelName 
     * @param query 
     * @param options 
     */
    public async dealFindAndPagination(modelName, query, options = {}) {
        if (query.where && Object.prototype.toString.call(query.where) === '[object Object]') {
            query.where.status = {
                [sequelize.Op.not]: this.config.constant.status.disable.v,
            };
        }

        return await this.dealPagination(this, async (defaultQuery) => {
            return await this.app.seqIns[modelName].findAndCountAll(Object.assign({}, defaultQuery, query));
        }, options);
    }

在controller中可直接使用

      let res = await service.staff.dealFindAndPagination('staff', {
            where,
            attributes: ['id', 'staff_id', 'name', 'company', 'email'],
            include: [
                { model: this.app.seqIns.personal, as: 'personal', attributes: ['id_card_num']},
            ],
        }, {
            current: 1,
            per_page: 1000,
        });

简单介绍webpack的原理

  • 整理一下webpack的运行原理
  • 简单介绍下loader、plugin(放到后面做)

核心概念

  • Entry - 入口,Webpack 执行构建的第一步从 entry 开始。
  • Module - 模块。在 Webpack 里一切皆模块,一个模块对应一个文件。Webpack 会从配置的 Entry 开始,递归找出所有依赖的模块。
  • Chunk - 代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader - 模块转换器, 用于将模块的原内容按照需求转换成新内容。
  • Plugin - 扩展插件,在 Webpack 构建流程中的特定时机会广播对应的事件,插件可以监听这些事件的发生,在特定的时机做对应的事情。

注意:Compiler 和 Compilation:

在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。 Compiler 和 Compilation 的含义如下:

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

另外,Compiler 和 Compilation 类都扩展(extend)自 Tapable,因此可以在 tapable 上查看可用hook

流程概括

webpack的运行流程是一个串行(线性)的过程,从启动到结束会依次执行以下流程。

  • 初始化参数 - 从配置文件和shell语句中读取与合并参数,得出最终的参数、配置。
  • 开始编译 - 用上一步得到的参数初始化 Compiler 对象,加载所有配置的 plugin,通过 compiler.run 方法开始执行编译
  • 确定入口 - 根据配置中的 entry 找出所有入口文件
  • 编译模块 - 从入口文件开始,根据配置的规则,调用 loader 对模块进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译 - 经过上一步的处理,得到了每个模块被转换后的最终内容以及它们之间的依赖关系
  • 输出资源 - 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每个 Chunk 转换为一个单独的文件加入输出列表中。【这是可以修改输出内容的最后机会】
  • 输出完成 - 在确定好输出内容后,根据配置确定输出的路径和文件名,将文件内容写入文件系统中

在以上过程中,webpack 会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后,会执行特定的逻辑,而且插件可以通过 webpack 注入的 compiler 实例,调用 webpack 提供的api改变 webpack 的运行结果。

流程细节

webpack 的构建流程可以分为三个阶段:

  • 初始化:启动构建,读取与合并配置,加载 plugin,实例化 compiler
  • 编译 - 从entry开始,对每个module串行调用对应的loader翻译文件的内容,再找到每个module依赖的module,递归进行编译处理
  • 输出 - 将编译后的module组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

webpack_process_detail

在每个阶段,会发生很多事件,webpack会将这些事件广播出来,plugin 可以监听到需要的事件,执行各自的功能。

  1. 初始化阶段

初始化阶段会发生的部分事件

事件名 类型 解释 注入回调的参数 Callback Parameters
初始化参数 - 从配置文件和shell语句中读取、合并参数,得出最终配置。这个过程中,同时会将配置中的插件实例化 new Plugin()
实例化Compiler - 用上一步得到的参数初始化 Compiler 实例,Compiler 负责文件监听和启动编译。在 Compiler 实例中包含完整的 webpack 配置,全局只有一个 Compiler 实例
加载插件 - 依次调用插件的apply方法(这个过程中,将注册监听),让插件可以监听后续的所有事件节点。同时向插件中传入 Compiler 实例的引用,以方便插件通过 Compiler 调用webpack提供的api
entryOption SyncBailHook 配置中的entry处理完后 context, entry
afterPlugins SyncHook 加载完所有内置的和配置的插件后 compiler
afterResolvers SyncHook resolver配置处理完后 compiler
environment SyncHook 准备编译环境时,在初始化完所有插件后
afterEnvironment SyncHook 编译环境准备完毕时
beforeRun AsyncSeriesHook 开始编译前的钩子 compiler
  1. 编译阶段

编译阶段会发生的部分事件

事件名 类型 解释 注入回调的参数 Callback Parameters
run AsyncSeriesHook 编译过程中,读取records前(records:每次构建后,保存module标识的数据。可用于追踪两次构建间的差异) compiler
watchRun AsyncSeriesHook 和run类似,当在监听模式下触发了一次新的编译时 compiler
compile SyncHook 在一次新的编译(compilation)创建前 compilationParams
compilation SyncHook 当 Webpack 以开发模式运行时,每当检测到文件的变化,便有一次新的 compilation 被创建 。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。compilation对象也提供了很多事件回调给插件进行扩展 compilation, compilationParams
make 一个新的 Compilation 创建完毕,从 entry 开始读取文件,根据文件类型和配置的loader对文件进行编译,编译完后再找出该文件依赖的文件,递归编译和解析结束时广播事件 compilation
afterCompile AsyncSeriesHook finishing and sealing the compilation compilation

在编译阶段中,最重要的事件是 compilation,因为在 compilation 阶段调用了loader,完成了每个模块的转换操作。此时可以在 compilation 对象中注册钩子,详情可见 https://webpack.js.org/api/compilation-hooks

  1. 输出阶段

输出阶段会发生的部分事件

事件 解释
shouldEmit 所有需要输出的文件已经生成,询问插件哪些文件需要输出,哪些不需要
emit 执行文件输出,可以在这里获取、修改输出内容
afterEmit 文件输出完毕
done 成功完成一次完整的编译和输出流程

在输出阶段,已经得到了各个模块经过转换后的结果和其依赖关系,并且将相关模块组合在一起形成一个个Chunk。在输出阶段会根据Chunk的类型(同步/异步),使用对应的模板生成最终要输出的文件内容。

至此,其实已经可以对一些 loader、plugin 阅读源码,或者自己尝试写一写了,参照文档:
https://webpack.docschina.org/contribute/writing-a-loader/
https://webpack.docschina.org/contribute/writing-a-plugin/

输出文件分析

虽然已经了解如何使用 Webpack ,也大致知道其工作原理,可是我们还可以关注下 Webpack 输出的 bundle.js 是什么样子的。

  • 为什么原来一个个的模块文件被合并成了一个单独的文件?
  • 为什么 bundle.js 能直接运行在浏览器中?

可以先看看,单入口(一个依赖)输出的文件内容,简写为如下:

(function(modules) {

  // 模拟 require 语句
  function __webpack_require__() {
  }

  // 执行存放所有模块数组中的第0个模块
  __webpack_require__(0);

})([/*存放所有模块的数组*/])

bundle.js 能直接运行在浏览器中的原因在于输出的文件中通过 webpack_require 函数定义了一个可以在浏览器中执行的加载函数来模拟 Node.js 中的 require 语句。

原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。

如果仔细分析 webpack_require 函数的实现,你还有发现 Webpack 做了缓存优化: 执行加载过的模块不会再执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。

分割代码时

入口文件:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["output"],{

 "./src/version-5/entry.js":

 (function(module, exports, __webpack_require__) {

let a = 1;
let b = 2;
let c = [a, b];

let result;
__webpack_require__.e(/*! import() | lodash */ "common-chunk").then(__webpack_require__.t.bind(null, /*! lodash */ "./node_modules/lodash/lodash.js", 7)).then( _ => {
    result = _.sum(c, row => row == b);
})
console.log(result);

module.exports = {
    result,
}

/***/ })

},[["./src/version-5/entry.js","runtime"]]]);

可以看到,使用了webpackJsonp进行加载,参数为:

  1. 在其他文件中存放着的模块的id
  2. 本文件包含的模块
  3. 需要执行模块(依赖)

然后看runtime.js,会发现与上一个例子中的文件比较相似,区别在于:

  • 多了一个 webpack_require.e 用于加载被分割出去的,需要异步加载的 Chunk 对应的文件;
  • 多了一个 webpackJsonp 函数用于从异步加载的文件中安装模块。

在使用了 CommonsChunkPlugin 去提取公共代码时输出的文件和使用了异步加载时输出的文件是一样的,都会有 webpack_require.e 和 webpackJsonp。 原因在于提取公共代码和异步加载本质上都是代码分割。

refer:

简单排序算法

冒泡排序

function bubleSort(arr) {
    for (let i = 0; i < arr.length; i++) {
        let flag = false
        for (let j = 0; j < arr.length - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
                flag = true
            }
        }
        if (!flag) break;
    }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.