0%

浏览器运行机制图:

image

浏览器的运行机制:layout:布局;

1、构建 DOM 树(parse):渲染引擎解析 HTML 文档,首先将标签转换成 DOM 树中的 DOM node(包括 js 生成的标签)生成内容树(Content Tree/DOM Tree);

2、构建渲染树(construct):解析对应的 CSS 样式文件信息(包括 js 生成的样式和外部 css 文件),而这些文件信息以及 HTML 中可见的指令(如<b></b>),构建渲染树(Rendering Tree/Frame Tree);

3、布局渲染树(reflow/layout):从根节点递归调用,计算每一个元素的大小、位置等,给出每个节点所应该在屏幕上出现的精确坐标;

4、绘制渲染树(paint/repaint):遍历渲染树,使用 UI 后端层来绘制每个节点。

重绘(repaint 或 redraw):

当盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。

重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。

触发重绘的条件:改变元素外观属性。如:color,background-color 等。

注意:table 及其内部元素可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用 table 布局页面的原因之一。

重排(重构/回流/reflow):

当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。

重绘和重排的关系:在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。所以,重排必定会引发重绘,但重绘不一定会引发重排

触发重排的条件:任何页面布局和几何属性的改变都会触发重排,比如:

1、页面渲染初始化;(无法避免)

2、添加或删除可见的 DOM 元素;

3、元素位置的改变,或者使用动画;

4、元素尺寸的改变——大小,外边距,边框;

5、浏览器窗口尺寸的变化(resize 事件发生时);

6、填充内容的改变,比如文本的改变或图片大小改变而引起的计算值宽度和高度的改变;

7、读取某些元素属性:(offsetLeft/Top/Height/Width,  clientTop/Left/Width/Height,  scrollTop/Left/Width/Height,  width/height,  getComputedStyle(),  currentStyle(IE) )

重绘重排的代价:耗时,导致浏览器卡慢。

优化:

1、浏览器自己的优化:浏览器会维护 1 个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会 flush 队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。

2、我们要注意的优化:我们要减少重绘和重排就是要减少对渲染树的操作,则我们可以合并多次的 DOM 和样式的修改。并减少对 style 样式的请求。

(1)直接改变元素的 className

(2)display:none;先设置元素为 display:none;然后进行页面布局等操作;设置完成后将元素设置为 display:block;这样的话就只引发两次重绘和重排;

(3)不要经常访问浏览器的 flush 队列属性;如果一定要访问,可以利用缓存。将访问的值存储起来,接下来使用就不会再引发回流;

(4)使用 cloneNode(true or false) 和 replaceChild 技术,引发一次回流和重绘;

(5)将需要多次重排的元素,position 属性设为 absolute 或 fixed,元素脱离了文档流,它的变化不会影响到其他元素;

(6)如果需要创建多个 DOM 节点,可以使用 DocumentFragment 创建完后一次性的加入 document;

1
2
3
4
5
6
7
8
9
10
11
var fragment = document.createDocumentFragment();

var li = document.createElement('li');
li.innerHTML = 'apple';
fragment.appendChild(li);

var li = document.createElement('li');
li.innerHTML = 'watermelon';
fragment.appendChild(li);

document.getElementById('fruit').appendChild(fragment);

(7)尽量不要使用 table 布局。

image

前言

我们都知道,javascript 从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互。

单线程意味着,javascript 代码在执行的任何时候,都只有一个主线程来处理所有的任务。

而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如 I/O 事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。

单线程是必要的,也是 javascript 这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的 dom 操作。试想一下 如果 javascript 是多线程的,那么当两个线程同时对 dom 进行一项操作,例如一个向其添加事件,而另一个删除了这个 dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript 选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。

当然,现如今人们也意识到,单线程在保证了执行顺序的同时也限制了 javascript 的效率,因此开发出了 web worker 技术。这项技术号称让 javascript 成为一门多线程语言。

然而,使用 web worker 技术开的多线程有着诸多限制,例如:所有新线程都受主线程的完全控制,不能独立执行。这意味着这些“线程” 实际上应属于主线程的子线程。另外,这些子线程并没有执行 I/O 操作的权限,只能为主线程分担一些诸如计算等任务。所以严格来讲这些线程并没有完整的功能,也因此这项技术并非改变了 javascript 语言的单线程本质。

可以预见,未来的 javascript 也会一直是一门单线程的语言。

话说回来,前面提到 javascript 的另一个特点是“非阻塞”,那么 javascript 引擎到底是如何实现的这一点呢?答案就是今天这篇文章的主角——event loop(事件循环)。

注:虽然 nodejs 中也存在与传统浏览器环境下的相似的事件循环。然而两者间却有着诸多不同,故把两者分开,单独解释。

正文

浏览器环境下 js 引擎的事件循环机制

1.执行栈与事件队列

当 javascript 代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。 但是我们这里说的执行栈和上面这个栈的意义却有些不同。

我们知道,当我们调用一个方法的时候,js 会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的 this 对象。 而当一系列方法被依次调用的时候,因为 js 是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。

当一个脚本第一次执行的时候,js 引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么 js 会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js 会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。这个过程反复进行,直到执行栈中的代码全部执行完毕。

下面这个图片非常直观的展示了这个过程,其中的 global 就是初次运行脚本时向执行栈中加入的代码:

image

从图片可知,一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值。

以上的过程说的都是同步代码的执行。那么当一个异步代码(如发送 ajax 请求数据)执行后会如何呢?前文提过,js 的另一大特点是非阻塞,实现这一点的关键在于下面要说的这项机制——事件队列(Task Queue)。

js 引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js 会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。

这里还有一张图来展示这个过程:

image

图中的 stack 表示我们所说的执行栈,web apis 则是代表一些异步事件,而 callback queue 即事件队列。

2.macro task 与 micro task

以上的事件循环过程是一个宏观的表述,实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)。

以下事件属于宏任务:

  • setInterval()
  • setTimeout()

以下事件属于微任务

  • new Promise()
  • new MutaionObserver()

前面我们介绍过,在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回调加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。

我们只需记住当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行

这样就能解释下面这段代码的结果:

1
2
3
4
5
6
7
8
9
10
setTimeout(function () {
console.log(1);
});

new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})

结果为:

1
2
3
2
3
1

node 环境下的事件循环机制

1.与浏览器环境有何不同?

在 node 中,事件循环表现出的状态与浏览器中大致相同。不同的是 node 中有一套自己的模型。node 中事件循环的实现是依靠的 libuv 引擎。我们知道 node 选择 chrome v8 引擎作为 js 解释器,v8 引擎将 js 代码分析后去调用对应的 node api,而这些 api 最后则由 libuv 引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上 node 中的事件循环存在于 libuv 引擎中。

2.事件循环模型

下面是一个 libuv 引擎中的事件循环的模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

注:模型中的每一个方块代表事件循环的一个阶段

这个模型是 node 官网上的一篇文章中给出的,我下面的解释也都来源于这篇文章。我会在文末把文章地址贴出来,有兴趣的朋友可以亲自看看原文。

3.事件循环各阶段详解

从上面这个模型中,我们可以大致分析出 node 中的事件循环的顺序:

外部输入数据–>轮询阶段(poll)–>检查阶段(check)–>关闭事件回调阶段(close callback)–>定时器检测阶段(timer)–>I/O 事件回调阶段(I/O callbacks)–>闲置阶段(idle, prepare)–>轮询阶段…

以上各阶段的名称是根据我个人理解的翻译,为了避免错误和歧义,下面解释的时候会用英文来表示这些阶段。

这些阶段大致的功能如下:

  • timers: 这个阶段执行定时器队列中的回调如 setTimeout()setInterval()
  • I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括 close 事件,定时器和setImmediate()的回调。
  • idle, prepare: 这个阶段仅在内部使用,可以不必理会。
  • poll: 等待新的 I/O 事件,node 在一些特殊情况下会阻塞在这里。
  • check: setImmediate()的回调会在这个阶段执行。
  • close callbacks: 例如socket.on('close', ...)这种 close 事件的回调。
    下面我们来按照代码第一次进入 libuv 引擎后的顺序来详细解说这些阶段:

poll 阶段

