MySQl 分析函数

ROW_NUMBER

表示根据 col1 分组,在分组内部根据 col2 排序,而这个值就表示每组内部排序后的顺序编码

1
row_number() over( partition by col1 order by col2);

ROW_NUMBER 返回行信息没有排名

RANK DENSE_RANK

1
2
3
4
5
select *,
row_number () over(partition by s.SId order by s.score) as row_number1,
rank () over(partition by s.SId order by s.score) as rank1,
dense_rank () over(partition by s.SId order by s.score) as dense_rank1
from SC s ;

ROW_NUMBER 连续排名
RANK 值相同排名相同,排名跳跃
DENSE_RANK 值相同排名相同,排名连续

查询所有同学最高分对应的科目名称

MySQL 局部变量

Transact-SQL 局部变量是可以保存特定类型的单个数据值的对象。在脚本或批处理中通常会使用变量

  • 作为计数器来计算循环执行的次数或控制循环执行的次数。
  • 保存流程控制中的测试值
  • 保存要由存储过程返回码或函数返回值返回的数据值。

SELECT @local_variable

1
2
SELECT { @local_variable { = | += | -= | *= | /= | %= | &= | ^= | |= } expression }
[ ,...n ] [ ; ]

将右边的值赋给左边的变量。

操作员 行动
= 将后面的表达式分配给变量。
+= 添加和分配
-= 减法和赋值
*= 相乘并赋值
/= 划分和分配
%= 取模和赋值
&= 按位与并赋值
^= 按位异或并赋值
| = 按位或并赋值

SELECT @local_variable 通常用于将单个值返回到变量中。但是,当表达式是列名时,它可以返回多个值。如果 SELECT 语句返回多个值,则为变量分配最后一个返回的值。

如果 SELECT 语句没有返回任何行,则变量保留其当前值。如果表达式是不返回值的标量子查询,则变量设置为 NULL。

一个 SELECT 语句可以初始化多个局部变量。

  • 查询最近 12 个月的数据量, 没有的用 0 补全

因为把表中的所有月份 group by 之后,已有的月份不足 12 个月, 需要补齐,所以先生成一张 12 个月的空表

注意子查询中的日期不能使用 date_format 因为会失去时间格式,导致上面的变量在 addDate 中不可使用

1
2
3
select DATE_FORMAT( @u := ADDDATE(@u,INTERVAL 1 Month),'%Y-%m') as date from (
select @u := ADDDATE(now(),INTERVAL -6 Month) from Student s limit 12
) as t;

在拼接已存在的数据

1
2
3
4
5
6
7
8
select DATE_FORMAT( @u := ADDDATE(@u,INTERVAL 1 Month),'%Y-%m') as date from (
select @u := ADDDATE(now(),INTERVAL -6 Month) from Student s limit 12
) as t

left join (
select count(*) as sum,DATE_FORMAT(s.date,'%Y-%m') as date from Student s
group by DATE_FORMAT(s.date,'%Y-%m')
) as t2 on t2.date = t.date

最后把空值处理为 0

1
2
3
4
5
6
7
8
9
10
select date, if(ISNULL(d.sum),0,d.sum) as sum from (
select DATE_FORMAT( @u := ADDDATE(@u,INTERVAL 1 Month),'%Y-%m') as date from (
select @u := ADDDATE(now(),INTERVAL -6 Month) from Student s limit 12
) as t

left join (
select count(*) as sum,DATE_FORMAT(s.date,'%Y-%m') as date from Student s
group by DATE_FORMAT(s.date,'%Y-%m')
) as t2 on t2.date = t.date
) as d

MySQL 日期时间函数

ADDDATE()

1
2
3
4
5
-- date时间字段名  expr时间间隔
-- unit单位 指的时间单位 天day 小时hour表示等
DATE_ADD(date,INTERVAL expr unit)

-- 采用这种写法的时候,使用方法与DATE_ADD一致
  • 日期+天数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT ADDDATE('2020-01-02 00:00:00', INTERVAL 1 day) from orders

--打印字段为:2020-01-03 00:00:00

-- 下面同理, 在写增加天为单位时 可以缩写成
SELECT ADDDATE('2020-01-02 00:00:00', 1) from orders

--默认为day
--打印字段为:2020-01-03 00:00:00

--如果减一天 则为 -1 这里也可写成字符串
SELECT ADDDATE('2020-01-01 00:00:00', '-1') from orders

--打印字段为:2020-01-01 00:00:00

  • 日期+小时
1
2
SELECT ADDDATE('2020-01-02 00:00:00', INTERVAL 1 HOUR) from orders
-- 打印字段为:2020-01-01 01:00:00
  • 日期+分钟
1
2
SELECT ADDDATE('2020-01-02 00:00:00', INTERVAL 1 MINUTE) from orders
-- 打印字段为:2020-01-01 00:01:00
  • 日期+秒
1
2
SELECT ADDDATE('2020-01-02 00:00:00', INTERVAL 1 SECOND) from orders
-- 打印字段为:2020-01-01 00:00:01

DATE_FORMAT()

date 参数是合法的日期。format 规定日期/时间的输出格式。

