VMWare 使用SSH链接

SSH客户端与服务端

openssh-client 客户端,如果想通过 ssh 链接其他服务器需要安装客户端

ssh-server 服务端,如果想让其他机器通过 ssh 链接本机,需要在本机开机 ssh 服务,即安装服务端

Ubuntu server版 默认没有安装 ssh-client
Ubuntu 桌面版 默认没有安装 ssh-server

Ubuntu 安装 ssh 客户端或服务端

1
2
3
4
$ sudo apt-get update //更新软件源
$ sudo apt-get install openssh-client //安装openssh-client
$ sudo apt-get install openssh-server //安装openssh-server
$ sudo service ssh start //启动ssh服务

centOS 安装 ssh

1
2
3
4
5
# 搜索 ssh 包名
yum search openssh

yum install openssh-clients.x86_64
yum install openssh-server.x86_64

检查虚拟机是否安装了 ssh-server

1
2
ps -e | grep ssh
# 1512 00:00:00 sshd

VMWare 配置

查询 IP

查询宿主机和虚拟机的 ip 备用

宿主机IP

虚拟机IP

建立映射

接下来就需要将宿主机和虚拟机的IP映射起来。

打开VMware的虚拟网络编辑器(编辑>虚拟网络编辑器):

检查子网Ip(Subnet IP) 和 子网掩码(Subnet mask), 正常情况下无需修改

如果保存时报错 子网ip和子网掩码不匹配,请检查子网IP, 格式为 xxx.xxx.xxx.0 他与子网掩码 255.255.255.0 对应

不可以写为 xxx.xxx.xxx.120 等其他数字,这表示具体子网中的一个网络设备,并不是子网IP

配置完成后可以使用 ssh 工具链接

1
ssh root@192.168.255.128

MySQL 练习题

创建表结构

  • 学生表 Student
1
2
3
4
5
6
7
8
9
create table Student(Sid varchar(6), Sname varchar(10), Sage datetime, Ssex varchar(10));
insert into Student values('01' , '赵雷' , '1990-01-01' , '男');
insert into Student values('02' , '钱电' , '1990-12-21' , '男');
insert into Student values('03' , '孙风' , '1990-05-20' , '男');
insert into Student values('04' , '李云' , '1990-08-06' , '男');
insert into Student values('05' , '周梅' , '1991-12-01' , '女');
insert into Student values('06' , '吴兰' , '1992-03-01' , '女');
insert into Student values('07' , '郑竹' , '1989-07-01' , '女');
insert into Student values('08' , '王菊' , '1990-01-20' , '女')
  • 成绩表 SC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
create table SC(Sid varchar(10), Cid varchar(10), score decimal(18,1));
insert into SC values('01' , '01' , 80);
insert into SC values('01' , '02' , 90);
insert into SC values('01' , '03' , 99);
insert into SC values('02' , '01' , 70);
insert into SC values('02' , '02' , 60);
insert into SC values('02' , '03' , 80);
insert into SC values('03' , '01' , 80);
insert into SC values('03' , '02' , 80);
insert into SC values('03' , '03' , 80);
insert into SC values('04' , '01' , 50);
insert into SC values('04' , '02' , 30);
insert into SC values('04' , '03' , 20);
insert into SC values('05' , '01' , 76);
insert into SC values('05' , '02' , 87);
insert into SC values('06' , '01' , 31);
insert into SC values('06' , '03' , 34);
insert into SC values('07' , '02' , 89);
insert into SC values('07' , '03' , 98)
  • 课程表 Course
1
2
3
4
create table Course(Cid varchar(10),Cname varchar(10),Tid varchar(10));
insert into Course values('01' , '语文' , '02');
insert into Course values('02' , '数学' , '01');
insert into Course values('03' , '英语' , '03')
  • 教师表 Teacher
1
2
3
4
create table Teacher(Tid varchar(10),Tname varchar(10));
insert into Teacher values('01' , '张三');
insert into Teacher values('02' , '李四');
insert into Teacher values('03' , '王五')

练习题

查询” 01 “课程比” 02 “课程成绩高的学生的信息及课程分数
查询平均成绩大于等于 60 分的同学的学生编号和学生姓名和平均成绩
查询在 SC 表存在成绩的学生信息
查询所有同学的学生编号、学生姓名、选课总数、所有课程的总成绩(没成绩的显示为 null )
查询姓李的老师数量
学过”张三”老师授课的同学的信息
没有学过”张三”老师授课的同学的信息
查询学过编号为”01”并且也学过编号为”02”的课程的同学的信息
查询学过编号为”01”但是没有学过编号为”02”的课程的同学的信息
查询没有学全所有课程的同学的信息
查询至少有一门课与学号为 01 同学所学相同的同学的信息
查询和 01 号的同学学习的课程,完全相同的其他同学的信息
查询所有同学最高分对应的学科名称

查询”01“课程比”02”课程成绩高的学生的信息及课程分数
1
2
3
4
5
6
7
8
9
10
11
12
13
select Sname as 姓名,t1.score as "语文" , t2.score as '数学'
from
(select SId ,score from SC as sc1 where sc1.CId='01') as t1,
(select SId ,score from SC as sc2 where sc2.CId='02') as t2,
(Select * from Student) as t3
where t1.SId=t2.SId and t2.SId =t3.SId and t1.score>t2.score;


select st.*, sc1.score as '语文',sc2.score as '数学'
from Student as st
left join SC as sc1 on st.SId = sc1.SId and sc1.CId ='01'
left join SC as sc2 on st.SId = sc2.SId and sc2.CId ='02'
where sc1.score > sc2.score;
查询平均成绩大于等于 60 分的同学的学生编号和学生姓名和平均成绩
1
2
3
4
5
select s.SId ,s.Sname,AVG(s2.score) avg_score
from Student s
left join SC s2 on s2.SId = s.SId
group by s.SId
having avg_score >= 60;

需要注意 where 和 having 的区别, where 是分组前筛选,所以一定写在 group by 的前面, having 是分组后筛选,这道题是想查找求完平均分数之后的结果

查询在 SC 表存在成绩的学生信息

第一种方法:将学生表和成绩表关联,过滤出没有成绩的条目,再用学生 id 分组

1
2
3
4
5
select s.*
from Student s
left join SC s2 on s2.SId = s.SId
where s2.score is not NULL
group by s.SId ;

第二种方法: 先将成绩表按学生 id 分组,再查询学生 id 在分组后的临时表中的学生信息

1
2
3
4
5
6
7
select s.*
from Student s
where s.SId in (
SELECT s.SId
from SC s
group by s.SId
);
查询所有同学的学生编号、学生姓名、选课总数、所有课程的总成绩(没成绩的显示为 null )
1
2
3
4
select s.SId ,s.Sname, COUNT(s2.CId),sum(s2.score)
from Student s
left join SC s2 on s2.SId = s.SId
group by s.SId ;
查询姓李的老师数量
1
2
3
select COUNT(*) as 数量
from Teacher t
where t.Tname LIKE "李%";
学过”张三”老师授课的同学的信息
1
2
3
4
5
select s.*,t.Tname  from Student s
inner join SC s2 on s.SId = s2.SId
inner join Course c on s2.CId = c.CId
inner join Teacher t on t.TId = c.TId
where t.Tname = "张三";
没有学过”张三”老师授课的同学的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

-- 排除学过张三课的同学,剩下的就是没学过张三课的同学
select s.* from Student s
WHERE s.SId not in (
select s.SId from Student s
inner join SC s2 on s.SId = s2.SId
inner join Course c on s2.CId = c.CId
inner join Teacher t on t.TId = c.TId
where t.Tname = "张三"
);

-- 查找每条成绩对应的课程信息,查找这些信息中是张三老师课的信息
-- 在查找这些信息对应的学生信息,并排除

SELECT s.* FROM Student s
WHERE s.SId NOT IN (
SELECT s2.SId from SC s2
inner JOIN Course c2 on c2.CId = s2.CId
WHERE c2.TId = (
SELECT t.TId from Teacher t
WHERE t.Tname = "张三"
)
);

查询学过编号为”01”并且也学过编号为”02”的课程的同学的信息
1
2
3
4
5
6
7
8
9
10
-- 利用inner join 过滤没有匹配结果的条目
SELECT * from Student s
inner join SC s2 on s2.SId =s.SId and s2.CId = "01"
inner join SC s3 on s3.SId =s.SId and s3.CId = "02";

-- 先分组在查询个数
select s.* from Student s
inner join SC s2 on s2.SId = s.SId
GROUP BY s.SId
HAVING sum(IF(s2.CId="01" or s2.CId="02",1,0)) >1;
查询学过编号为”01”但是没有学过编号为”02”的课程的同学的信息
1
2
3
4
5
6
SELECT  * from Student s
inner join SC s2 on s2.SId =s.SId and s2.CId = "01"
WHERE s.SId not in (
SELECT s.SId from Student s
inner join SC s2 on s2.SId =s.SId and s2.CId = "02"
);
查询没有学全所有课程的同学的信息
1
2
3
4
5
6
select s.* from Student s
left join SC s2 on s2.SId = s.SId
GROUP by s.SId
HAVING COUNT(*) < (
SELECT COUNT(*) from Course c
);
查询至少有一门课与学号为 01 同学所学相同的同学的信息
  1. 查找学号 01 同学学过的科目
  2. 查找每个同学学过的科目,排除 01 同学自己
  3. 查找每个同学学过的科目在 01 同学学过的科目中的同学
  4. 去除重复的同学信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
select s.* from Student s
left join SC s2 on s2.SId = s.SId
where s2.CId in (
select s2.CId from Student s
inner join SC s2 on s2.SId = s.SId and s2.SId ='01'
) and s.SId <> '01'
group by s.SId;

-- 优化查询01同学学过的科目,直接从成绩表中查
-- 使用distinct去重
Select distinct sc.SId, st.* from SC as sc
Join Student st
On sc.SId = st.SId and st.SId <> '01'
Where sc.CId in (Select CId from SC where SId = '01')
查询和 01 号的同学学习的课程,完全相同的其他同学的信息
  1. 查找所有同学学过的科目是在 01 同学学过的科目中的同学
  2. 学过和 01 同学相同课程的科目数时候和 01 同学的科目数相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
select s.* from Student s
join SC s2 on s2.SId = s.SId and s.SId <> '01' and s2.CId in (select s3.CId from SC s3 where s3.SId = '01')
group by s.SId
HAVING count(s.SId) = (
select count(*) from SC s3 where s3.SId = '01'
)


-- 也可以使用group concat 对比课程id
-- join的时候无需确认是否课程是01学过的课程


select s.* from Student s
join SC s2 on s2.SId = s.SId and s.SId <> '01'
group by s.SId
HAVING GROUP_CONCAT(s2.CId order by s2.CId desc) = (
select GROUP_CONCAT(s3.CId order by s3.CId desc) from SC s3 where s3.SId ='01'
)
查询所有同学最高分对应的学科名称
1
2
3
4
5
6
7
8
9
select * from (
select s.SId ,s.Sname ,t.CId,t.score,t.row from Student s
left join (
select s2.*,
row_number () over (partition by s2.SId order by s2.score desc) as row
from SC s2
) as t on s.SId = t.SId
left join Course c on c.CId = t.CId
) as t where t.row = '1'