当个 v8 引擎将 js 代码解析后传入 libuv 引擎后,循环首先进入 poll 阶段。poll 阶段的执行逻辑如下: 先查看 poll queue 中是否有事件,有任务就按先进先出的顺序依次执行回调。 当 queue 为空时,会检查是否有 setImmediate()的 callback,如果有就进入 check 阶段执行这些 callback。但同时也会检查是否有到期的 timer,如果有,就把这些到期的 timer 的 callback 按照调用顺序放到 timer queue 中,之后循环会进入 timer 阶段执行 queue 中的 callback。 这两者的顺序是不固定的,受到代码运行的环境的影响。如果两者的 queue 都是空的,那么 loop 会在 poll 阶段停留,直到有一个 i/o 事件返回,循环会进入 i/o callback 阶段并立即执行这个事件的 callback。

值得注意的是,poll 阶段在执行 poll queue 中的回调时实际上不会无限的执行下去。有两种情况 poll 阶段会终止执行 poll queue 中的下一个回调:1.所有回调执行完毕。2.执行数超过了 node 的限制。

check 阶段

check 阶段专门用来执行 setImmediate()方法的回调,当 poll 阶段进入空闲状态,并且 setImmediate queue 中有 callback 时,事件循环进入这个阶段。

close 阶段

当一个 socket 连接或者一个 handle 被突然关闭时(例如调用了 socket.destroy()方法),close 事件会被发送到这个阶段执行回调。否则事件会用 process.nextTick()方法发送出去。

timer 阶段

这个阶段以先进先出的方式执行所有到期的 timer 加入 timer 队列里的 callback,一个 timer callback 指得是一个通过 setTimeout 或者 setInterval 函数设置的回调函数。

I/O callback 阶段

如上文所言,这个阶段主要执行大部分 I/O 事件的回调,包括一些为操作系统执行的回调。例如一个 TCP 连接生错误时,系统需要执行回调来获得这个错误的报告。

4.process.nextTick,setTimeout 与 setImmediate 的区别与使用场景

在 node 中有三个常用的用来推迟任务执行的方法:process.nextTick,setTimeout(setInterval 与之相同)与 setImmediate

这三者间存在着一些非常不同的区别:

process.nextTick()

尽管没有提及,但是实际上 node 中存在着一个特殊的队列,即 nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,但是这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查 nextTick queue 中是否有任务,如果有,那么会先清空这个队列。与执行 poll queue 中的任务不同的是,这个操作在队列清空前是不会停止的。这也就意味着,错误的使用 process.nextTick()方法会导致 node 进入一个死循环。。直到内存泄漏。

那么何时使用这个方法比较合适呢?下面有一个例子:

1
2
3
const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

这个例子中当,当 listen 方法被调用时,除非端口被占用,否则会立刻绑定在对应的端口上。这意味着此时这个端口可以立刻触发 listening 事件并执行其回调。然而,这时候on('listening)还没有将 callback 设置好,自然没有 callback 可以执行。为了避免出现这种情况,node 会在 listen 事件中使用process.nextTick()方法,确保事件在回调函数绑定后被触发。

setTimeout()和 setImmediate()

在三个方法中,这两个方法最容易被弄混。实际上,某些情况下这两个方法的表现也非常相似。然而实际上,这两个方法的意义却大为不同。

setTimeout()方法是定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行。注意这个“第一时间执行”,这意味着,受到操作系统和当前执行任务的诸多影响,该回调并不会在我们预期的时间间隔后精准的执行。执行的时间存在一定的延迟和误差,这是不可避免的。node 会在可以执行 timer 回调的第一时间去执行你所设定的任务。

setImmediate()方法从意义上将是立刻执行的意思,但是实际上它却是在一个固定的阶段才会执行回调,即 poll 阶段之后。有趣的是,这个名字的意义和之前提到过的process.nextTick()方法才是最匹配的。node 的开发者们也清楚这两个方法的命名上存在一定的混淆,他们表示不会把这两个方法的名字调换过来—因为有大量的 node 程序使用着这两个方法,调换命名所带来的好处与它的影响相比不值一提。

setTimeout()和不设置时间间隔的setImmediate()表现上及其相似。猜猜下面这段代码的结果是什么?

1
2
3
4
5
6
7
setTimeout(() => {
console.log('timeout');
}, 0);

setImmediate(() => {
console.log('immediate');
});

实际上,答案是不一定。没错,就连 node 的开发者都无法准确的判断这两者的顺序谁前谁后。这取决于这段代码的运行环境。运行环境中的各种复杂的情况会导致在同步队列里两个方法的顺序随机决定。但是,在一种情况下可以准确判断两个方法回调的执行顺序,那就是在一个 I/O 事件的回调中。下面这段代码的顺序永远是固定的:

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});

答案永远是:

1
2
immediate
timeout

因为在 I/O 事件的回调中,setImmediate方法的回调永远在 timer 的回调前执行。

尾声

javascript 的事件循环是这门语言中非常重要且基础的概念。清楚的了解了事件循环的执行顺序和每一个阶段的特点,可以使我们对一段异步代码的执行顺序有一个清晰的认识,从而减少代码运行的不确定性。合理的使用各种延迟事件的方法,有助于代码更好的按照其优先级去执行。这篇文章期望用最易理解的方式和语言准确描述事件循环这个复杂过程,但由于作者自己水平有限,文章中难免出现疏漏。如果您发现了文章中的一些问题,欢迎在留言中提出,我会尽量回复这些评论,把错误更正。

一. 什么是 Tree-shaking

image

先来看一下 Tree-shaking 原始的本意

image

上图形象的解释了 Tree-shaking 的本意,本文所说的前端中的 tree-shaking 可以理解为通过工具”摇”我们的 JS 文件,将其中用不到的代码”摇”掉,是一个性能优化的范畴。具体来说,在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。

image

Tree-shaking 较早由 Rich_Harris 的 rollup 实现,后来,webpack2 也增加了 tree-shaking 的功能。其实在更早,google closure compiler 也做过类似的事情。三个工具的效果和使用各不相同,使用方法可以通过官网文档去了解,三者的效果对比,后文会详细介绍。

二. tree-shaking 的原理

image

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

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

Tree-shaking 和传统的 DCE 的方法又不太一样,传统的 DCE 消灭不可能执行的代码,而 Tree-shaking 更关注于消除没有用到的代码。下面详细介绍一下 DCE 和 Tree-shaking。

(1)先来看一下 DCE 消除大法

image

Dead Code 一般具有以下几个特征

  • 代码不会被执行,不可到达
  • 代码执行的结果不会被用到
  • 代码只会影响死变量(只写不读)

下面红框标示的代码就属于死码,满足以上特征

image

传统编译型的语言中,都是由编译器将 Dead Code 从 AST(抽象语法树)中删除,那 javascript 中是由谁做 DCE 呢?

首先肯定不是浏览器做 DCE,因为当我们的代码送到浏览器,那还谈什么消除无法执行的代码来优化呢,所以肯定是送到浏览器之前的步骤进行优化。

其实也不是上面提到的三个工具,rollup,webpack,cc 做的,而是著名的代码压缩优化工具 uglify,uglify 完成了 javascript 的 DCE,下面通过一个实验来验证一下。

以下所有的示例代码都能在 github 中找到

https://github.com/lin-xi/treeshaking/tree/master/rollup-webpack

分别用 rollup 和 webpack 将图 4 中的代码进行打包

image

中间是 rollup 打包的结果,右边是 webpack 打包的结果

可以发现,rollup 将无用的代码 foo 函数和 unused 函数消除了,但是仍然保留了不会执行到的代码,而 webpack 完整的保留了所有的无用代码和不会执行到的代码。

分别用 rollup + uglify 和 webpack + uglify 将图 4 中的代码进行打包

image

中间是配置文件,右侧是结果

可以看到右侧最终打包结果中都去除了无法执行到的代码,结果符合我们的预期。

(2) 再来看一下 Tree-shaking 消除大法

前面提到了 tree-shaking 更关注于无用模块的消除,消除那些引用了但并没有被使用的模块。

先思考一个问题,为什么 tree-shaking 是最近几年流行起来了?而前端模块化概念已经有很多年历史了,其实 tree-shaking 的消除原理是依赖于 ES6 的模块特性。

image

ES6 module 特点:

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import binding 是 immutable 的
    ES6 模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是 tree-shaking 的基础。

所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6 之前的模块化,比如我们可以动态 require 一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。

这是 ES6 modules 在设计时的一个重要考量,也是为什么没有直接采用 CommonJS,正是基于这个基础上,才使得 tree-shaking 成为可能,这也是为什么 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking。

我们还是通过例子来详细了解一下

