精华内容
下载资源
问答
  • treeShake

    2019-07-09 12:29:49
    1.treeShake的作用是什么: 2.treeShake原理是什么: ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是 tree-shaking的基础。所谓静态分析就是不执行代码,从字面量上对代码进行分析...

    segmentfault.com/a/119000001… juejin.im/post/5a4dca…

    
    而在js运行时会进行三件事:1语法分析  2.预编译  3.解释执行
    语法分析会在代码执行前对代码进行通篇检查,以排除一些低级错误
    预编译发生在代码执行的前一刻
    解释执行顾名思义就是执行代码
    
    
    js编译器:https://www.zhihu.com/question/25254230
    首先对源码进行分析(类似编译器前端做的工作:词法,语法分析等),得到AST后,
    进一步分析(数据流,控制流分析,类型系统等),获取到一些有用的信息(类似智能
    提示的那种,比如类型变换情况,转换是否安全,空指针检测,是否有潜在的bug等),
    帮助我们写出安全的代码.
    
    1.treeShake的作用是什么:
    2.treeShake原理是什么:
        ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是
        tree-shaking的基础。所谓静态分析就是不执行代码,从字面量上对代码进行分析,
        ES6之前的模块化,比如我们可以动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。
    --------------------------------------------------------------------------------
        用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE
    --------------------------------------------------------------------------------
        ES6的模块引入是静态分析的,故而可以在编译时正确判断到底加载了什么代码。
    --------------------------------------------------------------------------------
        分析程序流,判断哪些变量未被使用、引用,进而删除此代码。
    --------------------------------------------------------------------------------
    rollup只处理函数和顶层的import/export变量,不能把没用到的类的方法消除掉javascript动态语言的特性
    使得静态分析比较困难图7下部分的代码就是副作用的一个例子,如果静态分析的时候删除里run或者jump,程序运行时就可能报错,那就本末倒置了
    ,我们的目的是优化,肯定不能影响执行
    3.treeShake是函数级别还是文件级别:
    复制代码

    转载于:https://juejin.im/post/5d2459e751882556d16828ee

    展开全文
  • 解决方法 在Icon 前面加const修饰

    使用第三方阿里icon时候,run的时候没问题,build时候报下面错误

    在这里插入图片描述
    解决方法 在Icon 前面加const修饰
    在这里插入图片描述

    其他方法

    1.用命令 flutter build apk --no-tree-shake-icons 来打包,(不过这个方法不建议,因为以后都不能用菜单中的Build Apk来打包了。。。)

    2.在IconData之前加上const;

    3.直接换掉IconData中的图片(阿里妈妈网站的图片索引),用本地图片的加载方式去显示图片。

    展开全文
  • 始终使用Next.js进行TreeShake MUI吗? 是的! 您应该节省几千字节 此处使用的示例是从,它显示了如何正确删除未使用的代码。 有%的差异? 不从模块导入多个导出时,Initial几乎相同。 包装 前 后 百分比收益 @ ...
  • yarn add --dev serverless-plugin-tree-shake npm install --save-dev serverless-plugin-tree-shake 用法 plugins : - serverless-plugin-tree-shake package : # no need to spend time excluding dev ...
  • Tree Shaking概念详解

    千次阅读 2020-09-28 11:13:57
    Tree Shaking 值的就是当我引入一个模块的时候,我不引入这个模块的所有代码,我只引入我需要的代码,这就需要借助 webpack 里面自带的 Tree Shaking 这个功能来帮我们实现。 官方有标准的说法:Tree-shaking的本质...

    Tree Shaking 值的就是当我引入一个模块的时候,我不引入这个模块的所有代码,我只引入我需要的代码,这就需要借助 webpack 里面自带的 Tree Shaking 这个功能来帮我们实现。

    官方有标准的说法:Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)

    在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 Tree-Shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。

    在production 模式下不用在webpack.config.js中配置

    optimization: {
    usedExports: true

    }
    当在development模式下配置tree shaking时:
    webpack.config.js

    //HtmlWebpackPlugin
    //当我们整个打包过程结束的时候,自动生成一个html文件,
    //并把打包生成的自动引入到html这个文件中;
     
    var HtmlWebpackPlugin = require('html-webpack-plugin')
    const path = require('path')
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const webpack = require('webpack')
    module.exports = {
        //打包模式,当为‘production’,打包后的文件会被压缩,
        //当为‘development’时打包后的文件不会被压缩,按照开发环境进行打包
        mode: 'development', //在开发者模式中SourceMap默认已经被配置在项目里了。
        // devtool: 'none', //关闭SourceMap
        //cheap:在生成SourceMap的时候,可以不带列信息,只带行信息,不要包含loader里面的SourceMap的生成,
        //只对业务代码进行SourceMap的生成
        //module:对loader里面的代码也进行一个SourceMap的生成
        //eval:eval是一种执行方式
        devtool: 'cheap-module-eval-source-map', //打开SourceMap
        // devtool: 'cheap-module-source-map', //要线上的代码可以映射
        // 打包的入口文件
        entry: './src/index2.js',
        // entry: {
        //     main: './src/index2.js'
        // },
        devServer: {
            contentBase: './dist', //我们要在哪一个目录下去启动这个服务器
            open: true, //会自动的打开一个浏览器,然后自动访问服务器的地址(localhost:8080)
            hot: true, //指是否支持热更新(hmr)
            hotOnly: true, //即使不支持hmr或者hmr有问题,也不刷新浏览器
            proxy: {
                '/api': {
                    //当访问localhost:8080/api的时,它会直接帮你转发到http://localhost:3000
                    target: 'http://localhost:3000',
                    pathRewrite: { '^/api': '' }
                }
            }
        },
        module: {
            rules: [{
                    test: /\.js$/,
                    exclude: /node_modules/,
                    loader: 'babel-loader'
                },
                {
                    test: /\.(png|jpg|gif)$/,
                    use: {
                        loader: 'url-loader',
                        options: {
                            //url-loader会把图片转化成一个base64的字符串,
                            //然后直接放到bundle.js里,而不是单独生成一个图片文件,
                            //好处:不用再额外的请求图片的地址,省了一次http请求;
                            //缺点:如果图片特别大过大,打包生成的js文件就会很大,
                            //那么加载js文件的时间就会很长,所以在一开始很长的时间里,页面上什么都显示不出来;
                            //所以,url-loader最佳的使用方式:加一个limit
                            //如果图片小于limit的值就把图片变为一个base64的字符串放到打包好的js中,
                            //若大于limit的值,就以file-loader的生成一个图片放到dist目录下。
                            limit: 8192, //好处是可以对小图片的http请求数减少,提升网页加载数度
                            name: 'images/[name].[hash].[ext]'
                        }
                    }
                },
                {
                    test: /\.(css|scss|less)$/,
     
                    //"css-loader":会帮我们分析出几个css文件之间的关系,最终把这些css文件合并成一段css;
                    //"style-loader":会把‘css-loader’生成的内容挂在到页面的head部分
                    //在webpack的配置中,loader是有先后执行顺序的,
                    //loader的执行顺序是从下到上,从右到左;
                    //“sass-loader”会先对sass代码进行翻译,翻译为css代码后给到css-loader,
                    //都处理好了之后再交给‘style-loader’挂在到页面上
                    //postcss-loader会自动添加css3的厂商前缀;比如:  transform: translate(100px, 100px)
                    use: [
                        'style-loader',
                        {
                            loader: 'css-loader',
                            options: {
                                importLoaders: 2,
                                modules: true //使得css模块化,
                            }
                        },
                        'postcss-loader',
                        'sass-loader'
                    ]
                },
                {
                    // 可以打包字体文件
                    test: /\.(eot|ttf|svg)$/,
                    use: {
                        loader: 'file-loader'
                    }
                }
            ]
        },
     
        // 打包的出口文件
        output: {
            // 打包后的文件名
            // filename: 'bundle.js',
            filename: '[name].js', //name就是entry值的key:'main','sub'
            // 打包后的文件目录为'dist'
            path: path.resolve(__dirname, 'dist'),
            publicPath: '/' //表示的是我所有的打包生成的文件之间的引用前面都加一个根路径
        },
        //plugin可以在webpack运行到某个时刻的时候,帮你做一些事情(类似生命周期函数)
        plugins: [
            // CleanWebpackPlugin可以在每次打包的时候帮我们对dist目录做一个清空
            new CleanWebpackPlugin(),
            new HtmlWebpackPlugin({
                //HtmlWebpackPlugin帮我们自动的生成一个dist目录下的html文件
                template: './src/index.html'
            }),
            //HotModuleReplacementPlugin帮我们实现HMR
            new webpack.HotModuleReplacementPlugin()
        ],
        //在开发模式下配置 tree shakeing
        optimization: {
            usedExports: true
        }
    }
    

    在 package.json 中配置:

    {
        "name": "webpack-demo",
    //@babel/polyfill和css文件不使用tree shaking
        "sideEffects": [
            "@babel/polyfill",
            "*.css"
        ],
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
            "bundle": "webpack",
            "watch": "webpack --watch",
            "start": "webpack-dev-server",
            "server": "node server.js"
        },
        "author": "susie",
        "license": "ISC",
        "devDependencies": {
            "@babel/core": "^7.5.4",
            "@babel/plugin-transform-runtime": "^7.5.0",
            "@babel/preset-env": "^7.5.4",
            "@babel/preset-react": "^7.0.0",
            "autoprefixer": "^9.6.1",
            "babel-loader": "^8.0.6",
            "clean-webpack-plugin": "^3.0.0",
            "css-loader": "^3.0.0",
            "file-loader": "^4.0.0",
            "html-webpack-plugin": "^3.2.0",
            "node-sass": "^4.12.0",
            "postcss-loader": "^3.0.0",
            "sass-loader": "^7.1.0",
            "style-loader": "^0.23.1",
            "url-loader": "^2.0.1",
            "webpack": "^4.35.3",
            "webpack-cli": "^3.3.5",
            "webpack-dev-server": "^3.7.2"
        },
        "dependencies": {
            "@babel/polyfill": "^7.4.4",
            "@babel/runtime": "^7.5.4",
            "@babel/runtime-corejs2": "^7.5.4",
            "react": "^16.8.6",
            "react-dom": "^16.8.6"
        }
    }
    

    如果需要对某个模块不进行Tree Shaking

     "sideEffects": ["@babel/poly-fill"],  //该模块不进行Tree Shaking
    

    为什么某些引入模块不希望进行Tree Shaking呢?

    下面引入的style.css模块,如果也使用tree shaking,由于css文件没有导出任何模块,那么就有可能在打包的时候该引入模块就被摇晃掉了,导致bug。

    在这里插入图片描述
    在package.json中进行配置,即匹配到的任何css文件都不进行Tree Shaking

    在这里插入图片描述
    不使用Tree Shaking打包时,可以看到打包文件中exports provided: add, mins两种方法

    在这里插入图片描述在这里插入图片描述  前端的学习不是一蹴而就,不积跬步无以至千里,不积小流无以成江海。持续不断的努力才能让你我有所收获,专业的知识还得到机构去学习,培训机构的设立有其存在的必然性,你钱花对了吗?


    推荐阅读:
    5分钟学会 vue 路由守卫
    10分钟搞懂vuex
    浅谈hooks
    vue 组件设计原则
    深入解读 vue 修饰符 sync

    展开全文
  • tree-shaking 这个概念早就有,但却是在 rollup 中实现后才开始被重视,本着寻根究源好奇的心理,我们就先从 rollup 入手 tree-shaking 一探究竟吧~~ 那么,先让我们来康康 tree-shaking 是干啥的? 打包工具中的 ...

    左琳,微医前端技术部前端开发工程师。身处互联网浪潮之中,热爱生活与技术。

    Tip:本文所用 rollup 打包工具版本为 rollup v2.47.0。

    从 Webpack2.x 通过插件逐步实现 tree-shaking,到最近炙手可热的 Vite 构建工具也借助了 rollup 的打包能力,众所周知 Vue 和 React 也是使用 rollup 进行打包的,尤其当我们创建函数库、工具库等库的打包时,首选也是 rollup!那么到底是什么魔力让 rollup 经久不衰呢?答案也许就在 tree-shaking!

    一、 了解 Tree-shaking

    1. 什么是 Tree-shaking?

    tree-shaking 这个概念早就有,但却是在 rollup 中实现后才开始被重视,本着寻根究源好奇的心理,我们就先从 rollup 入手 tree-shaking 一探究竟吧~~

    那么,先让我们来康康 tree-shaking 是干啥的?

    打包工具中的 tree-shaking, 较早时候由 Rich_Harris 的 rollup 实现,官方标准说法:本质上消除无用的 JS 代码。就是说,当引入一个模块时,并不引入整个模块的所有代码,而是只引入我需要的代码,那些我不需要的无用代码就会被”摇“掉。

    后面从 Webpack2 开始 Webpack 也实现了 tree-shaking 功能,具体来说,在 Webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树的枝杈。而在实际情况中,虽然我们的功能文件依赖了某个模块,但其实只使用其中的某些功能而非全部。通过 tree-shaking,将没有使用的模块摇掉,这样就可以达到删除无用代码的目的。

    由此我们就知道了,tree-shaking 是一种消除无用代码的方式!

    但要注意的是,tree-shaking 虽然能够消除无用代码,但仅针对 ES6 模块语法,因为 ES6 模块采用的是静态分析,从字面量对代码进行分析。对于必须执行到才知道引用什么模块的 CommonJS 动态分析模块他就束手无策了,不过我们可以通过插件支持 CommonJS 转 ES6 然后实现 tree-shaking,只要思想不滑坡,办法总比困难多。

    总之,rollup.js 默认采用 ES 模块标准,但可以通过 rollup-plugin-commonjs 插件使之支持 CommonJS 标准,目前来说,在压缩打包体积方面,rollup 的优势相当明显!

    2. 为什么需要 Tree-shaking?

    今天的 Web 网页应用可以体积很大,尤其是 JavaScript 代码,但浏览器处理 JavaScript 是非常耗资源的,如果我们能将其中的无用代码去掉,仅提供有效代码给浏览器处理,无疑会大大减小浏览器的负担,而 tree-shaking 帮我们做到了这一点。

    从这个角度看,tree-shaking 功能属于性能优化的范畴。

    毕竟,减少 web 项目中 JavaScript 的无用代码,就是减小文件体积,加载文件资源的时间也就减少了,从而通过减少用户打开页面所需的等待时间,来增强用户体验。

    二、深入理解 Tree-shaking

    我们已经了解了 tree-shaking 的本质是消除无用的 js 代码。那么什么是无用代码?怎么消除无用代码?接下来让我们从 DCE 开始揭开它神秘的面纱,一探究竟吧~

    1. DCE(dead code elimination)

    无用代码在我们的代码中其实十分常见,消除无用代码也就拥有了自己的专业术语 - dead code elimination(DCE)。实际上,编译器可以判断出哪些代码并不影响输出,然后消除这些代码。

    tree-shaking 是 DCE 的一种新的实现,Javascript 同传统的编程语言不同的是,javascript 绝大多数情况需要通过网络进行加载,然后执行,加载的文件大小越小,整体执行时间更短,所以去除无用代码以减少文件体积,对 javascript 来说更有意义。tree-shaking 和传统的 DCE 的方法又不太一样,传统的 DCE 消灭不可能执行的代码,而 tree-shaking 更关注消除没有用到的代码。

    DCE

    • 代码不会被执行,不可到达

    • 代码执行的结果不会被用到

    • 代码只会影响死变量,只写不读

    传统编译型的预言都是由编译器将 Dead Code 从 AST (抽象语法树)中删除,了解即可。那么 tree-shaking 是如何 消除 javascript 无用代码的呢?

    tree-shaking 更关注于消除那些引用了但并没有被使用的模块,这种消除原理依赖于 ES6 的模块特性。所以先来了解一下 ES6 模块特性:

    ES6 Module

    • 只能作为模块顶层的语句出现

    • import 的模块名只能是字符串常量

    • import binding 是 immutable 的

    了解了这些前提,让我们动手用代码来验证下吧!

    2. Tree-shaking 消除

    tree-shaking 的使用前面已经介绍过,接下来的实验中,创建了 index.js 作为入口文件,打包生成代码到 bundle.js 中,除此之外的 a.js、util.js 等文件均作为被引用的依赖模块。

    1) 消除变量


    从上图中可以看到,我们定义的变量 b 和变量 c 都没有使用到,它们并没有出现在打包后的文件中。

    2) 消除函数


    从上图中可以看到,仅引入但未使用到的 util1()和 util2()函数方法并没有打包进来。

    3) 消除类

    仅增加引用但不调用时


    只引用类文件 mixer.js 但实际代码中并未用到 menu 的任何方法和变量时,我们通过实验可以看到,在新版本的 rollup 中消除类方法已经被实现了!

    4) 副作用

    但是,并不是说所有的副作用都被 rollup 解决了。参考相关文章,相对于 Webpack,rollup 在消除副作用方面有很大优势。但对于下列情况下的副作用,rollup 也无能为力:

    1)模块中类的方法未被引用 2)模块中定义的变量影响了全局变量

    参考下图,可以很清晰看到结果,大家也可以自己到rollup 官网提供的平台动手实践一下,:

    小结

    从上述打包结果我们可以看到,rollup 工具用于打包是非常轻量简洁的,从入口文件导入依赖模块到输出打包后的 bundle 文件,只保留了需要的代码。也就是说,在 rollup 打包中无需增加额外配置,只要你的代码符合 ES6 语法规范,就能实现 tree-shaking。Nice!

    那么,这个打包过程中的 tree-shaking 大概可以理解为必须具备以下两个关键实现:

    • ES6 的模块引入是静态分析的,可以在编译时正确判断到底加载了什么代码。

    • 分析程序流,判断哪些变量被使用、引用,打包这些代码。

    而 tree-shaking 的核心就包含在这个分析程序流的过程中:基于作用域,在 AST 过程中对函数或全局对象形成对象记录,然后在整个形成的作用域链对象中进行匹配 import 导入的标识,最后只打包匹配的代码,而删除那些未被匹配使用的代码。

    但同时,我们也要注意两点:

    • 尽可能少写包含副作用的代码,比如影响全局变量的这种操作尽可能避免;

    • 引用类实例化后,也会产生 rollup 处理不了的副作用。

    那么这个生成记录、匹配标识在程序流分析过程是如何实现的呢?

    接下来带你走进源码,一探究竟!

    三、 Tree-shaking 实现流程

    在解析流程中的 tree-shaking 实现之前,我们首先要了解两点前置知识:

    • rollup 中的 tree-shaking 使用 acorn 实现 AST 抽象语法树的遍历解析,acorn 和 babel 功能相同,但 acorn 更加轻量,在此之前 AST 工作流也是必须要了解的;

    • rollup 使用 magic-string 工具操作字符串和生成 source-map。

    流程图.png

    让我们从源码出发根据 tree-shaking 的核心原理详细地描述一下具体流程:

    • rollup()阶段,解析源码,生成 AST tree,对 AST tree 上的每个节点进行遍历,判断出是否 include(标记避免重复打包),是的话标记,然后生成 chunks,最后导出。

    • generate()/write()阶段,根据 rollup()阶段做的标记,进行代码收集,最后生成真正用到的代码。

    拿到源码 debug 起来~

    // perf-debug.js
    loadConfig().then(async config => // 获取收集配置
     (await rollup.rollup(config)).generate( 
      Array.isArray(config.output) ? config.output[0] : config.output
     )
    );
    

    debug 时可能最为关注的就是这一段代码了,一句话就是将输入打包为输出,也正对应上述流程。

    export async function rollupInternal(
     rawInputOptions: GenericConfigObject, // 传入参数配置
     watcher: RollupWatcher | null
    ): Promise<RollupBuild> {
     const { options: inputOptions, unsetOptions: unsetInputOptions } = await getInputOptions(
      rawInputOptions,
      watcher !== null
     );
     initialiseTimers(inputOptions);
    
     const graph = new Graph(inputOptions, watcher); // graph 包含入口以及各种依赖的相互关系,操作方法,缓存等,在实例内部实现 AST 转换,是 rollup 的核心
    
     const useCache = rawInputOptions.cache !== false; // 从配置中取是否使用缓存
     delete inputOptions.cache;
     delete rawInputOptions.cache;
    
     timeStart('BUILD', 1);
    
     try {
        // 调用插件驱动器方法,调用插件和提供插件环境上下文等
      await graph.pluginDriver.hookParallel('buildStart', [inputOptions]); 
      await graph.build();
     } catch (err) {
      const watchFiles = Object.keys(graph.watchFiles);
      if (watchFiles.length > 0) {
       err.watchFiles = watchFiles;
      }
      await graph.pluginDriver.hookParallel('buildEnd', [err]);
      await graph.pluginDriver.hookParallel('closeBundle', []);
      throw err;
     }
    
     await graph.pluginDriver.hookParallel('buildEnd', []);
    
     timeEnd('BUILD', 1);
    
     const result: RollupBuild = {
      cache: useCache ? graph.getCache() : undefined,
      closed: false,
      async close() {
       if (result.closed) return;
    
       result.closed = true;
    
       await graph.pluginDriver.hookParallel('closeBundle', []);
      },
      // generate - 将遍历标记处理过作为输出的抽象语法树生成新的代码
      async generate(rawOutputOptions: OutputOptions) {
       if (result.closed) return error(errAlreadyClosed());
          // 第一个参数 isWrite 为 false
       return handleGenerateWrite(
        false,
        inputOptions,
        unsetInputOptions,
        rawOutputOptions as GenericConfigObject,
        graph
       );
      },
      watchFiles: Object.keys(graph.watchFiles),
      // write - 将遍历标记处理过作为输出的抽象语法树生成新的代码
      async write(rawOutputOptions: OutputOptions) {
       if (result.closed) return error(errAlreadyClosed());
          // 第一个参数 isWrite 为 true
       return handleGenerateWrite(
        true,
        inputOptions,
        unsetInputOptions,
        rawOutputOptions as GenericConfigObject,
        graph
       );
      }
     };
     if (inputOptions.perf) result.getTimings = getTimings;
     return result;
    }
    

    单从这一段代码当然看不出来什么,下面我们一起解读源码来梳理 rollup 打包流程并探究 tree-shaing 的具体实现,为了更简单粗暴直接地看懂打包流程,我们对于源码中的插件配置中一律略过,只分析功能过程实现的核心流程。

    1. 模块解析

    获取文件绝对路径

    通过 resolveId()方法解析文件地址,拿到文件绝对路径,拿到绝对路径是我们的主要目的,更为细节的处理此处不作分析。

    export async function resolveId(
     source: string,
     importer: string | undefined,
     preserveSymlinks: boolean,) {
     // 不是以 . 或 / 开头的非入口模块在此步骤被跳过
     if (importer !== undefined && !isAbsolute(source) && source[0] !== '.') return null;
      // 调用 path.resolve,将合法文件路径转为绝对路径
     return addJsExtensionIfNecessary(
      importer ? resolve(dirname(importer), source) : resolve(source),
      preserveSymlinks
     );
    }
    
    // addJsExtensionIfNecessary() 实现
    function addJsExtensionIfNecessary(file: string, preserveSymlinks: boolean) {
     let found = findFile(file, preserveSymlinks);
     if (found) return found;
     found = findFile(file + '.mjs', preserveSymlinks);
     if (found) return found;
     found = findFile(file + '.js', preserveSymlinks);
     return found;
    }
    
    // findFile() 实现
    function findFile(file: string, preserveSymlinks: boolean): string | undefined {
     try {
      const stats = lstatSync(file);
      if (!preserveSymlinks && stats.isSymbolicLink())
       return findFile(realpathSync(file), preserveSymlinks);
      if ((preserveSymlinks && stats.isSymbolicLink()) || stats.isFile()) {
       const name = basename(file);
       const files = readdirSync(dirname(file));
    
       if (files.indexOf(name) !== -1) return file;
      }
     } catch {
      // suppress
     }
    }
    

    rollup()阶段

    rollup() 阶段做了很多工作,包括收集配置并标准化、分析文件并编译源码生成 AST、生成模块并解析依赖,最后生成 chunks。为了搞清楚 tree-shaking 作用的具体位置,我们需要解析更内层处理的代码。

    首先,通过从入口文件的绝对路径出发找到它的模块定义,并获取这个入口模块所有的依赖语句并返回所有内容。

    private async fetchModule(
     { id, meta, moduleSideEffects, syntheticNamedExports }: ResolvedId,
     importer: string | undefined, // 导入此模块的引用模块
     isEntry: boolean // 是否入口路径
    ): Promise<Module> { 
      ...
       // 创建 Module 实例
     const module: Module = new Module(
      this.graph, // Graph 是全局唯一的图,包含入口以及各种依赖的相互关系,操作方法,缓存等
      id,
      this.options,
      isEntry,
      moduleSideEffects, // 模块副作用
      syntheticNamedExports,
      meta
     );
     this.modulesById.set(id, module);
     this.graph.watchFiles[id] = true;
     await this.addModuleSource(id, importer, module);
     await this.pluginDriver.hookParallel('moduleParsed', [module.info]);
     await Promise.all([
       // 处理静态依赖
      this.fetchStaticDependencies(module),
      // 处理动态依赖
      this.fetchDynamicDependencies(module)
     ]);
     module.linkImports();
      // 返回当前模块
     return module;
    }
    

    分别在fetchStaticDependencies(module),fetchDynamicDependencies(module)中进一步处理依赖模块,并返回依赖模块的内容。

    private fetchResolvedDependency(
     source: string,
     importer: string,
     resolvedId: ResolvedId
    ): Promise<Module | ExternalModule> {
     if (resolvedId.external) {
      const { external, id, moduleSideEffects, meta } = resolvedId;
      if (!this.modulesById.has(id)) {
       this.modulesById.set(
        id,
        new ExternalModule( // 新建外部 Module 实例
         this.options,
         id,
         moduleSideEffects,
         meta,
         external !== 'absolute' && isAbsolute(id)
        )
       );
      }
    
      const externalModule = this.modulesById.get(id);
      if (!(externalModule instanceof ExternalModule)) {
       return error(errInternalIdCannotBeExternal(source, importer));
      }
       // 返回依赖的模块内容
      return Promise.resolve(externalModule);
     } else {
        // 存在导入此模块的外部引用,则递归获取这个入口模块所有的依赖语句
      return this.fetchModule(resolvedId, importer, false);
     }
    }
    

    每个文件都是一个模块,每个模块都会有一个 Module 实例。在 Module 实例中,模块文件的代码通过 acorn 的 parse 方法遍历解析为 AST 语法树。

    const ast = this.acornParser.parse(code, {
     ...(this.options.acorn as acorn.Options),
     ...options
    });
    

    最后将 source 解析并设置到当前 module 上,完成从文件到模块的转换,并解析出 ES tree node 以及其内部包含的各类型的语法树。

    setSource({
     alwaysRemovedCode,
     ast,
     code,
     customTransformCache,
     originalCode,
     originalSourcemap,
     resolvedIds,
     sourcemapChain,
     transformDependencies,
     transformFiles,
     ...moduleOptions
    }: TransformModuleJSON & {
     alwaysRemovedCode?: [number, number][];
     transformFiles?: EmittedFile[] | undefined;
    }) {
     this.info.code = code;
     this.originalCode = originalCode;
     this.originalSourcemap = originalSourcemap;
     this.sourcemapChain = sourcemapChain;
     if (transformFiles) {
      this.transformFiles = transformFiles;
     }
     this.transformDependencies = transformDependencies;
     this.customTransformCache = customTransformCache;
     this.updateOptions(moduleOptions);
    
     timeStart('generate ast', 3);
    
     this.alwaysRemovedCode = alwaysRemovedCode || [];
     if (!ast) {
      ast = this.tryParse();
     }
     this.alwaysRemovedCode.push(...findSourceMappingURLComments(ast, this.info.code));
    
     timeEnd('generate ast', 3);
    
     this.resolvedIds = resolvedIds || Object.create(null);
    
     this.magicString = new MagicString(code, {
      filename: (this.excludeFromSourcemap ? null : fileName)!, // 不包括 sourcemap 中的辅助插件
      indentExclusionRanges: []
     });
     for (const [start, end] of this.alwaysRemovedCode) {
      this.magicString.remove(start, end);
     }
    
     timeStart('analyse ast', 3);
      // ast 上下文环境,包装一些方法,比如动态导入、导出等,东西很多,大致看一看
     this.astContext = {
      addDynamicImport: this.addDynamicImport.bind(this), // 动态导入
      addExport: this.addExport.bind(this),
      addImport: this.addImport.bind(this),
      addImportMeta: this.addImportMeta.bind(this),
      code,
      deoptimizationTracker: this.graph.deoptimizationTracker,
      error: this.error.bind(this),
      fileName,
      getExports: this.getExports.bind(this),
      getModuleExecIndex: () => this.execIndex,
      getModuleName: this.basename.bind(this),
      getReexports: this.getReexports.bind(this),
      importDescriptions: this.importDescriptions,
      includeAllExports: () => this.includeAllExports(true), // include 相关方法标记决定是否 tree-shaking
      includeDynamicImport: this.includeDynamicImport.bind(this), // include...
      includeVariableInModule: this.includeVariableInModule.bind(this), // include...
      magicString: this.magicString,
      module: this,
      moduleContext: this.context,
      nodeConstructors,
      options: this.options,
      traceExport: this.getVariableForExportName.bind(this),
      traceVariable: this.traceVariable.bind(this),
      usesTopLevelAwait: false,
      warn: this.warn.bind(this)
     };
    
     this.scope = new ModuleScope(this.graph.scope, this.astContext);
     this.namespace = new NamespaceVariable(this.astContext, this.info.syntheticNamedExports);
      // 实例化 Program,将 ast 上下文环境赋给当前模块的 ast 属性上
     this.ast = new Program(ast, { type: 'Module', context: this.astContext }, this.scope);
     this.info.ast = ast;
    
     timeEnd('analyse ast', 3);
    }
    

    2. 标记模块是否可 Tree-shaking

    继续处理当前 module,根据 isExecuted 的状态及 treeshakingy 相关配置进行模块以及 es tree node 的引入,isExecuted 为 true 意味着这个模块已被添加入结果,以后不需要重复添加,最后也是根据 isExecuted 收集所有需要的模块从而实现 tree-shaking。

    // 以标记声明语句为例,includeVariable()、includeAllExports()方法不一一列出
    private includeStatements() {
     for (const module of [...this.entryModules, ...this.implicitEntryModules]) {
      if (module.preserveSignature !== false) {
       module.includeAllExports(false);
      } else {
       markModuleAndImpureDependenciesAsExecuted(module);
      }
     }
     if (this.options.treeshake) {
      let treeshakingPass = 1;
      do {
       timeStart(`treeshaking pass ${treeshakingPass}`, 3);
       this.needsTreeshakingPass = false;
       for (const module of this.modules) {
            // 根据 isExecuted 进行标记
        if (module.isExecuted) {
         if (module.info.hasModuleSideEffects === 'no-treeshake') {
          module.includeAllInBundle();
         } else {
          module.include(); // 标记
         }
        }
       }
       timeEnd(`treeshaking pass ${treeshakingPass++}`, 3);
      } while (this.needsTreeshakingPass);
     } else {
      for (const module of this.modules) module.includeAllInBundle();
     }
     for (const externalModule of this.externalModules) externalModule.warnUnusedImports();
     for (const module of this.implicitEntryModules) {
      for (const dependant of module.implicitlyLoadedAfter) {
       if (!(dependant.info.isEntry || dependant.isIncluded())) {
        error(errImplicitDependantIsNotIncluded(dependant));
       }
      }
     }
    }
    

    module.include 内部涉及到 ES tree node 了,由于 NodeBase 初始 include 为 false,所以还有第二个判断条件:当前 node 是否有副作用 side effects。这个是否有副作用是继承于 NodeBase 的各类 node 子类自身的实现,以及是否影响全局。rollup 内部不同类型的 es node 实现了不同的 hasEffects 实现,在不断优化过程中,对类引用的副作用进行了处理,消除引用却未使用的类,此处可结合第二章节中的 tree-shaking 消除进一步理解。

    include(): void { /  include()实现
     const context = createInclusionContext();
     if (this.ast!.shouldBeIncluded(context)) this.ast!.include(context, false);
    }
    

    3. treeshakeNode()方法

    在源码中有 treeshakeNode()这样一个方法去除无用代码,调用的时候也清楚地备注了 ---  防止重复声明相同的变量/节点,通过 included 标记节点代码是否已被包含,是的情况下 tree-shaking,同时还提供 removeAnnotations()方法删除多余注释代码。

    // 消除无用节点
    export function treeshakeNode(node: Node, code: MagicString, start: number, end: number) {
     code.remove(start, end);
     if (node.annotations) {
      for (const annotation of node.annotations) {
       if (!annotation.comment) {
        continue;
       }
       if (annotation.comment.start < start) {
        code.remove(annotation.comment.start, annotation.comment.end);
       } else {
        return;
       }
      }
     }
    }
    // 消除注释节点
    export function removeAnnotations(node: Node, code: MagicString) {
     if (!node.annotations && node.parent.type === NodeType.ExpressionStatement) {
      node = node.parent as Node;
     }
     if (node.annotations) {
      for (const annotation of node.annotations.filter((a) => a.comment)) {
       code.remove(annotation.comment!.start, annotation.comment!.end);
      }
     }
    }
    

    调用 treeshakeNode()方法的时机很重要!在渲染前 tree-shaking 并递归地去渲染。

    render(code: MagicString, options: RenderOptions, nodeRenderOptions?: NodeRenderOptions) {
      const { start, end } = nodeRenderOptions as { end: number; start: number };
      const declarationStart = getDeclarationStart(code.original, this.start);
    
      if (this.declaration instanceof FunctionDeclaration) {
       this.renderNamedDeclaration(
        code,
        declarationStart,
        'function',
        '(',
        this.declaration.id === null,
        options
       );
      } else if (this.declaration instanceof ClassDeclaration) {
       this.renderNamedDeclaration(
        code,
        declarationStart,
        'class',
        '{',
        this.declaration.id === null,
        options
       );
      } else if (this.variable.getOriginalVariable() !== this.variable) {
       // tree-shaking 以防止重复声明变量
       treeshakeNode(this, code, start, end);
       return;
          // included 标识做 tree-shaking
      } else if (this.variable.included) {
       this.renderVariableDeclaration(code, declarationStart, options);
      } else {
       code.remove(this.start, declarationStart);
       this.declaration.render(code, options, {
        isCalleeOfRenderedParent: false,
        renderedParentType: NodeType.ExpressionStatement
       });
       if (code.original[this.end - 1] !== ';') {
        code.appendLeft(this.end, ';');
       }
       return;
      }
      this.declaration.render(code, options);
     }
    

    类似的地方还有几处,tree-shaking 就是在这些地方发光发热的!

    // 果然我们又看到了 included
    ...
    if (!node.included) {
      treeshakeNode(node, code, start, end);
      continue;
    }
    ...
    if (currentNode.included) {
     currentNodeNeedsBoundaries
       ? currentNode.render(code, options, {
        end: nextNodeStart,
        start: currentNodeStart
       })
       : currentNode.render(code, options);
    } else {
       treeshakeNode(currentNode, code, currentNodeStart!, nextNodeStart);
    }
    ...
    

    4. 通过 chunks 生成代码(字符串)并写入文件

    在 generate()/write()阶段,将经处理生成后的代码写入文件,handleGenerateWrite()方法内部生成了 bundle 实例进行处理。

    async function handleGenerateWrite(...) {
      ...
     // 生成 Bundle 实例,这是一个打包对象,包含所有的模块信息
     const bundle = new Bundle(outputOptions, unsetOptions, inputOptions, outputPluginDriver, graph);
     // 调用实例 bundle 的 generate 方法生成代码
     const generated = await bundle.generate(isWrite);
     if (isWrite) {
      if (!outputOptions.dir && !outputOptions.file) {
       return error({
        code: 'MISSING_OPTION',
        message: 'You must specify "output.file" or "output.dir" for the build.'
       });
      }
      await Promise.all(
         // 这里是关键:通过 chunkId 生成代码并写入文件
       Object.keys(generated).map(chunkId => writeOutputFile(generated[chunkId], outputOptions))
      );
      await outputPluginDriver.hookParallel('writeBundle', [outputOptions, generated]);
     }
     return createOutput(generated);
    }
    

    小结

    一句话概括来说就是:从入口文件出发,找出所有它读取的变量,找一下这个变量是在哪里定义的,把定义语句包含进来,而无关的代码一律抛弃,得到的即为我们想要的结果。

    总结

    本文基于对 rollup 源码对其打包过程中的 tree-shaking 原理进行解读,其实可以发现,针对简单的打包流程而言,源码中并未对代码做额外的神秘操作,只是做了遍历标记使用收集并对收集到的代码打包输出以及 included 标记节点 treeshakeNode 以避免重复声明而已。

    当然最关键的还是内部静态分析并收集依赖,这个过程处理起来比较复杂,但核心其实还是针对遍历节点:找到当前节点依赖的变量,访问的变量以及这些变量的声明语句。

    作为一个轻量快捷的打包工具,rollup 在打包函数工具库方便具有很大优势。归功于其偏向于代码处理的优势,源码体量相较于 Webpack 也是轻量得多,但菜鸡本菜如我依然觉得读源码是一个枯燥的过程...

    但是!如果仅仅是本着弄懂原理的目的,不妨先只关注核心代码流程,边边角角的细节放在后面,也许能增强阅读愉悦体验、加快攻略源码的步伐!

    参考资料

    • Tree-Shaking 与无效代码消除

    • Tree-Shaking 性能优化实践 - 原理篇

    • 你的 Tree-Shaking 并没什么卵用

    • 原来 rollup 这么简单之 tree shaking 篇

    展开全文
  • tree shaking 及其工作原理

    千次阅读 2021-02-15 15:31:41
    写在前面今天这道题目是在和小红书的一位面试官聊的时候:我:如果要你选择一道题目来考察面试者,你最有可能选择哪一道?面试官:那应该就是介绍一下tree shaking及其工作原理?我:为什么...
  • flutter打包时报错

    2020-11-14 22:39:55
    报错信息:This application cannot tree shake icons fonts. It has non-constant instance What went wrong: Execution failed for task ‘:app:compileFlutterBuildRelease’. Process ‘command ‘D:\...
  • Tree-Shaking原理

    千次阅读 2020-02-10 22:42:25
    Tree-Shaking性能优化实践 - 原理篇 一. 什么是Tree-shaking 先来看一下Tree-shaking原始的本意 上图形象的解释了Tree-shaking 的本意,本文所说的前端中的tree-shaking可以理解为通过工具"摇"我们的JS...
  • CSS的tree shaking

    2020-09-07 09:28:21
    一、使用purifyCss,遍历代码,识别已经用到的CSS purifyCSS不再维护,使用purgecss-webpack-plugin基于purifycss开发的,mini-css-exract-plugin提取CSS插件配合使用 二、uncss,html通过jsdom加载,样式通过... ...
  • webpack 如何优雅的使用tree-shaking(摇树优化)

    万次阅读 多人点赞 2017-08-15 20:59:36
    webpack 如何优雅的使用tree-shaking1.什么是tree-shakingwebpack 2 的到来带来的最棒的新特性之一就是tree-shaking 。tree-shaking源自于rollup.js,先如今,webpack 2也有类似的做法。webpack 里的tree-shaking的...
  • webpack5的tree shaking值得了解

    千次阅读 2021-01-10 21:43:26
    什么是 tree shaking?tree shaking 的意思是,webpack 在打包的时候将会剔除掉被没有被使用到的代码达到减小报体积,缩短 http 请求时间,起到一定效果的页面优化。 那么我们如何使用 tree shaking 呢?这个功能...
  • 星号密码查看,拨号密码查看,bios密码查看,IE分级审查密码及IE自动完成密码查看软件侠客密码查看器4.35
  • Vue 3 | 万物皆可 TreeShaking

    千次阅读 2020-07-21 18:20:41
    关于 TreeShaking 这件事情,官方已经强调无数次了,我也在之前的文章中有提到,原因就是 Vue 团队希望能帮助开发者减小 Web 应用的体积。什么是 TreeShakingT...
  • Flutter安卓打包报错 版本: ...Framework • revision 9b2d32b605 (4 weeks ago) • 2021-01-22 14:36:39 -0800 Engine • revision 2f0af37152 Tools • Dart 2.10.5 ...flutter build apk --no-tree-shake-i
  • tree-shake-css-源码

    2021-05-25 20:31:19
    摇树CSS 您会注意到app1.js仅从./lib导入a ,这是一个同时导入a和b的文件。 因此,即使我们仅使用a内容,最终结果也是a和b的css。 我知道为什么会这样。 我不是要解释。 我只是想提出最好的方法(从lib模块的用户...
  • Tree Shaking只支持ES6的 ES module

    千次阅读 2019-03-31 12:09:36
    Tree Shaking只支持ES6的 ES module webpack.config.js module.exports={ mode:'development' ..., optimization:{ usedExports:true } } package.json { "name":"lesson", //方式一 "sideEffects":false,...
  • 本文作者“相学长”原文地址:http://suo.im/6nDqfr本文将探讨tree-shaking在当下的现状,以及研究为什么tree-shaking依旧举步维艰的原因,最终总结当下...
  • Tree Shaking只支持ES模块的使用,不支持require这种动态引入模块的方式。 前端中的tree-shaking可以理解为通过工具"摇"我们的JS文件,将其中用不到的代码"摇"掉,是一个性能优化的范畴。具体来说,在 webpack 项目...
  • 概念:1 个模块可能有多个⽅法,只要其中的某个⽅法使⽤到了,则整个⽂件都会被打到 bundle ⾥⾯去,tree shaking 就是只把⽤到的⽅法打⼊ bundle ,没⽤到的⽅法会在 uglify 阶段被擦除掉。 使⽤: webpack 默认...
  • 我们要学习“tree shaking”是啥以及如何使用。你会学到在webpack 4中使用“tree shaking”技术有什么要求,它能带来什么好处。开始吧! Webpack 4 Tree Shaking 首先,我们回答“tree shaking”是啥技术以及它能...
  • 最近从小被子那里学了不少 Tree-shaking 的知识,Tree-shaking 译作“摇树优化”,是 DCE(Dead Code Elimination)优化的一种实现。 Webpack 在编译阶段,通过分析模块依赖,对代码中未使用到的对象和 function ...
  • Webpack4: Tree-shaking 深度解析

    千次阅读 2019-02-15 04:36:25
    什么是Tree-shaking 所谓Tree-shaking就是‘摇’的意思,作用是把项目中没必要的模块全部抖掉,用于在不同的模块之间消除无用的代码,可列为性能优化的范畴。 Tree-shaking早期由rollup实现,后来webpack2也实现了...
  • 初级驱动程序入门者用,包含定时器,设备驱动中断处理等
  • 模块化(Commonjs与ES6 Module) 描述: 1.CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。... 参考文献 1.ES6模块化 2.Webpack tree shaking
  • 话不多说,直接上错误,This application cannot tree shake icons fonts. It has non-constant instances of IconData at the following locations: file:///Users/mac/workSpace/03_AndroidStudioProject/...
  • 计算机网络 课后题答案计算机网络 课后题答案计算机网络 课后题答案计算机网络 课后题答案计算机网络 课后题答案
  • 原文地址:How to do proper tree-shaking in Webpack 2 原文作者:Gábor Soós 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m… 译者:薛定谔的猫 校对者:lsvih、lampui 如何在 Webpack 2 中...
  • shake-the-tree-源码

    2021-05-06 21:12:58
    故事:上面有一棵树,上面长着大而多汁的红苹果。 我们想收集那些苹果,但我们无法拿到它们。 因为树很高。 我们必须摇树丢苹果。 之后,我们可以将苹果放到我们的苹果篮子里。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,557
精华内容 622
关键字:

treeshake