TypeScript 练习题

实现 Pick
实现 Readonly
元组转换为对象
第一个元素
实现 Exclude
Promise 返回值类型
实现 Array.Concat
实现 Array.includes
实现 Parameters
实现 ReturnType
实现 Omit
Pick Readonly
Deep Readonly
链式调用的类型
Promise.all
Type Lookup
Trim
Type Replace
追加参数
Flatten
AppendToObject
数字转字符串
StringToUnion
MergeKey
CamelCase & KebabCase
Diff
anyOf
isUnion

实现 Pick
1
2
3
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

利用 keyof 将对象类型转换成键值的联合类型

利用 extends 进行泛型约束, K 可以分配给 T, 表示 K 是 T 的子集.

利用 in 运算符,遍历联合类型

实现 Readonly
1
2
3
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
元组转换为对象
1
2
3
4
5
6
7
8
const tuple = ["tesla", "model 3", "model X", "model Y"] as const;

type TupleToObject<T extends readonly any[]> = {
[K in T[number]]: K;
};

type result = TupleToObject<typeof tuple>;
// expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

因为 in 运算符可以遍历联合类型,所以把元组 T 转换成联合类型,在进行遍历

第一个元素

实现一个通用 First,它接受一个数组 T 并返回它的第一个元素的类型。

1
2
3
4
5
6
7
8
9
type arr2 = [3, 2, 1];

type First<T extends readonly any[]> = T[0];

type First<T extends readonly any[]> = T extends [infer F, ...infer R]
? F
: never;

type head1 = First<arr1>; // expected to be 'a'

利用条件语句中 infer 类型推断,返回第一个元素所代表的类型

实现 Exclude

Exclude 的用法是从联合类型中,排除指定的属性

1
type Exclude<T, U> = T extends U ? never : T;

extends 条件类型, T 是否能分配给 U, 会去拿 T 中的每一项与 U 进行匹配, 如果当前项可以分配,表示 U 中存在这种类型,需要排除,所以返回 never. 如果不存在则返回这一项的类型.

Promise 返回值类型
1
type Awaited<T extends Promise<any>> = T extends Promise<infer R> ? R : T;
实现 Array.Concat
1
type Concat<T extends any[], U extends any[]> = [...T, ...U];
实现 Array.includes
1
type Includes<T extends any[], U> = U extends T[number] ? true : false;

利用 extends 条件类型可以进行联合类型的判断,, 首先吧元组转换为联合类型, 如果类型可分配表示 U 存在与元组中.

实现 Parameters

Parameters 作用是用于获得函数的参数类型组成的元组类型。

1
2
3
4
5
type Parameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
实现 ReturnType
1
2
3
4
5
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
实现 Omit
1
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Pick Readonly

指定属性 ReadOnly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type PickReadonly<T, K extends keyof T = keyof T> = {
[Key in Exclude<keyof T, K>]: T[Key];
} & {
readonly [Key in K]: T[Key];
};

interface Todo {
title: string;
description: string;
completed: boolean;
}

const todo: MyReadonly2<Todo, "title" | "description"> = {
title: "Hey",
description: "foobar",
completed: false,
};

todo.title = "Hello"; // Error: cannot reassign a readonly property
todo.description = "barFoo"; // Error: cannot reassign a readonly property
todo.completed = true; // OK
Deep Readonly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type X = {
x: {
a: 1;
b: "hi";
};
y: "hey";
};

type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

type Todo = DeepReadonly<X>; // should be same as `Expected`

type Expected = {
readonly x: {
readonly a: 1;
readonly b: "hi";
};
readonly y: "hey";
};
链式调用的类型

假设 key 只接受字符串而 value 接受任何类型,你只需要暴露它传递的类型而不需要进行任何处理。同样的 key 只会被使用一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Chainable<T = {}> = {
option: <K extends string, V>(k: K, v: V) => Chainable<T & { [P in K]: V }>;
get: () => T;
};

declare const config: Chainable;

const result = config
.option("foo", 123)
.option("name", "type-challenges")
.option("bar", { value: "Hello World" })
.get();

// 期望 result 的类型是:
interface Result {
foo: number;
name: string;
bar: {
value: string;
};
}

实现 Promise.all

ts 允许像遍历一个对象一样遍历类数组

1
2
3
4
5
6
7
8
9
10
11
12
13
type Awaited<T> =
T extends null | undefined ? T :
// special case for `null | undefined` when not in `--strictNullChecks` mode
T extends object & { then(onfulfilled: infer F): any } ?
// `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
F extends ((value: infer V, ...args: any) => any) ? // if the argument to `then` is callable, extracts the first argument
Awaited<V> :
// recursively unwrap the value
never :
// the argument to `then` was not callable
T;
// non-object or non-thenable
type PromiseAll<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;

Type Lookup

1
2
3
type LookUp<U, T extends string> = {
[K in T]: U extends { type: T } ? U : never;
}[T];

实现 Trim

类型推断可以用于字符串

1
2
3
4
5
type Trim<T extends string> = T extends ` ${infer R}`
? Trim<R>
: T extends `${infer R} `
? Trim<R>
: T;

Type Replace

ts 没有 indexOf 的能力, 通过条件类型判断两个类型是否匹配

1
2
3
4
5
type Replace<
T extends string,
P extends string,
U extends string
> = T extends `${infer F}${P}${infer R}` ? `${F}${U}${R}` : T;

追加参数

1
2
3
4
5
type AppendArgument<F extends (...args: any[]) => any, P> = F extends (
...args: infer R
) => infer L
? (...args: [...R, P]) => L
: F;

Flatten

1
2
3
4
5
6
7
8
type Flatten<T extends any[], A extends any[] = []> = T extends [
infer F,
...infer R
]
? F extends any[]
? Flatten<R, Flatten<F, A>>
: Flatten<R, [...A, F]>
: A;

AppendToObject

对象 Key 的类型约束,有两种方式

1
2
3
4
5
6
// K extends PropertyKey 其中 PropertyKey 为内置属性
// K extends keyof any

type AppendToObject<T, K extends PropertyKey, V> = {
[Key in keyof T | K]: Key extends keyof T ? T[Key] : V;
};

另一种是现实是重新遍历一次组合后的对象

1
2
3
4
5
6
7
type MapKey<T> = { [K in keyof T]: T[K] };

type AppendToObject<T, K extends keyof any, V> = MapKey<
T & {
[K1 in K]: V;
}
>;

数子转字符串

1
type Test<T extends number> = `${T}`;

可以把非字符串类型转为字符串,利用类型系统处理

1
2
3
4
5
type Test<T extends number | string | bigint> = `${T}` extends `-${infer R}`
? R
: T;

type dd = Test<"-123">; //123

StringToUnion

1
2
3
4
5
6
7
8
9
10
11
type StringToUnion<
T extends string,
A extends string[] = []
> = T extends `${infer F}${infer R}` ? StringToUnion<R, [...A, F]> : A[number];

type StringToUnion<
T extends string,
A = never
> = T extends `${infer F}${infer R}` ? StringToUnion<R, A | F> : A;

type Result = StringToUnion<Test>; // expected to be "1" | "2" | "3"

MergeKey

1
2
3
4
5
6
7
8
9
10
11
12
13
type Merge<T, P extends { [k in PropertyKey]: any }> = {
[K in keyof foo | keyof coo]: (foo & coo)[K] extends never
? P[K]
: (foo & coo)[K];
};

type Merge<F, S> = {
[K in keyof F | keyof S]: K extends keyof S
? S[K]
: K extends keyof F
? F[K]
: never;
};

CamelCase & KebabCase

aa-bb-cc => aaBbCc

1
2
3
4
5
6
7
type CamelCase<T extends string> = T extends `${infer F}-${infer D}${infer R}`
? CamelCase<`${F}${Uppercase<D>}${R}`>
: T;

type CamelCase<T extends string> = T extends `${infer F}-${infer D}`
? CamelCase<`${F}${Capitalize<D>}`>
: T;

AaBbCc => aa-bb-cc

1
2
3
4
5
6
7
8
9
10
type KebabCase<
T extends string,
P extends string = ""
> = T extends `${infer F}${infer R}`
? Lowercase<F> extends F
? KebabCase<R, `${P}${F}`>
: KebabCase<R, `${P}-${Lowercase<F>}`>
: P extends `-${infer R}`
? R
: never;

Diff

1
2
3
4
5
6
7
8
9
type Diff<T extends object, P extends object> = {
[K in
| Exclude<keyof T, keyof P>
| Exclude<keyof P, keyof T>]: K extends keyof T
? T[K]
: K extends keyof P
? P[K]
: never;
};

anyOf

实现一个类型,接受一个元组,如果元组中的每一个都为 false,返回 false,有一个为 true 则返回 true

需要注意联合类型是通过 T[number] 得到的,并不会进行条件类型分配,所以当联合类型可以分配给指定联合类型, 也就是联合类型中的每一个都在指定的联合类型中的时候返回 false

1
2
3
4
5
6
7
8
type AnyOf<T extends any[]> = T[number] extends
| ""
| 0
| false
| []
| Record<any, never>
? false
: true;

isUnion

利用 分配条件类型,联合类型在条件分配后会被转换为复合联合类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type IsUnion<T, O = T> = T extends O ? ([O] extends [T] ? false : true) : never;

type case1 = IsUnion<string>;
type case2 = IsUnion<string | number>; // true
type case3 = IsUnion<[string | number]>; // false

// 利用分配条件类型把基本类型的联合类型转换为符合类型的联合类型

type Test<T, O = T> = T extends O ? [T] : never;

type case1 = Test<string>; //[string]
type case2 = Test<string | number>; // [string] | [number]
type case3 = Test<[string | number]>; // [[string|number]]

CSS艺术 视觉效果

单侧投影

先来回忆一下 box-shadow 的几个属性值

1
2
/*          水平偏移  垂直偏移  模糊半径  扩张半径  颜色   内侧阴影*/
box-shadow: h-shadow v-shadow blur spread color inset;

模糊半径和扩张半径共同控制一个阴影的大小.

如果元素原始的尺寸是 100*100, 设置它的模糊半径为 5px,最终的阴影尺寸是 (100+5)*(100+5),所以如果没有进行平移操作, 每个边上都会延伸出 5px 的阴影.

这时再设置它的扩张半径属性,会在已有的阴影尺寸上再继续计算, 而且这个值可以是一个负值,如果设置扩张半径为-5px,它会压缩原有的阴影尺寸,最终变为 (105-5)*(105-5),这个属性并不会切割掉设置了阴影的部分,实际上可以看作是它压缩了实心部分也就是元素所占据的那部分的大小.因此现在每个边上都看不见阴影了,但阴影还是存在的,它被缩小到和元素面积相同,被元素覆盖住