面向过程编程函数和面向对象编程是 javascript 最常用的编程模式和代码组织方式,从这两个方面来实验:

  • 函数消除实验
  • 类消除实验

先看下函数消除实验

utils 中 get 方法没有被使用到,我们期望的是 get 方法最终被消除。

image

注意,uglify 目前不会跨文件去做 DCE,所以上面这种情况,uglify 是不能优化的。

先看看 rollup 的打包结果

image

完全符合预期,最终结果中没有 get 方法

再看看 webpack 的结果

image

也符合预期,最终结果中没有 get 方法

可以看到 rollup 打包的结果比 webpack 更优化

函数消除实验中,rollup 和 webpack 都通过,符合预期

再来看下类消除实验

增加了对 menu.js 的引用,但其实代码中并没有用到 menu 的任何方法和变量,所以我们的期望是,最终代码中 menu.js 里的内容被消除

image

image

rollup 打包结果

image

包中竟然包含了 menu.js 的全部代码

webpack 打包结果

image

包中竟然也包含了 menu.js 的全部代码

类消除实验中,rollup,webpack 全军覆没,都没有达到预期

image

这跟我们想象的完全不一样啊?为什么呢?无用的类不能消除,这还能叫做 tree-shaking 吗?我当时一度怀疑自己的 demo 有问题,后来各种网上搜索,才明白 demo 没有错。

下面摘取了 rollup 核心贡献者的的一些回答:

image

  • rollup 只处理函数和顶层的 import/export 变量,不能把没用到的类的方法消除掉
  • javascript 动态语言的特性使得静态分析比较困难
  • 图 7 下部分的代码就是副作用的一个例子,如果静态分析的时候删除 run 或者 jump,程序运行时就可能报错,那就本末倒置了,我们的目的是优化,肯定不能影响执行

再举个例子说明下为什么不能消除 menu.js,比如下面这个场景

1
2
3
4
5
6
7
8
9
10
11
function Menu() {
}

Menu.prototype.show = function() {
}

Array.prototype.unique = function() {
// 将 array 中的重复元素去除
}

export default Menu;

如果删除 menu.js,那 Array 的扩展也会被删除,就会影响功能。那也许你会问,难道 rollup,webpack 不能区分是定义 Menu 的 proptotype 还是定义 Array 的 proptotype 吗?当然如果代码写成上面这种形式是可以区分的,如果我写成这样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Menu() {
}

Menu.prototype.show = function() {
}

var a = 'Arr' + 'ay'
var b
if(a == 'Array') {
b = Array
} else {
b = Menu
}

b.prototype.unique = function() {
// 将 array 中的重复元素去除
}

export default Menu;

这种代码,静态分析是分析不了的,就算能静态分析代码,想要正确完全的分析也比较困难。

更多关于副作用的讨论,可以看这个

https://github.com/rollup/rollup/issues/349

image

tree-shaking 对函数效果较好

函数的副作用相对较少,顶层函数相对来说更容易分析,加上 babel 默认都是”use strict”严格模式,减少顶层函数的动态访问的方式,也更容易分析

我们开始说的三个工具,rollup 和 webpack 表现不理想,那 closure compiler 又如何呢?

将示例中的代码用 cc 打包后得到的结果如下:

image

天啊,这不就是我们要的结果吗?完美消除所有无用代码的结果,输出的结果非常性感

closure compiler, tree-shaking 的结果完美!

可是不能高兴得太早,能得到这么完美结果是需要条件的,那就是 cc 的侵入式约束规范。必须在代码里添加这样的代码,看红线框标示的

image

google 定义一整套注解规范 Annotating JavaScript for the Closure Compiler,想更多了解的,可以去看下官网。

侵入式这个就让人很不爽,google Closure Compiler 是 java 写的,和我们基于 node 的各种构建库不可能兼容(不过目前好像已经有 nodejs 版 Closure Compiler),Closure Compiler 使用起来也比较麻烦,所以虽然效果很赞,但比较难以应用到项目中,迁移成本较大。

说了这么多,总结一下:

三大工具的 tree-shaking 对于无用代码,无用模块的消除,都是有限的,有条件的。closure compiler 是最好的,但与我们日常的基于 node 的开发流很难兼容。

image

tree-shaking 对 web 意义重大,是一个极致优化的理想世界,是前端进化的又一个终极理想。

理想是美好的,但目前还处在发展阶段,还比较困难,有各个方面的,甚至有目前看来无法解

决的问题,但还是应该相信新技术能带来更好的前端世界。

但优化是一种态度,不因小而不为,不因艰而不攻。

三、Tree-Shaking 的工作原理

Tree-shaking (树摇)最早是由 Rollup 实现,是一种采用删除不需要的额外代码的方式优化代码体积的技术,webpack2 借鉴了这个特性也增加了 tree-shaking 的功能。

tree-shaking 只能在静态 modules 下工作,在 ES6 之前我们使用 CommonJS 规范引入模块,具体采用 require()的方式动态引入模块,这个特性可以通过判断条件解决按需加载的优化问题,具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
let module;



if(condition) {

module = require("HellowModule") ;

} else {

module = requitre('BeyModule');

}

但是 CommonJS 规范无法确定在实际运行前需要或者不需要某些模块,所以 CommonJS 不适合 tree-shaking 机制。

在 JavaScript 模块化方案中,只有 ES6 的模块方案:import()引入模块的方式采用静态导入,可以采用一次导入所有的依赖包再根据条件判断的方式,获取不需要的包,然后执行删除操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import hello from "Hellow";

import bey from "Bey";

import other from "Other"



if(condition) {

// hello

} else {

// bey

}

Tree-shaking 的实现原理

利用 ES6 模块特性:

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import binding 是 immutable 的,引入的模块不能再进行修改

代码删除:

  • uglify:判断程序流,判断变量是否被使用和引用,进而删除代码

实现原理可以简单的概况:

  1. ES6 Module 引入进行静态分析,故而编译的时候正确判断到底加载了那些模块
  2. 静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码

NodeJS 的 JavaScript 运行在单个进程的单个线程上,一个 JavaScript 执行进程只能利用一个 CPU 核心,而如今大多数 CPU 均为多核 CPU,为了充分利用 CPU 资源,Node 提供了 child_process 和 cluster 模块来实现多进程以及进程管理。本文将根据 Master-Worker 模式,搭建一个简单的服务器集群来充分利用多核 CPU 资源,探索进程间通信、负载均衡、进程重启等知识。

下图是 Master-Worker 模式,进程分为 master 进程和 worker 进程,master 进程负责调度或管理 worker 进程,worker 进程则负责具体的业务处理。在服务器层面,worker 可以是一个服务进程,负责处理来自客户端的请求,多个 worker 便相当于多个服务器,从而构成一个服务器集群。master 则是负责创建 worker,将来自客户端的请求分配到各个服务器上去处理,并监控 worker 的运行状态以及进行管理等操作。

image

本文将从 child_process 模块开始,熟悉该模块的基本用法。后面再继续进入 cluster 模块的学习。本文所用的代码示例可以从该仓库中找到–【multi-process】。

一、child_process

1.1、Hello world

child_process 模块提供了 spawn()、exec()、execFile()、fork()这 4 个方法用于创建子进程,本文将使用 fork()方法来创建子进程,fork()方法只需指定要执行的 JavaScript 文件模块,即可创建 Node 的子进程。下面是简单的 HelloWorld 示例,master 进程根据 CPU 数量创建出相应数量的 worker 进程,worker 进程中利用进程 ID 来标记自己。

以下是 master 进程代码,文件名为 master.js。

1
2
3
4
5
6
7
8
const childProcess = require('child_process')
const cpuNum = require('os').cpus().length

for (let i = 0; i < cpuNum; ++i) {
childProcess.fork('./worker.js')
}

console.log('Master: Hello world.')

以下是 worker 进程的代码,文件名为 worker.js。

1
console.log('Worker-' + process.pid + ': Hello world.')

执行node master.js,得到如下结果,master 创建 4 个 worker 后输出 HelloWorld 信息,每个 worker 也分别输出自己的 HelloWorld 信息。

image

1.2、父子进程间的通信

创建 worker 之后,接下来实现 master 和 worker 之间的通信。Node 父子进程之间可以通过on('message')send()来实现通信,on('message')其实是监听 message 事件,当该进程收到其他进程发送的消息时,便会触发 message 事件。send()方法则是用于向其他进程发送信息。master 进程中调用child_processfork()方法后会得到一个子进程的实例,通过这个实例可以监听来自子进程的消息或者向子进程发送消息。worker 进程则通过 process 对象接口监听来自父进程的消息或者向父进程发送消息。

