huahuadavids

huahuadavids 查看完整档案

上海编辑安徽工业大学  |  工业设计 编辑上海某公司  |  web前端工程师 编辑 github.com/huahuadavids 编辑
编辑

nothing to say, but day day up

个人动态

huahuadavids 发布了文章 · 1月19日

算法导论 第二章-算法基础

算法导论 第二章-算法基础

插入排序

插入排序,对有限元素比较合适的算法。和打牌差不多。

伪代码

INSERTION-SORT[A]
for j ← 2 to length[A] //  for 循环 j = 2, j < 数组A的length 
    do key ← A[j]      //  A[j] 的值 给变量 key
    i ← j-1            //  i = j - 1 
    while i > 0 and A[i] >key // 内层循环 
        do A[i+1] ← A[i]
        i ← i-1
    A[i+1] ← key

循环不变式用来理解算法的正确性
1.初始化:循环的第一次迭代之前,它为真。
2.保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真。
3.终止:在循环终止时,不变式为我们提供了一个有用的性质,该性质有助于证明算法是正确的

比如一个数组 [1, 22, 3, 4, 44, 55], 跟打牌一样,元素 A[1, j-1] 是手里的牌,[j, n] 是桌上的牌

数学归纳法,要证明某一性质正确,必须首先证明基本情况和一个归纳步骤都是正确的。
归纳法中,归纳步骤是无穷使用的。

就插入排序而言,证明一下。
初始化,当j=2,数组[1..j-1],就1个元素,显然是正确的
保持,证明没一次循环都能让循环不变式成立,在外层循环中,让A[j-1], A[j-2],A[j-3]向右移动,找到A[j]的合适位置。
终止,在j>n时,循环结束了,把j替换为n+1,子数组A[1, n]包含了原来A[1,n]的元素,现在排序好了,他就是整个数组,所以这是正确的。

算法的运行时间,是指在特定的输入时,所执行的基本操作数。

  // 插入排序 js版本实现 
  function insertSort(arr) {
    let len = arr.length;
    for (let a = 1; a < len; a++) {
      // 当前值,当前牌
      let cur = arr[a];
      let b = a - 1;
      // 找到一个合适的位置,把cur插进去
      // arr[b] 是排序好的最大的一个,如果这个不大于cur,那就不用排了
      while (b > 0 && cur < arr[b]) {
        arr[b + 1] = arr[b];
        b--;
      }
      arr[b + 1] = cur;
    }
    return arr;
  }

分析算法

  • 分析算法,通常我们想度量的是时间。
  • 我们一般假定是单处理器计算模型 RAM作为实现技术,指令一条一条执行,没有并发操作。
  • RAM模型,包含真实计算机的指令,如算数指令(加减乘除等)、数据移动指令、控制指令,每条这样的指令需要的时间都是常数。
  • 一般来说,算法的时间与输入规模同步增长。所以通常把一个算法的运行时间,描述成输入规模的函数。
  • 输入规模的最佳概念依赖于研究的问题。
  • 一个算法在特定输入上的运行时间,是执行的基本操作数或者步数。

最坏情况和平均情况分析

对于插入排序的分析。通过具体分析,在最优情况,即数组是排序好的情况下,运行时间可表示为

a*n+b

对于最差的情况,是

a * n的平方 + b * n + c 

在一般情况下,我们是考察算法的最坏运行情况。

增长的量级

运行时间的增长率,或者叫做增长的量级,这样我们只考虑公式的最高次项,以为当n很大,低阶项就不重要了
另外,还要忽略最高次项的常数次数。例如,插入排序的最差时间代价是 Θ(n2)
如果一个算法的最坏运行时间比另外一个低,那么我们认为它的效率更高。

算法设计

插入排序使用的是,增量方法,在排好序的子数组插入元素。

分治法

分治策略是,把原问题划分为n个规模较小,结构与原问题相似的子问题。
递归解决子问题,然后合并结果得到原问题的解。
分治模式,在每一层递归上,都有3个步骤。

  1. 分解,把原问题划分为子问题。
  2. 解决,递归解决子问题,子问题足够小则直接解决。
  3. 合并,子问题的解合并为原问题的解。

以下是归并排序的伪代码

MERGE(A, p, q, r)
    n1 = q-p+1
    n2 = r-q
    //let L[1....n1+1] and R[1....n2+1] be new arrays
    for i =1 to n1
         L[i] = A[p+i-1]
    for j=1 to n2
        R[j] = A[q+j]
    L[n1+1] = ∞
    L[n2+1] = ∞
    i=1
    j=1
    for k =p to r
        if L[i]<=R[j]
            A[k] = L[i]
            i = i + 1
        else
            A[k]=R[j]
            j = j+1

MERGE-SORT(A,p,r)
    if p < r
        q =(p+r)/2  //向下取整,不会打那个符号
        MERGE-SORT(A, p, q)
        MERGE-SORT(A, q+1, r)
        MERGE(A, p, q, r)

归并排序的关键是,合并两个已排序好的子序列。通过一个 MERGE(A,p,q,r) 来实现 p <= q <= r
前提是,子数组 A[p, q]A[q+1, r] 是排序好的数组。通过合并这两个排序好的子数组来代替当前
的子数组 A[p,r];

以下是JavaScript的实现,开始并不好理解,可以打开debugger,一步一步的去看,就比较清晰了

function mergeSort(arr, start, end) {
  if (start < end) {
    let m = Math.floor((start + end) / 2);
    mergeSort(arr, start, m);
    mergeSort(arr, m + 1, end);
    return merge(arr, start, m, end);
  }
}

function merge(arr, start, mid, end) {
  /*
    这是原书的代码
    // 创建左子数组
    let n1 = mid - start + 1;
    // 创建一个数组,size是子数组的长度+1
    let L = new Array(n1 + 1);
    for (let i = 0; i < n1; i++) {
      L[i] = arr[start + i];
    }

    // 创建右子数组
    let n2 = end - mid;
    // 创建一个数组,size是子数组的长度+1
    let R = new Array(n2 + 1);
    for (let j = 0; j < n2; j++) {
      R[j] = arr[mid + j + 1];
    }
  */

  // js代码可以很简单的实现上边的功能
  let L = [...arr.slice(start, mid + 1), Infinity];
  let R = [...arr.slice(mid + 1, end + 1), Infinity];
  let i = 0, j = 0;

  for (let k = 0; k < end - start + 1; k++) {
    // debugger
    if (L[i] <= R[j]) {
      arr[start + k] = L[i];
      i++;
    } else {
      arr[start + k] = R[j];
      j++;
    }
  }
  return arr;
}

分治法分析

当一个算法中含有递归调用时,运行时间可用一个递归方程表示。T(n)
如果问题规模足够小,如 n <= c(常量),那么表示为Θ(1)
把原问题分解为a个子问题,每个问题规模是原问题的 1/b, 分解问题是D(n) ,合并问题是C(n)
那么T(n) = aT(n/b) + D(n) + C(n)
归并算法的分析
分解,需要常量时间 D(n) = Θ(1)
解决, 递归解决两个规模是n/2的子问题,时间是2T(n/2)
合并,合并n的元素的子数组,merge时间是Θ(n), 所以C(n) = O(n)
所以带入 T(n) = aT(n/b) + D(n) + C(n) 就是
T(n) = 2T(n/2) + Θ(n) + Θ(n)
T(n) = 2T(n/2) + Θ(n)
我们用常数c代表Θ(1),于是得到 T(n) = 2T(n/2) + cn
递归树总的层数是 lgn+1 ,每一层的代价是 cn
所以 T(n) = cn(lgn+1) = nlgn

查看原文

赞 0 收藏 0 评论 0

huahuadavids 发布了文章 · 1月11日

重读webpack5

重读webpack5

基础补充

关于harmony modules
ES2015 modules 又叫做 harmony modules

关于副作用:
webpack的 side effect副作用,是指在import后执行特定行为的代码,而不是export一个或者多个,例如 pollyfill,全局css样式等

关于entry:
entry 对象是webpack开始 build bundle 的地方。

关于context:
context 是包含入口文件的目录的绝对字符串,默认就是当前目录,但是建议设置。

关于依赖图:
webpack是 dynamically bundle 依赖通过依赖图 dependency graph,避免打包没用的module。

关于Loader:
module loader可链式调用,链中的每个loader都将处理资源,调用顺序是反的。

关于图片:
webpack5内置了处理图片、字体文件,不需要额外的loader来处理 。

{
 test: /\.(png|svg|jpg|jpeg|gif)$/i,
 type: 'asset/resource',
}

关于 csv、xml
csv-loader来加载csv文件数据, xml-loader来加载xml文件数据 。
可以使用 parser 而不是loader来处理toml, yamljs and json5格式的资源,如下

// webpack.config.js
const yaml = require('yamljs');
module: {
    rules: [
       {
          test: /\.yaml$/i,
          type: 'json',
          parser: {
            parse: yaml.parse,
          }
        }
    ]
}
// 代码中使用
import yaml from './data.yaml';
console.log(yaml)

关于html-webpack-plugin:
html-webpack-plugin安装方式 npm i --save-dev html-webpack-plugin@next

关于manifest
webpack使用manifest来track module映射到bundle的关系,使用webpack-manifest-plugin

关于sourceMap
source maps 用做track js的error和warning,可以把编译后的代码指向源代码,定位异常的确切位置。

ESM

就是ECMAScript Modules,在package.json 中增加如下配置,强迫项目中的文件,使用ESM

{
  "type": "module"
}

除了这种方式, 文件可以设置模块方式通过 .mjs 或者 .cjs 的后缀.
.mjs强迫使用ESM模块 ,.cjs就是 CommonJs模块.

shim

两种情况,全局变量如jq或者浏览器polyfill。

ProvidePlugin让一个包在webpack的编译过程中作为一个可用变量,最后webpack发现这个变量使用了,就会在最后的
bundle中引入此包。例如把lodash变成全局变量,这样在模块中就不需要import了

plugins: [
  new webpack.ProvidePlugin({
      _: 'lodash',
  })
]

如果只需要lodash的一个chunk方法
new webpack.ProvidePlugin({
   join: ['lodash', 'join']
})

polyfill,详情

Asset Modules

Asset Modules 是一种允许用户使用assets文件(字体、图标等)而无需配置额外加载器的module。
webpack5之前,使用
raw-loader 以字符串形式导入文件
url-loader 将文件作为data URI内联到bundle中
file-loader 将文件发送到输出目录

Asset Modules使用如下方式来替代拿些loader
asset/resource 生成一个单独的文件并导出URL。以前可以通过使用file-loader实现
asset/inline 导出assets的data URI。以前可以通过使用url-loader实现
asset/source 导出资产的源代码。以前可以通过使用raw-loader实现
asset 自动选择是导出data URI还是生成单独的文件。以前可以通过使用具有asset大小限制的url-loader实现

如果使用webpack5,但是又不想修改之前的loader配置,可这么修改 type: 'javascript/auto'
这会停止asset module 再次处理那些assets

{
    test: /\.(png|jpg|gif)$/i,
    use: [
      {
        loader: 'url-loader',
        options: {
          limit: 8192,
        }
      }
    ],
    type: 'javascript/auto'
},

exclude 那些使用asset loader处理的新url

{
   test: /\.(png|jpg|gif)$/i,
   dependency: { not: ['url'] }, 
    use: [
      {
        loader: 'url-loader',
        options: {
          limit: 8192,
        }
      }
    ]
}

resource assets

// 如下,处理png文件 
 module: {
   rules: [
     {
       test: /\.png/,
       type: 'asset/resource'
     }
   ]
 },
 
 // 源代码
 import mainImage from './images/main.png';
 img.src = mainImage; // '/dist/151cfcfa1bd74779aadb.png'
 

自定义输出名