知道了这些实现单侧投影可能就有了一些思路,可以设置一个 4px 的模糊半径, 这样元素四边就都有了 4px 的阴影,再垂直或水平方向上偏移这个阴影 4px,现在一个边上就会有 8px 的阴影,它的对边会被遮盖住,两个临边还是 4px 的阴影,最后在使用扩张半径设置为-4px,把多余的阴影遮盖住,为了效果明显一点,偏移量可以比模糊半径多几个像素

1
box-shadow: 0px 6px 4px -4px #000;

不规则投影

也许你还是想用 box-shadow 来实现,但事实上 box-shadow 也无能为力,有些场景 box-shadow 并不会正确的显示阴影的效果.

  • 半透明的(图片,背景图片,border-image) 阴影不会穿过半透明区域,实际上还是围绕在元素周围
  • 元素设置了虚线或点或半透明的边框,但没有背景(或者 background-clip 不是 border-box 时)
  • 伪类元素拼接
  • 切角或折角的效果
  • clip-path 生成的图形

这里需要用到一个从 SVG 中借鉴来的属性 filter,因为模糊算法不一样肯能有细微的差别

1
filter: drop-shadow(4px 4px 2px red);

注意:这个属性会一视同仁的把所有透明区域都打上阴影,所以如果是元素中的文字,如果没有背景颜色也会打上阴影,而且不受 text-shadow 影响,因为他会给 text-shadow 的阴影打上阴影

色彩滤镜

有的时候想给张图片转换为灰度图,但又需要保留原有的对比度,最好是能与鼠标有交互效果.

虽然通过 canvas 可以通过脚本的方式进行修改,但是成本很高,也有性能问题. css 提供了滤镜系统可以使用,但不是所有的浏览器都兼容.

具体属性参考 MDNW3C

1
2
transition: 0.5 filter;
filter: sepia(1) saturate(4);

![0002.png]

这种方式基本满足效果,但是滤镜的叠加有时会显得过度不自然, 另外一种方式就是混合模式,将上下两层效果混合在一起,一个最重要的区别就是混合叫过不能使用动画,所以混合模式中,只能控制外层的样式过度进行混合

mix-blend-mode MDN W3C

1
2
3
4
5
6
<div class="wrap" > <div class="box" > </div > </div > .wrap {
background: hsla(1, 1, 100, 0.8);
}
.box {
mix-blend-mode: luminosity;
}

毛玻璃效果

首先能想到的就是文字模糊的问题, 如何能保证背景模糊但是不影响文字, 这里需要使用两个元素,但是为了简洁可以使用伪元素

另一个元素就是外层的背景图片元素,这里以 body 元素为例, 所以 html 结构如下

1
2
3
<body>
<div class="box">玻璃效果中的文字</div>
</body>

先把一些简单的样式实现,

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
body {
height: 100%;
background: url(./cover1.jpg) 0 / cover;
}

.box {
position: relative;
/*
当把一个元素移动到父元素下面的时候,一定要注意父元素的上级元素有没有背景
如果父元素上级元素有背景,那个移动的这个元素的背景会出现在父元素上级元素背景的下面
提升box的层级,防止before移动到box下面的时候背景会在body背景的下面
*/
z-index: 1;
}
.box::before {
content: "";
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: url(./cover1.jpg) 0 / cover;
/* 移动到父元素的下面,防止背景挡住文字 */
z-index: -1;
}

下一个重要的问题就是如何让 before 元素中的背景和 body 的背景完全对其,一种可能的办法是获取父元素背景的大小,子元素使用相同的大小,再获取子元素相对于父元素偏移量,为 background-position 设置相同的偏移量

虽然这种办法可行,但是一旦父元素存在滚动条会变的很复杂, 一个比较好的方法就是使用 background-attachment 属性.

background-attachment 设置背景图像是否固定或者随着页面的其余部分滚动。当属性值为 fixed 的时候表示,布景图相关于视口固定,所以随页面翻滚布景不动,相当于布景被设置在了 body 上。也就是说给任何元素的布景图设置 background-attachment:fixed;效果都是一样的,都是相关于视口,因为一个网页只要一个视口,该布景和元素现已没关系

所以分别给这两个元素添加这个属性,但要注意的是填充方式需要相同,否则会有错位的现象, background 最后一个属性就是 background-attachment 的简写形式.

最后通过一个负数的 margin 来解决模糊效果在临近边界的时候会衰弱, 并用外层元素的 overflow:hidden 把多余的部分剪裁掉

1
2
3
4
5
6
7
8
9
10
11
body {
background: url(./cover1.jpg) 0 / cover fixed;
}
.box {
overflow: hidden;
}
.box:after {
margin: -20px;
background: url(./cover1.jpg) 0 / cover fixed;
filter: blur(20px);
}

React v16 源码分析 ⑦ Diff 算法

设计动机

调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何高效的更新 UI,以保证当前 UI 与最新的树保持同步。

将一棵树转换成另一棵树的最小操作次数,即使使用最优的算法,该算法的复杂程度仍为 ,其中 n 是树中元素的数量。

如果在 React 中使用该算法,那么展示 1000 个元素则需要 10 亿次的比较。这个开销实在是太过高昂。于是 React 在以下两个假设的基础之上提出了一套 的启发式算法:

  • 两个不同类型的元素会产生出不同的树;
  • 开发者可以通过设置 key 属性,来告知渲染哪些子元素在不同的渲染下可以保存不变;

换一种说法就是 :

  • 只对同级比较,跨层级的 dom 不会进行复用
  • 不同类型节点生成的 dom 树不同,此时会直接销毁老节点及子孙节点,并新建节点
  • 可以通过 key 来对元素 diff 的过程提供复用的条件

单节点 Diff

  • key 和 type 相同表示可以复用节点
  • type 不同直接标记删除节点,然后新建节点
  • key 相同 type 不同,标记删除该节点和兄弟节点,然后新创建节点
1
2
3
4
5
6
7
// eg1
const A = <span key="a">0</span>;
const B = <span kay="a">1</span>;

// eg2
const A = <span key="a">0</span>;
const B = <span kay="b">1</span>;

还记得 reconcileChildren 方法么, 它是双缓存 Fiber 构建时候调用的方法,因为新的节点是单节点,所以会进入 reconcileSingleElement

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
//                              div Fiber    span 0 Fiber      span 1 JSXElement
function reconcileSingleElement(
returnFiber,
currentFirstChild,
element,
lanes
) {
var key = element.key;
var child = currentFirstChild;

while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to the first item in the list.

if (child.key === key) {
switch (child.tag) {
case Fragment: {
if (element.type === REACT_FRAGMENT_TYPE) {
deleteRemainingChildren(returnFiber, child.sibling);
var existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
}

break;
}

case Block:

default: // key 相同 type 相同 eg1
{
if (
child.elementType === element.type ||
// Keep this check inline so it only runs on the false path:
isCompatibleFamilyForHotReloading(child, element)
) {
// 因为新的节点是单节点,所以新的fiber节点不会再有兄弟节点
// 在重用之前兄弟节点标记为删除
deleteRemainingChildren(returnFiber, child.sibling);
// 通过 createWorkInProgress clone节点
// _existing3.sibling = null 因为上面的删除只是标记,而在链表中需要真正删除sibling属性
// 官方注释为: 在这里把 sibling 赋值为 null, 因为返回节点前很容易忘记赋值
var _existing3 = useFiber(child, element.props);

// ref 属性相关操作,重新为ref节点赋值
_existing3.ref = coerceRef(returnFiber, child, element);
_existing3.return = returnFiber;

// 返回 clone 的节点
return _existing3;
}

break;
}
} // Didn't match.

// key 相同 type 不同,节点本身和兄弟节点全部标记为删除, eg3
// ke 相同表示两个节点是对应的,所以兄弟节点无效标记为删除
// 但是type类型不同所以不能复用所以节点本身也需要标记为删除
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 如果 key 不相同,直接将这个节点删掉 eg2
deleteChild(returnFiber, child);
}
// 看兄弟节点是否可以通过 key 相同复用,例如
// current [a, p, span]
// new p
// 兄弟节点中有一个 p 节点可以复用
child = child.sibling;
}

// 如果没有节点可以复用, 就通过 JSXElement 创建一个新的 Fiber 节点
if (element.type === REACT_FRAGMENT_TYPE) {
var created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key
);
created.return = returnFiber;
return created;
} else {
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);

_created4.ref = coerceRef(returnFiber, currentFirstChild, element);
_created4.return = returnFiber;
return _created4;
}
}

多节点 diff

react 用 3 次循环实现了 react diff 算法, 下面是整个方法中的全局变量

1
2
3
4
5
6
7
8
9
10
11
12
// Diff结果的第一个节点
var resultingFirstChild = null;
// 新的子元素数组的集合
var knownKeys = new Set();
// 上次可复用的位置
var lastPlacedIndex = 0;
// 当前对比的老的元素
var oldFiber = currentFirstChild;
// 保存老的节点中下一个需要对比的元素
var nextOldFiber = null;
// 计数器,记录新元素遍历到的位置
var newIdx = 0;

第一次循环

  • newChildren[newIdx]oldFiber 比较
    • 如果新元素是 reactElement
      • key 相同,type 相同, 则会使用 useFiber 复用节点
      • key 相同,type 不同, 通过 JSXElement 创建新的节点
      • key 不同, 直接跳出循环,可能存在移动,用下一个循环处理
    • 如果新元素是文本元素
      • 由于文本元素没有 key,所以老的节点如果有 key,那么元素类型一定不同,直接跳出循环,等待下一个循环处理
      • 如果老的节点没有 key, 是否 tag=HostText,如果是则 useFiber 复用节点,如果不是 createFiberFromText 创建节点
    • 如果新元素是数组
      • 老元素没有 key, 直接跳出循环
      • 老元素有 key 且 oldFiber.tag===Fragment, 通过 createFiberFromFragment 创建新的子元素
      • 老元素有 key 且 oldFiber.tag!==Fragment, useFiber 复用节点

在复用或创建节点的同时也会传入新的 props 属性, 新的属性在 diffProperties 的时候会被解析,并添加到 updateQueue 中

如果不需要跳出循环, 通过 newFiber.alternate === null 判断返回的节点是复用的还是新建的,因为新建的节点没有 alternate,如果是新建的节点, oldFiber 的服级节点上删除 oldFiber, 因为已经有了新节点,且老的节点也没有被使用

下一步是给新的 Fiber 添加位置信息,调用 placeChild 方法, newFiber.index = newIndex, 并给 lastPlacedIndex 赋值
如果是新建的节点,会被标记为插入, lastPlacedIndex 不变,因为这个位置不能复用,如果是复用节点的 index < lastPlacedIndex,说明节点被移动了会打上移动的标记, 如果 index >= lastPlacedIndex 节点位置不需要移动, 因为所有小于这个节点位置的元素都会被移动到后面.

总结: 第一个循环主要处理属性的更新, key 不同则循环结束

第二次循环

如果 oldFiber 已经遍历到头, newChildren 还有剩余, 则会进入第二个循环, 将 newChildren 剩余的子元素,全部新建,并且标记为插入

第三次循环

如果第一次循环提前跳出, oldFiber, newChildren 都有剩余则会进入第三次循环