image

下面是简单示例,master 创建 worker 之后,向 worker 发送信息,worker 在收到 master 的信息后将信息输出,并回复 master。master 收到回复后输出信息。

master.js

1
2
3
4
5
6
7
8
const childProcess = require('child_process')
const worker = childProcess.fork('./worker.js')

worker.send('Hello world.')

worker.on('message', (msg) => {
console.log('[Master] Received message from worker: ' + msg)
})

worker.js

1
2
3
4
process.on('message', (msg) => {
console.log('[Worker] Received message from master: ' + msg)
process.send('Hi master.')
})

执行node master.js,结果如下,master 和 worker 可以正常通信。

image

1.3、Master 分发请求给 Worker 处理

进程通信时使用到的send()方法,除了发送普通的对象之外,还可以用于发送句柄。句柄是一种引用,可以用来标识资源,例如通过句柄可以标识一个 socket 对象、一个 server 对象等。利用句柄传递,可以实现请求的分发。master 进程创建一个 TCP 服务器监听特定端口,收到客户端的请求后,会得到一个 socket 对象,通过这个 socket 对象可以跟客户端进行通信从而处理客户端的请求。master 进程可以通过句柄传递将该 socket 对象发送给 worker 进程,让 worker 进程去处理请求。该模式的结构图如下,在 master 上还可以通过特定的算法实现负载均衡,将客户端的请求均衡地分发给 worker 去处理。

image

下面是一个简单示例。master 创建 TCP 服务器并监听 8080 端口,收到请求后将请求分发给 worker 处理。worker 收到 master 发来的 socket 以后,通过 socket 对客户端进行响应。为方便看到请求的处理情况,worker 给出的响应内容会说明该请求是被哪个 worker 处理。

master.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const childProcess = require('child_process')
const net = require('net')
const cpuNum = require('os').cpus().length

// 创建工作进程
let workers = []
let cur = 0
for (let i = 0; i < cpuNum; ++i) {
workers.push(childProcess.fork('./worker.js'))
console.log('Create worker-' + workers[i].pid)
}

// 创建TCP服务器
const server = net.createServer()

// 服务器收到请求后分发给工作进程去处理
// 通过轮转方式实现工作进程的负载均衡
server.on('connection', (socket) => {
workers[cur].send('socket', socket)
cur = Number.parseInt((cur + 1) % cpuNum)
})

server.listen(8080, () => {
console.log('TCP server: 127.0.0.1:8080')
})

worker.js

1
2
3
4
5
6
7
8
process.on('message', (msg, socket) => {
if (msg === 'socket' && socket) {
// 利用setTimeout模拟处理请求时的操作耗时
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid)
}, 10)
}
})

为了访问 TCP 服务器进行实验,这里需要写一个简单的 TCP 客户端,代码如下。该客户端会创建 10 个 TCP 连接,得到服务器响应之后将响应的内容进行输出。

tcp_client.js

1
2
3
4
5
6
7
8
9
10
11
const net = require('net')
const maxConnectCount = 10

for (let i = 0; i < maxConnectCount; ++i) {
net.createConnection({
port: 8080,
host: '127.0.0.1'
}).on('data', (data) => {
console.log(data.toString())
})
}

先执行node master.js启动服务器,然后执行 node tcp_client.js 启动客户端。得到的结果如下,10 个请求被分发到不同服务器上进行处理,并且可以看到 master 中的轮转分发请求起到了作用,实现了简单的负载均衡。

image

1.4、Worker 监听同一个端口

前面说过,send()方法可以传递句柄,通过传递句柄,我们除了发送 socket 对象之外,还可以直接发送一个 server 对象。我们可以在 master 进程中创建一个 TCP 服务器,将服务器对象直接发送给 worker 进程,让 worker 去监听端口并处理请求。这样的话,master 和 worker 进程都会监听相同端口,当客户端发起请求时,请求可能被 master 接收,也可能被 worker 接收。而 master 不负责处理业务,如果请求被 master 接收到,由于 master 上没有处理业务的逻辑,请求将无法得到处理。因此可以实现为如下图所示的模式,master 将 TCP 服务器发送给 worker 使得所有 worker 监听同一个端口以后,master 关闭对端口的监听。这样便只有 worker 在监听同一端口,请求将会都被 worker 进行处理,与 master 无关。

image

这种模式下,多个进程监听相同端口,当网络请求到来时,会进行抢占式调度,只有一个进程会抢到连接然后进行服务。因此,可以确保每个请求都会被特定的 worker 处理,而不是一个请求同时被多个 worker 处理。但由于是抢占式的调度,不能够保证每个 worker 的负载均衡。可能由于处理不同业务时 CPU 和 IO 繁忙度的不同导致进程抢到的请求数量不同,形成负载不均衡的情况。

下面是简单示例。

master.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const childProcess = require('child_process')
const net = require('net')
const cpuNum = require('os').cpus().length

// 创建工作进程
let workers = []
let cur = 0
for (let i = 0; i < cpuNum; ++i) {
workers.push(childProcess.fork('./worker.js'))
console.log('Create worker-' + workers[i].pid)
}

// 创建TCP服务器
const server = net.createServer()

server.listen(8080, () => {
console.log('TCP server: 127.0.0.1:8080')
// 监听端口后将服务器句柄发送给工作进程
for (let i = 0; i < cpuNum; ++i) {
workers[i].send('server', server)
}
// 关闭主线程服务器的端口监听
server.close()
})

worker.js

1
2
3
4
5
6
7
8
9
10
process.on('message', (msg, server) => {
if (msg === 'server' && server) {
server.on('connection', (socket) => {
// 利用setTimeout模拟处理请求时的操作耗时
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid)
}, 10)
})
}
})

继续使用之前的tcp_client来进行实验,先执行node master.js启动服务器,然后执行node tcp_client.js启动客户端。得到结果如下,请求可以被不同的 worker 进程处理,但由于 worker 进程是抢占式地为请求进行服务,所以不一定能实现每个 worker 的负载均衡。

image

1.5、进程重启

worker 进程可能因为某些异常情况而退出,为了提高集群的稳定性,master 进程需要监听子进程的存活状态,当子进程退出之后,master 进程要及时重启新的子进程。在 Node 中,子进程退出时,会在父进程中触发 exit 事件。父进程只需通过监听该事件便可知道子进程是否退出,并在退出的时候做出相应的处理。下面是在之前的监听同一端口模式下,增加了进程重启功能。进程重启时,master 进程需要重新传递 server 对象给新的 worker 进程,因此不能关闭 master 进程上的 server,否则在进程重启时 server 被关闭,得到的句柄将为空,无法正常传递。master 进程的 server 不关闭,会导致 master 进程也监听端口,会有部分请求被 master 进程接收,为了让着部分请求能够得到处理,可以在 master 进程添加处理业务的代码。由于 master 也参与了业务处理,业务处理进程的数量增加 1 个,所以 worker 进程可以少创建 1 个。这也就是下面简单示例中的做法。

这种实现方式使得 master 既进行进程管理又参与了业务处理,如果要保持 master 只负责进程管理而不涉及业务处理,可以采取另外一种实现方式:master 接收到请求后,按照前面 1.3 节的做法将请求转发给 worker 进行处理,这样 master 将继续只负责对 worker 进程的管理。

master.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const childProcess = require('child_process')
const net = require('net')
const cpuNum = require('os').cpus().length - 1

// 创建工作进程
let workers = []
let cur = 0
for (let i = 0; i < cpuNum; ++i) {
workers.push(childProcess.fork('./worker.js'))
console.log('Create worker-' + workers[i].pid)
}

// 创建TCP服务器
const server = net.createServer()

// 由于master进程也会监听端口。因此需要对请求做出处理
server.on('connection', (socket) => {
// 利用setTimeout模拟处理请求时的操作耗时
setTimeout(() => {
socket.end('Request handled by master')
}, 10)
})

server.listen(8080, () => {
console.log('TCP server: 127.0.0.1:8080')
// 监听端口后将服务器句柄发送给工作进程
for (let i = 0; i < cpuNum; ++i) {
workers[i].send('server', server)
// 工作进程退出后重启
workers[i].on('exit', ((i) => {
return () => {
console.log('Worker-' + workers[i].pid + ' exited')
workers[i] = childProcess.fork('./worker.js')
console.log('Create worker-' + workers[i].pid)
workers[i].send('server', server)
}
})(i))
}
// 关闭主线程服务器的端口监听
// server.close()
})