默认,asset/resource modules 使用 [hash][ext][query]` 的文件导出到输出文件夹,
我们可以修改这个使用output.assetModuleFilename覆盖

output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
   assetModuleFilename: 'images/[hash][ext][query]'
}

另外一种方法,是在rules中配置

{
   test: /\.html/,
   type: 'asset/resource',
   generator: {
     filename: 'static/[hash][ext][query]'
   }
}

Inline assets

// 配置 
{

  test: /\.svg/,
  type: 'asset/inline'
}

 // 源代码
 import metroMap from './images/metro.svg';
 block.style.background = `url(${metroMap})`; // url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo...vc3ZnPgo=)

自定义 data URI 生成器

默认webpack是使用base64算法实现的,也可以自定义实现

const path = require('path');
 const svgToMiniDataURI = require('mini-svg-data-uri');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.svg/,
        type: 'asset/inline',
       generator: {
         dataUrl: content => {
           content = content.toString();
           return svgToMiniDataURI(content);
         }
       }
      }
    ]
  },
};

Source assets

module: {
  rules: [
      {
       test: /\.txt/,
       type: 'asset/source',
      }
   ]
}

URL assets

// webpack 也会这样创建一个asset module
const logo = new URL('./logo.svg', import.meta.url);

// 不同的目标环境,webpack的处理不同
// target: web
new URL(__webpack_public_path__ + 'logo.svg', document.baseURI || self.location.href);

// target: webworker
new URL(__webpack_public_path__ + 'logo.svg', self.location);

// target: node, node-webkit, nwjs, electron-main, electron-renderer, electron-preload, async-node
new URL(__webpack_public_path__ + 'logo.svg', require('url').pathToFileUrl(__filename));

asset

如下,不指定哪种asset

 module: {
    rules: [
      {
       test: /\.txt/,
       type: 'asset',
       parser: {
                dataUrlCondition: {
                  maxSize: 4 * 1024 // 4kb
                }
        }
      }
    ]
  }

webpack会自动判断resource 还是 inline,如果文件尺寸小于8kb,就会被处理做inline,否则就是resource
可以使用parser.dataUrlCondition来覆盖。

开发服务

每次改了代码,重写打包会很麻烦,有3种解决方法,
webpack的watch模式
webpack-dev-server
webpack-dev-middleware,webpack-dev-middleware是一个包装器,它会将webpack处理过的文件发送到服务器,webpack-dev-server内部使用。

// 1. 使用webpack-dev-server,安装好包之后,在webpack.config.js 中配置
 devServer: {
    contentBase: './dist',
 }
// 2 使用 webpack-dev-middleware和express,安装之后,新建serverjs
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);

// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(
    webpackDevMiddleware(compiler, {
      publicPath: config.output.publicPath,
    })
);

// Serve the files on port 3000.
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

代码分割 codes splitting

lazy load 按需加载是优化应用的一种好方法。这种实践本质上涉及到按逻辑断点拆分代码,
然后在用户完成一些需要(或将需要)新代码块的操作后加载代码。这加快了应用程序的初始加载速度,并减轻了它的整体重量,
因为有些块甚至可能永远不会加载。

entry 分割

这样没什么用,用的lodash还是会整个打到最后的bundle中,虽然可以通过 import chunk from 'lodash/chunk'这种写法优化

// index.js
import _ from 'lodash'
console.log(  _.chunk([1,2,3,4], 2))

// index2.js
import _ from 'lodash'
console.log(_.join(['Another', 'module', 'loaded!'], ' '));

// webpack.config.js
entry: {
  index: './src/index.js',
  index2: './src/index2.js',
}

Prevent Duplication

入口的 dependOn 字段和 runtimeChunk: 'single' 的方法


// 入口配置
entry: {
    index: {
      import: './src/index.js',
      dependOn: 'shared',
    },
    index2: {
      import: './src/index2.js',
      dependOn: 'shared',
    },
    shared: 'lodash',
}
// webpack.config.js
 optimization: {
    runtimeChunk: 'single',
  }

SplitChunksPlugin允许我们将共同的依赖提取到一个现有的入口文件块或一个全新的chunk中。

 module.exports = {
    entry: {
      index: './src/index.js',
      another: './src/another-module.js',
    },
   optimization: {
     splitChunks: {
       chunks: 'all',
     },
   },
  };

动态导入 dynamic import

使用 require.ensure 或者 import()

// index.js 
// 我们需要 default 值的原因是,自从webpack 4以来,当导入CommonJS模块时,导入将不再解析为module.exports的值
import('lodash').then(({default: _}) => {
  console.log(
      _.chunk([1, 2, 3, 4], 2)
  )
})

Prefetching/Preloading modules

  • prefetch 预获取: 将来可能需要一些资源来支持运行。
  • preload 预加载: 在当前运行期间需要的资源。
  • preload chunk和父 chunk并行加载,prefetch chunk在父块加载完成后开始加载。
  • preload chunk具有中等优先级并立即下载。当浏览器空闲时,才会下载 prefetch chunk。
  • 父块应该立即请求preload chunk,prefetch chunk可能在将来的任何时候使用。
  • 浏览器支持不同。
  • 一个简单的预加载例子是,有一个总是依赖于一个大库的组件,这个库应该在一个单独的块中,例如 import(/* webpackPreload: true */ 'ChartingLibrary');
  • webpackPreload 不正确使用会对性能产生影响,谨慎使用。

缓存

浏览器会缓存文件,这会让web加载更快,减少不必要的流量。但是编译文件更新了就会造成麻烦。

Output Filename

contenthash 会根据文件内容计算一个字符串,文件内容变了就会改变。
但是哪怕内容重新打包,hash也不一定一样,webpack版本新的应该没这个问题,这是因为webpack在entry块中包含了某些样板文件,特别是 runtime 和 manifest。

output: {
  filename: '[name].[contenthash].js',
  path: path.resolve(__dirname, 'dist'),
}

提取样板

SplitChunksPlugin 可以用于将module拆分为单独的bundle。webpack提供了一个优化特性,可以使用optimize.runtimecchunk 选项将运行时代码分割成一个单独的chunk。将其设置为single,为所有块创建单个运行时bundle.

optimization: {
 runtimeChunk: 'single',
}

会得到下边的结果

                          Asset       Size  Chunks             Chunk Names
runtime.cc17ae2a94ec771e9221.js   1.42 KiB       0  [emitted]  runtime
   main.e81de2cf758ada72f306.js   69.5 KiB       1  [emitted]  main
                index.html  275 bytes          [emitted]

提取第三方库,像react等,它们一般不会变化,使用 cacheGroups 如下

optimization: {
     runtimeChunk: 'single',
     splitChunks: {
       cacheGroups: {
         vendor: {
           test: /[\\/]node_modules[\\/]/,
           name: 'common-libs',
           chunks: 'all',
         }
       }
     }
 }

得到如下结果

  asset common-libs.e6d769d50acd25e3ae56.js 1.36 MiB [emitted] [immutable] (name: common-libs) (id hint: vendor)
  asset runtime.2537ce2560d55e32a85c.js    15.9 KiB [emitted] [immutable] (name: runtime)
  asset index.f9c0d8e7e437c9cf3a6e.js     1.81 KiB [emitted] [immutable] (name: index)
  asset index.html 370 bytes [emitted]

Module ID

如果在index.js,增加一个引用新的文件的使用,重新打包,会发现,所有的hash都变了,但是公用库内容没变,hash还是变了,因为增加了新的文件会导致它们的moduleid发生变化,所以hash也变了。在配置中增加如下。

optimization: {
  // 告诉webpack在选择 模块id 时使用哪种算法,默认false 
  moduleIds: 'deterministic'
}

用webpack5测试,只有index.js的hash变了,runtime chunk 和 common-libs 都没变。

环境变量

webpack内置了环境变量的设置方法,

npx webpack --env NODE_ENV=local --env production --progress

但是module.exports必须是个方法

const path = require('path');

module.exports = env => {
  // Use env.<YOUR VARIABLE> here:
  console.log('NODE_ENV: ', env.NODE_ENV); // 'local'
  console.log('Production: ', env.production); // true

  return {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
  };
};

Production

开发和生产的目标差别很大。在开发中,我们需要强大的 source map 和一个本地主机服务器,可以实时重新加载或热模块替换。
在生产环境中,我们的目标转向关注小尺寸的bundle、轻量级的源映射和优化assets,以提高加载时间。
出于这种逻辑分离的目的,我们通常建议为每个环境编写单独的webpack配置。

新建webpack.common.js保存开发和生产共有的代码,webpack.dev.jswebpack.prod.js 配置各自环境,
需要 webpack-merge 来merge配置

依赖管理

如果你的require包含表达式则会创建一个上下文,因此在编译时不知道确切的模块

// 源目录
- modules/fn1.js
         /fn2.js   


// 源代码
let name = 'fn1';
const f = require("./modules/" + name + '.js')
console.log(f);


// webpack 这么处理
Directory: ./modules
Regular expression: /^.*\.js$/

生成一个context module。它包含了对该目录中所有模块的引用,匹配正则表达式的请求可能需要这些模块。
context模块包含一个映射,它将请求转换为模块id。

// webpack打包后有这么一个文件
var map = {
    "./fn1.js": 430,
    "./fn2.js": 698
};

这意味着支持动态需求,但会导致所有匹配的模块都包含在bundle中。

热更新 HMR

开启

如果是使用 webpack-dev-middleware 那么需要使用 webpack-hot-middleware 来开启热更新

// 在devserver开启
devServer: {
  contentBase: './dist',
  hot: true,
}

使用 node api

const webpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');

const config = require('./webpack.config.js');
const options = {
  contentBase: './dist',
  hot: true,
  host: 'localhost',
};

webpackDevServer.addDevServerEntrypoints(config, options);
const compiler = webpack(config);
const server = new webpackDevServer(compiler, options);

server.listen(5000, 'localhost', () => {
  console.log('dev server listening on port 5000');
});

摇树 Tree Shaking

摇树是用来删没用的代码的,它依赖于ES2015模块语法的静态结构就是 import 和 export,是由rollup发展而来。
webpack2之后,内置了对es6的支持以及未使用模块导出的检测。
webpack4扩展了这个功能,通过package.json的 sideEffects 字段,来表明 "纯文件" 可以安全删除。

源代码

// math.js

export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

// index.js

import {cube} from './math.js';
console.log(cube(5))

webpack.config.js

 mode: 'development',
 optimization: {
   usedExports: true,
 },

打包后的文件,没用的代码并没有删除

/*!*********************!*\
  !*** ./src/math.js ***!
  \*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "cube": () => /* binding */ cube
/* harmony export */ });
/* unused harmony export square */
function square(x) {
  return x * x;
}
function cube(x) {
  return x * x * x;
}

/***/ })

下边是没配置 usedExports: true,这个配置的作用,就是让webpack来判断哪些模块没使用,此配置依赖于
optimization.providedExports(告诉webpack哪些export是由模块提供的),默认就是true

/*!*********************!*\
  !*** ./src/math.js ***!
  \*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "square": () => /* binding */ square,
/* harmony export */   "cube": () => /* binding */ cube
/* harmony export */ });
function square(x) {
  return x * x;
}
function cube(x) {
  return x * x * x;
}

/***/ })

在100% ESM模块的世界中,识别副作用是很简单的,但是现在没到哪一步,所以需要在package.json中的sideEffects来告诉webpack,
标记文件在副作用树,上面提到的所有代码都没有副作用,所以我们可以简单地将该属性标记为false,以通知webpack它可以安全地修剪未使用的导出文件。

// 开启后,打包后的死代码,仍然没删除
{
  "name": "your-project",
  "sideEffects": false
}

// 如果有些文件确实有副作用,提供一个数组即可 
{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js"
  ]
}

注意,如果使用css-loader等加载样式,需要把css放到副作用数组中,防止在生产模式的时候,被webpack无意中删除。
还可以在webpack.config.js中的module.rules中的一个rule中指定。

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

side effects 和 usedExports(摇树)的区别

side effects可以直接抹去文件,例如
如果设置了 sideEffects: false, 然后在index.js引入一个 math.js但是不使用,打包后的bundle不会打包math.js
但是如果没设置,不使用的文件还是会在打包后的文件中,代码如下

/***/ 733:
/*!*********************!*\
  !*** ./src/math.js ***!
  \*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
/* unused harmony exports square, cube */
function square(x) {
  return x * x;
}
function cube(x) {
  return x * x * x;
}

/***/ })

usedExports 是依赖于terser来检测,在声明中的副作用,不如side effects直接
也无法直接跳过整个文件,React的高阶组件存在问题。

// 使用这个表明表示没有副作用,这会允许删掉这行代码,不分析他的side effect 
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);

因为文件的import声明不好判断可以在package.json 的 sideEffects 字段加入该文件
If no direct export from a module flagged with no-sideEffects is used,
the bundler can skip evaluating the module for side effects

// 使用这一行代码 
import { Button } from "@shopify/polaris"

// 下边是 @shopify/polaris 库里的文件 

// index.js
import './configure';
export * from './types';
export * from './components';
 
// components/index.js
export { default as Breadcrumbs } from './Breadcrumbs';
export { default as Button, buttonFrom, buttonsFrom, } from './Button';
export { default as ButtonGroup } from './ButtonGroup';

// package.json
"sideEffects": [
  "**/*.css",
  "**/*.scss",
  "./esnext/index.js",
  "./esnext/configure.js"
],

对于 import { Button } from "@shopify/polaris"这样代码,有以下4种情况

include it: include the module, evaluate it and continue analysing dependencies
skip over: don't include it, don't evaluate it but continue analysing dependencies
exclude it: don't include it, don't evaluate it and don't analyse dependencies

仔细分析代码经过的模块

index.js:     没使用直接export的代码, 但是用sideEffects标记了 -> include it
configure.js: 没使用直接export的代码, 但是用sideEffects标记了 -> include it
types/index.js: 没使用直接export的代码, 没sideEffects标记  -> exclude it
components/index.js: 没使用直接export的代码, 没sideEffects标记 , but reexported exports are used -> skip over
components/Breadcrumbs.js: 没使用直接export的代码, 没sideEffects标记 -> exclude it. 
This also excluded all dependencies like components/Breadcrumbs.css even if they are flagged with sideEffects.
components/Button.js: 使用直接export的代码, 没sideEffects标记-> include it
components/Button.css: 没使用直接export的代码, 但是用sideEffects标记了 -> include it

这样造成,直接引入的文件,只有4个

index.js: pretty much empty
configure.js
components/Button.js
components/Button.css

函数调用变副作用树

通过使用/*#__PURE__*/注释,可以告诉webpack函数调用是无副作用(纯)的。

它可以放在函数调用的前面,以标记它们为无副作用。传递给函数的参数没有被注释标记,可能需要单独标记。
当未使用变量声明中的初始值被认为是无副作用(pure)时,它将被标记为死代码,不会被执行,并被最小化者删除。
优化时启用此行为。innerGraph设置为true

/*#__PURE__*/ double(55);

减少代码

mode 设置为 production 即可,
--optimize-minimize 也可以开启 TerserPlugin,
module-concatenation-plugin这个插件在tree shaking中使用。

Build Performance

  • 保持 webpack、node、npm是最新版本
  • 使用 DllPlugin 打包
  • 在loader中减少解析范围
const path = require('path');
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, 'src'),
        loader: 'babel-loader',
      },
    ],
  },
};
Boot
  • 减少文件体积,移除没用的代码,
  • 使用缓存 cache配置

Module Federation

  • github demo
  • 多个独立的build应该构成唯一的应用程序。这些单独的build之间不应该有依赖关系,因此可以单独开发和部署它们。这通常被称为 微前端,但并不仅限于此
  • module两种,本地构建的模块(local module)、运行时容器加载的远程模块(remote module)。
  • 加载远程模块,是一个异步的加载chunk的操作,就是使用import、require.ensure或者require([])
  • 容器通过容器入口创建,并暴露出获取指定模块的方法,异步加载模块(chunk loading)和异步解析模块(解析期间与其他模块交叉执行)
  • 解析顺序,远程解析到本地,或者本地解析到远程,不会收到影响
  • 容器可以使用其他容器的模块,容器之间的依赖共享可以实现。
  • 容器可以标识模块为可重写,消费者提供重写方法,就是一个可以替换容器中可替换模块的模块
  • 当consumer提供一个模块时,容器的所有模块都将使用替换模块而不是本地模块,不提供替换模块,就使用本地模块
  • 容器用一种不需在被consumer重写时下载的方式来管理可重写模块,通常是通过将它们放入不同的chunk来实现的。
  • 替换模块的provider只提供异步加载方法,它允许容器按需加载替换模块。provider用在容器不需要的时候不加载的方式来管理替换模块,通常是通过将它们放入不同的chunk来实现的
  • name 用于标识容器中的可重写模块
  • 重写动作和容器暴露模块相似的方式类似,分为两步,异步加载和异步解析。
  • 当使用嵌套,给一个容器提供重写将会自动重写嵌套容器中具有相同“名称”的模块。
  • 重写必须在加载容器的模块之前提供。在初始块中使用的重写,只能被不使用Promise的同步模块重写。一旦被解析,重写项将不再是可重写的。
  • 每个构建都可作为容器,并可以消费别的构建作为容器使用。每个构建都可以通过从其容器中加载其他公开的模块来使用。
  • shared module是既可以重写又可以重写嵌套容器的模块。它们通常在每个构建中指向同一个模块,例如同一个库
  • packageName选项允许设置一个包名来查找所需的版本。默认情况下,自动推断模块请求,当自动推断应该被禁用时,将requiredVersion设置为false

Building blocks

  • OverridablesPlugin 此插件让一个模块,可重写
  • ContainerPlugin 此插件使用指定的exposed modules创建一个额外的容器entry,它内部使用OverridablesPlugin,并向容器的consumer公开override API
  • ContainerReferencePlugin 插件add特定的引用到容器作为externals,并允许从这些容器导入远程模块。它还调用这些容器的override API来提供对它们的override。本地重写(通过__webpack_override__或override API,当build也是一个容器时)和指定重写将被提供给所有引用的容器
  • ModuleFederationPlugin 此插件组合了ContainerPlugin和ContainerReferencePlugin,Overrides and overridables 会被组合到一个指定的共享模块列表中

Module Federation需要实现的目标

  • 应该可以公开和使用webpack支持的任何模块类型
  • chunk加载应并行加载所有需要的东西(web:到服务器的单程往返)
  • 从consumer到容器的控制,重写模块是单向操作,兄弟容器不能覆盖彼此的模块。
  • 应该独立于环境,web, Node.js等都可以用
  • 共享的相对和绝对请求(不使用也应该被提供、根据config.context来解析、不默认使用requiredVersion)
  • 共享的模块请求(按需提供,将匹配构建中使用的所有equal模块请求,将提供所有匹配模块,

它将从package.json中提取requiredVersion在图中的这个位置,可provide和consume多重不同版本当你有nested node_modules)

  • 带有后缀/共享的模块请求将匹配所有带有此前缀的模块请求

使用场景

  1. 每个页面单独构建

单个spa的每个页面都是在单独的构建中通过容器构建公开的,应用程序外壳是一个引用所有页面的单独构建的远程模块,
这样的话每个页面都可以单独部署,当路由更新或添加新路由时,应用程序外壳就会被部署。
应用程序外壳将常用库定义为共享模块,以避免在页面构建中重复使用它们

  1. 组件库作为容器

多应用共享一个公共的组件库,可以将其构建为公开每个组件的容器,每个应用消费组件库容器。
对组件库的更改可以单独部署而不需要重新部署所有应用程序。应用程序自动使用组件库的最新版本。

动态远程容器

容器接口支持get和init方法。init是一个异步兼容的方法,调用时只有一个参数:共享范围对象。
此对象在远程容器中用作共享scope,并由host填充provided modules。
它可以在运行时动态的连接 remote containers to a host container

(async () => {
  // 初始化shared scope,使用当前build和所有远程的provided modules
  await __webpack_init_sharing__('default');
  const container = window.someContainer; // or get the container somewhere else
  // Initialize the container, it may provide shared modules
  await container.init(__webpack_share_scopes__.default);
  const module = await container.get('./module');
})();

容器尝试提供共享模块,但如果共享模块已经被使用,则警告和提供的共享模块将被忽略。容器仍然可以使用它作为fallback。
通过这种方式,你可以动态加载一个A/B测试,它提供了一个共享模块的不同版本。

Package exports

一个包的package.json的exports字段,可以定义 import "package" 或者 import "package/sub/path"
的时候,哪些模块被使用。它替换了默认的行为。当定义了这些字段,只有这些字段是有效的,其他的都会
ModuleNotFound 错误。

{
  "exports": {
    ".": "./main.js",
    "./sub/path": "./secondary.js",
    "./prefix/": "./directory/",
    "./prefix/deep/": "./other-directory/",
    // 可以定义为一个数组,找到有效的就返回了
    "./things/": ["./good-things/", "./bad-things/"]
  }
}

// 结果如下
package                .../package/main.js
package/sub/path    .../package/secondary.js
package/prefix/some/file.js    .../package/directory/some/file.js
package/prefix/deep/file.js    .../package/other-directory/file.js
package/main.js    Error

条件式返回

{
  "exports": {
    ".": {
      "red": "./stop.js",
      "yellow": "./stop.js",
      "green": {
        "free": "./drive.js",
        "default": "./wait.js"
      },
      "default": "./drive-carefully.js"
    }
  }
}

// 上边的这个意思 
if (red && valid('./stop.js')) return './stop.js';
if (yellow && valid('./stop.js')) return './stop.js';
if (green) {
  if (free && valid('./drive.js')) return './drive.js';
  if (valid('./wait.js')) return './wait.js';
}
if (valid('./drive-carefully.js')) return './drive-carefully.js';
throw new ModuleNotFoundError();

剩下的,webpack原链接

查看原文

赞 2 收藏 1 评论 0

huahuadavids 发布了文章 · 1月8日

支付宝小程序开发笔记

开始

对于一个使用过Vue.js的前端来说,小程序和vue的语法很像,难道不大,增加了一些基于支付宝的内置功能,简单来说,支付宝就是一个浏览器,小程序是支付宝的Html而已.。

Tips, 开发工具编译经常不及时,故意写错代码,比如写错标签不闭合,或者乱写,就马上有效果 !!! 

小程序的使用

小程序无需安装,用户第一次使用小程序时,支付宝 App 会从服务器下载小程序的资源,下载后的小程序资源会缓存在支付宝的客户端一段时间

当用户再次打开已经缓存资源的小程序时,会跳过下载过程,能够更快地打开小程序

当用户首次打开小程序时候,小程序会处于前台运行状态

用户点击右上角关闭按钮关闭小程序,或者按下设备 Home 键离开支付宝 App 时,小程序并不会直接销毁,而是进入后台运行状态

从后台运行切换为前台运行: 当未被系统销毁的小程序再度被打开或者激活时,会从后台运行切换为前台运行

用户点击右上角关闭按钮关闭小程序时,小程序仅是进入后台运行,不会被销毁。只有当小程序进入后台运行状态一定时间,或者占用系统资源过高时,才会被真正销毁

核心的业务能力

支付收单、营销服务、会员管理 、芝麻信用、位置服务、供应链 、资金管理、金融服务,如蚂蚁借呗


小程序启动和入口


小程序启动方式

冷启动: 当用户打开未启动过,或者是已经销毁的小程序时,称为冷启动。此时小程序会执行初始化,初始化完成后,会触发 onLaunch 回调函数。

热启动: 当用户打开已经关闭但仍处于后台运行的小程序时,称为热启动。在这种情况下,小程序并不会被销毁后重启,而仅是从后台切换到前台,此时,onShow 函数会触发,onLaunch 回调函数不会被触发。

小程序的入口

  1. 扫一扫
  2. 搜索
  3. 朋友tab页
  4. 支付成功页
  5. 小程序收藏
  6. 生活号关联
  7. 卡包

小程序入口的场景值

在相关生命周期里,可以获取到时怎么进入小程序的,详细的数据

App({
  onLaunch(options) {
    console.log('App onLaunch Scene:', options.scene);//options.scene 是 String 类型的 
  },
  onShow(options) {
    console.log('App onShow Scene:', options.scene);
  },
})

代码层面

可以使用npm包,app.acss 作为全局样式,作用于当前小程序的所有页面

// 背景色渐变
background-image: linear-gradient(90deg, rgb(5, 131, 68) 0%, #3264C5 99%);

// getApp方法获取顶层app实例
var app = getApp();
console.log(app.globalData); // 获取 globalData

app.js

App() 代表顶层应用,管理所有页面和全局数据,以及提供生命周期回调等


App({
// 比如 alipays://platformapi/startapp?appId=1999&query=number%3D1&page=x%2Fy%2Fz 打开小程序 
  onLaunch(options) {
    // 第一次打开
    console.log(options.query);
    // {number:1}
    console.log(options.path);
    // x/y/z
  },
  onShow(options) {
    // 小程序启动,或从后台被重新打开
  },
  onHide() {
    // 小程序从前台进入后台
  },
  onError(msg) {
    // 小程序发生脚本错误或 API 调用出现报错
    console.log(msg);
  },
  globalData: {
  // 全局数据
    name: 'alipay',
  },
});

app.json

整个应用的配置

{
  // 配置页面
  "pages": [
    "pages/index/index"
  ],
   // 配置插件
  "plugins": {
    "myPlugin": {
      "version": "*",
      "provider": "2019120769656826"
    }
  },
  "window": {
    "transparentTitle": "always", // 导航栏透明设置 默认 none,支持 always 一直透明 / auto 滑动自适应 / none 不透明。 
    "titlePenetrate": true,
    "backgroundImageColor": "#3264C5",
    "defaultTitle": "defaultTitle", // 默认标题
    "allowsBounceVertical":"NO", // 允许下拉。默认"yes"
    "pullRefresh" : true, // 支持下拉刷新吗,默认 true,需要允许下拉才可以
    "titlePenetrate": "YES" // 是否允许导航栏点击穿透。默认 NO,支持 YES / NO 
    "titleImage":  // 导航栏图片地址 ,
    "titleBarColor": "rgba(0,0,0,0.1)"  // 导航栏背景色
  },
  // 配置底部导航tabs
  "tabBar": {
    "textColor": "#111",
    "selectedColor": "blue",
    "backgroundColor": "#ffffff",
    "items": [
      {
        "pagePath": "pages/index/index",
        "name": "首页",
        "icon" : "", // 小图标
        "activeIcon" : "",
      },
      {
        "pagePath": "pages/logs/logs",
        "name": "日志"
      }
    ]
  }
}

page.json

在 /pages 目录中的 .json 文件用于配置当前页面的窗口表现。页面配置比 app.json 全局配置简单得多,只能设置 window 相关配置项,但无需写 window 这个键

// css 
// page页面 page元素声明整个页面的样式
page {
  background-color: #fff;
}

// json 
// 配置 optionMenu  点击后触发 onOptionMenuClick 
{
  "optionMenu": {
    "icon": "https://img.alicdn.com/tps/i3/T1OjaVFl4dXXa.JOZB-114-114.png"
  },
  "titlePenetrate": "YES",
  "barButtonTheme": "light"
}

page.js

page.js是每个页面的逻辑

// pages/index/index.js
Page({
  // 和 vue一样,是对象时,所有页面公用,用函数保证每个页面数据独立
  // this.data无法修改数据,this.setData修改
  data: (){
      return {
        title: "Alipay",
      },
   }
  
  // 页面初始化时触发。一个页面只会调用一次。
  // query 为 my.navigateTo 和 my.redirectTo 中传递的 query 对象。
  // query 内容格式为:“参数名=参数值&参数名=参数值…”。
  onLoad(query) {
    // 页面加载
  },
  onShow() {
    // 页面显示
  },
 
  // onReady === didMount 
  onReady() {
    // 页面加载完成
  },
  onHide() {
    // 页面隐藏
    // 页面隐藏/切入后台时触发。 如 my.navigateTo 到其他页面或底部 tab 切换等。 
  },
  onUnload() {
    // 页面被关闭
    // 页面卸载时触发。 如 my.redirectTo 或 my.navigateBack 到其他页面等。 
  },
  onTitleClick() {
    // 标题被点击
  },
  onPullDownRefresh() {
    // 页面被下拉
  },
  onTabItemTap(){
   // 点击tabItem时触发
  }
  onPageScroll({scrollTop}){
  },
  onReachBottom() {
    // 页面被拉到底部
  },
  onShareAppMessage() {
   // 返回自定义分享信息
  },
  // 事件处理函数对象
  events: {
    onBack() {
      console.log('onBack');
    },
  },
  // 自定义事件处理函数
  viewTap() {
    this.setData({
      text: 'Set data for update.',
    });
  },
  // 自定义事件处理函数
  go() {
    // 带参数的跳转,从 page/ui/index 的 onLoad 函数的 query 中读取 type
    my.navigateTo({url:'/page/ui/index?type=mini'});
  },
  // 自定义数据对象
  customData: {
    name: 'alipay',
  },
});

page.axml

使用include 直接引入页面


// html 条件渲染 
<view a:if="{{length > 5}}"> 1 </view>
<view a:elif="{{length > 2}}"> 2 </view>
<view a:else> 3 </view>

<!-- index.axml -->
<include data-original="./header.axml"/>
<view> body </view>
<include data-original="./footer.axml"/>

<!-- header.axml -->
<view> header </view>
<!-- footer.axml -->
<view> footer </view>

使用import引入模板

<!-- item.axml -->
<template name="item">
  <text>{{text}}</text>
</template>

<import data-original="./item.axml"/>
<template is="item" data="{{text: 'forbar'}}"/>

页面跳转

// 1. 类似A链接的方式
<view class="page">
  <view class="page-description">导航栏</view>
  <navigator open-type="navigate" url="./navigate" hover-class="navigator-hover">跳转到新页面</navigator>
  <navigator open-type="redirect" url="./redirect" hover-class="navigator-hover">在当前页打开</navigator>
  <navigator open-type="switchTab" url="/page/API/index/index" hover-class="navigator-hover">跳转到另外一个 Tab - API</navigator>
  <navigator open-type="reLaunch" url="/page/component/index" hover-class="navigator-hover">重新打开</navigator>
  <navigator open-type="navigateBack" hover-class="navigator-hover">返回上一页面</navigator>
</view>

// 2. js跳转
 my.navigateTo({ url: './back' })
 my.redirectTo({ url: './back' })

acss

rpx(responsive pixel)可以根据屏幕宽度进行自适应,规定屏幕宽为 750rpx。以 Apple iPhone6 为例,屏幕宽度为 375px,共有 750 个物理像素,则 750 rpx = 375 px = 750 物理像素,1rpx = 0.5 px = 1 物理像素。

右上角的分享

Page ({

  data: {
   canIUseShareButton: true
  },
  // 用户点击分享按钮的时候会调用此事件需要返回一个对象(Object)类型,用于自定义分享内容
  onShareAppMessage () {
  return {
    title : '分享 View 组件' ,
    desc : 'View 组件很通用' ,
    path : 'page/component/view/view' ,
        };
      },
 });

event 事件

点击事件,如onTap,on开头就是想上冒泡,catchTap就是不冒泡

常用事件有,tap,longTap(长按,0.5s),touchStart,touchEnd,touchCancel,touchMove

event对象有 type,timeStamp,target,属性,自定义的数据,要 data-小写字母来传值,如data-ha-sa,取值的时候,会转成驼峰,haSa

不同的事件,如tap和touch的event不一样

缓存

set, get, clear, remove 4个方法
大小最多是10M
同步和异步两种方法
不建议使用cookie和session

my.setStorage('name','huahua')
my.setStorageAsync('name','huahua')

自定义组件(简直的vue的一样)

小程序基础库从 1.7.0 版本开始支持自定义组件功能。
通过 my.canIUse('component') 判断自定义组件功能是否在当前版本使用;

从 1.14.0 版本开始,自定义组件支持 component2 相关功能,通过调用 my.canIUse('component2') 可判断新自定义组件功能是否可在当前版本使用, component2 相关功能如下:新增 onInit、deriveDataFromProps 生命周期函数 ,支持使用 ref 获取自定义组件实例 。

[componentName].json 文件

{
  "component": true, // 必选,自定义组件的值必须是 true
  "usingComponents": {
    "other":"../other/index" // 依赖的组件
  }
}

组件的样式

<font color="#ccc" /> 组件的样式,如果写了page样式,是代表他使用者页面的样式,而不是他的样式

右键新建一个组件即可,使用时,在当前页面的json中进入组价声明即可

{
  "usingComponents": {
    "my-component":"/components/index/index"
  }
}

slot 插槽

// demo 组件
<view>
  this is demo component
  <slot>
    <view>this is default slot</view>
  </slot>
  haha
   <slot name="header"/>
</view>

// 使用自己的内容替换默认插槽
<demo>
  <view>this is my slot</view>
  <view slot="header">header</view>
</demo>

组件获取参数,通过在js文件中的data和props参数,可以使用组件自己的数据和外部传递的数据

// /components/index/index.js
Component({
  data: {
    x: 1,
  },
  props: {
    y: '',
  },
});

<!-- /components/index/index.axml -->
<view>component data: {{x}}</view>
<view>page data: {{y}}</view>

自定义组件声明周期

Component({
  data: {
    o: {
      value: "scope-value"
    }
  },
  onInit() { // 组件创建时触发
    console.log("i1 onInit", this.props, this.data);
  },
  deriveDataFromProps(nextProps) { // 组件创建时触发或更新时触发
    console.log("i1 deriveDataFromProps", nextProps, this.props, this.data);
  },
  didMount() { // 组件创建完毕时触发
    console.log("i1 didMount", this.props, this.data);
  },
  didUpdate(prevProps, prevData) { // 组件更新完毕时触发
    console.log("i1 didUpdate", prevProps, prevData, this.props, this.data);
  },
  didUnmount() { // 组件删除时触发
    console.log("i1 didUnmount");
  },
  methods: {
    change() {
      this.setData({ 'o.value': "changed-scope-value" });
    }
  }
});

mixin

用来封装组件的多个公用逻辑

// /mixins/lifecycle.js
export default {
  onInit(){
    console.log('init');
  }, 
  deriveDataFromProps(nextProps){},
  didMount(){},
  didUpdate(prevProps,prevData){},
  didUnmount(){},
};

// /components/index/index.js
import lifecycle from '/mixins/lifecycle';

const initialState = {
  data: {
    isLogin: false,
  },
};

const defaultProps = {
  props: {
    age: 30,
  },
};

const methods = {
  methods: {
      onTapHandler() {},
  },
}

Component({
  mixins: [
    lifecycle,
    initialState,
    defaultProps,
    methods
  ],
  data: {
    name: 'alipay',
  },
});

ref

ref可以获取组件的实例,获取到组件实例之后,可以直接操作组件

// /pages/index/index.js
Page({
  plus() {
    this.counter.plus();
  },
  // saveRef 方法的参数 ref 为自定义组件实例,运行时由框架传递给 saveRef
  saveRef(ref) {
    // 存储自定义组件实例,方便以后调用
    this.counter = ref;
  },
});
<!-- /pages/index/index.axml -->
<my-component ref="saveRef" />
<button onTap="plus">+</button>

// 定义组件时。component2,可以指定ref返回的值,不是默认的组件的this
Component({
  ref() {
    return { key: 'value' }
  }
})

内置标签

<view> 约等于 div
scroll-view 约等于 div里边加了 scroll-auto
swiper 内置轮播图
文本要用text组件
表单form和html差不多,有原生picker支持
地图 map,内置高德地图
可以使用canvas画布

API

  • my.SDKVersion 查看当前基础库版本号,可以在控台中心设置小程序需要库的最低版本号
  • my.hideKeyboard 隐藏输入键盘
  • my.startPullDownRefresh 主动触发下拉刷新,my.stopPullDownRefresh 主动结束下拉刷新,onPullDownRefresh 监听下拉刷新

通讯录联系人

 // 获取本地通讯录
 choosePhoneContact() {
    my.choosePhoneContact({
      success: (res) => {
        my.alert({
          content: `姓名:${res.name} 电话:${res.mobile}`
        });
      },
      fail: (res) => {
        my.alert({
          content: 'choosePhoneContact response: ' + JSON.stringify(res),
        });
      },
    });
  },
  // 获取支付宝通讯录
  chooseAlipayContact() {
    my.chooseAlipayContact({
      count: 1,
      success: (res) => {
        my.alert({
          content: `真实姓名:${res.contacts[0].realName} 邮箱:${res.contacts[0].email} 电话:${res.contacts[0].mobile}`
        });
      },
      fail: (res) => {
        my.alert({
          content: 'chooseAlipayContact response: ' + JSON.stringify(res)
        });
      },
    });
  },
  // 获取联系人
  chooseContact() {
    my.chooseContact({
      chooseType: 'multi', // 多选模式
      includeMe: true,     // 包含自己
      includeMobileContactMode: 'known',//仅包含双向手机通讯录联系人,也即双方手机通讯录都存有对方号码的联系人
      multiChooseMax: 1,  // 最多能选择1个联系人
      multiChooseMaxTips: '超过选择的最大人数了',
      success: (res) => {
        my.alert({
          content: `真实姓名:${res.contactsDicArray[0].realName} 展示姓名:${res.contactsDicArray[0].displayName} 电话:${res.contactsDicArray[0].mobile}`
        });
      },
      fail: (res) => {
        my.alert({
          content: 'chooseContact : ' + JSON.stringify(res)
        });
      },
    });
  },
  // 添加一条记录到通讯录
  addPhoneContact() {
    if (my.canIUse('addPhoneContact')) {
      my.addPhoneContact(this.data);
    } else {
      my.alert({ 
        title: '客户端版本过低',
        content: 'my.addPhoneContact() 需要 10.1.32 及以上版本'
      });
    }
  }

日期选择

  datePicker() {
    my.datePicker({
      currentDate: '2016-10-10',
      startDate: '2016-10-9',
      endDate: '2017-10-9',
      success: (res) => {
        my.alert({
          content: '您选择的日期为: ' + res.date
        });
      },
    });
  },
    datePickerHMS() {
    my.datePicker({
      format: 'HH:mm:ss',
      currentDate: '12:12:12',
      startDate: '11:11:11',
      endDate: '13:13:13',
      success: (res) => {
        my.alert({
          content: '您选择的日期为: ' + res.date
        });
      },
    });
  },

可用node包

优化建议

  • 将数据请求提前到 onLoad
  • 体积大时,拆子包

分包加载

  • 支付宝小程序从客户端 10.1.60 版本开始支持分包加载功能
  • 开发者配置 subPackages 后,服务端将按 subPackages 配置的路径进行打包,
  • subPackages 配置路径外的目录将被默认打包到主包中。
  • 启动页面和 tabBar 的所有页面都必须放在主包中。
  • 每个分包的根目录不能是另外一个分包内的子目录。
  • 分包之间不能相互引用对方包中的资源(比如图片和 js 脚本等),分包可以引用主包和自己包内的资源。
  • 分包和主包是分别独立打包的,同一个js模块会在主包和分包中分别存在
  • 使用preloadRule,进行分包预下载
{
  "pages": ["pages/index"],
  "subPackages": [
    {
      "root": "sub1",
      "pages": ["page1"]
    },
    {
      "root": "sub2",
      "pages": ["page2"]
    },
    {
      "root": "sub3",
      "pages": ["page3"]
    },
    {
      "root": "path/sub4",
      "pages": ["page4"]
    }
  ],
  "preloadRule": {
    "pages/index": {
      "network": "all",
      "packages": ["sub1"]
    },
    "sub1/page1": {
      "packages": ["sub2", "sub3"]
    },
    "sub3/page3": {
      "network": "wifi",
      "packages": ["path/sub4"]
    }
  }
}
查看原文

赞 0 收藏 0 评论 0

huahuadavids 发布了文章 · 1月5日

Spring 全量知识点笔记

Spring

What is Spring ?

Spring是一个以IoC(Inversion of Control,控制反转)和AOP(Aspect OrientedProgramming)为内核的框架。IoC是Spring的基础。IoC实现的是一种控制,简单地说,就是以前调用new构造方法来创建对象,现在变成使用Spring来创建对象。

spring 容器

  • Spring 容器是 Spring 框架的核心。容器将创建对象,把它们连接在一起,配置它们,

并管理他们的整个生命周期从创建到销毁。Spring 容器使用依赖注入(DI)来管理组成一个应用程序的组件。这些对象被称为 Spring Beans

  • Spring IoC 容器利用 Java 的 POJO 类和配置元数据来生成完全配置和可执行的系统或应用程序
  • 通过阅读配置元数据提供的指令,容器知道对哪些对象进行实例化,配置和组装。配置元数据可以通过 XML,Java 注释或 Java 代码来表示
  • 通常new一个类的实例,控制权由码农控制,而"控制反转"是指new实例工作不由码农来做而是交给Spring容器来做。
  • Spring中IOC容器的 BeanFactory (简单实现) 容器和有ApplicationContext(包含了BeanFactory的功能) 容器
  • Spring IoC容器是一个管理Bean的容器,在Spring的定义中,它要求所有的IoC容器都需要实现接口BeanFactory,它是一个顶级容器接口,下边是源码,由于BeanFactory的功能还不够强大,因此在BeanFactory的基础上,还设计了一个更为高级的接口ApplicationContext
  • ApplicationContext接口通过继承上级接口,进而继承BeanFactory接口,但是在BeanFactory的基础上,扩展了消息国际化接口(MessageSource)、环境可配置接口(EnvironmentCapable)、应用事件发布接口(ApplicationEventPublisher)和资源模式解析接口(ResourcePatternResolver),所以它的功能会更为强大

package org.springframework.beans.factory;

import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;

public interface BeanFactory {
  // 前缀
  String FACTORY_BEAN_PREFIX = "&";

  // 多个getbean方法
  Object getBean(String var1) throws BeansException;

  <T> T getBean(String var1, Class<T> var2) throws BeansException;

  Object getBean(String var1, Object... var2) throws BeansException;

  <T> T getBean(Class<T> var1) throws BeansException;

  <T> T getBean(Class<T> var1, Object... var2) throws BeansException;

  <T> ObjectProvider<T> getBeanProvider(Class<T> var1);

  <T> ObjectProvider<T> getBeanProvider(ResolvableType var1);
  // 是否包含bean
  boolean containsBean(String var1);
  // bean是否是单例
  boolean isSingleton(String var1) throws NoSuchBeanDefinitionException;
  // bean是否原型
  boolean isPrototype(String var1) throws NoSuchBeanDefinitionException;

  // 是否类型匹配 
  boolean isTypeMatch(String var1, ResolvableType var2) throws NoSuchBeanDefinitionException;

  boolean isTypeMatch(String var1, Class<?> var2) throws NoSuchBeanDefinitionException;
  // 获取bean的类型
  @Nullable
  Class<?> getType(String var1) throws NoSuchBeanDefinitionException;

  @Nullable
  Class<?> getType(String var1, boolean var2) throws NoSuchBeanDefinitionException;

  // 获取bean的别名
  String[] getAliases(String var1);
}

控制反转

  • IoC理论上是借助于“第三方”实现具有依赖关系对象之间的解耦
  • 即把各个对象类封装之后,通过IoC容器来关联这些对象类。这样对象之间就通过IoC容器进行联系,而对象之间没有直接联系

依赖注入

  • DI是Dependency Inject的缩写,译为“依赖注入”。
  • 所谓依赖注入,就是由IoC容器在运行期间动态地将某种依赖关系注入对象之中
// TextEditor 类依赖 SpellChecker 类,并实例化 SpellChecker 类
public class TextEditor {
   private SpellChecker spellChecker;  
   public TextEditor() {
      spellChecker = new SpellChecker();
   }
}

// 在spring中,可能是这样,构造函数注入
// TextEditor 不实例化 SpellChecker类,控制权交给Spring 
public class TextEditor {
   private SpellChecker spellChecker;
   public TextEditor(SpellChecker spellChecker) {
      this.spellChecker = spellChecker;
   }
}

spring bean

  • Spring如同一个工厂,用于生产和管理Spring容器中的Bean。要使用这个工厂,需要开发者对Spring的配置文件进行配置
  • bean 是一个被实例化,组装,并通过 Spring IoC 容器所管理的对象
  • bean是由用容器提供的配置元数据创建
<bean id="helloWorld"
      class="com.huahuadavids.wehcat.pojo.HelloWorld"
      lazy-init="true"         // lazy-init="true" 延迟初始化的 bean 告诉 IoC 容器在它第一次被请求时,而不是在启动时去创建一个 bean 实例
      init-method="initData"   // 在 bean 的所有必需的属性被容器设置之后,调用回调方法
      destroy-method="destroy" // 当包含该 bean 的容器被销毁时,使用回调方法
>
    <property name="message" value="Hello huahuadavids!"/>
</bean>

// java 源文件
// 第一步利用框架提供的 XmlBeanFactory() API 去生成工厂 bean 以及利用 ClassPathResource() API 去加载在路径 CLASSPATH 下可用的 bean 配置文件。
// XmlBeanFactory() API 负责创建并初始化所有的对象,即在配置文件中提到的 bean
XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));

// 第二步利用第一步生成的 bean 工厂对象的 getBean() 方法得到所需要的 bean。
// 这个方法通过配置文件中的 bean ID 来返回一个真正的对象,该对象最后可以用于实际的对象
HelloWorld obj = (HelloWorld) factory.getBean("helloWorld");
obj.getMessage();

// FileSystemXmlApplicationContext 是从文件系统找 bean的配置
// ApplicationContext context = new FileSystemXmlApplicationContext("/Users/davidzhang/Desktop/spring-starter/src/main/resources/beans.xml");
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
HelloWorld obj1 = (HelloWorld) context.getBean("helloWorld");
obj1.getMessage();

spring的单元测试 更方便的测试bean

这里需要一个包 spring test, spring boot 默认有了这个包


import org.junit.runner.RunWith;
import org.junit.Test;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

// 在测试文件中 这么使用 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:beans.xml") 指定配置文件 
public class AOPTest {

  @Resource(name="productDao")
  private ProductDao productDao;

  @Test
  public void demo1(){
    productDao.save();
    System.out.println("???");
  }
}

spring bean的使用过程

  1. 读取配置信息
  2. 实例化bean
  3. 把bean实例放到spring容器中
  4. 使用bean,从bean缓存池中取

spring bean的scope

  • 对有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域
  • singleton 在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,默认值

    // 两次输出的信息一样,证明 obj1 和 obj2是一样的,单例模式 
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    HelloWorld obj1 = (HelloWorld) context.getBean("helloWorld");
    obj1.setMessage("huhu");
    obj1.getMessage();
    HelloWorld obj2 = (HelloWorld) context.getBean("helloWorld");
    obj2.getMessage();
  • prototype 每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean()

bean的生命周期

  • 就是 Bean的定义——Bean的初始化——Bean的使用——Bean的销毁

bean初始化拦截器

// 1. 实现BeanPostProcessor 接口 
public class MyBeanPostProcessor implements BeanPostProcessor {

  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    Class beanClass = bean.getClass();
    if (beanClass == HelloWorld.class) {
      System.out.println("HelloWorld bean 对象初始化之前······");
    }
    return bean;
  }

  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    System.out.println("HelloWorld bean 对象初始化之后······");
    return bean;
  }
}

// 2. xml中配置 这表示应用于所有的bean 
<bean class="com.huahuadavids.wehcat.pojo.MyBeanPostProcessor" />

bean的定义继承

  • 就是继承一个bean的一些配置信息,而不是java的继承
// 1 两个类 
public class HelloWorld implements InitializingBean {

  private String message1;

  public void  getMessage2() {
    System.out.println("Hello World Message2 : " + message2);
  }

  public void setMessage2(String message2) {
    this.message2 = message2;
  }

  private String message2;

  public void setMessage1(String message1) {
    this.message1 = message1;
  }
  
  public void getMessage() {
    System.out.println("Hello World Message : " + message1);
  }
}

public class HelloIndia {
  private String message1;
  private String message2;
  private String message3;

  public void setMessage1(String message){
    this.message1  = message;
  }

  public void setMessage2(String message){
    this.message2  = message;
  }

  public void setMessage3(String message){
    this.message3  = message;
  }

  public void getMessage1(){
    System.out.println("India Message1 : " + message1);
  }

  public void getMessage2(){
    System.out.println("India Message2 : " + message2);
  }

  public void getMessage3(){
    System.out.println("India Message3 : " + message3);
  }
}

 // 2 xml的bean配置
 <bean id="helloWorld" class="com.huahuadavids.wehcat.pojo.HelloWorld">
        <property name="message1" value="world-message1"/>
        <property name="message2" value="world-message2"/>
    </bean>
    <bean id="helloIndia" class="com.huahuadavids.wehcat.pojo.HelloIndia" parent="beanTeamplate">
        <property name="message1" value="HelloIndia-message1"/>
        <property name="message3" value="HelloIndia-message3"/>
    </bean>
    <bean class="com.huahuadavids.wehcat.pojo.MyBeanPostProcessor" />
  • 继承模板
<bean id="beanTeamplate" abstract="true">
    <property name="message1" value="Hello bbbb!"/>
    <property name="message2" value="Hello aaaa World!"/>
    <property name="message3" value="Namaste India!"/>
</bean>
<bean id="helloIndia" class="com.demo.HelloIndia" parent="beanTeamplate">
  <property name="message1" value="Hello India!"/>
  <property name="message3" value="Namaste India!"/>
</bean>

Spring 基于构造函数的依赖注入

// 1 两个互相依赖的类
public class TextEditor {
   private SpellChecker spellChecker;
   public TextEditor(SpellChecker spellChecker) {
      System.out.println("Inside TextEditor constructor." );
      this.spellChecker = spellChecker;
   }
   public void spellCheck() {
      spellChecker.checkSpelling();
   }
}
public class SpellChecker {
  public SpellChecker(){
    System.out.println("SpellChecker constructor" );
  }
  public void checkSpelling() {
    System.out.println("SpellChecker checkSpelling..." );
  }
}

// 2. bean的xml配置
<!-- Definition for textEditor bean -->
<bean id="textEditor" class="com.demo.TextEditor">
   <constructor-arg ref="spellChecker"/>
</bean>

<!-- Definition for spellChecker bean -->
<bean id="spellChecker" class="com.demo.SpellChecker">
</bean>

// 3. 使用
AbstractApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
TextEditor te = (TextEditor) context.getBean("textEditor");
te.spellCheck();

// 4 如果构造函数有多个参数,顺序问题,就bean中顺序一样即可 ,最好的方式是指定 index
<bean id="textEditor" class="com.demo.TextEditor">
   <constructor-arg ref="spellChecker1" index="0"/>
   <constructor-arg ref="spellChecker2" index="1"/>
</bean>

// 5. bean 配置可以指定参数的类型
<bean id="exampleBean" class="examples.ExampleBean">
   <constructor-arg type="int" value="2001"/>
   <constructor-arg type="java.lang.String" value="Zara"/>
</bean>

Spring 基于设值函数的依赖注入

当容器调用一个无参的构造函数或一个无参的静态 factory 方法来初始化你的 bean 后,通过容器在你的 bean 上调用设值函数

// 1 修改 TextEditor 类
public class TextEditor {
  public SpellChecker getSpellChecker() {
    return spellChecker;
  }

  public void setSpellChecker(SpellChecker spellChecker) {
    System.out.println("SpellChecker setter");
    this.spellChecker = spellChecker;
  }

  private SpellChecker spellChecker;

  public void spellCheck() {
    spellChecker.checkSpelling();
  }
}

// 2 修改bean xml的配置 关联类作为一个属性传入
<bean id="textEditor" class="com.starbugs.wehcat.pojo.TextEditor">
 <property name="spellChecker" ref="spellChecker"/>
</bean>

p-namespace 配置XML

// 加入xml的命名空间
xmlns:p="http://www.springframework.org/schema/p"
       
// 上述配置可以写成  
<bean id="textEditor" class="com.starbugs.wehcat.pojo.TextEditor" p:spellChecker-ref="spellChecker" />
    

内部Bean bean内部配置 Bean

<bean id="textEditor" class="com.starbugs.wehcat.pojo.TextEditor">
   <property name="spellChecker">
       <bean id="spellChecker" class="com.starbugs.wehcat.pojo.SpellChecker" />
   </property>
</bean>

注入集合
集合中还可以注入类

// 1 定义pojo 
public class JavaBeanCollection {
  private List addressList;
  private Set  addressSet;
  private Map  addressMap;
  private Properties addressProp;
  
  public void setAddressList(List addressList) {
    this.addressList = addressList;
  }
  
  public List getAddressList() {
    System.out.println("List Elements :"  + addressList);
    return addressList;
  }
  
  public void setAddressSet(Set addressSet) {
    this.addressSet = addressSet;
  }
  
  public Set getAddressSet() {
    System.out.println("Set Elements :"  + addressSet);
    return addressSet;
  }

  public void setAddressMap(Map addressMap) {
    this.addressMap = addressMap;
  }
  
  public Map getAddressMap() {
    System.out.println("Map Elements :"  + addressMap);
    return addressMap;
  }
  
  public void setAddressProp(Properties addressProp) {
    this.addressProp = addressProp;
  }
  
  public Properties getAddressProp() {
    System.out.println("Property Elements :"  + addressProp);
    return addressProp;
  }
}
// 2 bean的xml配置
 <bean id="javaBeanCollection" class="com.starbugs.wehcat.pojo.JavaBeanCollection">
        <property name="addressList">
            <list>
                <value>list1</value>
                <value>list2</value>
                <value>list3</value>
            </list>
        </property>
        <property name="addressSet">
            <set>
               <ref bean="address2"/>
                <value>set1</value>
                <value>set2</value>
                <value>set3</value>
            </set>
        </property>
        <property name="addressMap">
            <map>
                <entry key="11" value="value-11" />
                <entry key="22" value="value-22" />
                <entry key ="three" value-ref="address2"/>
            </map>
        </property>
        <property name="addressProp">
            <props>
                <prop key="one">INDIA</prop>
                <prop key="two">Pakistan</prop>
                <prop key="three">USA</prop>
                <prop key="four">USA</prop>
            </props>
        </property>
    </bean>

spring 自动装配

byName 、 byType

// 1 定义类 使用setter注入 
public class TextEditor {
  private SpellChecker spellChecker;
  private String name;
  public void setSpellChecker( SpellChecker spellChecker ){
    System.out.println("TextEditor setSpellChecker");
    this.spellChecker = spellChecker;
  }
  public SpellChecker getSpellChecker() {
    return spellChecker;
  }
  public void setName(String name) {
    System.out.println("TextEditor setName");
    this.name = name;
  }
  public String getName() {
    return name;
  }
  public void spellCheck() {
    spellChecker.checkSpelling();
  }
}
// 2 xml 的Bean 配置
<bean id="textEditor" class="com.starbugs.wehcat.pojo.TextEditor" autowire="byName">
    <property name="name" value="huahua" />
</bean>

 <bean id="spellChecker" class="com.starbugs.wehcat.pojo.SpellChecker" />

由构造函数自动装配

// 1 类修改为构造方法注入

public TextEditor( SpellChecker spellChecker, String name ) {
  this.spellChecker = spellChecker;
  this.name = name;
}
// 2 修改xml配置
<bean id="textEditor" class="com.starbugs.wehcat.pojo.TextEditor" autowire="constructor">
    <constructor-arg ref="spellChecker"/>
    <constructor-arg value="huhuhu"/>
</bean>

基于注解的配置

注解来配置依赖注入,会被XML注入所覆盖。

@Required
注释应用于 bean 属性的 setter 方法,它表明受影响的 bean 属性在配置时必须放在 XML 配置文件中

public class Student {
  private Integer age;
  private String name;

  @Required
  public void setAge(Integer age) {
    this.age = age;
  }
  public Integer getAge() {
    return age;
  }

  @Required
  public void setName(String name) {
    this.name = name;
  }
  public String getName() {
    return name;
  }
}

// 标注了 @required的,必须要出现在xml的property 配置中
<bean id="student" class="com.starbugs.wehcat.pojo.Student">
   <property name="name" value="huahua"/>
   <property name="age" value="22"/>
</bean>

@Autowired

// @Autowired 修饰 setter ,相当于 配置了 byType 方式自动连接 
public class TextEditor {
   private SpellChecker spellChecker;
   @Autowired
   public void setSpellChecker( SpellChecker spellChecker ){
      this.spellChecker = spellChecker;
   }
   public SpellChecker getSpellChecker( ) {
      return spellChecker;
   }
   public void spellCheck() {
      spellChecker.checkSpelling();
   }
}

// 2 @Autowired 修饰属性,消除了setter,但是并不是为类自动产生了一个 setter 
@Autowired
private SpellChecker spellChecker;
 
// 3 @Autowired默认必须有Integer 这个bean,否则就报错,加上required = false就不会报错了 
@Autowired(required = false)
public void setAge(Integer age) {
  this.age = age;
}

@Qualifier
当你创建多个具有相同类型的 bean 时,并且想要用一个属性只为它们其中的一个进行装配

// 1 xml配置 
<bean id="student" class="com.starbugs.wehcat.pojo.Student">
     <property name="name" value="huahua"/>
     <property name="age" value="22"/>
 </bean>

 <bean id="student1" class="com.starbugs.wehcat.pojo.Student">
     <property name="name" value="name1"/>
     <property name="age" value="111"/>
 </bean>
// 2 使用类
public class Profile {

  @Autowired
  @Qualifier("student1")
  private Student student;

  public Profile(){
    System.out.println("Inside Profile constructor." );
  }

  public void printAge() {
    System.out.println("Age : " + student.getAge() );
  }

  public void printName() {
    System.out.println("Name : " + student.getName() );
  }
}

@Component
标注一个类,表示声明为一个spring配置的bean

// 1 xml的bean配置 开启组件扫描 
<context:component-scan base-package="com.starbugs.wehcat.pojo" />

// 2 pojo类中声明bean的id 
@Component("profile")

// 3 具体使用
AbstractApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Profile profile = (Profile) context.getBean("profile");
profile.printAge();
profile.printName();

@ComponentScan 组件扫描注解
spring boot的SpringBootApplication注解,默认集成了这个注解


@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
// 一个类中可重复定义
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
  // 定义扫描的包
  @AliasFor("basePackages")
  String[] value() default {};
  // 定义扫描的包
  @AliasFor("value")
  String[] basePackages() default {};
  
  // 定义扫描的类
  Class<?>[] basePackageClasses() default {};

   // bean name 生成器
  Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
  // 作用域解析器
  Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;
  // 作用域代理模式 
  ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

  // 资源匹配模式
  String resourcePattern() default "**/*.class";

  // 是否使用默认过滤器 
  boolean useDefaultFilters() default true;

  // 当满足过滤器条件是扫描
  ComponentScan.Filter[] includeFilters() default {};
  
  // 当不满足过滤器条件是扫描
  ComponentScan.Filter[] excludeFilters() default {};

  // 是否延迟初始化
  boolean lazyInit() default false;

  // 定义过滤器 
  @Retention(RetentionPolicy.RUNTIME)
  @Target({})
  public @interface Filter {
    FilterType type() default FilterType.ANNOTATION;

    @AliasFor("classes")
    Class<?>[] value() default {};

    @AliasFor("value")
    Class<?>[] classes() default {};

    String[] pattern() default {};
  }
}

@PostConstruct 、@PreDestroy

@PostConstruct ===  init-method
@PreDestroy === destroy-method

@Resources
提供一个bean的name字段注入,不可以用在构造函数上

 @Resource(name = "spellChecker")
 private SpellChecker spellChecker;

基于java的配置

  • 使用这种配置,可以不用xml,使用一个类,作为bean的来源
  • 带有 @Configuration 的注解类表示这个类可以使用 Spring IoC 容器作为 bean 定义的来源。
  • @Bean 注解告诉 Spring,一个带有 @Bean 的注解方法将返回一个对象,该对象应该被注册为在 Spring 应用程序上下文中的 bean
  • @Bean注解也可以配置初始化方法和销毁方法
// 1 创建一个配置类 

@Configuration
public class HelloWorldConfig {

  @Bean(initMethod = "init", destroyMethod = "cleanup" )
  public HelloWorld helloWorld(){
    return new HelloWorld();
  }
}

// 2 使用
public static void main(String[] args) {
   ApplicationContext ctx = 
   new AnnotationConfigApplicationContext(HelloWorldConfig.class); 
   HelloWorld helloWorld = ctx.getBean(HelloWorld.class);
   helloWorld.setMessage("Hello World!");
   helloWorld.getMessage();
}
// 3 还可以加载 refresh 多个配置类 
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(HelloWorldConfig.class);
ctx.refresh();
HelloWorld helloWorld = ctx.getBean(HelloWorld.class);

helloWorld.setMessage("Hello World!");
helloWorld.getMessage();

@Import 注解:
@import 注解允许从另一个配置类中加载 @Bean 定义

@Configuration
public class ConfigA {
   @Bean
   public A a() {
      return new A(); 
   }
}

// 这个配置可以使用上一个配置的信息 
@Configuration
@Import(ConfigA.class)
public class ConfigB {
   @Bean
   public B a() {
      return new A(); 
   }
}

spring事件处理

通过 ApplicationEvent 类和 ApplicationListener 接口来提供在 ApplicationContext 中处理事件。如果一个 bean 实现 ApplicationListener,那么每次 ApplicationEvent 被发布到 ApplicationContext 上,那个 bean 会被通知

// 1 实现 ApplicationListener 接口 
public class CStartEventHandler implements ApplicationListener<ContextStartedEvent> {
  @Override
  public void onApplicationEvent(ContextStartedEvent event) {
    System.out.println("ContextStartedEvent Received");
  }
}

// 2 xml中配置 
<bean id="cStartEventHandler" class="com.starbugs.wehcat.demos.CStartEventHandler" />
<bean id="cStopEventHandler" class="com.starbugs.wehcat.demos.CStopEventHandler" />

自定义事件

// 1 新建 事件类
public class CustomEvent extends ApplicationEvent {

  public CustomEvent(Object source) {
    super(source);
  }

  @Override
  public String toString() {
    return "CustomEvent{}";
  }
}

// 2 新建 handler类
public class CustomEventHandler implements ApplicationListener<CustomEvent> {
  @Override
  public void onApplicationEvent(CustomEvent event) {
    System.out.println(event.toString());
  }
}
// 3 新建publisher发布类
public class CustomEventPublisher implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher publisher;
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher){
      this.publisher = publisher;
    }
    public void publish() {
      CustomEvent ce = new CustomEvent(this);
      publisher.publishEvent(ce);
    }
}
// 4 配置类到xml里
// 5. 使用 
CustomEventPublisher cvp = (CustomEventPublisher) context.getBean("customEventPublisher");
cvp.publish();
cvp.publish();

AOP

传送门

Spring mvc

传送门

参考资料

  • Spring+Spring MVC+MyBatis从零开始学(吴为胜 杨章伟)
  • 深入浅出Spring Boot 2.x-杨开振
查看原文

赞 1 收藏 1 评论 0

huahuadavids 收藏了文章 · 1月4日

前端工程师的自我修养:React Fiber 是如何实现更新过程可控的

这是第 83 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:前端工程师的自我修养:React Fiber 是如何实现更新过程可控的

前言

从 React 16 开始,React 采用了 Fiber 机制替代了原先基于原生执行栈递归遍历 VDOM 的方案,提高了页面渲染性能和用户体验。乍一听 Fiber 好像挺神秘,在原生执行栈都还没搞懂的情况下,又整出个 Fiber,还能不能愉快的写代码了。别慌,老铁!下面就来唠唠关于 Fiber 那点事儿。

什么是 Fiber

Fiber 的英文含义是“纤维”,它是比线程(Thread)更细的线,比线程(Thread)控制得更精密的执行模型。在广义计算机科学概念中,Fiber 又是一种协作的(Cooperative)编程模型,帮助开发者用一种【既模块化又协作化】的方式来编排代码。

图片

简单点说,Fiber 就是 React 16 实现的一套新的更新机制,让 React 的更新过程变得可控,避免了之前一竿子递归到底影响性能的做法。

关于 Fiber 你需要知道的基础知识

1 浏览器刷新率(帧)

页面的内容都是一帧一帧绘制出来的,浏览器刷新率代表浏览器一秒绘制多少帧。目前浏览器大多是 60Hz(60帧/s),每一帧耗时也就是在 16ms 左右。原则上说 1s 内绘制的帧数也多,画面表现就也细腻。那么在这一帧的(16ms) 过程中浏览器又干了啥呢?

图片

通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:

  1. 接受输入事件
  2. 执行事件回调
  3. 开始一帧
  4. 执行 RAF (RequestAnimationFrame)
  5. 页面布局,样式计算
  6. 渲染
  7. 执行 RIC (RequestIdelCallback)

第七步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。这里提一下,如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。

2. JS 原生执行栈

React Fiber 出现之前,React 通过原生执行栈递归遍历 VDOM。当浏览器引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并将其压入执行栈,接下来每遇到一个函数调用,又会往栈中压入一个新的上下文。比如:

function A(){
  B();
  C();
}
function B(){}
function C(){}
A();

引擎在执行的时候,会形成如下这样的执行栈:
图片

浏览器引擎会从执行栈的顶端开始执行,执行完毕就弹出当前执行上下文,开始执行下一个函数,直到执行栈被清空才会停止。然后将执行权交还给浏览器。由于 React 将页面视图视作一个个函数执行的结果。每一个页面往往由多个视图组成,这就意味着多个函数的调用。

如果一个页面足够复杂,形成的函数调用栈就会很深。每一次更新,执行栈需要一次性执行完成,中途不能干其他的事儿,只能"一心一意"。结合前面提到的浏览器刷新率,JS 一直执行,浏览器得不到控制权,就不能及时开始下一帧的绘制。如果这个时间超过 16ms,当页面有动画效果需求时,动画因为浏览器不能及时绘制下一帧,这时动画就会出现卡顿。不仅如此,因为事件响应代码是在每一帧开始的时候执行,如果不能及时绘制下一帧,事件响应也会延迟。

3. 时间分片(Time Slicing)

时间分片指的是一种将多个粒度小的任务放入一个时间切片(一帧)中执行的一种方案,在 React Fiber 中就是将多个任务放在了一个时间片中去执行。

4. 链表

在 React Fiber 中用链表遍历的方式替代了 React 16 之前的栈递归方案。在 React 16 中使用了大量的链表。例如:

  • 使用多向链表的形式替代了原来的树结构

例如下面这个组件:

<div id="id">
  A1
  <div id="B1">
    B1
     <div id="C1"></div>
  </div>
  <div id="B2">
      B2
  </div>
</div>

会使用下面这样的链表表示:
图片

  • 副作用单链表

图片

  • 状态更新单链表

图片

  • ...

链表是一种简单高效的数据结构,它在当前节点中保存着指向下一个节点的指针,就好像火车一样一节连着一节

图片

遍历的时候,通过操作指针找到下一个元素。但是操作指针时(调整顺序和指向)一定要小心。

链表相比顺序结构数据格式的好处就是:

  1. 操作更高效,比如顺序调整、删除,只需要改变节点的指针指向就好了。
  2. 不仅可以根据当前节点找到下一个节点,在多向链表中,还可以找到他的父节点或者兄弟节点。

但链表也不是完美的,缺点就是:

  1. 比顺序结构数据更占用空间,因为每个节点对象还保存有指向下一个对象的指针。
  2. 不能自由读取,必须找到他的上一个节点。

React 用空间换时间,更高效的操作可以方便根据优先级进行操作。同时可以根据当前节点找到其他节点,在下面提到的挂起和恢复过程中起到了关键作用。

React Fiber 是如何实现更新过程可控?

前面讲完基本知识,现在正式开始介绍今天的主角 Fiber,看看 React Fiber 是如何实现对更新过程的管控。

图片

更新过程的可控主要体现在下面几个方面:

  1. 任务拆分
  2. 任务挂起、恢复、终止
  3. 任务具备优先级

1. 任务拆分

前面提到,React Fiber 之前是基于原生执行栈,每一次更新操作会一直占用主线程,直到更新完成。这可能会导致事件响应延迟,动画卡顿等现象。

在 React Fiber 机制中,它采用"化整为零"的战术,将调和阶段(Reconciler)递归遍历 VDOM 这个大任务分成若干小任务,每个任务只负责一个节点的处理。例如:

import React from "react";
import ReactDom from "react-dom"
const jsx = (
    <div id="A1">
    A1
    <div id="B1">
      B1
      <div id="C1">C1</div>
      <div id="C2">C2</div>
    </div>
    <div id="B2">B2</div>
  </div>
)
ReactDom.render(jsx,document.getElementById("root"))

这个组件在渲染的时候会被分成八个小任务,每个任务用来分别处理 A1(div)、A1(text)、B1(div)、B1(text)、C1(div)、C1(text)、C2(div)、C2(text)、B2(div)、B2(text)。再通过时间分片,在一个时间片中执行一个或者多个任务。这里提一下,所有的小任务并不是一次性被切分完成,而是处理当前任务的时候生成下一个任务,如果没有下一个任务生成了,就代表本次渲染的 Diff 操作完成。

2. 挂起、恢复、终止

再说挂起、恢复、终止之前,不得不提两棵 Fiber 树,workInProgress tree 和 currentFiber tree。

workInProgress 代表当前正在执行更新的 Fiber 树。在 render 或者 setState 后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集当前节点的副作用,整棵树构建完成后,会形成一条完整的副作用链。

currentFiber 表示上次渲染构建的 Filber 树。在每一次更新完成后 workInProgress 会赋值给 currentFiber。在新一轮更新时 workInProgress tree 再重新构建,新 workInProgress 的节点通过 alternate 属性和 currentFiber 的节点建立联系。

在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,收集副作用。同时也会复用和 currentFiber 对应的节点对象,减少新创建对象带来的开销。也就是说无论是创建还是更新,挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务,大致过程如下:

图片

当没有下一个任务需要执行的时候,workInProgress tree 构建完成,开始进入提交阶段,完成真实 DOM 更新。

在构建 workInProgressFiber tree 过程中可以通过挂起、恢复和终止任务,实现对更新过程的管控。下面简化了一下源码,大致实现如下:

let nextUnitWork = null;//下一个执行单元
//开始调度
function shceduler(task){
     nextUnitWork = task; 
}
//循环执行工作
function workLoop(deadline){
  let shouldYield = false;//是否要让出时间片交出控制权
  while(nextUnitWork && !shouldYield){
    nextUnitWork = performUnitWork(nextUnitWork)
    shouldYield = deadline.timeRemaining()<1 // 没有时间了,检出控制权给浏览器
  }
  if(!nextUnitWork) {
    conosle.log("所有任务完成")
    //commitRoot() //提交更新视图
  }
  // 如果还有任务,但是交出控制权后,请求下次调度
  requestIdleCallback(workLoop,{timeout:5000}) 
}
/*
 * 处理一个小任务,其实就是一个 Fiber 节点,如果还有任务就返回下一个需要处理的任务,没有就代表整个
 */
function performUnitWork(currentFiber){
  ....
  return FiberNode
}

挂起

当第一个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。

恢复

在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。这样完美的解决了调和过程一直占用主线程的问题。

那么问题来了他是如何判断一帧是否有空闲时间的呢?答案就是我们前面提到的 RIC (RequestIdleCallback) 浏览器原生 API,React 源码中为了兼容低版本的浏览器,对该方法进行了 Polyfill。

当恢复执行的时候又是如何知道下一个任务是什么呢?答案在前面提到的链表。在 React Fiber 中每个任务其实就是在处理一个 FiberNode 对象,然后又生成下一个任务需要处理的 FiberNode。顺便提一嘴,这里提到的FiberNode 是一种数据格式,下面是它没有开美颜的样子:

class FiberNode {
  constructor(tag, pendingProps, key, mode) {
    // 实例属性
    this.tag = tag; // 标记不同组件类型,如函数组件、类组件、文本、原生组件...
    this.key = key; // react 元素上的 key 就是 jsx 上写的那个 key ,也就是最终 ReactElement 上的
    this.elementType = null; // createElement的第一个参数,ReactElement 上的 type
    this.type = null; // 表示fiber的真实类型 ,elementType 基本一样,在使用了懒加载之类的功能时可能会不一样
    this.stateNode = null; // 实例对象,比如 class 组件 new 完后就挂载在这个属性上面,如果是RootFiber,那么它上面挂的是 FiberRoot,如果是原生节点就是 dom 对象
    // fiber
    this.return = null; // 父节点,指向上一个 fiber
    this.child = null; // 子节点,指向自身下面的第一个 fiber
    this.sibling = null; // 兄弟组件, 指向一个兄弟节点
    this.index = 0; //  一般如果没有兄弟节点的话是0 当某个父节点下的子节点是数组类型的时候会给每个子节点一个 index,index 和 key 要一起做 diff
    this.ref = null; // reactElement 上的 ref 属性
    this.pendingProps = pendingProps; // 新的 props
    this.memoizedProps = null; // 旧的 props
    this.updateQueue = null; // fiber 上的更新队列执行一次 setState 就会往这个属性上挂一个新的更新, 每条更新最终会形成一个链表结构,最后做批量更新
    this.memoizedState = null; // 对应  memoizedProps,上次渲染的 state,相当于当前的 state,理解成 prev 和 next 的关系
    this.mode = mode; // 表示当前组件下的子组件的渲染方式
    // effects
    this.effectTag = NoEffect; // 表示当前 fiber 要进行何种更新
    this.nextEffect = null; // 指向下个需要更新的fiber
    this.firstEffect = null; // 指向所有子节点里,需要更新的 fiber 里的第一个
    this.lastEffect = null; // 指向所有子节点中需要更新的 fiber 的最后一个
    this.expirationTime = NoWork; // 过期时间,代表任务在未来的哪个时间点应该被完成
    this.childExpirationTime = NoWork; // child 过期时间
    this.alternate = null; // current 树和 workInprogress 树之间的相互引用
  }
}

额…看着好像有点上头,这是开了美颜的样子:

图片

是不是好看多了?在每次循环的时候,找到下一个执行需要处理的节点。

function performUnitWork(currentFiber){
    //beginWork(currentFiber) //找到儿子,并通过链表的方式挂到currentFiber上,每一偶儿子就找后面那个兄弟
  //有儿子就返回儿子
  if(currentFiber.child){
    return currentFiber.child;
  } 
  //如果没有儿子,则找弟弟
  while(currentFiber){//一直往上找
    //completeUnitWork(currentFiber);//将自己的副作用挂到父节点去
    if(currentFiber.sibling){
      return currentFiber.sibling
    }
    currentFiber = currentFiber.return;
  }
}

在一次任务结束后返回该处理节点的子节点或兄弟节点或父节点。只要有节点返回,说明还有下一个任务,下一个任务的处理对象就是返回的节点。通过一个全局变量记住当前任务节点,当浏览器再次空闲的时候,通过这个全局变量,找到它的下一个任务需要处理的节点恢复执行。就这样一直循环下去,直到没有需要处理的节点返回,代表所有任务执行完成。最后大家手拉手,就形成了一颗 Fiber 树。

图片

终止

其实并不是每次更新都会走到提交阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因。

3. 任务具备优先级

React Fiber 除了通过挂起,恢复和终止来控制更新外,还给每个任务分配了优先级。具体点就是在创建或者更新 FiberNode 的时候,通过算法给每个任务分配一个到期时间(expirationTime)。在每个任务执行的时候除了判断剩余时间,如果当前处理节点已经过期,那么无论现在是否有空闲时间都必须执行改任务。

图片

同时过期时间的大小还代表着任务的优先级。

任务在执行过程中顺便收集了每个 FiberNode 的副作用,将有副作用的节点通过 firstEffect、lastEffect、nextEffect 形成一条副作用单链表 AI(TEXT)-B1(TEXT)-C1(TEXT)-C1-C2(TEXT)-C2-B1-B2(TEXT)-B2-A。

图片

其实最终都是为了收集到这条副作用链表,有了它,在接下来的渲染阶段就通过遍历副作用链完成 DOM 更新。这里需要注意,更新真实 DOM 的这个动作是一气呵成的,不能中断,不然会造成视觉上的不连贯。

关于 React Fiber 的思考

1. 能否使用生成器(generater)替代链表

在 Fiber 机制中,最重要的一点就是需要实现挂起和恢复,从实现角度来说 generator 也可以实现。那么为什么官方没有使用 generator 呢?猜测应该是是性能方面的原因。生成器不仅让您在堆栈的中间让步,还必须把每个函数包装在一个生成器中。一方面增加了许多语法方面的开销,另外还增加了任何现有实现的运行时开销。性能上远没有链表的方式好,而且链表不需要考虑浏览器兼容性。

2. Vue 是否会采用 Fiber 机制来优化复杂页面的更新

这个问题其实有点搞事情,如果 Vue 真这么做了是不是就是变相承认 Vue 是在"集成" Angular 和 React 的优点呢?React 有 Fiber,Vue 就一定要有?

两者虽然都依赖 DOM Diff,但是实现上且有区别,DOM Diff 的目的都是收集副作用。Vue 通过 Watcher 实现了依赖收集,本身就是一种很好的优化。所以 Vue 没有采用 Fiber 机制,也无伤大雅。

总结

React Fiber 的出现相当于是在更新过程中引进了一个中场指挥官,负责掌控更新过程,足球世界里管这叫前腰。抛开带来的性能和效率提升外,这种“化整为零”和任务编排的思想,可以应用到我们平时的架构设计中。

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

huahuadavids 赞了文章 · 1月4日

前端工程师的自我修养:React Fiber 是如何实现更新过程可控的

这是第 83 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:前端工程师的自我修养:React Fiber 是如何实现更新过程可控的

前言

从 React 16 开始,React 采用了 Fiber 机制替代了原先基于原生执行栈递归遍历 VDOM 的方案,提高了页面渲染性能和用户体验。乍一听 Fiber 好像挺神秘,在原生执行栈都还没搞懂的情况下,又整出个 Fiber,还能不能愉快的写代码了。别慌,老铁!下面就来唠唠关于 Fiber 那点事儿。

什么是 Fiber

Fiber 的英文含义是“纤维”,它是比线程(Thread)更细的线,比线程(Thread)控制得更精密的执行模型。在广义计算机科学概念中,Fiber 又是一种协作的(Cooperative)编程模型,帮助开发者用一种【既模块化又协作化】的方式来编排代码。

图片

简单点说,Fiber 就是 React 16 实现的一套新的更新机制,让 React 的更新过程变得可控,避免了之前一竿子递归到底影响性能的做法。

关于 Fiber 你需要知道的基础知识

1 浏览器刷新率(帧)

页面的内容都是一帧一帧绘制出来的,浏览器刷新率代表浏览器一秒绘制多少帧。目前浏览器大多是 60Hz(60帧/s),每一帧耗时也就是在 16ms 左右。原则上说 1s 内绘制的帧数也多,画面表现就也细腻。那么在这一帧的(16ms) 过程中浏览器又干了啥呢?

图片

通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:

  1. 接受输入事件
  2. 执行事件回调
  3. 开始一帧
  4. 执行 RAF (RequestAnimationFrame)
  5. 页面布局,样式计算
  6. 渲染
  7. 执行 RIC (RequestIdelCallback)

第七步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。这里提一下,如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。

2. JS 原生执行栈

React Fiber 出现之前,React 通过原生执行栈递归遍历 VDOM。当浏览器引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并将其压入执行栈,接下来每遇到一个函数调用,又会往栈中压入一个新的上下文。比如:

function A(){
  B();
  C();
}
function B(){}
function C(){}
A();

引擎在执行的时候,会形成如下这样的执行栈:
图片

浏览器引擎会从执行栈的顶端开始执行,执行完毕就弹出当前执行上下文,开始执行下一个函数,直到执行栈被清空才会停止。然后将执行权交还给浏览器。由于 React 将页面视图视作一个个函数执行的结果。每一个页面往往由多个视图组成,这就意味着多个函数的调用。

如果一个页面足够复杂,形成的函数调用栈就会很深。每一次更新,执行栈需要一次性执行完成,中途不能干其他的事儿,只能"一心一意"。结合前面提到的浏览器刷新率,JS 一直执行,浏览器得不到控制权,就不能及时开始下一帧的绘制。如果这个时间超过 16ms,当页面有动画效果需求时,动画因为浏览器不能及时绘制下一帧,这时动画就会出现卡顿。不仅如此,因为事件响应代码是在每一帧开始的时候执行,如果不能及时绘制下一帧,事件响应也会延迟。

3. 时间分片(Time Slicing)

时间分片指的是一种将多个粒度小的任务放入一个时间切片(一帧)中执行的一种方案,在 React Fiber 中就是将多个任务放在了一个时间片中去执行。

4. 链表

在 React Fiber 中用链表遍历的方式替代了 React 16 之前的栈递归方案。在 React 16 中使用了大量的链表。例如:

  • 使用多向链表的形式替代了原来的树结构

例如下面这个组件:

<div id="id">
  A1
  <div id="B1">
    B1
     <div id="C1"></div>
  </div>
  <div id="B2">
      B2
  </div>
</div>

会使用下面这样的链表表示:
图片

  • 副作用单链表

图片

  • 状态更新单链表

图片

  • ...

链表是一种简单高效的数据结构,它在当前节点中保存着指向下一个节点的指针,就好像火车一样一节连着一节

图片

遍历的时候,通过操作指针找到下一个元素。但是操作指针时(调整顺序和指向)一定要小心。

链表相比顺序结构数据格式的好处就是:

  1. 操作更高效,比如顺序调整、删除,只需要改变节点的指针指向就好了。
  2. 不仅可以根据当前节点找到下一个节点,在多向链表中,还可以找到他的父节点或者兄弟节点。

但链表也不是完美的,缺点就是:

  1. 比顺序结构数据更占用空间,因为每个节点对象还保存有指向下一个对象的指针。
  2. 不能自由读取,必须找到他的上一个节点。

React 用空间换时间,更高效的操作可以方便根据优先级进行操作。同时可以根据当前节点找到其他节点,在下面提到的挂起和恢复过程中起到了关键作用。

React Fiber 是如何实现更新过程可控?

前面讲完基本知识,现在正式开始介绍今天的主角 Fiber,看看 React Fiber 是如何实现对更新过程的管控。

图片

更新过程的可控主要体现在下面几个方面:

  1. 任务拆分
  2. 任务挂起、恢复、终止
  3. 任务具备优先级

1. 任务拆分

前面提到,React Fiber 之前是基于原生执行栈,每一次更新操作会一直占用主线程,直到更新完成。这可能会导致事件响应延迟,动画卡顿等现象。

在 React Fiber 机制中,它采用"化整为零"的战术,将调和阶段(Reconciler)递归遍历 VDOM 这个大任务分成若干小任务,每个任务只负责一个节点的处理。例如:

import React from "react";
import ReactDom from "react-dom"
const jsx = (
    <div id="A1">
    A1
    <div id="B1">
      B1
      <div id="C1">C1</div>
      <div id="C2">C2</div>
    </div>
    <div id="B2">B2</div>
  </div>
)
ReactDom.render(jsx,document.getElementById("root"))

这个组件在渲染的时候会被分成八个小任务,每个任务用来分别处理 A1(div)、A1(text)、B1(div)、B1(text)、C1(div)、C1(text)、C2(div)、C2(text)、B2(div)、B2(text)。再通过时间分片,在一个时间片中执行一个或者多个任务。这里提一下,所有的小任务并不是一次性被切分完成,而是处理当前任务的时候生成下一个任务,如果没有下一个任务生成了,就代表本次渲染的 Diff 操作完成。

2. 挂起、恢复、终止

再说挂起、恢复、终止之前,不得不提两棵 Fiber 树,workInProgress tree 和 currentFiber tree。

workInProgress 代表当前正在执行更新的 Fiber 树。在 render 或者 setState 后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集当前节点的副作用,整棵树构建完成后,会形成一条完整的副作用链。

currentFiber 表示上次渲染构建的 Filber 树。在每一次更新完成后 workInProgress 会赋值给 currentFiber。在新一轮更新时 workInProgress tree 再重新构建,新 workInProgress 的节点通过 alternate 属性和 currentFiber 的节点建立联系。

在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,收集副作用。同时也会复用和 currentFiber 对应的节点对象,减少新创建对象带来的开销。也就是说无论是创建还是更新,挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务,大致过程如下:

图片

当没有下一个任务需要执行的时候,workInProgress tree 构建完成,开始进入提交阶段,完成真实 DOM 更新。

在构建 workInProgressFiber tree 过程中可以通过挂起、恢复和终止任务,实现对更新过程的管控。下面简化了一下源码,大致实现如下:

let nextUnitWork = null;//下一个执行单元
//开始调度
function shceduler(task){
     nextUnitWork = task; 
}
//循环执行工作
function workLoop(deadline){
  let shouldYield = false;//是否要让出时间片交出控制权
  while(nextUnitWork && !shouldYield){
    nextUnitWork = performUnitWork(nextUnitWork)
    shouldYield = deadline.timeRemaining()<1 // 没有时间了,检出控制权给浏览器
  }
  if(!nextUnitWork) {
    conosle.log("所有任务完成")
    //commitRoot() //提交更新视图
  }
  // 如果还有任务,但是交出控制权后,请求下次调度
  requestIdleCallback(workLoop,{timeout:5000}) 
}
/*
 * 处理一个小任务,其实就是一个 Fiber 节点,如果还有任务就返回下一个需要处理的任务,没有就代表整个
 */
function performUnitWork(currentFiber){
  ....
  return FiberNode
}

挂起

当第一个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。

恢复

在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。这样完美的解决了调和过程一直占用主线程的问题。

那么问题来了他是如何判断一帧是否有空闲时间的呢?答案就是我们前面提到的 RIC (RequestIdleCallback) 浏览器原生 API,React 源码中为了兼容低版本的浏览器,对该方法进行了 Polyfill。

当恢复执行的时候又是如何知道下一个任务是什么呢?答案在前面提到的链表。在 React Fiber 中每个任务其实就是在处理一个 FiberNode 对象,然后又生成下一个任务需要处理的 FiberNode。顺便提一嘴,这里提到的FiberNode 是一种数据格式,下面是它没有开美颜的样子:

class FiberNode {
  constructor(tag, pendingProps, key, mode) {
    // 实例属性
    this.tag = tag; // 标记不同组件类型,如函数组件、类组件、文本、原生组件...
    this.key = key; // react 元素上的 key 就是 jsx 上写的那个 key ,也就是最终 ReactElement 上的
    this.elementType = null; // createElement的第一个参数,ReactElement 上的 type
    this.type = null; // 表示fiber的真实类型 ,elementType 基本一样,在使用了懒加载之类的功能时可能会不一样
    this.stateNode = null; // 实例对象,比如 class 组件 new 完后就挂载在这个属性上面,如果是RootFiber,那么它上面挂的是 FiberRoot,如果是原生节点就是 dom 对象
    // fiber
    this.return = null; // 父节点,指向上一个 fiber
    this.child = null; // 子节点,指向自身下面的第一个 fiber
    this.sibling = null; // 兄弟组件, 指向一个兄弟节点
    this.index = 0; //  一般如果没有兄弟节点的话是0 当某个父节点下的子节点是数组类型的时候会给每个子节点一个 index,index 和 key 要一起做 diff
    this.ref = null; // reactElement 上的 ref 属性
    this.pendingProps = pendingProps; // 新的 props
    this.memoizedProps = null; // 旧的 props
    this.updateQueue = null; // fiber 上的更新队列执行一次 setState 就会往这个属性上挂一个新的更新, 每条更新最终会形成一个链表结构,最后做批量更新
    this.memoizedState = null; // 对应  memoizedProps,上次渲染的 state,相当于当前的 state,理解成 prev 和 next 的关系
    this.mode = mode; // 表示当前组件下的子组件的渲染方式
    // effects
    this.effectTag = NoEffect; // 表示当前 fiber 要进行何种更新
    this.nextEffect = null; // 指向下个需要更新的fiber
    this.firstEffect = null; // 指向所有子节点里,需要更新的 fiber 里的第一个
    this.lastEffect = null; // 指向所有子节点中需要更新的 fiber 的最后一个
    this.expirationTime = NoWork; // 过期时间,代表任务在未来的哪个时间点应该被完成
    this.childExpirationTime = NoWork; // child 过期时间
    this.alternate = null; // current 树和 workInprogress 树之间的相互引用
  }
}

额…看着好像有点上头,这是开了美颜的样子:

图片

是不是好看多了?在每次循环的时候,找到下一个执行需要处理的节点。

function performUnitWork(currentFiber){
    //beginWork(currentFiber) //找到儿子,并通过链表的方式挂到currentFiber上,每一偶儿子就找后面那个兄弟
  //有儿子就返回儿子
  if(currentFiber.child){
    return currentFiber.child;
  } 
  //如果没有儿子,则找弟弟
  while(currentFiber){//一直往上找
    //completeUnitWork(currentFiber);//将自己的副作用挂到父节点去
    if(currentFiber.sibling){
      return currentFiber.sibling
    }
    currentFiber = currentFiber.return;
  }
}

在一次任务结束后返回该处理节点的子节点或兄弟节点或父节点。只要有节点返回,说明还有下一个任务,下一个任务的处理对象就是返回的节点。通过一个全局变量记住当前任务节点,当浏览器再次空闲的时候,通过这个全局变量,找到它的下一个任务需要处理的节点恢复执行。就这样一直循环下去,直到没有需要处理的节点返回,代表所有任务执行完成。最后大家手拉手,就形成了一颗 Fiber 树。

图片

终止

其实并不是每次更新都会走到提交阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因。

3. 任务具备优先级

React Fiber 除了通过挂起,恢复和终止来控制更新外,还给每个任务分配了优先级。具体点就是在创建或者更新 FiberNode 的时候,通过算法给每个任务分配一个到期时间(expirationTime)。在每个任务执行的时候除了判断剩余时间,如果当前处理节点已经过期,那么无论现在是否有空闲时间都必须执行改任务。

图片

同时过期时间的大小还代表着任务的优先级。

任务在执行过程中顺便收集了每个 FiberNode 的副作用,将有副作用的节点通过 firstEffect、lastEffect、nextEffect 形成一条副作用单链表 AI(TEXT)-B1(TEXT)-C1(TEXT)-C1-C2(TEXT)-C2-B1-B2(TEXT)-B2-A。

图片

其实最终都是为了收集到这条副作用链表,有了它,在接下来的渲染阶段就通过遍历副作用链完成 DOM 更新。这里需要注意,更新真实 DOM 的这个动作是一气呵成的,不能中断,不然会造成视觉上的不连贯。

关于 React Fiber 的思考

1. 能否使用生成器(generater)替代链表

在 Fiber 机制中,最重要的一点就是需要实现挂起和恢复,从实现角度来说 generator 也可以实现。那么为什么官方没有使用 generator 呢?猜测应该是是性能方面的原因。生成器不仅让您在堆栈的中间让步,还必须把每个函数包装在一个生成器中。一方面增加了许多语法方面的开销,另外还增加了任何现有实现的运行时开销。性能上远没有链表的方式好,而且链表不需要考虑浏览器兼容性。

2. Vue 是否会采用 Fiber 机制来优化复杂页面的更新

这个问题其实有点搞事情,如果 Vue 真这么做了是不是就是变相承认 Vue 是在"集成" Angular 和 React 的优点呢?React 有 Fiber,Vue 就一定要有?

两者虽然都依赖 DOM Diff,但是实现上且有区别,DOM Diff 的目的都是收集副作用。Vue 通过 Watcher 实现了依赖收集,本身就是一种很好的优化。所以 Vue 没有采用 Fiber 机制,也无伤大雅。

总结

React Fiber 的出现相当于是在更新过程中引进了一个中场指挥官,负责掌控更新过程,足球世界里管这叫前腰。抛开带来的性能和效率提升外,这种“化整为零”和任务编排的思想,可以应用到我们平时的架构设计中。

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 10 收藏 9 评论 1

huahuadavids 赞了文章 · 2020-04-26

react 中的异步渲染

header-11ea97c36869e5a3314f5c140d2d1b6d-7930b.jpeg

React Fiber

  • Diff 算法

熟悉 react 的朋友都知道,在 react 中有个核心的算法,叫 diff 算法。web 界面由 dom 树组成,不同的 dom 树会渲染出不同的界面。react 使用 virtual dom 来表示 dom 树,而 diff 算法就是用于比较 virtual dom 树的区别,并更新界面需要更新的部分。diff 算法和 virtual dom 的完美结合的过程被称为 reconciler,这可是 react 攻城拔寨的绝对利器。有了 reconciler,开发者可以脱身操作真实的 dom 树,只需要向 react 描述界面的状态,而 react 会帮助你高效的完成真正 dom 操作。
reconciler-d8d7812617894d5c10b9da6816e4be89-8bb97.jpeg

在 react16 之前的 reconciler 叫 stack reconciler,fiber 是 react 新的 reconciler,这次更新到 fiber 架构是一次重量级的核心架构的替换,react 为了完成这次替换已经准备了两三年的时间了。

那么 fiber 究竟有什么好的呢?

Fiber 为何出现

不知道大家有没有遇到过这样的情况,点击一个页面的按钮时感觉到页面没有任何的反应,让你怀疑电脑是不是死机了,然后你快速切出浏览器,发现电脑并没有死机,于是再切回浏览器,这时候才发现页面终于更新了。为什么会出现这种情况?在多数情况下,可能是因为浏览器忙着执行相关的 js 代码,导致浏览器主线程没有及时响应用户的操作或者没有及时更新界面。下面这张图就表示了这种现象,你的公司只有一个程序员 (main thread),当这个程序员在执行你的任务 (your code) 时,处于沉浸式编程的状态,无法响应外部的其他事件,什么下班吃饭,都是不存在的。这就像浏览器忙着执行 js 代码的时候,不会去执行页面更新等操作。
mainthread-eb46d69eaaeb4f2644082b4cf6525fb8-db9b4.jpeg

本着顾客是上帝的原则,作为一名优秀的开发者,怎么能够允许出现这种情况降低用户的体验呢。因此 react 团队引入了异步渲染这个概念,而采用 fiber 架构可以实现这种异步渲染的方式。

原先的 stack reconciler 像是一个递归执行的函数,从父组件调用子组件的 reconciler 过程就是一个递归执行的过程,这也是为什么被称为 stack reconciler 的原因。当我们调用 setState 的时候,react 从根节点开始遍历,找出所有的不同,而对于特别庞大的 dom 树来说,这个递归遍历的过程会消耗特别长的时间。在这个期间,任何交互和渲染都会被阻塞,这样就给用户一种“死机”的感觉。

stackreconciler-e90e3cd4afad3fe3a0684ce14865d195-87f44.jpeg

fiber 的出现解决了这个问题,它把 reconciler 的过程拆分成了一个个的小任务,并在完成了小任务之后暂停执行 js 代码,然后检查是否有需要更新的内容和需要响应的事件,做出相应的处理后再继续执行 js 代码。这样就给了用户一种应用一直在运行的感觉,提高了用户的体验。
fiberreconciler-cf0ee9cf8c460da97f5a377b5d6b6267-8598e.jpeg

Fiber 如何做到异步渲染

在做显示方面的工作时,经常会听到一个目标叫 60 帧,这表示的是画面的更新频率,也就是画面每秒钟更新 60 次。这是因为在 60 帧的更新频率下,页面在人眼中显得流畅,无明显卡顿。每秒钟更新 60 次也就是每 16ms 需要更新一次页面,如果更新页面消耗的时间不到 16ms,那么在下一次更新时机来到之前会剩下一点时间执行其他的任务,只要保证及时在 16ms 的间隔下更新界面就完全不会影响到页面的流畅程度。fiber 的核心正是利用了 60 帧原则,实现了一个基于优先级和 requestIdleCallback 的循环任务调度算法。

requestIdleCallback-18eba080f1aa2a3ddaa538b4827158e1-50b73.jpeg

requestIdleCallback 是浏览器提供的一个 api,可以让浏览器在空闲的时候执行回调,在回调参数中可以获取到当前帧剩余的时间,fiber 利用了这个参数,判断当前剩下的时间是否足够继续执行任务,如果足够则继续执行,否则暂停任务,并调用 requestIdleCallback 通知浏览器空闲的时候继续执行当前的任务。

function fiber(剩余时间) {
 if (剩余时间 > 任务所需时间) {
 做任务;
 } else {
 requestIdleCallback(fiber);
 }
}

fiber 还会为不同的任务设置不同的优先级,高优先级任务是需要马上展示到页面上的,比如你正在输入框中输入文字,你肯定希望你的手指在键盘上敲下每一个按键时,输入框能立马做出反馈,这样你才能知道你的输入是否正确,是否有效。低优先级的任务则是像从服务器传来了一些数据,这个时候需要更新页面,比如这篇文章喜欢的人数+1 或是评论+1,这并不是那么紧急的更新,延迟 100-200ms 并不会有多大差别,完全可以在后面进行处理。fiber 会根据任务优先级来动态调整任务调度,优先完成高优先级的任务。

{ 
 Synchronous: 1, // 同步任务,优先级最高
 Task: 2, // 当前调度正执行的任务
 Animation 3, // 动画
 High: 4, // 高优先级
 Low: 5, // 低优先级
 Offscreen: 6, // 当前屏幕外的更新,优先级最低
}

在 fiber 架构中,有一种数据结构,它的名字就叫做 fiber,这也是为什么新的 reconciler 叫做 fiber 的原因。fiber 其实就是一个 js 对象,这个对象的属性中比较重要的有 stateNode、tag、return、child、sibling 和 alternate。

Fiber = {
 tag // 标记任务的进度
 return // 父节点
 child // 子节点
 sibling // 兄弟节点
 alternate // 变化记录
 .....
};

我们可以看出 fiber 基于链表结构,拥有一个个指针,指向它的父节点子节点和兄弟节点,在 diff 的过程中,依照节点连接的关系进行遍历。

fiber 可能存在的问题
在 fiber 中,更新是分阶段的,具体分为两个阶段,首先是 reconciliation 的阶段,这个阶段在计算前后 dom 树的差异,然后是 commit 的阶段,这个阶段将把更新渲染到页面上。第一个阶段是可以打断的,因为这个阶段耗时可能会很长,因此需要暂停下来去执行其他更高优先级的任务,第二个阶段则不会被打断,会一口气把更新渲染到页面上。

hookfunction-b8fa3821391fe30eaf4194064c228e81-97d7d.jpeg
由于 reconciliation 的阶段会被打断,可能会导致 commit 前的这些生命周期函数多次执行。react 官方目前已经把 componentWillMount、componentWillReceiveProps 和 componetWillUpdate 标记为 unsafe,并使用新的生命周期函数 getDerivedStateFromProps 和 getSnapshotBeforeUpdate 进行替换。

还有一个问题是饥饿问题,意思是如果高优先级的任务一直插入,导致低优先级的任务无法得到机会执行,这被称为饥饿问题。对于这个问题官方提出的解决方案是尽量复用已经完成的操作来缓解。相信官方也正在努力提出更好的方法去解决这个问题。

原文链接:https://knownsec-fed.com/2018...

查看原文

赞 6 收藏 4 评论 0

huahuadavids 回答了问题 · 2020-01-17

webpack4单页面如何提取第三方公共库

webpack的dllplugin

关注 4 回答 4

huahuadavids 回答了问题 · 2020-01-08

git的新问题

貌似 .git 目录的问题,在该目录搜一下是否有 *拉取错误 4个字

关注 2 回答 3

huahuadavids 回答了问题 · 2020-01-05

解决webpack打包如何将index.html同级的静态资源文件夹中的文件打包进去

这个不用打包啊,直接拷贝过去就行,不是有个webpack的拷贝插件

关注 3 回答 2

认证与成就

  • 获得 56 次点赞
  • 获得 20 枚徽章 获得 1 枚金徽章, 获得 7 枚银徽章, 获得 12 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-06-27
个人主页被 2.4k 人浏览