首先把 oldFiber 转换成 map 格式,方便用 key 迅速查找对应的节点,如果新元素的 key 在 map 中存在则复用节点,如果不存在则新建节点

调用第一次循环中的 placeChild , 为元素排序

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
function reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChildren,
lanes
) {
{
// First, validate keys.
var knownKeys = null;

for (var i = 0; i < newChildren.length; i++) {
var child = newChildren[i];
knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
}
}

var resultingFirstChild = null;
var previousNewFiber = null;
var oldFiber = currentFirstChild;
var lastPlacedIndex = 0;
var newIdx = 0;
var nextOldFiber = null;

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}

var newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes
);

if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}

break;
}

if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}

lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}

previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}

if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}

if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);

if (_newFiber === null) {
continue;
}

lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);

if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = _newFiber;
} else {
previousNewFiber.sibling = _newFiber;
}

previousNewFiber = _newFiber;
}

return resultingFirstChild;
} // Add all children to a key map for quick lookups.

var existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves.

for (; newIdx < newChildren.length; newIdx++) {
var _newFiber2 = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes
);

if (_newFiber2 !== null) {
if (shouldTrackSideEffects) {
if (_newFiber2.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
existingChildren.delete(
_newFiber2.key === null ? newIdx : _newFiber2.key
);
}
}

lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);

if (previousNewFiber === null) {
resultingFirstChild = _newFiber2;
} else {
previousNewFiber.sibling = _newFiber2;
}

previousNewFiber = _newFiber2;
}
}

if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(function (child) {
return deleteChild(returnFiber, child);
});
}

return resultingFirstChild;
}

属性 Diff

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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
function diffProperties(
domElement,
tag,
lastRawProps,
nextRawProps,
rootContainerElement
) {
// lastRawProps {children:'内容'}
// nextRawProps {children:'内容改变'}

var updatePayload = null;
var lastProps;
var nextProps;

// 这些元素类型,会被添加上特有的元素默认属性
switch (tag) {
case "input":
lastProps = getHostProps(domElement, lastRawProps);
nextProps = getHostProps(domElement, nextRawProps);
updatePayload = [];
break;

case "option":
lastProps = getHostProps$1(domElement, lastRawProps);
nextProps = getHostProps$1(domElement, nextRawProps);
updatePayload = [];
break;

case "select":
lastProps = getHostProps$2(domElement, lastRawProps);
nextProps = getHostProps$2(domElement, nextRawProps);
updatePayload = [];
break;

case "textarea":
lastProps = getHostProps$3(domElement, lastRawProps);
nextProps = getHostProps$3(domElement, nextRawProps);
updatePayload = [];
break;

default:
lastProps = lastRawProps;
nextProps = nextRawProps;

if (
typeof lastProps.onClick !== "function" &&
typeof nextProps.onClick === "function"
) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(domElement);
}

break;
}

// 验证一些特殊属性,是否合法 例如 dangerouslySetInnerHTML
assertValidProps(tag, nextProps);
var propKey;
var styleName;
var styleUpdates = null;

for (propKey in lastProps) {
// 如果旧的属性没有,而新的属性有,那个就跳出直接分析新的属性
if (
nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null
) {
continue;
}

if (propKey === STYLE) {
var lastStyle = lastProps[propKey];
// 如果是style属性,提取出属性的key
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
if (!styleUpdates) {
styleUpdates = {};
}

styleUpdates[styleName] = "";
}
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN);
else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
);
else if (propKey === AUTOFOCUS);
else if (registrationNameDependencies.hasOwnProperty(propKey)) {
// This is a special case. If any listener updates we need to ensure
// that the "current" fiber pointer gets updated so we need a commit
// to update this element.
if (!updatePayload) {
updatePayload = [];
}
} else {
// For all other deleted properties we add it to the queue. We use
// the allowed property list in the commit phase instead.
(updatePayload = updatePayload || []).push(propKey, null);
}
}

for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp = lastProps != null ? lastProps[propKey] : undefined;

// 如果新的属性值为假,或者没有变化,就跳出循环
if (
!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
(nextProp == null && lastProp == null)
) {
continue;
}

if (propKey === STYLE) {
{
if (nextProp) {
// Freeze the next style object so that we can assume it won't be
// mutated. We have already warned for this in the past.
Object.freeze(nextProp);
}
}

if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
for (styleName in lastProp) {
if (
lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))
) {
if (!styleUpdates) {
styleUpdates = {};
}

styleUpdates[styleName] = "";
}
} // Update styles that changed since `lastProp`.

for (styleName in nextProp) {
if (
nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]
) {
if (!styleUpdates) {
styleUpdates = {};
}

styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
if (!styleUpdates) {
if (!updatePayload) {
updatePayload = [];
}

updatePayload.push(propKey, styleUpdates);
}

styleUpdates = nextProp;
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
var nextHtml = nextProp ? nextProp[HTML$1] : undefined;
var lastHtml = lastProp ? lastProp[HTML$1] : undefined;

if (nextHtml != null) {
if (lastHtml !== nextHtml) {
(updatePayload = updatePayload || []).push(propKey, nextHtml);
}
}
}
// 会以数组的形式,同时插入 key 和 value ['children','内容改变']
else if (propKey === CHILDREN) {
if (typeof nextProp === "string" || typeof nextProp === "number") {
(updatePayload = updatePayload || []).push(propKey, "" + nextProp);
}
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
);
else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
// We eagerly listen to this even though we haven't committed yet.
if (typeof nextProp !== "function") {
warnForInvalidEventListener(propKey, nextProp);
}

if (propKey === "onScroll") {
listenToNonDelegatedEvent("scroll", domElement);
}
}

if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
// that the "current" props pointer gets updated so we need a commit
// to update this element.
updatePayload = [];
}
} else if (
typeof nextProp === "object" &&
nextProp !== null &&
nextProp.$$typeof === REACT_OPAQUE_ID_TYPE
) {
// If we encounter useOpaqueReference's opaque object, this means we are hydrating.
// In this case, call the opaque object's toString function which generates a new client
// ID so client and server IDs match and throws to rerender.
nextProp.toString();
} else {
// For any other property we always add it to the queue and then we
// filter it out using the allowed property list during the commit.
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
}

if (styleUpdates) {
{
validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]);
}

(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
}

return updatePayload;
}

React v16 源码分析 ⑥ Fiber与Effects链表

1
2
3
4
5
6
7
8
9
10
11
const App: React.FC = () => {
const [content, setContent] = useState("内容");
return (
<>
<h1 onClick={() => setContent("内容改变")} role="presentation">
标题
</h1>
<p>{content}</p> 2020.01.01
</>
);
};

回到 completeUnitOfWork 方法,看一下链表的创建过程

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
function completeUnitOfWork(unitOfWork) {
do {
completeWork();

if ((completedWork.flags & Incomplete) === NoFlags) {
setCurrentFiber(completedWork);

if (
returnFiber !== null &&
// Do not append effects to parents if a sibling failed to complete
(returnFiber.flags & Incomplete) === NoFlags
) {
// Append all the effects of the subtree and this fiber onto the effect
// list of the parent. The completion order of the children affects the
// side-effect order.
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}

if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}

returnFiber.lastEffect = completedWork.lastEffect;
} // If this fiber had side-effects, we append it AFTER the children's
// side-effects. We can perform certain side-effects earlier if needed,
// by doing multiple passes over the effect list. We don't want to
// schedule our own side-effect on our own list because if end up
// reusing children we'll schedule this effect onto itself since we're
// at the end.

var flags = completedWork.flags;
// Skip both NoWork and PerformedWork tags when creating the effect
// list. PerformedWork effect is read by React DevTools but shouldn't be
// committed.

// 创建Effect链表时跳过 tag 为 NoWork 和PerformedWork
// 他们只会被 DevTools 使用不应该提交

if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}

returnFiber.lastEffect = completedWork;
}
}
}
completedWork = returnFiber; // Update the next thing we're working on in case something throws.

workInProgress = completedWork;
} while (completedWork !== null); // We've reached the root.
}

首先,分析一下挂载时的链表创建过程, 第一个结束 completeWork 的是 h1 元素, 它的 returnFiber 是 App, 由于 h1 的 flags 是 0, 因为首次渲染是没有标记副作用,所以 App 和 h1 并不会通过 Effect 指针相连, 同理 p 和 文本元素,也是一样处理

下一个节点是 AppFiber, 它的 returnFiber 是 RootFiber, 由于 App 节点首次渲染的时候需要插入到挂载元素中, 所以它有 Placement 副作用,它的值大于 PerformedWork(标记节点处理过的副作用) ,首次挂载时的 Effect 链表如下

1
2
returnFiber.firstEffect = completedWork;
returnFiber.lastEffect = completedWork;

更新时, h1 绑定的函数是匿名函数,所以会携带副作用, 因为第一次执行的时候 h1 和它 returnFiber 的 firstEffect 和 lastEffect,都为 null,所以最先建立这两个节点的联系

下一个节点是 p 节点, 它的副作用也需要添加到 Effect 链表上,所以通过 lastEffect 指针找到当前副作用链表的最后一个副作用,它的下一个副作用就是当前的 p 节点

最后调整 returnFiber 的 lastEffect 指针,指向新的副作用 p 节点. 总结来说,如果下一级的子元素携带副作用, 通过 lastEffect 指针找到最后的副作用,并通过 nextEffect 延长 Effect 链表, 如果是上级元素携带副作用,则修改 firstEffect 指针延长 Effect 链表

下面增加一点组件的复杂性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Box: React.FC<{ content: string }> = (props) => {
const [number, setNumber] = useState(1);
return (
<p onClick={() => setNumber((v) => v + 1)}>
{props.content}
<span>{number}</span>
</p>
);
};

const App: React.FC = () => {
const [content, setContent] = useState("内容");
return (
<>
<h1 onClick={() => setContent("内容改变")} role="presentation">
标题 <p>{content}</p>
</h1>
<Box content={content} />
</>
);
};

这个组件最终形成的 Fiber 树如下

当点击 h1 标签后

第一个进入的是标题文本节点,但是文本节点不存在副作用,所以会跳过这个节点

下一个节点是 P 节点, 内容改变携带了副作用,所以会最先添加到链表中和 returnFiber 也就是 h1 Fiber 相连

下一个节点是 h1 会被拼接到 effectList 最后面, 这次 returnFiber 是 App, 它的 firstEffect 就是 h1 的 firstEffect, 换句话说也就是,上层节点会延长 effect 链表的头部, 会继承上一个节点 firstEffect

下一个节点是 Box 中的文本节点, 会和他的上级节点形成 effect 链表

下一个是 span 节点, 因为还没有点击 p 标签,所以 span 没有携带副作用,直接跳过

下一个是 span 上级的 p 节点, returnFiber 是 Box 会成为新的头部

下一个是个 Box 节点, returnFiber 是 App,相当于在末尾追加了 effect 链表, 所以修改了 App lastEffect 指针,并且延长了 h1 的 nextEffect

最后遍历到根节点 rootFiber 相当于头部延长 effect 链表