1
DATE_FORMAT(date,format)
格式 描述
%a 缩写星期名
%b 缩写月名
%c 月,数值
%D 带有英文前缀的月中的天
%d 月的天,数值(00-31)
%e 月的天,数值(0-31)
%f 微秒
%H 小时 (00-23)
%h 小时 (01-12)
%I 小时 (01-12)
%i 分钟,数值(00-59)
%j 年的天 (001-366)
%k 小时 (0-23)
%l 小时 (1-12)
%M 月名
%m 月,数值(00-12)
%p AM 或 PM
%r 时间,12-小时(hh:mm:ss AM 或 PM)
%S 秒(00-59)
%s 秒(00-59)
%T 时间, 24-小时 (hh:mm:ss)
%U 周 (00-53) 星期日是一周的第一天
%u 周 (00-53) 星期一是一周的第一天
%V 周 (01-53) 星期日是一周的第一天,与 %X 使用
%v 周 (01-53) 星期一是一周的第一天,与 %x 使用
%W 星期名
%w 周的天 (0=星期日, 6=星期六)
%X 年,其中的星期日是周的第一天,4 位,与 %V 使用
%x 年,其中的星期一是周的第一天,4 位,与 %v 使用
%Y 年,4 位
%y 年,2 位

NOW() CURDATE() CURTIME()

函数返回当前的日期和时间。

NOW() CURDATE() CURTIME()
2008-12-29 16:25:46 2008-12-29 16:25:46

身份认证

认证(Authentication)

在互联网中证明自己的身份:

  • 用户名密码登录
  • 邮箱发送登录链接
  • 手机号接收验证码
  • 通过第三方受信任的程序登陆,例如微信登陆

授权(Authorization)

用户授予第三方应用访问该用户某些资源的权限,在使用微信登陆第三方系统前,需要给第三方系统授权,允许使用微信登陆

实现授权的方式有:cookie、session、token、OAuth

凭证(Credentials)

实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身

  • HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。

  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。

  • cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。

属性 说明
name=value 键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型- 如果值为 Unicode 字符,需要为字符编码。- 如果值为二进制数据,则需要使用 BASE64 编码。
domain 指定 cookie 所属域名,默认是当前域名
path 指定 cookie 在哪个路径(路由)下生效,默认是 ‘/‘。如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read。
maxAge cookie 失效的时间,单位秒。如果为整数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1。
expires 过期时间,在设置的某个时间点后该 cookie 就会失效。一般浏览器的 cookie 都是默认储存的,当关闭浏览器结束这个会话的时候,这个 cookie 也就会被删除
secure 该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL 等,在网络上传输数据之前先将数据加密。默认为 false。当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。
httpOnly 如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全

什么是 Session

Session 一般配合 Cookie 使用,是一种记录服务器和客户端会话状态的机制

  • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
  • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
  • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
  • 在做负载均衡的时候, 需要把每一个请求分配到上一次处理他的服务器中, 这个问题可以通过 IP hash 来解决.

  • 如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session. 在同一服务中的所有不同域名的网站都可以自动登陆.一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
    另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

JWT

首先说明 Token 是一个宽泛的概念, 其本质都是让客户端保存一些信息, 从而降低服务器的压力. JWT 是其中比较好的实现.

JWT(JSON Web Token) 服务器认证以后,字符串发回给用户,就像下面这样。服务器完全只靠这个对象认定用户身份。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

  • Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据.alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256), typ 属性表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT。

最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。

  • Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了 7 个官方字段,供选用。也可以添加自己定义的字段, JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个 JSON 对象也要使用 Base64URL 算法转成字符串。

1
2
3
4
5
6
7
8
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

  • Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照图片中的公式产生签名。

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。

JWT 使用方法

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息 Authorization 字段里面。

1
Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

JWT 的特点

