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);
}

CSS艺术 背景与边框

半透明边框

背景颜色会延伸到边框的下面,如果给元素一个虚线边框就能看到

使用 background-clip 让元素的背景被内边框裁掉

1
2
3
4
5
width: 100px;
height: 100px;
border: 20px dashed hsla(0, 0%, 100%, 0.5);
background: darkgoldenrod;
background-clip: padding-box;

多重边框

box-shadow 模拟

原理就是让扩张半径增大,偏移量以及模糊值都为 0,需要注意,阴影并不会占据空间大小,需要处理元素的位置。

1
2
3
4
5
width: 100px;
height: 100px;
background: darkgoldenrod;
box-shadow: 0 0 0 10px #0000ff, 0 0 0 20px #00ff00, 0 0 0 30px #ff0000;
margin: 30px 0 0 30px;

outline

使用 outline + border 可以实现两侧边框,比 box-shadow 更灵活。

注意,outline 的边框可能和圆角不贴和。一些老的浏览器版本中可能存在

1
2
3
4
5
6
width: 100px;
height: 100px;
background: darkgoldenrod;
border: 10px solid #0000ff;
outline: 10px solid #00ff00;
border-radius: 10px;

使用负的 outline-offset 实现缝线的效果

1
2
3
4
5
6
width: 100px;
height: 100px;
background: darkgoldenrod;
outline: 1px dashed #fff;
border-radius: 10px;
outline-offset: -10px;

背景图片

background 简写属性如下

1
background:bg-color bg-image position/bg-size bg-repeat bg-origin bg-clip bg-attachment initial|inherit;

background-position 允许指定每个方向的偏移量,在简写属性中添 right bottom,可以防止background-position不被支持的时候位置误差过大

1
2
3
4
width: 100px;
height: 100px;
background: darkgoldenrod url(./cover1.jpg) right bottom / 20px 20px no-repeat;
background-position: right 20px bottom 20px;

如果你想让背景图片出现在右下角,但是又要空出与 padding 相等的距离,可以使用 background-origin 可以修改出现背景出现的区域

1
2
3
4
5
width: 100px;
height: 100px;
padding:20px;
background: darkgoldenrod url(./cover1.jpg) right bottom / 20px 20px no-repeat;
background-origin: content-box;

也可使用 calc() 函数

1
2
3
4
5
width: 100px;
height: 100px;
box-sizing: border-box;
background: darkgoldenrod url(./cover1.jpg) right bottom / 20px 20px no-repeat;
background-position: calc(100% - 20px) calc(100% - 20px);

条纹背景

首先我们想实现的是一个实色过度的背景,也就是没有渐变的效果

当下个颜色的起点在,上一个颜色的终点时,这时候没有空间让渐变产生就会是一个实色的背景