当点击 p 标签触发更新, 会重新构建整个 effect 链表, 最先进入 complete 的 span 节点, 所以会和他的父节点生成 effect 链表

构建步骤与第一次更新时类似

React v16 源码分析 ④ render阶段执行流程

render

传入 JSXElement 对象和挂载节点.

何验证根节点是否合法,挂载节点为真,且必须是以下节点之一

1
2
3
4
5
6
7
8
9
10
function isValidContainer(node) {
return !!(
node &&
(node.nodeType === ELEMENT_NODE ||
node.nodeType === DOCUMENT_NODE ||
node.nodeType === DOCUMENT_FRAGMENT_NODE ||
(node.nodeType === COMMENT_NODE &&
node.nodeValue === " react-mount-point-unstable "))
);
}

挂载节点必须是首次挂载,已经挂载过的节点不能再次执行 React.render(element,container),首次挂载之后会给元素打上一个标记 __reactContainer$xxx 是一个自定义字符串后面是随机数, 用这个标记来判断元素是否挂载过

而且 internalContainerInstanceKey = FiberRoot 会被赋值为 FiberRoot

legacyRenderSubtreeIntoContainer(null, element, container, false, callback)

首先尝试清空挂载节点中的内容,如果挂载节点中有其他的节点已经通过 render 渲染过,会提示错误

1
2
3
while ((rootSibling = container.lastChild)) {
container.removeChild(rootSibling);
}

这里创建出 FiberFootRootFiber两个节点, 并且通过指针相互引用

container._reactRootContainer = new ReactDOMBlockingRoot() 挂载元素上会打上一个标记, 赋值为 RootFiber 构造函数的实例,而 render 方法 _reactRootContainer 中与__reactContainer$xxx 共同判断节点是否挂载过

对于已经渲染过的节点会通过 _reactRootContainer 直接复用 FiberRoot, 并执行 updateContainer 批量更新, 如果是首次渲染则执行 unbatchedUpdates非批量更新,立即调用 updateContainer,同步执行尽快展示元素.

updateContainer(element, container, parentComponent, callback)

在首次执行前会标记上下文环境,因为也可能是程序运行之后,人为调用非批量更新,所以这个方法可能重复执行

1
2
3
4
5
6
// 保存之前的上下文
var prevExecutionContext = executionContext;
// 删除掉批量更新的标记
executionContext &= ~BatchedContext;
// 添加非批量更新的标记
executionContext |= LegacyUnbatchedContext;

下面这里定义了几个比较关键的变量

requestUpdateLane 传入了 FiberRoot, 计算出更新优先级为 1 (SyncLane)

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
// 计算一个时间戳
var eventTime = requestEventTime();

// 计算更新优先级
var lane = requestUpdateLane(container.current);

// 创建更新对象并添加到更新队列中
var update = {
eventTime: eventTime,
lane: lane,
tag: UpdateState,
// element 是 render 方法中传入的 JSXElement 对象
payload: {element:element},
callback: null,
next: null
};

// updateQueue 是一个对象

var updateQueue = {
baseState:null
effects:null
firstBaseUpdate:null
lastBaseUpdate:null
shared:{pending: null}
}

// 如果这是第一个更新,会被处理成循环链表
if (updateQueue.pending === null) {
update.next = update;
} else {
// 如果有了正在等待的更新,则链接到循环链表中
update.next = pending.next;
pending.next = update;
}
updateQueue.share.pending = update;

scheduleUpdateOnFiber(fiber, lane, eventTime)

checkForNestedUpdates() 检查是否嵌套的更新过多

拿到上 RootFiber 计算出的更新优先级, 与 fiber 上的优先级合并,如果当前节点不是根节点会一直递归到根节点. 首次执行时 fiber = FiberRoot

1
var root = markUpdateLaneFromFiberToRoot(fiber, lane);

在 FiberRoot 上更新 pendingLanes

1
2
3
4
5
6
7
8
9
10
11
function markRootUpdated(root, updateLane, eventTime) {
root.pendingLanes |= updateLane;
var higherPriorityLanes = updateLane - 1; // Turns 0b1000 into 0b0111
// 处于当前优先级左边的会通过&被删除掉
// 其实就是删除了较低的优先级
root.suspendedLanes &= higherPriorityLanes;
root.pingedLanes &= higherPriorityLanes;
var eventTimes = root.eventTimes;
var index = laneToIndex(updateLane);
eventTimes[index] = eventTime;
}

获取当前的执行优先级 var priorityLevel = getCurrentPriorityLevel() 这个优先级与 lane 是有区别的

检查上下文环境,准备分析构建 Fiber 树

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
// 如果传出的优先级是同步的
if (lane === SyncLane) {
if (
// 检查是非批量更新的状态
(executionContext & LegacyUnbatchedContext) !== NoContext &&
// 检查还没有开始渲染
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// Register pending interactions on the root to avoid losing traced interaction data.
schedulePendingInteractions(root, lane);
// This is a legacy edge case. The initial mount of a ReactDOM.render-ed
// root inside of batchedUpdates should be synchronous, but layout updates
// should be deferred until the end of the batch.

performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);

if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbackQueue();
}
}
} else {
// Schedule a discrete update but only if it's not Sync.
if (
(executionContext & DiscreteEventContext) !== NoContext && // Only updates at user-blocking priority or greater are considered
// discrete, even inside a discrete event.
(priorityLevel === UserBlockingPriority$2 ||
priorityLevel === ImmediatePriority$1)
) {
// This is the result of a discrete event. Track the lowest priority
// discrete update per root so we can flush them early, if needed.
if (rootsWithPendingDiscreteUpdates === null) {
rootsWithPendingDiscreteUpdates = new Set([root]);
} else {
rootsWithPendingDiscreteUpdates.add(root);
}
} // Schedule other updates after in case the callback is sync.

ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
}

performSyncWorkOnRoot(fiberRoot)

执行 renderRootSync(root, lanes)

执行结束后,赋值 finishWork 为最新的 Fiber 树,并进入提交节点,渲染元素

1
2
3
4
var finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root);

renderRootSync(root, lanes)

这个方法可以算是构建 Fiber 树的起点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function renderRootSync(root, lanes) {
// 缓存执行环境
var prevExecutionContext = executionContext;
// 执行环境标记为渲染环境
executionContext |= RenderContext;
// 调用 createWorkInProgress 创建新的 RootFiber 作为 WorkInProgress
prepareFreshStack(root, lanes);

do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);

workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}

CSS艺术 绘制形状

椭圆

border-radius 可以指定数值或百分比,当使用百分比的时候,可以让 border 按各自边长计算圆角,实现椭圆

1
2
3
4
5
6
.box {
width: 200px;
height: 100px;
background: goldenrod;
border-radius: 50%;
}

半橢圓

border-radius 是一个简写的属性, 它的完整属性可以表述四个角的圆角

border-top-left-radius
border-top-right-radius
border-bottom-left-radius
border-bottom-right-radius

属性的两个长度或百分比值定义了椭圆的四分之一外边框的边缘角落的形状。第一个值是水平半径,第二个是垂直半径。如果省略第二个值,它是从第一个复制。如果任一长度为零,角落里是方的,不圆润。水平半径的百分比是指边界框的宽度,而垂直半径的百分比是指边界框的高度。

这样我们只需要指定上边两个角或下边两个角的圆角即可

1
2
3
4
5
6
7
.box {
width: 200px;
height: 100px;
background: goldenrod;
border-top-left-radius: 50% 100%;
border-top-right-radius:50% 100%;
}

可以是使用简写的属性, border-radius 可以用 / 分隔两组值,左边代表四个角的水平半径,右边代表垂直半径

而且不同的个数代表不同的位置,这与 border 类似

50% top-left:50% | top-right:50% | bottom-right:50% | bottom-left:50%
50% 40% top-left:50% | top-right:40% | bottom-right:50% | bottom-left:40%
50% 40% 30% top-left:50% | top-right:40% | bottom-right:30% | bottom-left:40%
50% 40% 30% 20% top-left:50% | top-right:40% | bottom-right:30% | bottom-left:20%

所以分析一下这个半椭圆

  • 水平方向上面的两个角是 50%, 暂时可以写为 50% 50% 0 0 / xx xx xx xx
  • 垂直方向上面两个角是 100%,现在变为 50% 50% 0 0 / 100% 100% 0 0
  • 因为半椭圆的垂直方向占据了整个元素的高度,所以不能使用简写属性, 必须要指定上面两垂直半径是100%, 这样弧度才会从底部延伸到顶部
    现在垂直半径后两个为0,意味着对应的水平半径即使给了也不会生效,因为不能只通过一个半轴长度画椭圆,最终能够属性会变为 50% / 100% 100% 0 0
1
2
3
4
5
6
.box {
width: 200px;
height: 100px;
background: goldenrod;
border-radius: 50%/ 100% 100% 0 0;
}

同理如果你想画一个垂直方向的半椭圆

1
2
3
4
5
6
.box {
width: 200px;
height: 100px;
background: goldenrod;
border-radius: 100% 0 0 100%/50%;
}

1/4 椭圆也是同样的道理,只需指定一个角上的半径

1
2
3
4
5
6
.box {
width: 200px;
height: 100px;
background: goldenrod;
border-radius: 100% 0 0 0;
}

这个网址里你可以看到各种通过圆角制作的精美按钮

平行四边形/菱形

你可能很容易想到使用 skew, 但是有一些细节需要注意, 如果 skew 作用在一个有文字的元素上, 那么里面的文字也会被拉伸

想解决这个问题, 可能会想到使用两个元素嵌套, 让里面的元素使用反向 skew, 让文字重新边正

有没有一种方式,可以不嵌套元素,还能让文字不受影响, 办法就是使用 伪元素, 因为伪元素和元素本身不属于嵌套关系,所以更容易处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.el {
position: relative;
/* 写文字相关的样式 */
}

/* 写背景形状相关的样式 */
.el::after {
content: "";
position: absolute;
left:0;
top:0;
right:0;
bottom: 0;
/* 放在文字元素下面 */
z-index: -1;
}

对于菱形, 是四边相等的平行四边形, 最容易想到的就是旋转一个正方形

1
2
3
width: 100px;
height: 100px;
transform: rotate(45deg);

但是中心线长度不相等平行四边形会遇到一点麻烦, 最核心的一个问题就是, 拉伸后的高度应该等于宽度, 以下面这个 宽为100,高为40 的长方形为例

想求的是 AGF 的角度, 那么只要求出 FGE 就可以了, sinFGE = AG / FG 所以 FCE = srcsin(40/100)

再把 FCE 转成角度 FCE = srcsin(40/100) * 360 / (2 * PI) = 23.5781(deg)

那么 AGF = (90 - 23.57)deg

1
2
3
width: 100px;
height: 40px;
transform: skew(-66.43deg);

菱形剪裁

有时候希望一张图片能剪裁成菱形的形状, 我们已经知道菱形如何制作,所以很容易想到用两个元素嵌套

第一步把外层的元素旋转并处理成菱形, 里面的元素反向旋转修正, 又因为拉伸之后图片的上下可能填不满,所以需要通过缩放填满外层元素