(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。

(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

React v16 源码分析 ⑩ 事件系统

合成事件

先看一个案例

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
export default class App extends React.Component {
state = {
show: false,
};
ref = React.createRef();
componentDidMount() {
const that = this;
document.addEventListener("click", () => {
that.setState({ show: false });
console.log("document");
});
this.ref.current.addEventListener("click", () => {
console.log("ref");
});
}
render() {
return (
<div
ref={this.ref}
onClick={() => this.setState({ show: true }, console.log("dom"))}
>
点击 {this.state.show ? "show" : "null"}
</div>
);
}
}

当点击事件触发的时候会发现点击事件无效, 先打印出 ref,再打印 dom,后打印出 document,先分析一个打印顺序的问题

如果事件是绑定在原生上的,那么 render 的执行会早于 componentDidMount,所以按理来说打印 dom 的事件应该先执行,在执行打印 ref 的事件,但显然结果不是这样.

其实这就是 React 合成事件(SyntheticEvent),在项目初始化的时候, 在 react v16 版本会把所有的事件绑定在 document 元素上, v17 版本把元素修改为 root 元素,用官方的话讲就是:不需要使用 addEventListener 为已创建的 DOM 元素添加监听器。事实上,你只需要在该元素初始渲染的时候添加监听器即可。

因为这个原因,在冒泡阶段最先执行的是 div 的原生事件,又因为 div 上的合成事件虽然绑定在 document 上,但绑定时机在项目初始化的时候, 而 document 上原生的绑定事件是在 componentDidMount 生命周期中,所以按照原生事件的行为,先绑定的先执行,会先打印 dom 后打印 document,看一个复杂的例子:

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
import React from "react";

export default class App extends React.Component {
parentRef = React.createRef();
childRef = React.createRef();
componentDidMount() {
this.parentRef.current.addEventListener(
"click",
() => {
console.log("父元素原生事件捕获");
},
true
);
this.parentRef.current.addEventListener("click", () => {
console.log("父元素原生事件冒泡");
});
this.childRef.current.addEventListener(
"click",
() => {
console.log("子元素原生事件捕获");
},
true
);
this.childRef.current.addEventListener("click", () => {
console.log("子元素原生事件冒泡");
});
document.addEventListener(
"click",
() => {
console.log("document 捕获");
},
true
);
document.addEventListener("click", () => {
console.log("document 冒泡");
});
}
parentBubble = () => {
console.log("父组件React事件冒泡");
};
parenteCapture = () => {
console.log("父组件React事件捕获");
};
childBubble = () => {
console.log("子组件React事件冒泡");
};
childeCapture = () => {
console.log("子组件React事件捕获");
};
render() {
return (
<div
ref={this.parentRef}
onClick={this.parentBubble}
onClickCapture={this.parenteCapture}
>
<p
ref={this.childRef}
onClick={this.childBubble}
onClickCapture={this.childeCapture}
>
事件
</p>
</div>
);
}
}

v16 版本

1
2
3
4
5
6
7
8
9
10
document 捕获
父元素原生事件捕获
子元素原生事件捕获
子元素原生事件冒泡
父元素原生事件冒泡
父组件React事件捕获
子组件React事件捕获
子组件React事件冒泡
父组件React事件冒泡
document 冒泡

虽然合成事件绑定在 document 元素上但只能在冒泡阶段触发, 因为合成事件绑定在冒包阶段而不是捕获阶段, 因此会先执行所有原生事件的捕获阶段, 捕获阶段结束之后由于合成事件在 document 仍然不会执行, 紧接着执行原生的冒泡事件.直到冒泡到 document 元素由于合成事件绑定的早,所有一次性的执行了合成事件的捕获和冒泡阶段,最后才是 document 冒泡事件

v17 版本

1
2
3
4
5
6
7
8
9
10
11

document 捕获
父组件React事件捕获
子组件React事件捕获
父元素原生事件捕获
子元素原生事件捕获
子元素原生事件冒泡
父元素原生事件冒泡
子组件React事件冒泡
父组件React事件冒泡
document 冒泡

由于在 v17 版本中事件是绑定在 root 上的,所以在捕获阶段可以执行合成事件的 冒泡阶段.

简单实现

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
<body>
<div id="root">
<div id="parent">
<p id="child">事件</p>
</div>
</div>
</body>
<script>
let root = document.getElementById("root");
let parent = document.getElementById("parent");
let child = document.getElementById("child");

parent.addEventListener(
"click",
() => {
console.log("父元素原生事件捕获");
},
true
);
parent.addEventListener("click", () => {
console.log("父元素原生事件冒泡");
});
child.addEventListener(
"click",
() => {
console.log("子元素原生事件捕获");
},
true
);
child.addEventListener("click", () => {
console.log("子元素原生事件冒泡");
});

document.addEventListener(
"click",
function () {
console.log("document 捕获");
},
true
);
document.addEventListener("click", function () {
console.log("document 冒泡");
});

parent.onClick = function () {
console.log("父组件React事件冒泡");
};
parent.onClickCapture = () => {
console.log("父组件React事件捕获");
};
child.onClick = function () {
console.log("子组件React事件冒泡");
};
child.onClickCapture = () => {
console.log("子组件React事件捕获");
};
</script>

v16 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
document.addEventListener("click", dispatchEvent);
function dispatchEvent(event) {
let paths = [];
let current = event.target;
while (current) {
paths.push(current);
current = current.parentNode;
}
for (let i = paths.length - 1; i >= 0; i--) {
let handle = paths[i].onClickCapture;
handle && handle();
}
for (let i = 0; i < paths.length; i++) {
let handle = paths[i].onClick;
handle && handle();
}
}

v17 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function dispatchEvent(event, useCapture) {
let paths = [];
let current = event.target;
while (current) {
paths.push(current);
current = current.parentNode;
}
if (useCapture) {
for (let i = paths.length - 1; i >= 0; i--) {
let handle = paths[i].onClickCapture;
handle && handle();
}
} else {
for (let i = 0; i < paths.length; i++) {
let handle = paths[i].onClick;
handle && handle();
}
}
}
root.addEventListener("click", (e) => dispatchEvent(e, true), true);
root.addEventListener("click", (e) => dispatchEvent(e, false));

事件注册

这一过程是在全局执行的

1
2
3
4
5
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();

先把所有的事件分为以下 5 种类型

  • simpleEvents

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    for (var i = 0; i < simpleEventPluginEvents.length; i++) {
    var eventName = simpleEventPluginEvents[i];
    var domEventName = eventName.toLowerCase();
    var capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);

    // 把事件转换成 onEvent 的格式
    registerSimpleEvent(domEventName, "on" + capitalizedEvent);
    }

    registerSimpleEvent(ANIMATION_END, "onAnimationEnd");
    registerSimpleEvent(ANIMATION_ITERATION, "onAnimationIteration");
    registerSimpleEvent(ANIMATION_START, "onAnimationStart");
    registerSimpleEvent("dblclick", "onDoubleClick");
    registerSimpleEvent("focusin", "onFocus");
    registerSimpleEvent("focusout", "onBlur");
    registerSimpleEvent(TRANSITION_END, "onTransitionEnd");
  • enterLeaveEvents

    1
    2
    3
    4
    registerDirectEvent("onMouseEnter", ["mouseout", "mouseover"]);
    registerDirectEvent("onMouseLeave", ["mouseout", "mouseover"]);
    registerDirectEvent("onPointerEnter", ["pointerout", "pointerover"]);
    registerDirectEvent("onPointerLeave", ["pointerout", "pointerover"]);
  • changeEvents

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    registerTwoPhaseEvent("onChange", [
    "change",
    "click",
    "focusin",
    "focusout",
    "input",
    "keydown",
    "keyup",
    "selectionchange",
    ]);
  • selectEvent

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    registerTwoPhaseEvent("onSelect", [
    "focusout",
    "contextmenu",
    "dragend",
    "focusin",
    "keydown",
    "keyup",
    "mousedown",
    "mouseup",
    "selectionchange",
    ]);
  • beforeInputEvents

    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
    function registerEvents() {
    registerTwoPhaseEvent("onBeforeInput", [
    "compositionend",
    "keypress",
    "textInput",
    "paste",
    ]);
    registerTwoPhaseEvent("onCompositionEnd", [
    "compositionend",
    "focusout",
    "keydown",
    "keypress",
    "keyup",
    "mousedown",
    ]);
    registerTwoPhaseEvent("onCompositionStart", [
    "compositionstart",
    "focusout",
    "keydown",
    "keypress",
    "keyup",
    "mousedown",
    ]);
    registerTwoPhaseEvent("onCompositionUpdate", [
    "compositionupdate",
    "focusout",
    "keydown",
    "keypress",
    "keyup",
    "mousedown",
    ]);
    }

registerSimpleEvent 是为了保存原生事件和合成事件名称之间的对应关系,并且调用 registerTwoPhaseEvent

1
2
3
4
5
6
var topLevelEventsToReactNames = new Map();
// click onClick
function registerSimpleEvent(domEventName, reactName) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}