worker.js

1
2
3
4
5
6
7
8
9
10
process.on('message', (msg, server) => {
if (msg === 'server' && server) {
server.on('connection', (socket) => {
// 利用setTimeout模拟处理请求时的操作耗时
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid)
}, 10)
})
}
})

执行node master.js启动服务器后,可以通过任务管理器直接杀掉进程来模拟进程异常退出。可以看到 worker 进程退出后,master 能够发现并及时创建新的 worker 进程。任务管理器中的 Node 进程数量恢复原样。

image

image

执行node tcp_client.js启动客户端,客户端发出的连接请求被处理的情况如下,同样地,由于监听同一端口,进程之间采取抢占式服务,不一定保障负载均衡。

image

1.6、处理 HTTP 服务

前面的示例所使用的是 TCP 服务器,如果要处理 HTTP 请求,需要使用 HTTP 服务器。而 HTTP 其实是基于 TCP 的,发送 HTTP 请求的时候同样也会发起 TCP 连接。只需要对前面的 TCP 服务器进行一点小改动便可以支持 HTTP 了。在进程中新增 HTTP 服务器,当 TCP 服务器收到请求时,把请求提交给 HTTP 服务器处理即可。下面是 worker 进程的改动示例。

worker.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const http = require('http')
const httpServer = http.createServer((req, res) => {
// 利用setTimeout模拟处理请求时的操作耗时
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Request handled by worker-' + process.pid)
}, 10)
})

process.on('message', (msg, server) => {
if (msg === 'server' && server) {
server.on('connection', (socket) => {
// 提交给HTTP服务器处理
httpServer.emit('connection', socket)
})
}
})

image

二、cluster

前面简单描述了使用 child_process 实现单机 Node 集群的做法,需要处理挺多的细节。Node 提供了 cluster 模块,该模块提供了更完善的 API,除了能够实现多进程充分利用 CPU 资源以外,还能够帮助我们更好地进行进程管理和处理进程的健壮性问题。下面是简单示例,if 条件语句判断当前进程是 master 还是 worker,master 进程会执行 if 语句块包含的代码,而 worker 进程则执行 else 语句块包含的代码。master 进程中,利用 cluster 模块创建了与 CPU 数量相应的 worker 进程,并通过监听 cluster 的 online 事件来判断 worker 的创建成功。在 worker 进程退出后,会触发 master 进程中 cluster 模块上的 exit 事件,通过监听该事件可以了解 worker 进程的退出情况并及时 fork 新的 worker。最后,worker 进程中只需创建服务器监听端口,对客户端请求做出处理即可。(这里设置相同端口 8080 之后,所有 worker 都将监听同一个端口)

server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const cluster = require('cluster')

if (cluster.isMaster) {
const cpuNum = require('os').cpus().length

for (let i = 0; i < cpuNum; ++i) {
cluster.fork()
}

// 创建进程完成后输出提示信息
cluster.on('online', (worker) => {
console.log('Create worker-' + worker.process.pid)
})

// 子进程退出后重启
cluster.on('exit', (worker, code, signal) => {
console.log('[Master] worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal)
cluster.fork()
})
} else {
const net = require('net')
net.createServer().on('connection', (socket) => {
// 利用setTimeout模拟处理请求时的操作耗时
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid)
}, 10)
}).listen(8080)
}

执行node server.js启动服务器,继续按照之前的做法,利用任务管理器杀死进程,可以看到在进程被杀后 master 能够及时启动新的 worker。

image

继续运行 tcp_client,可以看到服务器能够正常处理请求。

image

三、小结

利用child_processcluster模块能够很好地实现Master-Worker模式多进程架构,实现单机服务器集群,充分利用了多核 CPU 资源。通过进程通信能够实现进程管理、重启以及负载均衡,从而提高集群的稳定性和健壮性。

前言

我从用 git 就一直用 rebase,但是新的公司需要用 merge 命令,我不是很明白,所以查了一些资料,总结了下面的内容,如果有什么不妥的地方,还望指正,我一定虚心学习。

merge 和 rebase

标题上的两个命令:merge 和 rebase 都是用来合并分支的。

这里不解释 rebase 命令,以及两个命令的原理,详细解释参考这里

下面的内容主要说的是两者在实际操作中的区别。

什么是分支

分支就是便于多人在同一项目中的协作开发。比方说:每个人开发不同的功能,在各自的分支开发过程中互不影响,完成后都提交到 develop 分支。极大的提高了开发的效率。

合并分支

每个人创建一个分支进行开发,当开发完成,需要合并到 develop 分支的时候,就需要用到合并的命令。

什么是冲突

合并的时候,有可能会产生冲突。

冲突的产生是因为在合并的时候,不同分支修改了相同的位置。所以在合并的时候 git 不知道那个到底是你想保留的,所以就提出疑问(冲突提醒)让你自己手动选择想要保留的内容,从而解决冲突。

merge 和 rebase 的区别

  1. 采用 merge 和 rebase 后,git log 的区别,merge 命令不会保留 merge 的分支的 commit
  2. image

处理冲突的方式:

  • (一股脑)使用merge命令合并分支,解决完冲突,执行git add .git commit -m'fix conflict‘。这个时候会产生一个 commit。
  • (交互式)使用 rebase 命令合并分支,解决完冲突,执行git add .git rebase --continue,不会产生额外的 commit。这样的好处是,‘干净’,分支上不会有无意义的解决分支的 commit;坏处,如果合并的分支中存在多个 commit,需要重复处理多次冲突。
  1. git pullgit pull --rebase区别:git pull做了两个操作分别是‘获取’和合并。所以加了 rebase 就是以 rebase 的方式进行合并分支,默认为 merge。

git mergegit merge --no-ff的区别

1、我自己尝试 merge 命令后,发现:merge 时并没有产生一个 commit。不是说 merge 时会产生一个 merge commit 吗?

注意:只有在冲突的时候,解决完冲突才会自动产生一个 commit。

如果想在没有冲突的情况下也自动生成一个 commit,记录此次合并就可以用:git merge –no-ff 命令,下面用一张图来表示两者的区别:

image

2、如果不加 –no-ff 则被合并的分支之前的 commit 都会被抹去,只会保留一个解决冲突后的 merge commit。

如何选择合并分支的方式

我的理解:主要是看哪个命令用的熟练,能够有效的管理自己的代码;还有就是团队用的是哪种方式。

我对于 rebase 比较熟悉,所以我一般都用rebase,但是现在的公司用的是merge --no-ff命令合并分支。所以,我在工作上就用 merge,个人项目就用 rebase。

也可以两者结合:

获取远程项目中最新代码时:git pull --rebase,这个是隐性的合并远程分支的代码不会产生额外的 commit(但是如果存在冲突的 commit 太多就像上面说的,需要处理很多遍冲突)。

合并到分支的时候:git merge --no-ff,自动一个merge commit,便于管理(这看管理人员怎么认为了)

总结

看懂上面的两幅图就行了。

  1. commit log 的区别
  2. 处理冲突的方式

git 如何正确回滚代码

方法一,删除远程分支再提交

① 首先保证当前工作区是干净的,并且和远程分支代码一致

1
2
3
$ git co currentBranch
$ git pull origin currentBranch
$ git co ./

② 备份当前分支(如有必要)

1
$ git branch currentBranchBackUp

③ 恢复到指定的 commit hash

1
$ git reset --hard resetVersionHash //将当前branch的HEAD指针指向commit hash

image

④ 删除当前分支的远程分支

1
2
$ git push origin :currentBranch
$ //或者这么写git push origin --delete currentBranch

⑤ 把当前分支提交到远程

1
$ git push origin currentBranch

方法二,强制 push 远程分支

① 首先保证当前工作区是干净的,并且和远程分支代码一致

② 备份当前分支(如有必要)

③ 恢复到指定的 commit hash

1
$ git reset --hard resetVersionHash

④ 把当前分支强制提交到远程

1
$ git push -f origin currentBranch

方法三,从回滚位置生成新的 commit hash

① 首先保证当前工作区是干净的,并且和远程分支代码一致

② 备份当前分支(如有必要)

③ 使用 git revert 恢复到指定的 commit hash,当前分支恢复到 a>3 版本(见下图)

a)此方法会产生一条多余的 commit hash&log,其实 1c0ce98 和 01592eb 内容上是一致的