1
2
background:linear-gradient(#58a 50%,#fba 50%);
background-size:100% 30px;

还有一个简写的方法,如果某个颜色的位置比整个列表中他前面颜色的位置都要小,这个颜色的起始位置会被设置为前面颜色中的最大位置

1
2
background:linear-gradient(#58a 50%,#fba 0);
background-size:100% 30px;

如果想要垂直方向的条纹,需要给定一个角度,并修改背景大小

1
2
background:linear-gradient(90deg,#58a 50%,#fba 50%);
background-size:30px 100% ;

另一种常用的技巧是,把条纹的主色设置为背景色,再用另一种半透明的颜色覆盖,更容易修改

45度斜向条纹

如果理所当然的把角度改为其他角度,就以为能得到条纹背景,那就错了

因为旋转的只是一个背景单元(贴片)中的背景,而不是整个背景,他们拼在一起的时候会产生锯齿

所以需要在一个单元(贴片)中完整的画出条纹,在使用这个单元(贴片)去铺满背景

现在需要加几个锚点,在单元中画出相间的四条背景线,把这些单元拼接在一起的时候就会形成45度的斜向条纹

1
2
background:linear-gradient(45deg,#58a 25%,#fba 0,#fba 50%,#58a 0,#58a 75%,#fba 0);
background-size:30px 30px ;

其他角度的斜向条纹

但是其他角度的时候,会法相还是无法实现,比如 60度

所以css还提供了一个加强版的线性渐变,可以将你画出的部分当作单元并重复铺满整个背景

1
background:repeating-linear-gradient(60deg,#58a,#58a 15px,#fba 0,#fba 30px);

在实现条纹背景的时候,通常两个颜色属于一个色系,可以将主色设置为背景,副色作为条纹背景盖在上面,而且好处是不支持的时候可以显示主色的背景

1
2
3
4
5
6
7
8
background: repeating-linear-gradient(
60deg,
hsla(0, 0%, 100%, 0.1),
hsla(0, 0%, 100%, 0.1) 15px,
transparent 0,
transparent 30px
),
#58a;

网格背景

利用半透明的叠加,可以创建对比更明显的网格

1
2
3
4
background: white;
background: linear-gradient(90deg,rgba(200, 0, 0, 0.5) 50%,transparent 0),
linear-gradient(rgba(200, 0, 0, 0.5) 50%, transparent 0);
background-size: 30px 30px;

也可以让渐变的起始宽度为1px.创建更细的网格线

1
2
3
4
background: white;
background: linear-gradient(90deg, #58a 1px, transparent 0),
linear-gradient(#58a 1px, transparent 0);
background-size: 30px 30px;

也可以加重一些边框,形成层次更深的网格

1
2
3
4
5
6
7
background:  
linear-gradient(white 2px,transparent 0),
linear-gradient(90deg, white 2px,transparent 0),
linear-gradient(hsla(0,0%,100%,0.3) 1px,transparent 0),
linear-gradient(90deg,hsla(0,0%,100%,0.3) 1px,transparent 0)
#58a;
background-size: 75px 75px,75px 75px,15px 15px,15px 15px;

更复杂的背景案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
background: radial-gradient(
circle at 0% 50%,
rgba(96, 16, 48, 0) 9px,
#613 10px,
rgba(96, 16, 48, 0) 11px
)
0px 10px,
radial-gradient(
at 100% 100%,
rgba(96, 16, 48, 0) 9px,
#613 10px,
rgba(96, 16, 48, 0) 11px
),
#8a3;
background-size: 20px 20px;

波点背景

想实现这样的效果,我们从图形中切出一个小方块.用这个方块铺满整个背景

1
2
3
background:  
radial-gradient(tan 30%,transparent 0) #58a;
background-size:30px 30px;

但是现在看着还不是很饱满,我们可以生成两层图案,通过背景定位放到稍微错开的位置

1
2
3
4
5
6
background:  
radial-gradient(tan 30%,transparent 0),
radial-gradient(tan 30%,transparent 0),
#58a;
background-size:30px 30px;
background-position:0 0 ,15px 15px;

使用一个 mixin 让代码更容易维护

1
2
3
4
5
6
7
8
9
/*          单元格大小, 点的半径, 回退颜色, 点的颜色*/
@mixin polka($size, $dot, $base, $accent){
background:$base;
background-image:
radial-gradient($accent $dot,transparent 0),
radial-gradient($accent $dot,transparent 0)
background-size:$size $size;
background-position:0 0,$size/2 $size/2;
}

棋盘

期盼的效果看起来简单但实际上有一点麻烦,因为没有一种渐变能实现一个正方形中的1/4个小正方形的效果.

所以需要换一种思路,先实现正方形的两个对角,再用这两个对角,和其他正方形中的对角拼成一个小正方形

1
2
3
4
5
6
7
8
background: linear-gradient(
45deg,
#ccc 25%,
transparent 0,
transparent 75%,
#ccc 0
);
background-size: 30px 30px;

但是在一个渐变里面连续实现两个三角型,没有办法控制他们的位置进行拼接,所以需要分成两个渐变,并控制第二个渐变的背景位置

1
2
3
4
5
6
background: linear-gradient(45deg, #ccc 25%, transparent 0),
linear-gradient(45deg, transparent 75%, #ccc 75%),
linear-gradient(-45deg, #ccc 25%, transparent 0),
linear-gradient(-45deg, transparent 75%, #ccc 75%);
background-position: 0 0, -15px 15px, 0 -15px, -15px 0;
background-size: 30px 30px;

部分浏览器已经支持角向渐变,可以直接画出1/4个正方形

1
2
background: conic-gradient(red,yellow,lime,aqua,blue,fuchsia,red);
background-size: 30px 30px;

伪随机背景

如果背景是不透明的,而且是连续的,那就会每隔background-size指定的像素后就会重复一次.

所以可以考虑将背景大小设为不同的数值,并且渐变不会铺满整个背景,让他们相互覆盖,形成随机

1
2
3
4
background: linear-gradient(90deg, #fb3, 10px, transparent 0),
linear-gradient(90deg, #ab4, 20px, transparent 0),
linear-gradient(90deg, #655, 30px, transparent 0);
background-size: 40px 100%, 60px 100%, 80px 100%;

但是使用整数还是容易被察觉,每隔240px也就是各个背景大小的最小公倍数,所以这里可以把背景大小换成质数

1
2
3
4
background: linear-gradient(90deg, #fb3, 10px, transparent 0),
linear-gradient(90deg, #ab4, 20px, transparent 0),
linear-gradient(90deg, #655, 30px, transparent 0);
background-size: 41px 100%, 61px 100%, 71px 100%;

图像边框

如何实现把一张照片中间部分当作内容区域,剩下区域当作背景的效果.

最简单的想法是,通过两个元素下面的元素用上面的元素遮挡.这个方法是可行的.但是如果只用一个元素呢?

如果你想到的 background-image 那可能会有一点问题, background-image 会将背景图片按九宫格划分,在四个边上的背景会被拉伸或者重复.

其实还可以用多重背景来做, 用 background-clip 控制背景的显示区域,下面用一个夸张的样式看下现在的效果

1
2
3
4
5
6
7
width: 320px;
height: 180px;
border: 100px solid transparent;
background: linear-gradient(white, transparent), url(./cover1.jpg);
background-size: cover;
background-clip: padding-box, border-box;
background-origin: padding-box;

在边框上已经有指定的背景,但是背景没有从边框的左上角开始,这是因为 background-origin 默认是 padding-box 会从内边框的左上角开始,所以边框上的图片是重复平铺之后扩展出来的图片,下面稍微修改一下

1
2
3
4
5
6
7
width: 320px;
height: 180px;
border: 20px solid transparent;
background: linear-gradient(#fff, #fff), url(./cover1.jpg);
background-size: cover;
background-clip: padding-box, border-box;
background-origin: border-box;

下面是简化后的属性

1
2
3
4
5
6
width: 320px;
height: 180px;
border: 20px solid transparent;
background:
linear-gradient(#fff, #fff) padding-box,
url(./cover1.jpg) border-box 0 / cover;

信封效果边框

可以利用背景的渐变并应用在边框上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
width: 320px;
height: 180px;
border: 10px solid transparent;
background: linear-gradient(#fff, #fff) padding-box,
repeating-linear-gradient(
-45deg,
transparent 0,
transparent 12.5%,
red 0,
red 25%,
transparent 0,
transparent 37.5%,
#58a 0,
#58a 50%,
transparent 0
) 0 / 5em 5em;

动态虚线边框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
width: 320px;
height: 180px;
border: 1px solid transparent;
background:
linear-gradient(#fff, #fff) padding-box,
repeating-linear-gradient(
-45deg,
transparent 0,
transparent 25%,
#000 0,
#000 50%
) 0 / 0.5em 0.5em;
animation: ani 10s linear infinite;
}

@keyframes ani {
0% {
background-position:0
}
100% {
background-position:100%
}
}

Gitlab 私有化部署

最后更新

2024-08-12

官方安装包

安装必要依赖

1
2
sudo apt-get update
sudo apt-get install -y curl openssh-server ca-certificates tzdata perl

安装邮件服务,配置项选择 Internet Site,mail name 填写当前服务器 DNS.

1
sudo apt-get install -y postfix

添加仓库

1
curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | sudo bash

安装, 填写需要访问的域名

1
sudo apt-get install gitlab-ee

前置nginx 配置, 非 gitlab 内部 nginx

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
stream {
server {
listen 24922;
proxy_pass 192.168.48.227:22;
}
}

http {
server {
listen 9348 ssl;
listen [::]:9348 ssl;
http2 on;
server_name gitlab.iftrue.club gitlab.iftrue.me;

location / {
proxy_pass http://192.168.48.227;

proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_redirect off;

# Pass along essential headers
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Ssl on; #
}
}
}

修改配置文件 /etc/gitlab/gitlab.rb

1
2
3
4
5
6
7
8
9
10
11
# 修改外部访问地址,用于项目的下载地址,需要有完整的协议和端口号
external_url "https://gitlab.iftrue.me"

# 修改ssh端口,用于 ssh 克隆项目

gitlab_rails['gitlab_shell_ssh_port'] = 24922

# 保存后应用配置
sudo gitlab-ctl stop
sudo gitlab-ctl reconfigure
sudo gitlab-ctl start

如果在 Gitlab 前面有统一的反向代理,无需 Gitlab 本身处理 SSL 证书,可以将 Gitlab 的 nginx 配置为 80 端口。[文档]

1
2
3
4
5
nginx['enable'] = true
nginx['listen_port'] = 80
nginx['listen_https'] = false

sudo gitlab-ctl reconfigure

Docker 方式安装

只需要准备好证书文件,配置.yml 文件即可使用

/docker/gitlab/docker-compose.yml

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
version: '3.6'
services:
gitlab:
image: gitlab/gitlab-ee:latest
container_name: gitlab
restart: always
# 必填:访问gitlab的域名
hostname: 'gitlab.iftrue'
environment:
GITLAB_OMNIBUS_CONFIG: |
# 必填: 外部访问gitlab的地址
# 用于内部生成外部访问的链接,例如 clone地址
# 即使通过nginx代理访问gitlab, 协议也必须相同
external_url 'https://gitlab.iftrue.com'
# 首次登录时的免密
gitlab_rails['initial_root_password']='xxxx'
# ssh 端口
gitlab_rails['gitlab_shell_ssh_port'] = 24922
ports:
# 外部和内部端口必须与external_url端口相同
- '9348:9348'
- '24922:22'
volumes:
- './config:/etc/gitlab'
- './logs:/var/log/gitlab'
- './data:/var/opt/gitlab'
shm_size: '256m'
# 设置日志大小,避免磁盘写满
logging:
driver: "json-file"
options:
max-size: "50m" # 单个日志文件最大为 50MB
max-file: "5" # 最多保留 5 个日志文件

启动服务,需要等待一段时间,观察 docker 状态是否是 healthy

1
2
3
#  拉取最新镜像
docker compose pull
docker compose up -d

获取/修改 初始密码

1
docker exec -it  gitlab /bin/bash

查看初始密码,安装 gitlab 后 24 小时会自动删除

1
cat /etc/gitlab/initial_root_password

修改初始密码

1
2
3
4
5
gitlab-rails console                   # 进入命令行
u=User.where(id:1).first # 查找root用户
u.password='12345678' # 修改密码
u.password_confirmation='12345678' # 确认密码
u.save # 保存配置

nginx 配置

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
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name gitlab.iftrue.me;

location / {
# 如果 external_url 设置了 https 就要访问https地址
# 可以选择关闭强制https跳转的配置
proxy_pass https://192.168.48.213:9348;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}


# 因为社区版的 nginx 不支持 tcp 流量转发,因此下面配置无效
# 可以使用防火墙进行转发

# server {
# listen 24922 ssl;
# listen [::]:24922 ssl;
# server_name gitlab.iftrue.me;
# location / {
# proxy_pass https://192.168.48.213:24922;
# }
# }

Docker 安装 nextcloud

配置文件

/docker/nextCloud/docker-compose.yml

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
version: '3'

services:
db:
image: mariadb:10.5
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
restart: always
volumes:
- ./db:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=myPassword # db.env环境变量中的相同
env_file:
- db.env

redis:
image: redis:alpine
restart: always

app:
image: nextcloud:apache
restart: always
volumes:
- ./nextcloud:/var/www/html
environment:
- VIRTUAL_HOST=nextcloud.iftrue.me
- LETSENCRYPT_HOST=nextcloud.iftrue.me
- LETSENCRYPT_EMAIL=sunzhiqi@live.com
- MYSQL_HOST=db
- REDIS_HOST=redis
env_file:
- db.env
depends_on:
- db
- redis
networks:
- proxy-tier
- default

cron:
image: nextcloud:apache
restart: always
volumes:
- ./nextcloud:/var/www/html
entrypoint: /cron.sh
depends_on:
- db
- redis

proxy:
build: ./proxy
restart: always
ports:
- 7186:80
- 37186:443
labels:
com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: "true"
volumes:
- ./certs:/etc/nginx/certs:ro
- ./vhost.d:/etc/nginx/vhost.d
- ./html:/usr/share/nginx/html
- /var/run/docker.sock:/tmp/docker.sock:ro
networks:
- proxy-tier

letsencrypt-companion:
image: nginxproxy/acme-companion
restart: always
volumes:
- ./certs:/etc/nginx/certs
- ./acme:/etc/acme.sh
- ./vhost.d:/etc/nginx/vhost.d
- ./html:/usr/share/nginx/html
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy-tier
depends_on:
- proxy

# self signed
# omgwtfssl:
# image: paulczar/omgwtfssl
# restart: "no"
# volumes:
# - certs:/certs
# environment:
# - SSL_SUBJECT=servhostname.local
# - CA_SUBJECT=my@example.com
# - SSL_KEY=/certs/servhostname.local.key
# - SSL_CSR=/certs/servhostname.local.csr
# - SSL_CERT=/certs/servhostname.local.crt
# networks:
# - proxy-tier

volumes:
db:
nextcloud:
certs:
acme:
vhost.d:
html:

networks:
proxy-tier:

/docker/nextCloud/db.env

1
2
3
MYSQL_PASSWORD=myPassword
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud

/docker/nextCloud/proxy/Dockerfile

1
2
FROM nginxproxy/nginx-proxy:alpine
COPY uploadsize.conf /etc/nginx/conf.d/uploadsize.conf

/docker/nextCloud/proxy/uploadsize.conf

1
2
client_max_body_size 10G;
proxy_request_buffering off;

启动

1
2
cd /docker/nextCloud
docker-compose
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信