registerTwoPhaseEvent 会调用 registerDirectEvent 用于绑定合成事件名称和其对应的真正事件,一个合成事件对应多个原生事件,也就是 JSX 中绑定的事件,可能会触发多个原生事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function registerTwoPhaseEvent(registrationName, dependencies) {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + "Capture", dependencies);
}

function registerDirectEvent(registrationName, dependencies) {
// 合成事件和原生事件的对应关系 {onAbort:['abort']}
registrationNameDependencies[registrationName] = dependencies;

{
var lowerCasedName = registrationName.toLowerCase();

// 内部用于验证事件的对象,保存的是驼峰命名和非驼峰命名之间的关系 {onclick:onClick}
possibleRegistrationNames[lowerCasedName] = registrationName;

if (registrationName === "onDoubleClick") {
possibleRegistrationNames.ondblclick = registrationName;
}
}
//所有原生事件名称的 set 集合 set([click,cancel])
for (var i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}

注册过程就是通过几个全局变量完整的保存了,原生事件名称(click),原生事件绑定名称(onClick),合成事件名称 中的对应关系

事件绑定

绑定事件发生在 rendercreateRoot 方法中, 在创建了 root 节点之后会调用 listenToAllSupportedEvents, 会对所有的原生事件调用绑定函数

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
function listenToAllSupportedEvents(rootContainerElement) {
allNativeEvents.forEach(function (domEventName) {
// 特殊处理,这个方法只会在 document 上
if (domEventName !== "selectionchange") {
// 排除那些没有冒泡阶段的事件
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});

ownerDocument[listeningMarker] = true;
listenToNativeEvent("selectionchange", false, ownerDocument);
}

function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
var eventSystemFlags = 0;

// 添加捕获阶段的标识
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}

addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener
);
}

function addTrappedEventListener(
targetContainer,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
isDeferredListenerForLegacyFBSupport
) {
// 创建事件回调函数
var listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
);

var isPassiveListener = undefined;
// 是否支持 passive 参数
// addEventListener 第三个参数可以写为一个对象 { capture: false, once: true,passive:true }
// capture 表示冒泡阶段执行还是捕获阶段执行
// once 标识绑定事件只会执行一次就被移除
// passive 是否执行默认事件,有些默认事件和浏览器行为绑定比如移动端 touchstart 会触发浏览器滚动
// 如果使用 event.preventDefault() 会调用函数后才会停止默认行为, 可以使用 passive:false 直接阻止默认行为
if (passiveBrowserEventsSupported) {
if (
domEventName === "touchstart" ||
domEventName === "touchmove" ||
domEventName === "wheel"
) {
isPassiveListener = true;
}
}

targetContainer = targetContainer;
var unsubscribeListener;

// 利用 addEventListener 绑定事件监听函数

if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
);
} else {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener
);
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener
);
}
}
}

createEventListenerWrapperWithPriority 会调用 getEventPriority 按事件名称为事件定义不同的优先级,不同的优先级对应不同的事件处理函数

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
function getEventPriority(domEventName) {
switch (domEventName) {
// Used by SimpleEventPlugin:
case "cancel":
case "click":
case "close":
case "contextmenu":
case "copy":
case "cut":
case "auxclick":
case "dblclick":
case "dragend":
case "dragstart":
case "drop":
case "focusin":
case "focusout":
case "input":
case "invalid":
case "keydown":
case "keypress":
case "keyup":
case "mousedown":
case "mouseup":
case "paste":
case "pause":
case "play":
case "pointercancel":
case "pointerdown":
case "pointerup":
case "ratechange":
case "reset":
case "resize":
case "seeked":
case "submit":
case "touchcancel":
case "touchend":
case "touchstart":
case "volumechange": // Used by polyfills:
// eslint-disable-next-line no-fallthrough

case "change":
case "selectionchange":
case "textInput":
case "compositionstart":
case "compositionend":
case "compositionupdate": // Only enableCreateEventHandleAPI:
// eslint-disable-next-line no-fallthrough

case "beforeblur":
case "afterblur": // Not used by React but could be by user code:
// eslint-disable-next-line no-fallthrough

case "beforeinput":
case "blur":
case "fullscreenchange":
case "focus":
case "hashchange":
case "popstate":
case "select":
case "selectstart":
return DiscreteEventPriority;

case "drag":
case "dragenter":
case "dragexit":
case "dragleave":
case "dragover":
case "mousemove":
case "mouseout":
case "mouseover":
case "pointermove":
case "pointerout":
case "pointerover":
case "scroll":
case "toggle":
case "touchmove":
case "wheel": // Not used by React but could be by user code:
// eslint-disable-next-line no-fallthrough

case "mouseenter":
case "mouseleave":
case "pointerenter":
case "pointerleave":
return ContinuousEventPriority;

case "message": {
// We might be in the Scheduler callback.
// Eventually this mechanism will be replaced by a check
// of the current priority on the native scheduler.
var schedulerPriority = getCurrentPriorityLevel();

switch (schedulerPriority) {
case ImmediatePriority:
return DiscreteEventPriority;

case UserBlockingPriority:
return ContinuousEventPriority;

case NormalPriority:
case LowPriority:
// TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
return DefaultEventPriority;

case IdlePriority:
return IdleEventPriority;

default:
return DefaultEventPriority;
}
}

default:
return DefaultEventPriority;
}
}

function createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
) {
var eventPriority = getEventPriority(domEventName);
var listenerWrapper;

switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;

case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;

case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}

return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer
);
}

事件执行

当一个事件触发时, 会调用事件绑定时创建的回调函数,这个函数会以批处理的形式调用事件处理方法 dispatchEventsForPlugins

1
2
3
4
5
6
7
8
batchedUpdates(function () {
return dispatchEventsForPlugins(
domEventName, // 事件名称
eventSystemFlags, // 捕获阶段标识
nativeEvent, // 原生事件对象
ancestorInst
);
});

dispatchEventsForPlugins 首先获取触发事件的元素调用 extractEvents 方法,从原生 DOM 上的 stateNode 获取到 FiberNode,并且尝试获取当前元素的绑定事件,在根据不同的事件优先级,包装成不同的合成事件对象

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
function dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
) {
var nativeEventTarget = getEventTarget(nativeEvent);
var dispatchQueue = [];

// 内部调用不同的处理函数,用于处理没有原生事件对应的 React 事件,例如 onBeforeInput
// if (shouldProcessPolyfillPlugins) {
// extractEvents$2(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// extractEvents$1(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// extractEvents$3(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// }
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}

function extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
) {
// 不同的事件类型,对应不同的合成事件构造函数
// 内部实现 preventDefault stopPropagation
switch (domEventName) {
case "click":
SyntheticEventCtor = SyntheticMouseEvent;
case "drag":
SyntheticEventCtor = SyntheticDragEvent;
}

// 从当前节点循环遍历到根节点, 收集所有上级节点中绑定当前方法的节点和它的绑定函数
var _listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly
);

if (_listeners.length > 0) {
// Intentionally create event lazily.
var _event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget
);

dispatchQueue.push({
event: _event,
listeners: _listeners,
});
}
}