b)git revert 是以要回滚的 commit hash(1c0ce98)为基础,新生成一个 commit hash(01592eb)

1
$ git revert resetVersionHash

image

④ 提交远程分支

1
$ git push origin currentBranch

方法四,从回滚位置生成新的分支 merge

① 首先保证当前工作区是干净的,并且和远程分支代码一致

② 备份当前分支(如有必要)

③ 把当前工作区的 HEAD 指针指向回滚的 commit hash(注意不是 branch 的 HEAD 指针)

Notice:这个时候工作区 HEAD 没有指向分支,称为匿名分支 detached HEAD

这个时候提交 commit 后无法保存状态,git 中的任何提交必须是在当前工作区 HEAD 所在分支的 HEAD 上进行 push hash 入栈,所以 HEAD 必须是属于某个分支的 HEAD 位置,提交才生效。

1
$ git co resetVersionHash

④ 以该 commit hash 创建一个新的分支

1
$ git co -b newRevertedHash

⑤ 切换到当前分支,合并 newRevertedHash。

1
2
$ git co currentBranch
$ git merge newRevertedHash

⑥ 进行代码 diff,完成代码回滚,push 到远程 currentBranch

Notice: 也可以直接 hotfix,从要回滚的地方直接重新打包一个新 tag 包,发版本 hotFixVersion 即可。

前言

koa 被认为是第二代 node web framework,它最大的特点就是独特的中间件流程控制,是一个典型的洋葱模型。koa 和 koa2 中间件的思路是一样的,但是实现方式有所区别,koa2 在 node7.6 之后更是可以直接用 async/await 来替代 generator 使用中间件,本文以最后一种情况举例。

洋葱模型

下面两张图是网上找的,很清晰的表明了一个请求是如何经过中间件最后生成响应的,这种模式中开发和使用中间件都是非常方便的

image

image

来看一个 koa2 的 demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Koa = require('koa');

const app = new Koa();
const PORT = 3000;

// #1
app.use(async (ctx, next)=>{
console.log(1)
await next();
console.log(1)
});
// #2
app.use(async (ctx, next) => {
console.log(2)
await next();
console.log(2)
})

app.use(async (ctx, next) => {
console.log(3)
})

app.listen(PORT);
console.log(`http://localhost:${PORT}`);

访问 http://localhost:3000,控制台打印:

1
2
3
4
5
1
2
3
2
1

怎么样,是不是有一点点感觉了。当程序运行到 await next()的时候就会暂停当前程序,进入下一个中间件,处理完之后才会在回过头来继续处理。也就是说,当一个请求进入,#1 会被第一个和最后一个经过,#2 则是被第二和倒数第二个经过,依次类推。

实现

koa 的实现有几个最重要的点

  1. context 的保存和传递
  2. 中间件的管理和 next 的实现

翻看源码我们发现
app.listen 使用了 this.callback()来生成 node 的 httpServer 的回调函数

1
2
3
4
5
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}

那就再来看 this.callback

1
2
3
4
5
6
7
8
9
10
11
12
callback() {
const fn = compose(this.middleware);

if (!this.listeners('error').length) this.on('error', this.onerror);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};

return handleRequest;
}

这里用 compose 处理了一下 this.middleware,创建了 ctx 并赋值为 createContext 的返回值,最后返回了 handleRequest。

this.middleware 看起来应该是中间件的集合,查了下代码,果不其然:

1
this.middleware = [];
1
2
3
4
5
6
7
8
9
10
11
12
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}

抛开兼容和判断,这段代码只做了一件事:

1
2
3
4
use(fn) {
this.middleware.push(fn);
return this;
}

原来当我们 app.use 的时候,只是把方法存在了一个数组里。
那么 compose 又是什么呢。跟踪源码可以看到 compose 来自 koa-compose 模块,代码也不多:(去掉了一些不影响主逻辑的判断)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function compose (middleware) {
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)

function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}

比较关键的就是这个 dispatch 函数了,它将遍历整个 middleware,然后将 context 和 dispatch(i + 1)传给 middleware 中的方法。

1
2
3
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))

这段代码就很巧妙的实现了两点:

1
2
3
1. 将`context`一路传下去给中间件

2. 将`middleware`中的下一个中间件`fn`作为未来`next`的返回值

这两点也是洋葱模型实现的核心。
再往下看代码实际上就没有太多花样了。
createContext 和 handleRequest 做的事实际上是把 ctx 和中间件进行绑定,也就是第一次调用 compose 返回值的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
request.ip = request.ips[0] || req.socket.remoteAddress || '';
context.accept = request.accept = accepts(req);
context.state = {};
return context;
}

handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

一、封装 autoTry 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<script>

function foo(params) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
JSON.parse('{{');
return resolve(params);
} catch (e) {
return reject(e);
}
}, 1000);
})
}

// ========doing======
function autoTry(fn, times) {
const retry = (params) => new Promise((resolve, reject) => {
fn(params).then(
(response) => { resolve(response) }
).catch(e => {
console.log('retrying: ' + new Date(), e)
if (times > 1) {
times -= 1
retry()
} else { reject(e) }
})
})

return retry
}

// ========test======
func = autoTry(foo, 3);
func({ a: 1, b: 2 }).then((res) => {
console.log(`结果:${JSON.stringify(res)}`);
}, (error) => {
console.log(error)
});

</script>
</body>

</html>

二、一个 n 个大小写字母组成的字符串按 ascii 码从小到大排序 查找字符串中第 k 个最小 ascii 码的字母输出该字母所在字符串位置索引