1
2
3
<div id='box'>
<img />
</div>
1
2
3
4
5
6
7
.box {
transform: rotate(-78deg) skew(-66.43deg);
overflow: hidden;
}
.box img{
transform: skew(66.43deg) scale(3);
}

除了这种比较传统的方法, 现在我们有了一个新的属性可以完成这个效果 clip-path, 可以指定点的位置并链接成图形, 如果使用百分比会按照自身的尺寸解析

1
2
3
4
5
6
7
8
9
10
11
img{
width: 320px;
height: 180px;
clip-path: polygon(50% 0,100% 50%,50% 100%,0 50%);
transition: 1s clip-path;
}

img:hover {
clip-path: polygon(0 0,100% 0,100% 100%,0 100%);

}

切角效果

看过背景与边框一章之后,很容易会想到用渐变的方式是来实现,另外通常情况下会考虑使用scss来处理

1
2
3
4
5
background: #5a8;
background: linear-gradient(-45deg, transparent 20px, #5a8 0) right,
linear-gradient(45deg, transparent 20px, #fd2 0) left;
background-size: 50% 100%;
background-repeat: no-repeat;

1
2
3
4
5
6
7
8
/* hack */
background: #5a8;
background: linear-gradient(-135deg, transparent 20px, #5a8 0) top right,
linear-gradient(135deg, transparent 20px, #542 0) top left,
linear-gradient(-45deg, transparent 20px, #fd2 0) bottom right,
linear-gradient(45deg, transparent 20px, #e93 0) bottom left;
background-size: 50% 50%;
background-repeat: no-repeat;

1
2
3
4
5
6
7
8
background: #5a8;
background:
radial-gradient(circle at top right , transparent 20px, #5a8 0) top right,
radial-gradient(circle at top left,transparent 20px, #542 0) top left,
radial-gradient(circle at bottom right, transparent 20px, #fd2 0) bottom right,
radial-gradient(circle at bottom left, transparent 20px, #e93 0) bottom left;
background-size: 50% 50%;
background-repeat: no-repeat;

上面的方法算是比较完美的解决了这个问题,其中有一点不足就是代码量比较多,可能难以维护

还可以换一个思路,使用 svg + border-image 这种解决方案, svg 当作边框背景, 创造一个可以被九宫格分割的svg图片, 让九宫格的四个角为折角就能实现我们的需求

有几个细节需要注意一下, fill 属性需要编码, 需要添加 background-clip 属性,否则背景颜色会延伸到边框区域, 添加一个 border 属性用于hack, 在 border-image 不支持的时候可以回退

1
2
3
4
5
6
7
8
border: 20px solid #58a;
height: 140px;
background-clip: padding-box;
background: #58a;
border-image: 1 url('data:image/svg+xml,\
<svg xmlns="http://www.w3.org/2000/svg" width="3" height="3" fill="%2358a">\
<polygon points="0,1 1,0 2,0 3,1 3,2 2,3 1,3 0,2"/>\
</svg>');

梯形

从上面的平行四边形中可能会受到一点启发,但实际上在二维变化中,没有一种办法可以将矩形或其他图形,转换成梯形.

也许可以想到利用两个伪类实现梯形两边,但是一旦需要添加边框或圆角, 这种中方案立刻就没有了操作性.

既然二维不行,可以思考一下三维中的实现办法, 可以利用透视让矩形的一条边远离我们,从而在视觉上实现梯形的效果.

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
.box {
position: relative;
width: 200px;
height: 60px;
line-height:60px;
font-size: 20px;
color: #fff;
text-align: center;
}
.box:after {
content: "";
position: absolute;
left:0;
top:0;
bottom: 0;
right: 0;
margin: auto;
z-index: -1;
border-radius: 8px;
border: 2px solid darkorchid;
background: #58a;
/* 保持底边固定,整个图形围绕底边旋转 */
transform-origin: bottom;
/* 第一个属性是景深, 用于表现出3D效果,经过空间旋转的矩形在视觉上高度会缩小, 所以通过放大高度来使变换后的图形和之前的图形,高度相同 */
transform:perspective(300px) rotateX(30deg) scaleY(1.25);
}

当需要只有一边倾斜的梯形是,只需要修改修改变换中心. 这个中心可以理解为视觉中是一个直角坐标系, 这个中心点永远在你视线的正前方.
当变换中心设置为 bottom 的时候,相当于把这个元素的底边放在了视线中心上,但是左右两边会被视线中心平分, 所以元素绕 x 轴转动的时候,左右两边因为透视会向中间收缩.
当变换中心设置为 bottom ,left 的时候, 除了底边在视线中心上,左边也在视线中心, 所以旋转的时候,左边只有高度的变化,而不会因为透视,向中间收缩,因为这条边垂直与你的视线.

1
2
3
4
.box:after {
transform-origin: bottom left;
transform: perspective(91px) rotateX(18deg) scaleY(1.25);
}

饼图

先思考一下实现一个双色的饼图需要几个元素, 其实两个元素就够了,其中一个是伪元素, 实现思路是把元素的背景色设置成渐变的两半,伪元素大小为元素的一半,这样就可以把底色漏出来,而显示进度的那一半颜色可以用伪元素覆盖住.

通过旋转伪元素,并切换伪元素的颜色来显示饼图的大小, 说起来简单但是实现起来细节很多

先来实现一个 20% 的饼图, 这里用到了 turn 这个表示圈的单位, 0.2turn 表示的就是 0.2 * 360deg, 可以让你免于计算角度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.box{
position: relative;
width: 200px;
height: 200px;
background: yellowgreen;
background-image: linear-gradient(90deg, transparent 50%,#655 0);
border-radius: 50%;
overflow: hidden;
}
.box::after {
content:'';
position: absolute;
width: 50%;
height: 100%;
border-radius: 0 100% 100% 0 / 50%;
left: 50%;
top:0;
background-color: inherit;
transform-origin: left center;
transform: rotate(0.2turn);
}

但是当角度超过 50% 就会有一些问题,因为伪类的颜色还是和没有占比区域的颜色相同,所以还没法表现超出 50% 的饼图, 第一步需要修改伪类的颜色

但是伪类已经旋转了 180deg,仅仅改变颜色会和另一半颜色拼在一起显示出一个 100% 的饼图,所以需要减去半圈 0.5turn, 如果表示 70% 只需要旋转 (0.7turn - 0.5turn) = 0.2turn 就够了

1
2
3
4
5
6
7
8
9
10
11
12
13
.box{
position: relative;
width: 200px;
height: 200px;
background: yellowgreen;
background-image: linear-gradient(90deg, transparent 50%,#655 0);
border-radius: 50%;
overflow: hidden;
}
.box::after {
background-color: #665;
transform: rotate(0.2turn);
}

到这里似乎已经可以实现饼图的效果了,但如果想修改一个比例,我们能会修改颜色,修改圈数,能不能只通过一个属性就控制为元素的颜色和角度,这里会用到很多 animation 相关的属性

第一点需要解决如何让超过 50% 之后,颜色自动改变,可能只用 animation 有这个能力,因为没有什么选择器可以判断元素是不是旋转过了一半,而 animation 可以控制动画的执行位置

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
.box{
position: relative;
width: 200px;
height: 200px;
background: yellowgreen;
background-image: linear-gradient(90deg, transparent 50%,#655 0);
border-radius: 50%;
overflow: hidden;
}
.box::after {
content:'';
position: absolute;
width: 50%;
height: 100%;
border-radius: 0 100% 100% 0 / 50%;
left: 50%;
top:0;
background-color: inherit;
transform-origin: left center;
animation: bg 100s step-end infinite,ani 50s linear infinite;
animation-delay: -10s;
animation-play-state: paused;
}
@keyframes bg {
50% {
background: #655;
}
}

@keyframes ani {
to {
transform: rotate(0.5turn);
}
}

step-end 的目的就是在动画指定到一半也就是 50s 的时候,颜色突然改变,而这时也恰好旋转了半圈因为 ani 动画的执行时间是 50s 旋转半圈, 另外需要使用 animation-play-state: paused 把动画暂停住,这样在一个合适的角度就能显示出比例

这里用到了 animation-delay 很少使用到的属性, 一个负的延时时间,这是有意义的,它的行为与 0s 延时类似,都会立即执行动画,但是一个负值表示动画已经开始播放,并且持续了对应的时间,效果就是显示第一帧的时候,好像动画已经播放了这么长时间.所以指定一个负值来表示已经旋转过的角度. 如果是 70% 那可以设置为 animation-delay: -70s

React源码分析 $1 全局对象或变量

FiberRoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function FiberRootNode(containerInfo, tag, hydrate) {
this.tag = tag;
this.containerInfo = containerInfo;
this.pendingChildren = null;
this.current = null;
this.pingCache = null;
this.finishedWork = null;
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
this.hydrate = hydrate;
this.callbackNode = null;
this.callbackPriority = NoLanePriority;
this.eventTimes = createLaneMap(NoLanes);
this.expirationTimes = createLaneMap(NoTimestamp);
this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;
this.finishedLanes = NoLanes;
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);

Fiber

react15 在 render 阶段的 reconcile 是不可打断的,这会在进行大量节点的 reconcile 时可能产生卡顿,因为浏览器所有的时间都交给了 js 执行,并且 js 的执行时单线程。为此 react16 之后就有了 scheduler 进行时间片的调度,给每个 task(工作单元)一定的时间,如果在这个时间内没执行完,也要交出执行权给浏览器进行绘制和重排,所以异步可中断的更新需要一定的数据结构在内存中来保存工作单元的信息,这个数据结构就是 Fiber。

  • 工作单元:Fiber 最重要的功能就是作为工作单元,保存原生节点或者组件节点对应信息(包括优先级),这些节点通过指针的形似形成 Fiber 树
  • 增量渲染:通过 jsx 对象和 current Fiber 的对比,生成最小的差异补丁,应用到真实节点上
  • 根据优先级暂停、继续、排列优先级:Fiber 节点上保存了优先级,能通过不同节点优先级的对比,达到任务的暂停、继续、排列优先级等能力,也为上层实现批量更新、Suspense 提供了基础
  • 保存状态: : 因为 Fiber 能保存状态和更新的信息,所以就能实现函数组件的状态更新,也就是 hooks
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
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode
) {
//作为静态的数据结构 保存节点的信息
this.tag = tag; //对应组件的类型
this.key = key; //key属性
this.elementType = null; //元素类型
this.type = null; //func或者class
this.stateNode = null; //真实dom节点

//作为fiber数架构 连接成fiber树
this.return = null; //指向父节点
this.child = null; //指向child
this.sibling = null; //指向兄弟节点
this.index = 0;

this.ref = null;

//用作为工作单元 来计算state
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;

this.mode = mode;

//effect相关
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;

//优先级相关的属性
this.lanes = NoLanes;
this.childLanes = NoLanes;

//current和workInProgress的指针
this.alternate = null;
}

updateQueue

另外 updateQueue 属性在节点创建的时候添加 Fiber 对象上

1
2
3
4
5
6
7
8
9
10
11
12
function initializeUpdateQueue(fiber) {
var queue = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
fiber.updateQueue = queue;
}

优先级

1
2
3
4
5
6
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

Flags

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
// Don't change these two values. They're used by React Dev Tools.
export const NoFlags = /* */ 0b00000000000000000000000000;
export const PerformedWork = /* */ 0b00000000000000000000000001;

// You can change the rest (and add more).
export const Placement = /* */ 0b00000000000000000000000010;
export const Update = /* */ 0b00000000000000000000000100;
export const PlacementAndUpdate = /* */ Placement | Update;
export const Deletion = /* */ 0b00000000000000000000001000;
export const ChildDeletion = /* */ 0b00000000000000000000010000;
export const ContentReset = /* */ 0b00000000000000000000100000;
export const Callback = /* */ 0b00000000000000000001000000;
export const DidCapture = /* */ 0b00000000000000000010000000;
export const ForceClientRender = /* */ 0b00000000000000000100000000;
export const Ref = /* */ 0b00000000000000001000000000;
export const Snapshot = /* */ 0b00000000000000010000000000;
export const Passive = /* */ 0b00000000000000100000000000;
export const Hydrating = /* */ 0b00000000000001000000000000;
export const HydratingAndUpdate = /* */ Hydrating | Update;
export const Visibility = /* */ 0b00000000000010000000000000;
export const StoreConsistency = /* */ 0b00000000000100000000000000;

export const LifecycleEffectMask =
Passive | Update | Callback | Ref | Snapshot | StoreConsistency;

// Union of all commit flags (flags with the lifetime of a particular commit)
export const HostEffectMask = /* */ 0b00000000000111111111111111;

// These are not really side effects, but we still reuse this field.
export const Incomplete = /* */ 0b00000000001000000000000000;
export const ShouldCapture = /* */ 0b00000000010000000000000000;
export const ForceUpdateForLegacySuspense = /* */ 0b00000000100000000000000000;
export const DidPropagateContext = /* */ 0b00000001000000000000000000;
export const NeedsPropagation = /* */ 0b00000010000000000000000000;
export const Forked = /* */ 0b00000100000000000000000000;

// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
export const RefStatic = /* */ 0b00001000000000000000000000;
export const LayoutStatic = /* */ 0b00010000000000000000000000;
export const PassiveStatic = /* */ 0b00100000000000000000000000;

// These flags allow us to traverse to fibers that have effects on mount
// without traversing the entire tree after every commit for
// double invoking
export const MountLayoutDev = /* */ 0b01000000000000000000000000;
export const MountPassiveDev = /* */ 0b10000000000000000000000000;

// Groups of flags that are used in the commit phase to skip over trees that
// don't contain effects, by checking subtreeFlags.

export const BeforeMutationMask =
// TODO: Remove Update flag from before mutation phase by re-landing Visibility
// flag logic (see #20043)
Update |
Snapshot |
(enableCreateEventHandleAPI
? // createEventHandle needs to visit deleted and hidden trees to
// fire beforeblur
// TODO: Only need to visit Deletions during BeforeMutation phase if an
// element is focused.
ChildDeletion | Visibility
: 0);

export const MutationMask =
Placement |
Update |
ChildDeletion |
ContentReset |
Ref |
Hydrating |
Visibility;
export const LayoutMask = Update | Callback | Ref | Visibility;

// TODO: Split into PassiveMountMask and PassiveUnmountMask
export const PassiveMask = Passive | ChildDeletion;

// Union of tags that don't get reset on clones.
// This allows certain concepts to persist without recalculating them,
// e.g. whether a subtree contains passive effects or portals.
export const StaticMask = LayoutStatic | PassiveStatic | RefStatic;

RootTag

1
2
3
4
export type RootTag = 0 | 1;

export const LegacyRoot = 0;
export const ConcurrentRoot = 1;

simpleEventPluginEvents

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
const simpleEventPluginEvents = [
"abort",
"auxClick",
"cancel",
"canPlay",
"canPlayThrough",
"click",
"close",
"contextMenu",
"copy",
"cut",
"drag",
"dragEnd",
"dragEnter",
"dragExit",
"dragLeave",
"dragOver",
"dragStart",
"drop",
"durationChange",
"emptied",
"encrypted",
"ended",
"error",
"gotPointerCapture",
"input",
"invalid",
"keyDown",
"keyPress",
"keyUp",
"load",
"loadedData",
"loadedMetadata",
"loadStart",
"lostPointerCapture",
"mouseDown",
"mouseMove",
"mouseOut",
"mouseOver",
"mouseUp",
"paste",
"pause",
"play",
"playing",
"pointerCancel",
"pointerDown",
"pointerMove",
"pointerOut",
"pointerOver",
"pointerUp",
"progress",
"rateChange",
"reset",
"resize",
"seeked",
"seeking",
"stalled",
"submit",
"suspend",
"timeUpdate",
"touchCancel",
"touchEnd",
"touchStart",
"volumeChange",
"scroll",
"toggle",
"touchMove",
"waiting",
"wheel",
];

React v16 源码分析 ⑤ Fiber与双缓存结构

fiber 是一种架构,一种数据类型,一个调用栈的帧。

指向父节点使用 return 链接,指的是执行完 completeWork 返回的下一个 fiberNode,这里有一个返回的动作,所有用 return。

双缓存类似于显卡的前缓冲区和后缓冲区,当新的图像写入后缓冲区之后,前后缓冲区交换。一个是真实的 UI 对应的 fiberTree,一个是即将更新的 UI 对应的 fiberTree,通过 alternate 关联。

先创建一个组件,下面这个了例子在大部分的章节都会用到

1
2
3
4
5
6
7
8
9
10
11
const App: React.FC = () => {
const [content, setContent] = useState("内容");
return (
<>
<h1 onClick={() => setContent("内容改变")} role="presentation">
标题
</h1>
<p>{content}</p> 2020.01.01
</>
);
};

mount 阶段

准备 workInProgress 节点,此节点为 rootFiber 节点的一个副本,无论是更新还是挂载阶段都会先创建这个节点,然后从这个节点头部开始,递归处理每一个节点,最终形成一个 fiber 链表,对整个链表处理之后会交换两个 Fiber 树

createWorkInProgress

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
function prepareFreshStack(root, lanes) {
workInProgressRoot = root;
// 传入rootFiber
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes =
subtreeRenderLanes =
workInProgressRootIncludedLanes =
lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
}

function createWorkInProgress(current, pendingProps) {
var workInProgress = current.alternate;

// workInProgress 不存在则创建新的节点
if (workInProgress === null) {
// 我们使用了双缓冲池技术因为我们知道只需要最多两个版本的树
// 我们存放没有使用的节点以便复用,这是延时创建避免被从不会更新任务,占用额外的对象.
// 这也允许我们在需要的时候回收的内存.

// 克隆出新的rootFiber节点
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;

// workInProgress 通过alternate属性链接原始的 FiberRoot 对象
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
}

return workInProgress;
}

到目前位置 Fiber 树的结构为

递归处理的开始,在performUnitOfWork 中传入刚刚克隆出的 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
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}

function performUnitOfWork(unitOfWork) {
// 当前正被处理的这个是Fiber的镜像,任何操作都不应该依赖于它
// 但在这里依赖它,意味在处理进程中不需要额外的字段

// 这个是原始的 rootFiber 节点
var current = unitOfWork.alternate;

// 一个全局current变量指向当前被处理的节点
setCurrentFiber(unitOfWork);
var next;

next = beginWork(current, unitOfWork, subtreeRenderLanes);

// 处理结束后清空current=null指针
resetCurrentFiber();
unitOfWork.memoizedProps = unitOfWork.pendingProps;

// complete 阶段
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}

beginWork 简单来说这个方法就是从根节点解析,如果节点不存在就创建对应的 fiber 节点,如果存在就判断是复用还是重新创建, 并通过 return, sibling 等指针链接各个 fiber 节点
最终构建出一颗 Fiber 树, 而这个 Fiber 树与原始的 Fiber 树之间通过 alternate 指针相连. 注意 beginWork 处理的都是 js 对象, 对真正 DOM 元素的创建是在 completeWork 中进行的.

第一个节点是 rootFiber 节点,因为它的镜像节点已经在克隆 rootFiber 节点的时候通过 alternate 指针与原始的 rootFiber 相关联,所以会进入 if(current!==null) 分支, 最终 didReceiveUpdate 标记为 false

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
// current 为 alternate 节点,也就是原始的 Fiber 树中的镜像节点
// workInProgress 为当前正在处理的节点
// subtreeRenderLanes 为当前正处理的节点的优先级
function beginWork(current, workInProgress, renderLanes) {
if (current !== null) {
var oldProps = current.memoizedProps;
var newProps = workInProgress.pendingProps;
// props改变
// 上下文对象改变
// Force a re-render if the implementation changed due to hot reload
// 这三个状态改变会被标记为 didReceiveUpdate 更新
if (oldProps !== newProps || hasContextChanged() || (
workInProgress.type !== current.type )) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
}
//
else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.

switch (workInProgress.tag) {
case HostRoot:
// 不同的 fiber 类型用不同的函数处理
// 没有对 fiber 进行操作,只是处理上下文
}
// 如果节点可以复用,会通过这个函数处理
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See https://github.com/facebook/react/pull/19216.
didReceiveUpdate = true;
} else {
// An update was scheduled on this fiber, but there are no new props
// nor legacy context. Set this to false. If an update queue or context
// consumer produces a changed value, it will set this to true. Otherwise,
// the component will assume the children have not changed and bail out.
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}

switch (workInProgress.tag) {
// updateHostRoot(...)
// updateHostComponent(...)
}
}

会跳出 if 条件,进入到下面的 switch 处理

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
function updateHostRoot(current, workInProgress, renderLanes) {
var updateQueue = workInProgress.updateQueue;

// 把一些原始rootFiber上的属性,复制到 workInProgress
cloneUpdateQueue(current, workInProgress);

// render方法调用时传入的App jsxElement,被添加到updateQueue
// 这个方法会将App从update对象中取出,放到memoizedState中
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
var nextState = workInProgress.memoizedState;

// nextChildren 为render方法传入的 App 组件
var nextChildren = nextState.element;

// 如果和之前的组件相同,则复用该节点
if (nextChildren === prevChildren) {
resetHydrationState();
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// 进入协调子节点的方法
// 原始rootFiber, 当前处理的rootFiber, App节点
reconcileChildren(current, workInProgress, nextChildren, renderLanes);

return workInProgress.child;
}

reconcileChildren

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
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.

workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}

这两个协调节点的处理函数都是通过一个函数生成的,区别就是 mountChildFibers 不会处理副作用,reconcileChildFibers 会在 fiber 节点上添加副作用的标识,标记删除或更新等操作

1
2
var reconcileChildFibers = ChildReconciler(true);
var mountChildFibers = ChildReconciler(false);

由于原始节点 current 存在, 会进入 reconcileChildFibers,根据不同的子元素类型使用不同的处理方法,Diff 算法也发生在这里

reconcileChildFibers

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
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
// not as a fragment. Nested arrays on the other hand will be treated as
// fragment nodes. Recursion happens at the normal flow.
// Handle top level unkeyed fragments as if they were arrays.
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
// We treat the ambiguous cases above the same.

// 这个条件用于判断是否是Fragment,如果是顶层的Fragment会直接从props中取出,把他当作子元素
var isUnkeyedTopLevelFragment =
typeof newChild === "object" &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;

if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}

var isObject = typeof newChild === "object" && newChild !== null;

if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes
)
);

case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes)
);
}
}

if (typeof newChild === "string" || typeof newChild === "number") {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
"" + newChild,
lanes
)
);
}

if (isArray$1(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
return deleteRemainingChildren(returnFiber, currentFirstChild);
}

最终 reconcileSingleElement 会使用 App 生成新的 fiber 节点,并通过 return 指针与 rootFiber 相连

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function reconcileSingleElement(
returnFiber,
currentFirstChild,
element,
lanes
) {
/*
在这个函数中会判断组件的tag类型,需要注意的函数组件并不会一开始就被标记为FunctionComponent
因为函数可能没有继承React.Component 是用户自己的行为,所以会在处理这个节点的时候单独判断

type会按照以下顺序判断
type是否是一个函数,如果是原型链上有没有isReactComponent 如果有就是类组件
type是否是一个字符串, 如果是就是HTMLElement
type是不是内置的类型,例如 REACT_FRAGMENT_TYPE 等
如果都不是会被标记为 mountIndeterminateComponent=2 表示一个待定的组件
*/
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);
_created4.return = returnFiber;
return _created4;
}

placeSingleChild 会把节点的 flag 标记为 Placement 标识这是需要插入的元素

1
newFiber.flags = Placement;

最后通过 return workInProgress.child 返回下一个 fiber , 也就是刚刚创建的 AppFiber, 现在链表的结构

回到 performUnitOfWork 方法,next 现在为 App fiber,next!==null 表示还有 App Fiber 这个节点需要处理,所以并不会进入 complete 这个分支,下面经过 workLoopSync 再次进入 performUnitOfWork 方法

由于 React.createElement 创建 virtualDOM 的是否并不会分析组件类型,只能简单区是否是 html 元素, 不能区分是函数组件还是类组件, 所以这里会进入 tag=2 的分支,用于处理一个不确定的组件

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
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes
);
}
}