function processDispatchQueue(dispatchQueue, eventSystemFlags) {
var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;

// 循环事件队列,依次触发合成事件
for (let i = 0; i < dispatchQueue.length; i++) {
const {event, listeners} = dispatchQueue[i];
var previousInstance;

(function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean
): void {
let previousInstance;
if (inCapturePhase) {
// 反响遍历模拟捕获阶段
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const { instance, currentTarget, listener } = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
// 正向循环模拟冒泡阶段
for (let i = 0; i < dispatchListeners.length; i++) {
const { instance, currentTarget, listener } = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
})()

rethrowCaughtError();
}

React v16 源码分析 ⑪ 更新流程

useState 与 setState 更新流程大致相同,只是入口函数不同。

react 更新是有优先级的,高优先级的任务可能会打断低优先级任务的,低优先级任务会在高优先级更新后的状态上进行更新。

不同组件用不同的处理函数:

  • ReactDOM.createRoot 对应 HostRoot
  • this.setState 对应 ClassComponent
  • this.forceUpdate 对应 ClassComponent
  • useState dispatcher 对应 FunctionComponent
  • useReducer dispatcher 对应 FunctionComponent

HostRoot,ClassComponent 对应如下的结构,tag 字段区分不同的更新触发场景:

  • ReplaceState: 代表在 ClassComponent 生命周期函数中直接改变 this.state
  • UpdateState: 默认情况,通过 ReactDOM.createRoot 或者 this.setState 触发更新
  • CaptureUpdate: 代表发生错误的情况下在 ClassComponent 或 HostRoot 中触发更新(比如通过 getDerivedStateFromError 方法)
  • ForceUpdate: 代表通过 this.forceUpdate 触发更新
1
2
3
4
5
6
7
8
9
10
11
12
13
function createUpdate(eventTime, lane) {
const update = {
eventTime,
lane,
// 区分触发更新的场景
tag: UpdateState,
payload: null,
// UI 渲染后触发的回调函数
callback: null,
next: null,
};
return update;
}

函数组件触发更新时的 update 对象结构:

1
2
3
4
5
6
7
8
const update = {
lane,
action,
// 优化策略相关
hasEagerState: false,
eagerState: null,
next: null,
};
  • 承载更新内容的字段不同,类组件是 payload 字段
  • 更新的紧急程度是 lane 字段表示的
  • 更新之间的顺序,通过 next 字段指向下一个 update,从而形成一个链表

updateQueue 是一个 update 对象组成的链表

  • baseState: 参与计算的初始 state, update 基于该 state 计算新的 state, 可以类比为心智模型中的 master 分支。
  • firstBaseUpdate 与 lastBaseUpdate: 表示更新前该 FiberNode 中已保存的 update, 以链表的形式串联起来。链表头部为 firstBaseUpdate,链表尾部为 lastBaseUpdate。
  • shared.pending: 触发更新后,产生的 update 会保存在 shared.pending 中形成单向环状链表。计算 state 时,该环状链表会被拆分并接在 lastBaseUpdate 后面。
1
2
3
4
5
6
7
8
const updateQueue = {
baseState: null,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
};

一轮跟新后如果有两个低优先级更新没有处理,那么这两个更新会放在 baseState 上,如果有另外两个更新进来会放在 shared.pending 上并且形成一个环状链表,下一次 commit 时 shared.pending 会被拼接到 baseState 后面,循环处理哪些符合优先级的更新,基于符合条件的更新,来计算最终的state.

在类组件上会挂载 setState 方法, 在调用的时候会把状态添加到队列中

1
2
3
4
5
6
function Component(props, context, updater) {
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, "setState");
};

在 render 阶段的 beginWork 中会调用类组件对应的方法创建 Fiber 节点, 类组件会在这时实例化,实例化完成后立即执行 adoptClassInstance 方法, 为实例提供用于更新的 this.updater 对象

1
2
3
4
5
6
7
8
9
10
function constructClassInstance() {
var instance = new ctor(props, context);
adoptClassInstance(workInProgress, instance);
}

function adoptClassInstance(workInProgress, instance) {
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
set(instance, workInProgress);
}

当事件被触发之后会调用 updater.enqueueSetState, 与首次 render 阶段时为 FiberRoot 创建更新队列相似

在首次渲染时已经通过 initializeUpdateQueue 为节点初始化了更新队列,现在需要把 update 对象添加到队列中

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
var classComponentUpdater = {
isMounted: isMounted,
enqueueSetState: function (inst, payload, callback) {
var fiber = get(inst);
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
var update = createUpdate(eventTime, lane);
update.payload = payload;

if (callback !== undefined && callback !== null) {
update.callback = callback;
}

enqueueUpdate(fiber, update);
var root = scheduleUpdateOnFiber(fiber, lane, eventTime);

if (root !== null) {
entangleTransitions(root, fiber, lane);
}
},
enqueueReplaceState: function (inst, payload, callback) {
var fiber = get(inst);
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
var update = createUpdate(eventTime, lane);
update.tag = ReplaceState;
update.payload = payload;

if (callback !== undefined && callback !== null) {
update.callback = callback;
}

enqueueUpdate(fiber, update);
var root = scheduleUpdateOnFiber(fiber, lane, eventTime);

if (root !== null) {
entangleTransitions(root, fiber, lane);
}
},
enqueueForceUpdate: function (inst, callback) {
var fiber = get(inst);
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
var update = createUpdate(eventTime, lane);
update.tag = ForceUpdate;

if (callback !== undefined && callback !== null) {
update.callback = callback;
}

enqueueUpdate(fiber, update);
var root = scheduleUpdateOnFiber(fiber, lane, eventTime);

if (root !== null) {
entangleTransitions(root, fiber, lane);
}
},
};

update 对象会被添加到 updateQueue.shared.pending 中,并且形成循环链表,如果有新的更新会成为新的头节点

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
function enqueueUpdate(fiber, update, lane) {
var updateQueue = fiber.updateQueue;

if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return;
}

var sharedQueue = updateQueue.shared;

if (isInterleavedUpdate(fiber)) {
var interleaved = sharedQueue.interleaved;

if (interleaved === null) {
// This is the first update. Create a circular list.
update.next = update;
// At the end of the current render, this queue's interleaved updates will
// be transferred to the pending queue.

pushInterleavedQueue(sharedQueue);
} else {
update.next = interleaved.next;
interleaved.next = update;
}

sharedQueue.interleaved = update;
} else {
var pending = sharedQueue.pending;

if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}

sharedQueue.pending = update;
}
}

接着触发事件的 Fiber 节点会被传入 scheduleUpdateOnFiber 进行调度, 其中会执行 markUpdateLaneFromFiberToRoot 将每一个 Fiber 节点的 lanes 都合并到父级节点上,这样在 rootFiber 节点上就包含了所有子节点的更新优先级信息

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
function markUpdateLaneFromFiberToRoot(sourceFiber, lane) {
// Update the source fiber's lanes
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
var alternate = sourceFiber.alternate;

if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}

{
if (
alternate === null &&
(sourceFiber.flags & (Placement | Hydrating)) !== NoFlags
) {
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}

var node = sourceFiber;
var parent = sourceFiber.return;

while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;

if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
} else {
{
if ((parent.flags & (Placement | Hydrating)) !== NoFlags) {
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}
}

node = parent;
parent = parent.return;
}

if (node.tag === HostRoot) {
var root = node.stateNode;
return root;
} else {
return null;
}
}

React v16 源码分析 ③ concurrent 模式简介

React 官方提供了三种模式可以选择:

  • legacy 模式: ReactDOM.render(, rootNode)。这是当前 React app 使用的方式。当前没有计划删除本模式,但是这个模式可能不支持这些新功能。
  • blocking 模式: ReactDOM.createBlockingRoot(rootNode).render()。目前正在实验中。作为迁移到 concurrent 模式的第一个步骤。
  • concurrent 模式: ReactDOM.createRoot(rootNode).render()。目前在实验中,未来稳定之后,打算作为 React 的默认开发模式。这个模式开启了所有的新功能。

但即使对于最新版本的 React 也没有主动提供 createRoot 这个 api,需要安装 Alpha 版本, 效果仍然相当于 legacy 模式.

版本演化

React v15 的版本中用到了 Reconciler 和 Renderer 两个部分

这样存在的问题就是对节点的更新是递归同步更新的,如果节点非常多,即使只有一次 state 变更,React 也需要进行复杂的递归更新,更新一旦开始,中途就无法中断,直到遍历完整颗树,才能释放主线程。

而 React v16 添加了前两章提到的,Scheduler, Fiber 这些都为最终实现 concurrent 模式提供了支持