1
2
3
4
5
6
function lookup(str,key){
if(typeof str !='string'||key<1)return -1;
let value = str.split("").sort()[key-1];
return str.indexOf(value);
}
console.log(lookup('asasdskdjdfgnsdkfnmsASDdf',5)+1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let getIndexChar = (str,index)=>{
let sortChar = []
for(let i=0;i<str.length;i++) {
sortChar.push(str.charCodeAt(i))
}
sortChar = sortChar.sort((a,b)=>{
return a-b
})

let indexCode = -1
for(let i=0;i<str.length;i++) {
if(str[i].charCodeAt(0)==sortChar[index]) {
indexCode = i
}
}

return indexCode
}
getIndexChar('asdEQW',1) //4

三、ts 工具函数

1、实现一个 ts 的工具函数 GetOnlyFnProps ,提取泛型类型 T 中字段类型是函数的工具函数,其中 T 属于一个对象。

1
2
3
4
5
6
7
type GetOnlyFnKeys<T extends object> = {
[Key in keyof T]: T[K] extends Function ? K : never
}

type GetOnlyFnProps<T extends object> = {
[K in GetOnlyFnKeys<T>]: T[K]
}

实现一个 ts 的工具函数 UnGenericPromise ,提取 Promise 中的泛型类型

1
type UnGenericPromise<T extends Promise<any>> = T extends Promise<infer U> ? U : never

四、分页加载

h5_demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import React, { Component } from 'react';
import {PREFIX_URL ,request ,TITLE} from '../../common';
import DetailedList from "../../components/movie/DetailedListComponent";
import Loading from '../../components/common/LoadingComponent';

const PS = 5;
class FilmList extends Component {
constructor(...args) {
super(...args);
document.title = '艾米电影推荐';
this.stgId = localStorage.getItem("stgId") || "";
this.id = this.props.match.params.id;
this.state = {
listData: "",
page: 1,
pageSize: PS,
hasMore: false,
desc:'',
loading:true,
display:'none'
};
}
async componentWillMount() {
// console.log("片单ID-->", this.id);
await this.getList();
}
async getList(page=1) {
let url = `${PREFIX_URL}movie_client/list`+
`?stgId=${this.stgId}&movieListId=${this.id}&page=${page}&pageSize=${this.state.pageSize}`;
let res = await request(url);
if (res && res.success) {
document.title = res.title;
let listData = "";
if (page === 1) {
listData = res.data;
} else {
listData = this.state.listData;
listData = listData.concat(res.data);
}
await this.setState({
listData,
loading:false,
display:'block',
desc:res.desc,
page:res.page.current,
hasMore: res.page.current < res.page.pages
});
}
}
async componentWillUnmount () {
document.title = TITLE;
}
async refresh () {
await this.setState({page: 1});
await this.getList(1);
}
async loadMore() {
await this.setState({page: this.state.page + 1});
await this.getList(this.state.page);
}
render() {
return (
<div className="page-movie-detailed-list">
{
!!this.state.listData &&
<DetailedList
refresh={this.refresh.bind(this)}
loadMore={this.loadMore.bind(this)}
hasMore={this.state.hasMore}
listData={this.state.listData}
desc={this.state.desc}
style={{'display':this.state.display}}/>}
{this.state.loading ? <Loading/> : '' }
</div>
);
}
}

export default FilmList;

PC_demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { Button, Col, message as Message, Row, Icon } from 'antd'
import { Link } from 'react-router-dom'
import React from 'react'
import MessageListFilter, { IMessageListFilter } from './components/MessageListFilter'
import MessageListTable from './components/MessageListTable'
import MessageService from '../../../service/driverManage/MessageService'

export default class MessageList extends React.Component {
// public constructor(props: any){
// super(props)
// }
public state = {
filterProps: {
filter: {
beginCreatedTime: '',
endCreatedTime: '', // 日期
sendStatus: '-1', //发送状态
},
},
tableProps: {
page: {
current: 1,
pageSize: 10,
total: 0,
},
data: [],
},
searching: false,
}

public componentDidMount() {
this.reSearch()
}

public filterChange = (params: IMessageListFilter) => {
this.setState({
filterProps: {
filter: {
...this.state.filterProps.filter,
...params,
},
},
})
}

public search = async () => {
this.setState({
searching: true,
})
const { code, message, data } = await MessageService.pageMessageList({
...this.state.filterProps.filter,
...{
sendStatus:
this.state.filterProps.filter.sendStatus !== '-1'
? this.state.filterProps.filter.sendStatus
: null,
},
pageNum: this.state.tableProps.page.current,
pageSize: this.state.tableProps.page.pageSize,
})
this.setState({
searching: false,
})
if (code !== 200) {
Message.error(message)
return
}
const { current, pageSize } = this.state.tableProps.page
let rows = []
rows =
data.rows && data.rows.length
? data.rows.map((item: any, index: number) => {
return {
...item,
num: (current - 1) * pageSize + index + 1,
}
})
: []
this.setState({
tableProps: {
page: {
...this.state.tableProps.page,
total: data.total,
},
data: rows,
},
})
}

public reSearch = () => {
const { tableProps } = this.state
const {
page: { pageSize, total },
} = tableProps

this.setState(
{
tableProps: {
...tableProps,
page: {
pageSize,
total,
current: 1,
},
},
},
() => {
this.search()
}
)
}

public pageChange = (page: object) => {
this.setState({
tableProps: {
...this.state.tableProps,
page,
},
})
this.search()
}

public getDetail = (params: any) => {
;(this.props as any).history.push('messageDetail', { id: 6, type: 0 })
}
public render() {
const { filterProps, tableProps, searching } = this.state
return (
<section>
<MessageListFilter
loading={searching}
{...filterProps}
onChange={this.filterChange}
onSearch={this.reSearch}
></MessageListFilter>
<Row style={{ marginBottom: '10px' }}>
<Col>
<Button type="primary">
<Link to={`/fast/message/messageCreate`}>
<Icon type="plus" />
<span style={{ marginLeft: '8px' }}>新建推送</span>
</Link>
</Button>
</Col>
</Row>
<MessageListTable
{...tableProps}
onPagechange={this.pageChange}
getDetail={this.getDetail}
onSearch={this.search}
loading={searching}
></MessageListTable>
</section>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import { Button, Table, Col, Modal, message as Message } from 'antd'
import React from 'react'
import { PaginationConf } from '../../../../config/antd/PaginationConf'
import { Link } from 'react-router-dom'
import MessageService from '../../../../service/driverManage/MessageService'
export interface IMessageTable {
num: string | number
creatTime: string
driverName: string
driverPhoneNum: string
receiptType: number
receipt: number | string
output: number | string
banace: number | string
}
export interface IProps {
data: IMessageTable[]
page: {
current: number
pageSize: number
total: number
}
onPagechange?: (page: { current: number; pageSize: number; total: number }) => void
getDetail: (param: any) => void
onSearch: () => void
loading: boolean
}

const { confirm } = Modal
class MessageListTable extends React.Component<IProps> {
public constructor(props: IProps) {
super(props)
;(this as any).columns = [
{
title: '序号',
dataIndex: 'num',
align: 'center',
},
{
title: '发送时间',
dataIndex: 'sendTime',
align: 'center',
},
{
title: '发送范围',
dataIndex: 'sendScope',
align: 'center',
},
{
title: '消息中心',
dataIndex: 'canIntoMessageCenter',
align: 'center',
},
{
title: '标题',
dataIndex: 'title',
align: 'center',
},
{
title: '操作人',
dataIndex: 'createdBy',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createdTime',
align: 'center',
},
{
title: '发送状态',
dataIndex: 'sendStatus',
align: 'center',
},
{
title: '操作',
align: 'center',
render(row: any) {
return (
<div>
{row.canCancelSend ? (
<Col>
<Button
type="primary"
onClick={() => {
cancel(row)
}}
>
取消发送
</Button>
{new Date(row.sendTime).getTime() - new Date().getTime() > 5 * 60 * 60 * 1000 ? (
<Button type="primary" style={{ marginLeft: '5px' }}>
<Link to={`/fast/message/messageUpdate/${row.id}`}>查看</Link>
</Button>
) : (
<Button type="primary" style={{ marginLeft: '5px' }}>
<Link to={`/fast/message/messageDetail/${row.id}`}>查看</Link>
</Button>
)}
</Col>
) : (
<Button type="primary">
<Link to={`/fast/message/messageDetail/${row.id}`}>查看</Link>
</Button>
)}
</div>
)
},
},
]
function cancel(row: any) {
const { onSearch } = props
if (new Date(row.sendTime).getTime() - new Date().getTime() > 5 * 60 * 60 * 1000) {
confirm({
title: '提示',
content: '是否确认取消',
async onOk() {
const { code, message } = await MessageService.messageCancel(row.id)
if (code !== 200) {
Message.error(message)
return
}
Message.success('取消成功')
onSearch()
},
onCancel() {
return
},
})
} else {
Modal.info({
title: '提示',
content: <div>距离预计发送时间不到5分钟 禁止取消</div>,
okText: '关闭',
})
}
}
}
public render() {
const { data, page, onPagechange, loading } = this.props
return (
<section>
<Table
dataSource={data}
loading={loading}
// @ts-ignore
columns={this.columns}
rowKey={'id'}
size={'middle'}
// scroll={{ x: 2500 }}
pagination={{
...PaginationConf,
...this.props.page,
onChange: (toCurrent, pageSize) => {
page.current = toCurrent
page.pageSize = pageSize as number

if (onPagechange) {
onPagechange(page)
}
},
onShowSizeChange: (current, toPageSize) => {
page.current = current
page.pageSize = toPageSize

if (onPagechange) {
onPagechange(page)
}
},
showTotal: (total: number) => `共计 ${total} 条`,
}}
></Table>
</section>
)
}
}

export { MessageListTable }
export default MessageListTable

一.它们几乎完全相同,但是 PureComponent 通过 prop 和 state 的浅比较来实现 shouldComponentUpdate,某些情况下可以用 PureComponent 提升性能

1.所谓浅比较(shallowEqual),即 react 源码中的一个函数,然后根据下面的方法进行是不是 PureComponent 的判断,帮我们做了本来应该我们在 shouldComponentUpdate 中做的事情

1
2
3
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);
}

而本来我们做的事情如下,这里判断了 state 有没有发生变化(prop 同理),从而决定要不要重新渲染,这里的函数在一个继承了 Component 的组件中,而这里 this.state.person 是一个对象,你会发现,在这个对象的引用没有发生变化的时候是不会重新 render 的(即下面提到的第三点),所以我们可以用 shouldComponentUpdate 进行优化,这个方法如果返回 false,表示不需要重新进行渲染,返回 true 则重新渲染,默认返回 true

1
2
3
shouldComponentUpdate(nextProps, nextState) {
return (nextState.person !== this.state.person);
}

2.上面提到的某些情况下可以使用 PureComponent 来提升性能,那具体是哪些情况可以,哪些情况不可以呢,实践出真知

3.如下显示的是一个 IndexPage 组件,设置了一个 state 是 isShow,通过一个按钮点击可以改变它的值,结果是:初始化的时候输出的是 constructor,render,而第一次点击按钮,会输出一次 render,即重新渲染了一次,界面也会从显示 false 变成显示 true,但是当这个组件是继承自 PureComponent 的时候,再点击的时,不会再输出 render,即不会再重新渲染了,而当这个组件是继承自 Component 时,还是会输出 render,还是会重新渲染,这时候就是 PureComponent 内部做了优化的体现

4.同理也适用于 string,number 等基本数据类型,因为基本数据类型,值改变了就算改变了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { PureComponent } from 'react';

class IndexPage extends PureComponent{
constructor() {
super();
this.state = {
isShow: false
};
console.log('constructor');
}
changeState = () => {
this.setState({
isShow: true
})
};
render() {
console.log('render');
return (
<div>
<button onClick={this.changeState}>点击</button>
<div>{this.state.isShow.toString()}</div>
</div>
);
}
}

5.当这个 this.state.arr 是一个数组时,且这个组件是继承自 PureComponent 时,初始化依旧是输出 constructor 和 render,但是当点击按钮时,界面上没有变化,也没有输出 render,证明没有渲染,但是我们可以从下面的注释中看到,每点击一次按钮,我们想要修改的 arr 的值已经改变,而这个值将去修改 this.state.arr,但是因为在 PureComponent 中浅比较这个数组的引用没有变化所以没有渲染,this.state.arr 也没有更新,因为在 this.setState()以后,值是在 render 的时候更新的,这里涉及到 this.setState()的知识

6.但是当这个组件是继承自 Component 的时候,初始化依旧是输出 constructor 和 render,但是当点击按钮时,界面上出现了变化,即我们打印处理的 arr 的值输出,而且每点击一次按钮都会输出一次 render,证明已经重新渲染,this.state.arr 的值已经更新,所以我们能在界面上看到这个变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { PureComponent } from 'react';

class IndexPage extends PureComponent{
constructor() {
super();
this.state = {
arr:['1']
};
console.log('constructor');
}
changeState = () => {
let { arr } = this.state;
arr.push('2');
console.log(arr);
// ["1", "2"]
// ["1", "2", "2"]
// ["1", "2", "2", "2"]
// ....
this.setState({
arr
})
};
render() {
console.log('render');
return (
<div>
<button onClick={this.changeState}>点击</button>
<div>
{this.state.arr.map((item) => {
return item;
})}
</div>
</div>
);
}
}

7.下面的例子用扩展运算符产生新数组,使 this.state.arr 的引用发生了变化,所以初始化的时候输出 constructor 和 render 后,每次点击按钮都会输出 render,界面也会变化,不管该组件是继承自 Component 还是 PureComponent 的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { PureComponent } from 'react';

class IndexPage extends PureComponent{
constructor() {
super();
this.state = {
arr:['1']
};
console.log('constructor');
}
changeState = () => {
let { arr } = this.state;
this.setState({
arr: [...arr, '2']
})
};
render() {
console.log('render');
return (
<div>
<button onClick={this.changeState}>点击</button>
<div>
{this.state.arr.map((item) => {
return item;
})}
</div>
</div>
);
}
}

8.上面的情况同样适用于对象的情况

二.PureComponent 不仅会影响本身,而且会影响子组件,所以 PureComponent 最佳情况是展示组件

1.我们让 IndexPage 组件里面包含一个子组件 Example 来展示 PureComponent 是如何影响子组件的

2.父组件继承 PureComponent,子组件继承 Component 时:下面的结果初始化时输出为 constructor,IndexPage render,example render,但是当我们点击按钮时,界面没有变化,因为这个 this.state.person 对象的引用没有改变,只是改变了它里面的属性值所以尽管子组件是继承 Component 的也没有办法渲染,因为父组件是 PureComponent,父组件根本没有渲染,所以子组件也不会渲染

3.父组件继承 PureComponent,子组件继承 PureComponent 时:因为渲染在父组件的时候就没有进行,相当于被拦截了,所以子组件是 PureComponent 还是 Component 根本不会影响结果,界面依旧没有变化

4.父组件继承 Component,子组件继承 PureComponent 时:结果和我们预期的一样,即初始化是会输出 constructor,IndexPage render,example render,但是点击的时候只会出现 IndexPage render,因为父组件是 Component,所以父组件会渲染,但是
当父组件把值传给子组件的时候,因为子组件是 PureComponent,所以它会对 prop 进行浅比较,发现这个 person 对象的引用没有发生变化,所以不会重新渲染,而界面显示是由子组件显示的,所以界面也不会变化

5.父组件继承 Component,子组件继承 Component 时:初始化是会输出 constructor,IndexPage render,example render,当我们第一次点击按钮以后,界面发生变化,后面就不再改变,因为我们一直把它设置为 sxt2,但是每点击一次都会输出 IndexPage render,example render,因为每次不管父组件还是子组件都会渲染

6.所以正如下面第四条说的,如果 state 和 prop 一直变化的话,还是建议使用 Component,并且 PureComponent 最好作为展示组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//父组件
import React, { PureComponent, Component } from 'react';
import Example from "../components/Example";

class IndexPage extends PureComponent{
constructor() {
super();
this.state = {
person: {
name: 'sxt'
}
};
console.log('constructor');
}
changeState = () => {
let { person } = this.state;
person.name = 'sxt2';
this.setState({
person
})
};
render() {
console.log('IndexPage render');
const { person } = this.state;
return (
<div>
<button onClick={this.changeState}>点击</button>
<Example person={person} />
</div>
);
}
}
//子组件
import React, { Component } from 'react';

class Example extends Component {

render() {
console.log('example render');
const { person } = this.props;
return(
<div>
{person.name}
</div>
);
}
}

三.若是数组和对象等引用类型,则要引用不同,才会渲染

四.如果 prop 和 state 每次都会变,那么 PureComponent 的效率还不如 Component,因为你知道的,进行浅比较也是需要时间

五.若有 shouldComponentUpdate,则执行它,若没有这个方法会判断是不是 PureComponent,若是,进行浅比较

1.继承自 Component 的组件,若是 shouldComponentUpdate 返回 false,就不会渲染了,继承自 PureComponent 的组件不用我们手动去判断 prop 和 state,所以在 PureComponent 中使用 shouldComponentUpdate 会有如下警告:

IndexPage has a method called shouldComponentUpdate(). shouldComponentUpdate should not be used when extending React.PureComponent. Please extend React.Component if shouldComponentUpdate is used.

也是比较好理解的,就是不要在 PureComponent 中使用 shouldComponentUpdate,因为根本没有必要

正如标题,react 项目在打包完成后发现 chunk.js 文件比较大,导致打开首页需要时间比较久,因此,需要进行优化。
其实仔细考虑一下不难发现,由于打包后将所有资源都打包到了一个 chunk.js 下,导致所有资源都一起加载了,所以,进入页面会很慢。咱们的预期目标是进入首页只加载首页的资源,进入详情页至加载详情页的资源。那么,react-loadable 是你不错的选择。

1、首先,要想使用它需要先安装它。

1
2
yarn add react-loadable
yarn add babel-plugin-syntax-dynamic-import

根据 npm 官网找到 react-loadable 完成配置,当然我为了以后方便使用对 react-loadable 进行了封装。

2 其次,在 utils 文件夹下新建 loadable.js 文件,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from "react";
import { Spin } from 'antd';
import Loadable from "react-loadable";

import './index.less'

// 加载动画
const loadingComponent = () => {
return <div className={'spin-loading'}>
<div><Spin size="large" /></div>
</div>;
};

// 当不传加载动画时候使用默认的加载动画
export default (loader, loading = loadingComponent) => {
return Loadable({
loader,
loading,
});
};

3、在 index.js 文件里配置路由时候就可以使用如下方式进行懒加载处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import * as serviceWorker from "./serviceWorker";
import { BrowserRouter as Router, Route } from "react-router-dom";

import { createBrowserHistory } from "history";
import loadable from './utils/loadable'

const App = loadable(() => import("./router/login/App"));
const MoveVideo = loadable(() => import("./router/video"));
const UserReg = loadable(() => import("./router/userReg/index"));
const FoodList = loadable(() => import("./router/food/index"));

ReactDOM.render(
<Router history={createBrowserHistory()}>
<Route exact path="/" component={App} />
<Route path="/user-reg/" component={UserReg} />
<Route path="/food-list" component={FoodList} />
<Route path="/video" component={MoveVideo} />
</Router>,
document.getElementById("root")
);

serviceWorker.unregister();

4、再次进行打包编译,就会发现多出很多 chunk.js 文件,这就是从原来一个拆成多个,这样加载速度就会得到提升。