function mountIndeterminateComponent(
_current,
workInProgress,
Component,
renderLanes
) {
// 首先会尝试执行这个函数,拿到函数的返回值
var value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes
);

// 判断是否具有类组件的行为,如果有则按类组件处理不,并重新标记tag
if (
typeof value === "object" &&
value !== null &&
typeof value.render === "function" &&
value.$$typeof === undefined
) {
//...
workInProgress.tag = ClassComponent;
//...
} else {
// 如果不是类组件才会标记为函数组件
workInProgress.tag = FunctionComponent;
}

// 再次调用协调函数
reconcileChildren(null, workInProgress, value, renderLanes);
}

和上次调用reconcileChildren的时候不同,这次传入的 current=null, 也就是这个节点还没有镜像节点,所以会使用 mountChildFibers 处 理

因为这两个方法只是区分是否是初次挂载,所以大致的流程相同,进入 reconcileChildFibers 会检查到这是一个 Fragment 节点,所以直接取出它的子元素,是一个由 h1,p,文本元素 三个 virtualDOM 组成的数组, 进入 reconcileChildrenArray 多节点的 Diff 算法就发生在这里

最终这个方法会将三个子节点通过 sibling 指针相连,并返回头节点 h1, 作为下一个工作单元,现在链表的结构为