但是在 React v16 版本中只是对能实现 concurrent 模式的这些模块进行了尝试. 最重要的还是在 v16.8 版本中发布了全新了 Hooks Api.

在 v17.0 版本的时候又提出了全新的 lane 模型用于处理更新的优先级. 虽然在 v17.0 版本中没有重大的更新,但是 concurrent 模式已经可以与老的模式稳定共存.

启动过程

传入不同的 RootTag 用于标记不同的类型.这个变量会参与到初始化流程,优先级判断的逻辑中.

更新入口

legacy 模式

1
2
3
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});

concurrent 模式

1
2
3
4
5
6
ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
// 执行更新
updateContainer(children, root, null, null);
};

异同点

不同的模式,传入不同的 rootTag 类型,最终都调用了 updateContainer 函数串联了 react-dom 与 react-reconciler.

legacy 下的更新会先调用 unbatchedUpdates, 更改执行上下文为 LegacyUnbatchedContext, 之后调用 updateContainer 进行更新.

concurrent 和 blocking 不会更改执行上下文, 直接调用 updateContainer 进行更新.

另外在 React 官方文档中可以找到:

legacy 模式在合成事件中有自动批处理的功能,但仅限于一个浏览器任务。非 React 事件想使用这个功能必须使用 unstable_batchedUpdates。在 blocking 模式和 concurrent 模式下,所有的 setState 在默认情况下都是批处理的,这也意味着 concurrent 模式下所有的更新都是异步的.

Tree Shaking 过程分析

什么是 Tree Shaking

Tree Shaking 是 DCE(Dead Code Elimination) 的一种实现,即清除无用代码,这个功能最早是在 Rollup 中实现的,随后 webpack 在 2.0 版本中也实现了此功能

基本使用

1
2
3
4
5
6
module.exports = {
mode: "production",
optimization: {
usedExports: true,
},
};

webpack4 添加了 sideEffects 配置属性,用于声明那些模块是没有副作用的,从而可以安全的移除.

如上面的例子中模块中包含两个纯函数,所以可以设置 sideEffects 为 false, 可以安全的删除 square

1
2
3
4
{
"name": "your-project",
"sideEffects": false
}

sideEffects 可能会比 usedExports 更加有效,因为它是声明式的告诉,那些模块/文件可以跳过.

usedExports 依赖 terser 来检测语句中的副作用。对于如下的模块,默认不会被移除,因为不确定其中是否有副作用.虽然可以使用标注来解决,但是 sideEffect 使用的更多.

1
2
3
4
5
// a.js
class A {}
Array.prototype.slice = () => {};
// 表示一个无副作用的模块,可以删除
export default /*#__PURE__*/ A;

实现原理

Make 阶段,收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中
Seal 阶段,遍历 ModuleGraph 标记模块导出变量有没有被使用
生成产物时,若变量没有被其它模块使用则删除对应的导出语句

Make 收集阶段

这个阶段需要分析每个文件中有那些模块被导出,把这些导出模块转换为 webpack 内部对象,并添加到当前模块依赖中

1
2
3
4
export const bar = "bar";
export const foo = "foo";

export default "foo-bar";

最终转换为三个内部对象

在编译结束之后会触发回调,这时会遍历 dependencies 数组,找到所有的导出对象转换为 ExportInfo 记录在 ModuleGraph 中,至此 webpack 可以直接访问各模块的导出值.

Seal 标记阶段

标记的主要作用就是删除没有使用模块的导出语句,可以看见对于导出但是没有使用的模块会添加未使用的标记,并且不会被导出,但是标记阶段不会删除代码,删除的过程是 Terser 等压缩工具实现的

1
2
3
4
5
6
7
8
9
10
11
12
/***/ (function (module, __webpack_exports__, __webpack_require__) {
"use strict";
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__["a"] = cube;
function square(x) {
return x * x;
}

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

这一阶段会触发 optimizeDependencies,从 entry 入口文件开始遍 ModuleGraph 所有的模块

接着遍历所有 exportInfo 数组,为每一个 exportInfo 执行 getDependencyReferencedExports 方法,确定其对应的 dependency 对象有否被其它模块使用

被任意模块使用到的导出值,调用 exportInfo.setUsedConditionally 方法将其标记为已被使用,内部修改 exportInfo._usedInRuntime 记录导出如何被使用

这一过程是通过 FlagDependencyUsagePlugin 插件完成的

生成代码

调用 HarmonyExportXXXDependency.Template.apply 生成代码,方法内部会读取 exportsInfo, 分别为使用和未使用的导出值创建不同的 HarmonyExportInitFragment, 保存到 initFragments 数组并循环生成代码.

最终的的无用代码会被压缩工具删除.

对比 Rollup

与 webpack 相同 Rollup 也是基于 ES 模块化做静态分析, 但是由于打包方式不同,Rollup 的打包文件在同一作用域下,所以还可以进行流程分析.

Rollup 可以删除被引用但是没有使用的模块,但也不是所有的副作用 Rollup 都可以处理,包括:

未使用的 class 中的方法(可以通过 enhancement 配置解决)

对全局对象的赋值 window.a = 1

最佳实践

  • 避免无意义的赋值语句
1
2
3
4
5
6
7
// math.js
export const a = 1;
export const b = 2;

// index.js
import { a, b } from "./index";
const f = a;

通过静态分析只能判断:模块导出变量是否被其它模块引用,或引用模块的主体代码中有没有出现这个变量

另外最重要的是因为 JS 这种动态类型语言引用的 a 可能存在副作用

  • 必要的时候使用 /*__PURE__*/ 纯函数标注

  • 禁用 babel 的导入导出语句转换

    当 babel 的 module 配置为 commonjs 的时候,将不能正确识别未使用的模块

    1
    2
    3
    4
    5
    6
    presets: [
    "babel-preset-env",
    {
    modules: "commonjs",
    },
    ];
  • 将”sideEffects”属性添加到项目 package.json 文件中。

  • 使用明确的导出语句

1
2
3
4
5
6
7
8
export default {
bar: "bar",
foo: "foo",
};

// 修改为
export bar;
export foo;
  • 使用支持 Tree Shaking 的工具包

例如:使用 lodash-es 替代 lodash ,或者使用 babel-plugin-lodash 实现类似效果

Rollup 与 Webpack 比较

跟随前端技术的演化

当前端演化到单页应用阶段的时候,对于复杂的单页应用有两个问题需要解决,而 Webpack 就是在这时产生的.

  • 代码分割

这意味着可以按需加载,不用再等待整个应用都被下载并解析完成。

  • 静态资源

图片、CSS 等静态资源可以直接导入到你的 app 中,就和其它的模块、节点一样能够进行依赖管理。可以放在任意的文件夹中, Webpack 会帮助处理文件路径, 包括添加哈希值,最终输出在指定的文件夹中.

包括 Webpack 在内的大多数打包器,都是将模块封装在函数中,通过 Webpack 实现的 require 方法,组织模块间的调用.

而 Rollup 利用了 ES6 的模块设计,将所有的模块的代码都放在同一个位置, 因此更加精简执行速度也会更快. Rollup 交互式解释器(REPL)

但是同时这样也导致 Rollup 失去了代码分割的功能, 也不支持模块热替换(HMR),另一个痛点是通过插件处理大多数 CommonJS 文件的时候,一些代码将无法被翻译为 ES2015。而与之相反 Webpack 可以很好而处理.

如何选择

在开发应用时使用 Webpack,开发库时使用 Rollup

这虽然不是绝对的,但是很多开源项目提供有经验,如果有很多的静态资源,再或者你做的东西深度依赖 CommonJS,毫无疑问选择 Webpack.

如果你的代码基于 ES2015 模块编写,并且你做的东西是准备给他人使用的,你或许可以考虑使用 Rollup。

对于包作者一定使用 pkg.module

对于第三方开源库,在 ES6 模块化规范出现以前,一定要注意模块系统的区别,有人喜欢 Browserify 有人喜欢 AMD,在 UMD 出现之后有了一些改善,但是仍然无法完全信任.

现在给你的库的 package.json 文件增加一个 “module”: “dist/my-library.es.js” 入口,可以让你的库同时支持 UMD 与 ES2015。 Webpack 和 Rollup 都使用了 pkg.module 来尽可能的生成效率更高的代码——在一些情况下,它们都能使用 tree-shake 来精简掉你的库中未使用的部分。

React v16 源码分析 ⑨ 组件生命周期

从横向的结构分析, 生命周期包括 Render phase Pre-commit phase Commit phase 这些生命周期是与代码逻辑相符的,很容易找到类型名称的函数调用

纵向的 Mounting Updating Unmounting 是从 DOM 的表现层面划分的,真正对 DOM 的处理分散在上面的不同阶段之中

下面以横向的结构划分,也就是以代码的执行逻辑分析不同阶段的生命周期方法

Render

Render 阶段也就是 Fiber 树 构建的阶段, 对于首次渲染的节点会创建新的 FiberNode, 对于更新的节点会检查是需要新建还是复用

在构建 Fiber 树的过程中,会在每个节点处理之前调用 createWorkInProgress,当发现此节点还没有创建,会调用 createFiber 创建对应的 FiberNode

将 FiberNode 传入 beginWork 按 Fiber 的 tag 类型,调用不同的方法处理, 如果 Fiber 的类型是 class component 会调用 constructClassInstance 实例化

紧接着会执行 getDerivedStateFromPropscomponentWillMount, 因此这两个生命周期在挂载阶段和更新阶段是都会执行的

1
2
3
4
5
6
7
8
9
10
11
function constructClassInstance(workInProgress, ctor, props) {
var instance = new ctor(props, context);

if (typeof getDerivedStateFromProps === "function") {
getDerivedStateFromProps(nextProps, prevState);
}

if (typeof instance.componentWillMount === "function") {
instance.componentWillMount();
}
}

如果是函数组件则会直接执行

1
var children = Component(props, secondArg);

实例化类组件之后会执行 finishClassComponent, 在这里会执行类组件的 render 方法

1
2
3
function finishClassComponent() {
instance.render();
}

如果节点已经挂载过, 类组件会进入更新的逻辑,这里会执行 shouldComponentUpdate 并返回一个 boolean, 用作是否执行 componentWillUpdate() render() componentDidUpdate() 的依据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function updateClassInstance() {
var shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext
);
if (shouldUpdate) {
if (typeof instance.componentWillUpdate === "function") {
instance.componentWillUpdate(newProps, newState, nextContext);
}
if (typeof instance.componentDidUpdate === "function") {
workInProgress.flags |= Update;
}
if (typeof instance.getSnapshotBeforeUpdate === "function") {
workInProgress.flags |= Snapshot;
}
}
}

Pre-commit

Pre-commit 阶段实际就是 commit 阶段 前半部分

其中 getSnapshotBeforeUpdate 将会被执行

Commit

指的就是 commit 阶段 后半部分

在这个阶段会真实的操作 DOM,并且重新绑定 Ref

可以看到 componentDidMount componentDidUpdate 会在这个阶段执行

其中 commitMutationEffects 阶段会对标记删除的节点执行 commitDeletion, componentWillUnmount 会在这个函数中执行

  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信