再次处理的是 h1 节点, 因为初次挂载它的 alternate 也没有构建,所以直接进入对应的 case 处理

在处理 h1 Fiber 的时候会检查是不是存在唯一的文本子节点,如果存在子元素为null

1
2
3
4
5
6
7
8
function updateHostComponent(current, workInProgress, renderLanes) {
if (isDirectTextChild) {
// 我们把Host Node 唯一字节点当作一个特殊用例,这是一个常见的问题,不会当作一个子节点处理
// 在执行环境中会处理,依然可以获取这个props,这可以避免用另一个Fiber遍历处理
nextChildren = null;
}
return workInProgress.child;
}

h1 的 beginWork 结束之后会进入 completeWork, 因为 h1 有为文本子节点,并不会算作他的子元素,所以他的 child = null

需要注意的是,completeWork 并不一定在所有的节点的 beginWork 执行完成后才会执行,当一个节点没有子节点需要处理的时候,就会进入 completeWork,

complete 阶段

completeWork 最重要的目的之一就是对比新老节点,把 fiber 对应的真实 DOM 元素创建出来或添加更新属性, 并与 fiber 节点相关联,另一个目的是 构建 Effects 链表

这个方法会遍历传入元素的 siblings 兄弟元素,如果没有会返回父级元素,一直递归到根节点

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 completeUnitOfWork(unitOfWork) {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
var completedWork = unitOfWork;

do {
next = completeWork(current, completedWork, subtreeRenderLanes);

// 结束执行后有一段处理 Effect 链表的逻辑
// ...
var siblingFiber = completedWork.sibling;

if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
} // Otherwise, return to the parent

completedWork = returnFiber;
// Update the next thing we're working on in case something throws.

workInProgress = completedWork;
} while (completedWork !== null); // We've reached the root.
}

completeWork 与 beginWork 方法设计类似,不同的 fiber 类型,会进入不同的 case 处理,但以下的节点类型会返回 null,这些类型的 Fiber 节点都没有真实的 DOM 元素与之对应

1
2
3
4
5
6
7
8
9
10
11
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;

h1 元素首次进入, 会进入对应的 HostComponent 处理,这时的 alternate 节点还没有渲染,所以 current=null, 会进入 else 分支

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
case HostComponent:
{
var type = workInProgress.type;

if (current !== null && workInProgress.stateNode != null) {
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
}else{
// 会直接调用原生的DOM方法创建元素
// domElement = ownerDocument.createElement(type);
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);

// 如果元素还有其他子元素例如 <h1><span/><span/></h1>
// 会循环遍历子元素,将子元素的原生DOM,插入到 h1 的原生DOM中
appendAllChildren(instance, workInProgress, false, false);
// 重新赋值 stateNode 指针,指向原生DOM
workInProgress.stateNode = instance;
// Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.

// 通过原生DOM方法将props中的属性添加在原生DOM上
// node.addAttribute(_attributeName);
// 如果是点击事件,则会加入到事件系统的队列中
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {

// 添加更新的副作用 workInProgress.flags |= Update;
markUpdate(workInProgress);
}
}
}
return null;
}

跳出 complete 后,下一个节点是 p,与 h1 类似,还是会先进入 beginWork, 由于没有子节点,所以紧接着进入 completeWork, 当最后一个文本节点处理之后会执行 completedWork = returnFiber 也就是对 AppFiber 执行 completeWork, 所以直接返回 null,最终所有的节点都遍历之后形成的链表为

update 阶段

点击 h1 标签状态更新,依然会进入 createWorkInProgress 第一个节点是 RootFiber,alternate 已经存在,如果不存在则创建一个 FiberInWorkProgress 并用 alternate 相连

与 render 阶段相同都会进入 beginWork , 第一个节点为 FiberRoot, 与首次渲染不同,这次的 renderLanes!==updateLanes 表示这个节点需要渲染,但是当前节点不需要更新,这也就意味这这个节点可以复用,会进入下面的复用的逻辑

会直接克隆当前节点,并返回子节点 App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
//
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
return null;
} else {
// 当前的节点不需要更新但是子节点需要更新,克隆子节点继续
// 会再次调用 createWorkInProgress 克隆当前节点的子节点,并通过 alternate 指针,与原节点相连
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}

现在的 Fiber 树结构为

下面进入的是 App 节点, 因为这个节点需要更新,但是又没有新的属性或上下文,所以会进入下面的分支,
把是否收到更新标记为 false,并重新构建此节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See https://github.com/facebook/react/pull/19216.
didReceiveUpdate = true;
} else {
// An update was scheduled on this fiber, but there are no new props
// nor legacy context. Set this to false. If an update queue or context
// consumer produces a changed value, it will set this to true. Otherwise,
// the component will assume the children have not changed and bail out.

// 这个Fiber上有一个更新被调度,但是没有新的属性或上下文,就设置为false
// 如果一个更新队列或上下文,消费了一个改变的值,会被设置为true
// 否则组件则会假定子元素没有改变并跳出
didReceiveUpdate = false;
}

这一次的 App 节点已经不是 mount 时候无法确定的节点,而是一个 FunctionComponent,会进入对应的 case 处理

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
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps,
renderLanes
) {
var context;
var nextChildren;

setIsRendering(true);

// 会在创建 virtualDOM 时检查是否更新,如果是 didReceiveUpdate 标记为true
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes
);

// 如果节点不需要更新则会继续走复用节点的逻辑
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} // React DevTools reads this flag.

// 否则创建新的Fiber节点并返回
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}

h1 更新阶段, 会进入 if 分支执行 updateHostComponent, 并且因为 props 发生改变,didReceiveUpdate 会被标记为 true. h1 的文本发生了改变,但由于这是它的唯一文本节点所以不需要额外处理,只当作节点更新即可.

下面会紧接着进入 h1 的 completeWork, 进入对应的 case 进行处理, updateHostComponent$1 中会调用 diffProperties 进行属性的 Diff, 最终把 Diff 后的属性添加到 updateQueue 中

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
case HostComponent:
{
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;

if (current !== null && workInProgress.stateNode != null) {

// 执行属性Diff算法
// var updatePayload = diffProperties(domElement, type, oldProps, newProps);
// workInProgress.updateQueue = updatePayload;
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);

if (current.ref !== workInProgress.ref) {
markRef$1(workInProgress);
}
} else {
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
// Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
}

return null;
}

以下一个进来的 p 节点为例, 新的文本是 内容改变, 旧的文本是 内容,与 h1 的处理过程类似,最后会执行 RootFiber 的 completeWork 会给 fiber 添加 snapshot 副作用标记

1
2
3
4
5
// Schedule an effect to clear this container at the start of the next commit.
// This handles the case of React rendering into a container with previous children.
// It's also safe to do for updates too, because current.child would only be null
// if the previous render was null (so the the container would already be empty).
workInProgress.flags |= Snapshot;

最终依次处理所有节点之后,生成一个新的 Fiber 树

双缓存结构

在内存中构建并直接替换的技术叫做双缓存。

当首次 update 结束,这时产生的两个 Fiber 树就是,Fiber 树的双缓存结构

当 render 阶段执行结束之后会进入 commitRoot

1
2
3
4
5
6
7
8
9
10
11
function performSyncWorkOnRoot(root) {
// 原 rootFiber 的镜像节点,也就是 workInProgress
var finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
// 提交阶段结束之后会重新复制current
// root.current = finishedWork;
// FiberRoot的 current 指针会指向最新构建的 Fiber 树
// 并将原有的 Fiber 树回收 workInProgress = null
commitRoot(root);
}
  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信