├── .gitignore
├── README.md
├── actionGif.gif
├── demo copy.md
├── demo.md
├── miniReact
├── README.md
├── index.html
├── index.js
└── mini
│ ├── createElement.js
│ ├── index.js
│ └── render.js
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── UI
│ ├── Popover
│ │ ├── README.md
│ │ ├── index.css
│ │ └── index.js
│ ├── copy
│ │ ├── example.js
│ │ ├── img
│ │ │ └── icon_12_copy@2x.png
│ │ ├── index.css
│ │ └── index.js
│ ├── drawer
│ │ ├── example.js
│ │ ├── img
│ │ │ ├── cancel.png
│ │ │ └── cancel_hover.png
│ │ ├── index.css
│ │ └── index.js
│ ├── input
│ │ ├── img
│ │ │ ├── cancel.png
│ │ │ └── clear.png
│ │ ├── index.css
│ │ └── index.js
│ ├── layout
│ │ ├── example.js
│ │ ├── index.css
│ │ └── index.js
│ └── table
│ │ ├── config.js
│ │ ├── example.js
│ │ ├── img
│ │ ├── empty@2x.png
│ │ └── right@2x.png
│ │ ├── index copy.js
│ │ ├── index.css
│ │ └── index.js
├── index.js
├── problems
│ ├── callback
│ │ └── index.js
│ ├── children
│ │ └── index.js
│ ├── cloneElement
│ │ └── index.js
│ └── memo
│ │ └── index.js
├── reportWebVitals.js
└── setupTests.js
├── useState
├── _index.js
├── index.html
└── index.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 筆記
2 |
3 | ## 前言 📝
4 |
5 | > 👉
6 | > 一、实现一个 useState
7 | > 二、react 生命周期
8 | > 三、effect 第二个参数的影响
9 |
10 | ---
11 |
12 | ## 实现一个 useState 🤖
13 |
14 | - 見 useSate
15 |
16 | ---
17 |
18 | ## react 生命周期
19 |
20 | this.setState-->reconcile 算法(diff 算法)计算出变化状态 即 render 阶段--->reactDom 渲染器讲状态变换渲染到视图 commit 阶段
21 |
22 | #### 首次 render 时:
23 |
24 | 调用 this.setData()时
25 |
26 | - render 阶段:
27 |
28 | 构建 fiber 树:
29 | 1、自上而下,深度遍历的方式创建,先儿子后兄弟的创建最后回到根节点
30 | 2、每创建完一个节点,执行 render 阶段 的方法 constructor-->getDerivedstateFromProps/componentWillMount-->render
31 |
32 | - commit 阶段:
33 |
34 | fiber 树渲染到 Dom,会从子节点开始执行生命周期函数 componentDidMount 直到根组件
35 |
36 | 调用 this.setData()时
37 |
38 | - render 阶段:
39 |
40 | 1、复用之前的节点创建一棵 fiber 树,不会执行节点的生命周期
41 | 经过 diff 算法,标记变换
42 |
43 | - commit 阶段:
44 |
45 | 执行标记点的变换,对应的视图变换 执行 componentDidUpdate、getSnapshotBeforeUpdate
46 | 新创建的 fiber 树替换之前的 fiber 树,等待下一次调用 this.setData()
47 |
48 | ---
49 |
50 | ## useEffect 第二个参数的影响
51 |
52 | - useEffect(fn)--->mount、update、
53 | - useEffect(fn,[])--->mount、
54 | - useEffect(fn,[xx])--->mount、xx 变换时、
55 |
56 | - render 阶段到 commit 阶段 传递了一条包含不同 fiber 节点的 effect 链表(update、delete、create 操作时分别进行标记)即 effectTag,
57 | - commit 阶段分为三个阶段
58 | - beforeMutation 阶段:
59 |
60 | - Mutation 阶段:
61 | - appendChild Dom 节点插入视图
62 | - layout 阶段:
63 | - 同步调用 componentDidMount
64 | - 同步调用 uselayoutEffect
65 | - useEffect 会在三个子阶段执行完成后异步的调用
66 |
67 | ---
68 |
--------------------------------------------------------------------------------
/actionGif.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blazer233/init-react/e655bffc93e159c9d912b873c324e361048d2ec6/actionGif.gif
--------------------------------------------------------------------------------
/demo copy.md:
--------------------------------------------------------------------------------
1 | ## requestIdleCallback
2 |
3 | 调度 -> 协调 -> 渲染
4 |
5 | 快速响应的制约:
6 | cpu 瓶颈和 io 瓶颈
7 |
--------------------------------------------------------------------------------
/demo.md:
--------------------------------------------------------------------------------
1 | ## requestIdleCallback
2 |
3 | 调度 -> 协调 -> 渲染
4 |
5 | 在 react 老版本
6 |
7 | 1. Reconciler 和 Renderer 是交替工作的,当 Reconciler 第一个 li 在页面上 Renderer 后,第二个 li 再进入 Reconciler。
8 | 2. 同步进行,如果更新时中断更新,此时后面的步骤都还未执行,得到更新不完全的 DOM
9 |
10 | 在 react 新版本
11 |
12 | 1. 更新工作从递归变成了可以中断的循环过程。每次循环都会调用 shouldYield 判断当前是否有剩余时间。
13 |
14 | ```javascript
15 | function workLoopConcurrent() {
16 | // Perform work until Scheduler asks us to yield
17 | while (workInProgress !== null && !shouldYield()) {
18 | workInProgress = performUnitOfWork(workInProgress);
19 | }
20 | }
21 | /*
22 | workInProgress 代表当前已创建的 workInProgress fiber。
23 |
24 | performUnitOfWork 方法会创建下一个 Fiber 节点并赋值给 workInProgress,并将 workInProgress 与已创建的 Fiber 节点连接起来构成Fiber树。
25 | */
26 | ```
27 |
28 | 2. econciler 与 Renderer 不再是交替工作。当 Scheduler 将任务交给 Reconciler 后,Reconciler 会为变化的 DOM 打上代表 增/删/更新 的标记 ,且由于性能问题只会同级进行比较,如果一个 DOM 节点在前后两次更新中跨越了层级,那么 React 不会尝试复用他。(二进制记录):
29 |
30 | ```javascript
31 | export const Placement = /* */ 0b0000000000010;
32 | export const Update = /* */ 0b0000000000100;
33 | export const PlacementAndUpdate = /* */ 0b0000000000110;
34 | export const Deletion = /* */ 0b0000000001000;
35 | ```
36 |
37 | 双缓存
38 |
39 | > 在 React 中最多会同时存在两棵 Fiber 树。
40 |
41 | - 当前屏幕上显示内容对应的 Fiber 树称为 current Fiber 树
42 | - 正在内存中构建的 Fiber 树称为 workInProgress Fiber 树。
43 |
44 | mount 时
45 |
46 | 1. 首次执行 ReactDOM.render 会创建 fiberRoot 和 rootFiber。
47 | 1. fiberRoot 是整个应用的根节点 (一个)
48 | 2. rootFiber 是所在组件树的根节点 (多个)
49 |
50 | fiberRoot.current 指向的 rootFiber
51 |
52 | 2. 接下来进入 render 阶段,根据组件返回的 JSX 在内存中依次创建 Fiber 节点并连接在一起构建 Fiber 树,被称为 workInProgress Fiber 树。
53 |
54 | 在构建 workInProgress Fiber 树时会尝试复用 current Fiber 树中已有的 Fiber 节点内的属性,在首屏渲染时只有 rootFiber 存在对应的 current fiber(即 rootFiber.alternate)首屏时只有 rootFiber 存在对应的 current fiber
55 |
56 | 3. 最后,fiberRoot 的 current 指针指向 workInProgress Fiber 树使其变为 current Fiber 树
57 |
58 | update 时
59 |
60 | 1. 会开启一次新的 render 阶段并构建一棵新的 workInProgress Fiber 树。和 mount 时一样,workInProgress fiber 的创建可以复用 current Fiber 树对应的节点数据。
61 |
62 | React 通过 ClassComponent 实例原型上的 isReactComponent 变量判断是否是 ClassComponent
63 |
64 | ## render 阶段
65 |
66 | #### “递”阶段
67 |
68 | rootFiber 开始向下深度优先遍历。为遍历到的每个 Fiber 节点调用 beginWork 方法 。
69 | 该方法会根据传入的 Fiber 节点创建子 Fiber 节点,并将这两个 Fiber 节点连接起来。
70 |
71 | ```javascript
72 | function beginWork(
73 | current: Fiber | null,
74 | workInProgress: Fiber,
75 | renderLanes: Lanes
76 | ): Fiber | null {
77 | // ...省略函数体
78 | }
79 | //current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
80 | //workInProgress:当前组件对应的Fiber节点
81 | //renderLanes:优先级相关,在讲解Scheduler时再讲解
82 | ```
83 |
84 | 通过 current === null ?来区分组件是处于 mount 还是 update
85 | 可以通过 current 是否存在来分别处理
86 |
87 | - update: current 存在,可以复用 current 节点,这样就能克隆 current.child 作为 workInProgress.child,而不需要新建 workInProgress.child。
88 |
89 | - mount:除 fiberRootNode 以外,current === null。会根据 fiber.tag 不同,进入不同类型 Fiber 的创建逻辑 如:LazyComponent、FunctionComponent、ClassComponent
90 |
91 | 进入 reconcileChildren
92 |
93 | - mount 的组件,他会创建新的子 Fiber 节点
94 | - update 的组件,他会将当前组件与该组件在上次更新时对应的 Fiber 节点进行 diff 算法比较,将比较的结果生成新 Fiber 节点,由于同级的 Fiber 节点是由 sibling 指针链接形成的单链表,即不支持双指针遍历。(区别 Vue)
95 |
96 | ```javascript
97 | function reconcileChildren(
98 | current: Fiber | null,
99 | workInProgress: Fiber,
100 | nextChildren: any,
101 | renderLanes: Lanes
102 | ) {
103 | if (current === null) {
104 | // 对于mount的组件
105 | workInProgress.child = mountChildFibers(
106 | workInProgress,
107 | null,
108 | nextChildren,
109 | renderLanes
110 | );
111 | } else {
112 | // 对于update的组件
113 | workInProgress.child = reconcileChildFibers(
114 | workInProgress,
115 | current.child,
116 | nextChildren,
117 | renderLanes
118 | );
119 | }
120 | }
121 | ```
122 |
123 | 1. 生成新的子 Fiber 节点并赋值给 workInProgress.child,作为本次 beginWork 返回值,并作为下次 performUnitOfWork 执行时 workInProgress 的传参
124 | 2. reconcileChildFibers 会为生成的 Fiber 节点带上 effectTag 属性,作为变更的标签 ,mount 时只会打上 Placement 的 teg,即插入
125 |
126 | #### “归”阶段
127 |
128 | 在“归”阶段会调用 completeWork 处理 Fiber 节点。
129 | 当某个 Fiber 节点执行完 completeWork,如果其存在兄弟 Fiber 节点(即 fiber.sibling !== null),会进入其兄弟 Fiber 的“递”阶段。
130 | 如果不存在兄弟 Fiber,会进入父级 Fiber 的“归”阶段。
131 |
132 | mount 时主要处理
133 |
134 | - 为 Fiber 节点生成对应的 DOM 节点
135 | - 将子孙 DOM 节点插入刚生成的 DOM 节点中
136 | - 与 update 逻辑中的 updateHostComponent 类似的处理 props 的过程
137 |
138 | update 时主要处理
139 |
140 | - onClick、onChange 等回调函数的注册
141 | - 处理 style prop
142 | - 处理 DANGEROUSLY_SET_INNER_HTML prop
143 | - 处理 children prop
144 |
145 | 最终会被赋值给 workInProgress.updateQueue,并最终会在 commit 阶段被渲染在页面上。
146 |
147 | ```javascript
148 | //对不同fiber.tag调用不同的处理逻辑。
149 | function completeWork(
150 | current: Fiber | null,
151 | workInProgress: Fiber,
152 | renderLanes: Lanes,
153 | ): Fiber | null {
154 | const newProps = workInProgress.pendingProps;
155 |
156 | switch (workInProgress.tag) {
157 | case IndeterminateComponent:
158 | case LazyComponent:
159 | case SimpleMemoComponent:
160 | case FunctionComponent:
161 | case ForwardRef:
162 | case Fragment:
163 | case Mode:
164 | case Profiler:
165 | case ContextConsumer:
166 | case MemoComponent:
167 | return null;
168 | case ClassComponent: {
169 | // ...省略
170 | return null;
171 | }
172 | case HostRoot: {
173 | // ...省略
174 | updateHostContainer(workInProgress);
175 | return null;
176 | }
177 | case HostComponent: {
178 | // ...省略
179 | return null;
180 | }
181 | ```
182 |
183 | 最后
184 |
185 | - 每个执行完 completeWork 且存在 effectTag 的 Fiber 节点会被保存在一条被称为 effectList 的单向链表中。
186 | - 所有有 effectTag 的 Fiber 节点都会被追加在 effectList 中,最终形成一条以 rootFiber.firstEffect 为起点的单向链表。
187 |
188 | ```javascript
189 | nextEffect nextEffect
190 | rootFiber.firstEffect -----------> fiber -----------> fiber
191 | ```
192 |
193 | ## commit 阶段
194 |
195 | #### before mutation 之前
196 |
197 | 1. 触发 useEffect 回调与其他同步任务。由于这些任务可能触发新的渲染,所以这里要一直遍历执行直到没有任务
198 | 2. 将 effectList 赋值给 firstEffect
199 | 由于每个 fiber 的 effectList 只包含他的子孙节点
200 | 所以根节点如果有 effectTag 则不会被包含进来
201 | 所以这里将有 effectTag 的根节点插入到 effectList 尾部
202 | 这样才能保证有 effect 的 fiber 都在 effectList 中
203 |
204 | #### before mutation 阶段(执行 DOM 操作前)
205 |
206 | 1. 整个过程就是遍历 effectList 并调用 commitBeforeMutationEffects 函数处理。
207 | 2. 调用 getSnapshotBeforeUpdate 生命周期钩子。
208 |
209 | 新的 react 中 render 阶段的任务可能中断/重新开始,对应的组件在 render 阶段的生命周期钩子(即 componentWillXXX)可能触发多次。
210 | 所以标记为 UNSAFE,React 提供了替代的生命周期钩子 getSnapshotBeforeUpdate(同步)。
211 |
212 | 3. 调度 useEffect。分配何时执行最终在 Layout 阶段完成后再异步执行。
213 |
214 | 1. before mutation 阶段在 scheduleCallback 中调度 flushPassiveEffects
215 | 2. layout 阶段之后将 effectList 赋值给 rootWithPendingPassiveEffects
216 | 3. scheduleCallback 触发 flushPassiveEffects,flushPassiveEffects 内部遍历 rootWithPendingPassiveEffects
217 |
218 | useEffect 异步执行的原因主要是防止同步执行时阻塞浏览器渲染
219 |
220 | #### mutation 阶段(执行 DOM 操作)
221 |
222 | 遍历 effectList,对每个 Fiber 节点执行如下三个操作:
223 |
224 | 1. 根据 ContentReset effectTag 重置文字节点
225 | 2. 更新 ref
226 | 3. 根据 effectTag 分别处理 插入 DOM、更新 DOM、删除 DOM
227 |
228 | Placement 时
229 |
230 | 1. 获取父级 DOM 节点
231 | 2. 获取 Fiber 节点的 DOM 兄弟节点
232 | 3. 根据 DOM 兄弟节点和父级节点执行插入操作。
233 |
234 | Update 时
235 |
236 | 1. 会执行 useLayoutEffect hook 销毁函数
237 |
238 | Deletion 时
239 |
240 | 1. 递归 componentWillUnmount 生命周期钩子, 从页面移除 Fiber 节点对应 DOM 节点
241 | 2. 解绑 ref
242 | 3. 调度 useEffect 的销毁函数
243 |
244 | #### layout 阶段(执行 DOM 操作后)
245 |
246 | 1. 对于 ClassComponent,他会通过 current === null?区分是 mount 还是 update,调用 componentDidMount 或 componentDidUpdate 。
247 | 2. 触发状态更新的 this.setState 如果赋值了第二个参数回调函数,也会在此时调用。
248 | 3. current Fiber 树切换
249 |
--------------------------------------------------------------------------------
/miniReact/README.md:
--------------------------------------------------------------------------------
1 | # 从 html 实现一个 react🎅
2 | ## 前言 📝
3 |
4 | > 👉 我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。[官网地址](https://react.docschina.org/)
5 |
6 | 
7 |
8 | react 的理念是在于对大型项目的`快速响应`,对于新版的 react 16.8 而言更是带来的全新的理念`fiber`去解决网页快速响应时所伴随的问题,即 CPU 的瓶颈,传统网页浏览受制于浏览器刷新率、js 执行时间过长等因素会造成页面掉帧,甚至卡顿
9 |
10 | react 由于自身的底层设计从而规避这一问题的发生,所以 react16.8 的面世对于前端领域只办三件事:快速响应、快速响应、还是 Tmd 快速响应 !,这篇文章将会从一个 html 出发,跟随 react 的 fiber 理念,仿一个非常基础的 react
11 |
12 | ---
13 |
14 | ## 一开始的准备工作 🤖
15 |
16 | ### html
17 |
18 | 我们需要一个 html 去撑起来整个页面,支撑 react 运行,页面中添加`
`,之后添加一个 script 标签,因为需要使用`import`进行模块化构建,所以需要为 script 添加 type 为`module`的属性
19 |
20 | ```
21 |
22 |
23 |
24 |
25 |
26 |
27 | Document
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | ```
37 |
38 | 推荐安装一个 `Live Server` 插件,有助于我们对代码进行调试,接下来的操作也会用到
39 |
40 | ### JavaScript
41 |
42 | 我们会仿写一个如下的 react,实现一个基础的操作,在 `` 绑定事件,将输入的值插入在 `` 标签内:
43 |
44 | ```JavaScript
45 | ...
46 | function App() {
47 | return (
48 |
49 |
50 |
Hello {value}
51 |
52 |
53 | );
54 | }
55 | ...
56 | ```
57 |
58 | 
59 |
60 | 在 react 进行 babel 编译的时候,会将 `JSX` 语法转化为 `React.createElement()` 的形式,如上被 retuen 的代码就会被转换成
61 |
62 | ```JavaScript
63 | ...
64 | React.createElement(
65 | "div",
66 | null,
67 | React.createElement("input", {
68 | onInput: updateValue,
69 | value: value,
70 | }),
71 | React.createElement("h2", null, "Hello ", value),
72 | React.createElement("hr", null)
73 | );
74 | ...
75 | ```
76 |
77 | > [在线地址](https://www.babeljs.cn/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.6&spec=false&loose=false&code_lz=Q&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=env%2Creact%2Cstage-2&prettier=false&targets=&version=7.14.7&externalPlugins=)
78 |
79 | 从转换后的代码我们可以看出 React.createElement 支持多个参数:
80 |
81 | 1. type,节点类型
82 |
83 | 2. config, 节点上的属性,比如 id 和 href
84 |
85 | 3. children, 子元素了,子元素可以有多个,类型可以是简单的文本,也可以还是 React.createElement,如果是 React.createElement,其实就是子节点了,子节点下面还可以有子节点。这样就用 React.createElement 的嵌套关系实现了 HTML 节点的树形结构。
86 |
87 | 我们可以按照 `React.createElement` 的形式仿写一个可以实现同样功能的 `createElement` 将 jsx 通过一种简单的数据结构展示出来即 `虚拟DOM` 这样在更新时,新旧节点的对比也可以转化为虚拟 DOM 的对比
88 |
89 | ```JavaScript
90 | {
91 | type:'节点标签',
92 | props:{
93 | props:'节点上的属性,包括事件、类...',
94 | children:'节点的子节点'
95 | }
96 | }
97 | ```
98 |
99 | 这里我们可以写一个函数实现下列需求
100 |
101 | - 原则是将所有的参数返回到一个对象上
102 | - children 也要放到 props 里面去,这样我们在组件里面就能通过 props.children 拿到子元素
103 | - 当子组件是文本节点时,通过构造一种 type 为 `TEXT_ELEMENT` 的节点类型表示
104 |
105 | ```JavaScript
106 | /**
107 | * 创建虚拟 DOM 结构
108 | * @param {type} 标签名
109 | * @param {props} 属性对象
110 | * @param {children} 子节点
111 | * @return {element} 虚拟 DOM
112 | */
113 | const createElement = (type, props, ...children) => ({
114 | type,
115 | props: {
116 | ...props,
117 | children: children.map(child =>
118 | typeof child === "object"
119 | ? child
120 | : {
121 | type: "TEXT_ELEMENT",
122 | props: {
123 | nodeValue: child,
124 | children: [],
125 | },
126 | }
127 | ),
128 | },
129 | });
130 | ```
131 |
132 | > [react 中 createElement 源码实现](https://github.com/facebook/react/blob/60016c448bb7d19fc989acd05dda5aca2e124381/packages/react/src/ReactElement.js#L348)
133 |
134 | 实现 `createElement` 之后我们可以拿到虚拟 DOM,但是还需要 `render` 将代码渲染到页面,此时我们需要对 `index.js` 进行处理,添加输入事件,将 `createElement` 和 `render` 通过 import 进行引入,render 时传入被编译后的虚拟 DOM 和页面的根元素 `root`, 最后再进行`executeRender`调用,页面被渲染,在页面更新的时候再次调用`executeRender`进行更新渲染
135 |
136 | ```JavaScript
137 | import {createElement,render} from "./mini/index.js";
138 | const updateValue = e => executeRender(e.target.value);
139 | const executeRender = (value = "World") => {
140 | const element = createElement(
141 | "div",
142 | null,
143 | createElement("input", {
144 | onInput: updateValue,
145 | value: value,
146 | }),
147 | createElement("h2", null, "Hello ", value),
148 | createElement("hr", null)
149 | );
150 | render(element, document.getElementById("root"));
151 | };
152 |
153 | executeRender();
154 | ```
155 |
156 | ## render 的时候做了什么 🥔
157 |
158 | ### before 版本
159 |
160 | `render` 函数帮助我们将 element 添加至真实节点中,首先它接受两个参数:
161 |
162 | > 1. 根组件,其实是一个 JSX 组件,也就是一个 createElement 返回的虚拟 DOM
163 | > 1. 父节点,也就是我们要将这个虚拟 DOM 渲染的位置
164 |
165 | 在 react 16.8 之前,渲染的方法是通过一下几步进行的
166 |
167 | 1. 创建 element.type 类型的 dom 节点,并添加到 root 元素下(文本节点特殊处理)
168 | 2. 将 element 的 props 添加到对应的 DOM 上,事件进行特殊处理,挂载到 document 上(react17 调整为挂在到 container 上)
169 | 3. 将 element.children 循环添加至 dom 节点中;
170 |
171 | 拿到虚拟 dom 进行如上三步的递归调用,渲染出页面 类似于如下流程
172 |
173 | ```javascript
174 | /**
175 | * 将虚拟 DOM 添加至真实 DOM
176 | * @param {element} 虚拟 DOM
177 | * @param {container} 真实 DOM
178 | */
179 | const render = (element, container) => {
180 | let dom;
181 | /*
182 | 处理节点(包括文本节点)
183 | */
184 | if (typeof element !== "object") {
185 | dom = document.createTextNode(element);
186 | } else {
187 | dom = document.createElement(element.type);
188 | }
189 | /*
190 | 处理属性(包括事件属性)
191 | */
192 | if (element.props) {
193 | Object.keys(element.props)
194 | .filter((key) => key != "children")
195 | .forEach((item) => {
196 | dom[item] = element.props[item];
197 | });
198 | Object.keys(element.props)
199 | .filter((key) => key.startsWith("on"))
200 | .forEach((name) => {
201 | const eventType = name.toLowerCase().substring(2);
202 | dom.addEventListener(eventType, nextProps[name]);
203 | });
204 | }
205 | if (
206 | element.props &&
207 | element.props.children &&
208 | element.props.children.length
209 | ) {
210 | /*
211 | 循环添加到dom
212 | */
213 | element.props.children.forEach((child) => render(child, dom));
214 | }
215 | container.appendChild(dom);
216 | };
217 | ```
218 |
219 | ### after 版本(fiber)
220 |
221 | 当我们写完如上的代码,会发现这个递归调用是有问题的
222 |
223 | 如上这部分工作被 React 官方称为 renderer,renderer 是第三方可以自己实现的一个模块,还有个核心模块叫做 reconsiler,reconsiler 的一大功能就是 diff 算法,他会计算出应该更新哪些页面节点,然后将需要更新的节点虚拟 DOM 传递给 renderer,renderer 负责将这些节点渲染到页面上,但是但是他却是同步的,一旦开始渲染,就会将所有节点及其子节点全部渲染完成这个进程才会结束。
224 |
225 | React 的官方演讲中有个例子,可以很明显的看到这种同步计算造成的卡顿:
226 |
227 | 
228 |
229 | 当 dom tree 很大的情况下,JS 线程的运行时间可能会比较长,在这段时间浏览器是不会响应其他事件的,因为 JS 线程和 GUI 线程是互斥的,JS 运行时页面就不会响应,这个时间太长了,用户就可能看到卡顿,
230 |
231 | 此时我们可以分为两步解决这个问题
232 |
233 | - 允许中断渲染工作,如果有优先级更高的工作插入,则暂时中断浏览器渲染,待完成该工作后,恢复浏览器渲染;
234 | - 将渲染工作进行分解,分解成一个个小单元;
235 |
236 | #### solution I 引入一个新的 Api
237 |
238 | requestIdleCallback 接收一个回调,这个回调会在浏览器空闲时调用,每次调用会传入一个 IdleDeadline,可以拿到当前还空余多久, options 可以传入参数最多等多久,等到了时间浏览器还不空就强制执行了。
239 |
240 | > [window.requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback) 将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件
241 | >
242 | > > 但是这个 API 还在实验中,兼容性不好,所以 React 官方自己实现了一套。本文会继续使用 requestIdleCallback 来进行任务调度
243 |
244 | ```JavaScript
245 | // 下一个工作单元
246 | let nextUnitOfWork = null
247 | /**
248 | * workLoop 工作循环函数
249 | * @param {deadline} 截止时间
250 | */
251 | function workLoop(deadline) {
252 | // 是否应该停止工作循环函数
253 | let shouldYield = false
254 |
255 | // 如果存在下一个工作单元,且没有优先级更高的其他工作时,循环执行
256 | while (nextUnitOfWork && !shouldYield) {
257 | nextUnitOfWork = performUnitOfWork(
258 | nextUnitOfWork
259 | )
260 |
261 | // 如果截止时间快到了,停止工作循环函数
262 | shouldYield = deadline.timeRemaining() < 1
263 | }
264 |
265 | // 通知浏览器,空闲时间应该执行 workLoop
266 | requestIdleCallback(workLoop)
267 | }
268 | // 通知浏览器,空闲时间应该执行 workLoop
269 | requestIdleCallback(workLoop)
270 |
271 | // 执行单元事件,并返回下一个单元事件
272 | function performUnitOfWork(nextUnitOfWork) {
273 | // TODO
274 | }
275 |
276 | ```
277 |
278 | #### solution II 创建 fiber 的数据结构
279 |
280 | Fiber 之前的数据结构是一棵树,父节点的 children 指向了子节点,但是只有这一个指针是不能实现中断继续的。比如我现在有一个父节点 A,A 有三个子节点 B,C,D,当我遍历到 C 的时候中断了,重新开始的时候,其实我是不知道 C 下面该执行哪个的,因为只知道 C,并没有指针指向他的父节点,也没有指针指向他的兄弟。
281 |
282 | Fiber 就是改造了这样一个结构,加上了指向父节点和兄弟节点的指针:
283 |
284 | - child 指向子组件
285 | - sibling 指向兄弟组件
286 | - return 指向父组件
287 |
288 | 
289 |
290 | 每个 fiber 都有一个链接指向它的第一个子节点、下一个兄弟节点和它的父节点。这种数据结构可以让我们更方便的查找下一个工作单元,假定 `A` 是挂在 root 上的节点 fiber 的渲染顺序也如下步骤
291 |
292 | 1. 从 root 开始,找到第一个子节点 A;
293 | 2. 找到 A 的第一个子节点 B
294 | 3. 找到 B 的第一个子节点 E
295 | 4. 找 E 的第一个子节点,如无子节点,则找下一个兄弟节点,找到 E 的兄弟节点 F
296 | 5. 找 F 的第一个子节点,如无子节点,也无兄弟节点,则找它的父节点的下一个兄弟节点,找到 F 的 父节点的兄弟节点 C;
297 | 6. 找 C 的第一个子节点,找不到,找兄弟节点,D
298 | 7. 找 D 的第一个子节点,G
299 | 8. 找 G 的第一个子节点,找不到,找兄弟节点,找不到,找父节点 D 的兄弟节点,也找不到,继续找 D 的父节点的兄弟节点,找到 root;
300 | 9. 上一步已经找到了 root 节点,渲染已全部完成。
301 |
302 | 我们通过这个数据结构实现一个 fiber
303 |
304 | ```JavaScript
305 | //创建最初的根fiber
306 | wipRoot = {
307 | dom: container,
308 | props: { children: [element] },
309 | };
310 | performUnitOfWork(wipRoot);
311 | ```
312 |
313 | 随后调用`performUnitOfWork`自上而下构造整个 fiber 树
314 |
315 | ```JavaScript
316 | /**
317 | * performUnitOfWork用来执行任务
318 | * @param {fiber} 我们的当前fiber任务
319 | * @return {fiber} 下一个任务fiber任务
320 | */
321 | const performUnitOfWork = fiber => {
322 | if (!fiber.dom) fiber.dom = createDom(fiber); // 创建一个DOM挂载上去
323 | const elements = fiber.props.children; //当前元素下的所有同级节点
324 | // 如果有父节点,将当前节点挂载到父节点上
325 | if (fiber.return) {
326 | fiber.return.dom.appendChild(fiber.dom);
327 | }
328 |
329 | let prevSibling = null;
330 | /*
331 | 之后代码中我们将把此处的逻辑进行抽离
332 | */
333 | if (elements && elements.length) {
334 | elements.forEach((element, index) => {
335 | const newFiber = {
336 | type: element.type,
337 | props: element.props,
338 | return: fiber,
339 | dom: null,
340 | };
341 | // 父级的child指向第一个子元素
342 | if (index === 0) {
343 | fiber.child = newFiber;
344 | } else {
345 | // 每个子元素拥有指向下一个子元素的指针
346 | prevSibling.sibling = newFiber;
347 | }
348 | prevSibling = fiber;
349 | });
350 | }
351 | // 先找子元素,没有子元素了就找兄弟元素
352 | // 兄弟元素也没有了就返回父元素
353 | // 最后到根节点结束
354 | // 这个遍历的顺序是从上到下,从左到右
355 | if (fiber.child) {
356 | return fiber.child;
357 | } else {
358 | let nextFiber = fiber;
359 | while (nextFiber) {
360 | if (nextFiber.sibling) {
361 | return nextFiber.sibling;
362 | }
363 | nextFiber = nextFiber.return;
364 | }
365 | }
366 | }
367 | ```
368 |
369 | ### after 版本(reconcile)
370 |
371 | #### currentRoot
372 |
373 | reconcile 其实就是虚拟 DOM 树的 diff 操作,将更新前的 fiber tree 和更新后的 fiber tree 进行比较,得到比较结果后,仅对有变化的 fiber 对应的 dom 节点进行更新。
374 |
375 | - 删除不需要的节点
376 | - 更新修改过的节点
377 | - 添加新的节点
378 |
379 | 新增 currentRoot 变量,保存根节点更新前的 fiber tree,为 fiber 新增 alternate 属性,保存 fiber 更新前的 fiber tree
380 |
381 | ```JavaScript
382 | let currentRoot = null
383 | function render (element, container) {
384 | wipRoot = {
385 | // 省略
386 | alternate: currentRoot
387 | }
388 | }
389 | function commitRoot () {
390 | commitWork(wipRoot.child)
391 | /*
392 | 更改fiber树的指向,将缓存中的fiber树替换到页面中的fiber tree
393 | */
394 | currentRoot = wipRoot
395 | wipRoot = null
396 | }
397 |
398 | ```
399 |
400 | 1. 如果新老节点类型一样,复用老节点 DOM,更新 props
401 |
402 | 2. 如果类型不一样,而且新的节点存在,创建新节点替换老节点
403 |
404 | 3. 如果类型不一样,没有新节点,有老节点,删除老节点
405 |
406 | #### reconcileChildren
407 |
408 | 1. 将 performUnitOfWork 中关于新建 fiber 的逻辑,抽离到 reconcileChildren 函数
409 | 2. 在 reconcileChildren 中对比新旧 fiber;
410 |
411 | 在对比 fiber tree 时
412 |
413 | - 当新旧 fiber 类型相同时 保留 dom,`仅更新 props,设置 effectTag 为 UPDATE`;
414 | - 当新旧 fiber 类型不同,且有新元素时 `创建一个新的 dom 节点,设置 effectTag 为 PLACEMENT`;
415 | - 当新旧 fiber 类型不同,且有旧 fiber 时 `删除旧 fiber,设置 effectTag 为 DELETION`
416 |
417 | ```JavaScript
418 | /**
419 | * 协调子节点
420 | * @param {fiber} fiber
421 | * @param {elements} fiber 的 子节点
422 | */
423 | function reconcileChildren(wipFiber, elements) {
424 | let index = 0;// 用于统计子节点的索引值
425 | let oldFiber = wipFiber.alternate && wipFiber.alternate.child; //更新时才会产生
426 | let prevSibling;// 上一个兄弟节点
427 | while (index < elements.length || oldFiber) {
428 | /**
429 | * 遍历子节点
430 | * oldFiber判断是更新触发还是首次触发,更新触发时为元素下所有节点
431 | */
432 | let newFiber;
433 | const element = elements[index];
434 | const sameType = oldFiber && element && element.type == oldFiber.type; // fiber 类型是否相同点
435 | /**
436 | * 更新时
437 | * 同标签不同属性,更新属性
438 | */
439 | if (sameType) {
440 | newFiber = {
441 | type: oldFiber.type,
442 | props: element.props, //只更新属性
443 | dom: oldFiber.dom,
444 | parent: wipFiber,
445 | alternate: oldFiber,
446 | effectTag: "UPDATE",
447 | };
448 | }
449 | /**
450 | * 不同标签,即替换了标签 or 创建新标签
451 | */
452 | if (element && !sameType) {
453 | newFiber = {
454 | type: element.type,
455 | props: element.props,
456 | dom: null,
457 | parent: wipFiber,
458 | alternate: null,
459 | effectTag: "PLACEMENT",
460 | };
461 | }
462 | /**
463 | * 节点被删除了
464 | */
465 | if (oldFiber && !sameType) {
466 | oldFiber.effectTag = "DELETION";
467 | deletions.push(oldFiber);
468 | }
469 |
470 | if (oldFiber) oldFiber = oldFiber.sibling;
471 | // 父级的child指向第一个子元素
472 | if (index === 0) {
473 | // fiber的第一个子节点是它的子节点
474 | wipFiber.child = newFiber;
475 | } else {
476 | // fiber 的其他子节点,是它第一个子节点的兄弟节点
477 | prevSibling.sibling = newFiber;
478 | }
479 | // 把新建的 newFiber 赋值给 prevSibling,这样就方便为 newFiber 添加兄弟节点了
480 | prevSibling = newFiber;
481 | // 索引值 + 1
482 | index++;
483 | }
484 | }
485 | ```
486 |
487 | 在 commit 时,根据 fiber 节点上`effectTag`的属性执行不同的渲染操作
488 |
489 | ### after 版本(commit)
490 |
491 | 在 commitWork 中对 fiber 的 effectTag 进行判断,处理真正的 DOM 操作。
492 |
493 | 1. 当 fiber 的 effectTag 为 PLACEMENT 时,表示是新增 fiber,将该节点新增至父节点中。
494 | 2. 当 fiber 的 effectTag 为 DELETION 时,表示是删除 fiber,将父节点的该节点删除。
495 | 3. 当 fiber 的 effectTag 为 UPDATE 时,表示是更新 fiber,更新 props 属性。
496 |
497 | ```JavaScript
498 | /**
499 | * @param {fiber} fiber 结构的虚拟dom
500 | */
501 | function commitWork(fiber) {
502 | if (!fiber) return;
503 | const domParent = fiber.parent.dom;
504 | if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
505 | domParent.appendChild(fiber.dom);
506 | } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
507 | updateDom(fiber.dom, fiber.alternate.props, fiber.props);
508 | } else if (fiber.effectTag === "DELETION") {
509 | domParent.removeChild(fiber.dom);
510 | }
511 |
512 | // 递归操作子元素和兄弟元素
513 | commitWork(fiber.child);
514 | commitWork(fiber.sibling);
515 | }
516 | ```
517 |
518 | 此时我们着重来看`updateDom`发生了什么,我们拿到 dom 上被改变的新旧属性,进行操作
519 |
520 | ```JavaScript
521 | /*
522 | isEvent :拿到事件属性
523 | isProperty :拿到非节点、非事件属性
524 | isNew :拿到前后改变的属性
525 | */
526 | const isEvent = key => key.startsWith("on");
527 | const isProperty = key => key !== "children" && !isEvent(key);
528 | const isNew = (prev, next) => key => prev[key] !== next[key];
529 |
530 |
531 | /**
532 | * 更新dom属性
533 | * @param {dom} fiber dom
534 | * @param {prevProps} fiber dom上旧的属性
535 | * @param {nextProps} fiber dom上新的属性
536 | */
537 | function updateDom(dom, prevProps, nextProps) {
538 | /**
539 | * 便利旧属性
540 | * 1、拿到on开头的事件属性
541 | * 2、拿到被删除的事件
542 | * 3、已删除的事件取消监听
543 | */
544 | Object.keys(prevProps)
545 | .filter(isEvent)
546 | .filter(key => !(key in nextProps))
547 | .forEach(name => {
548 | const eventType = name.toLowerCase().substring(2);
549 | dom.removeEventListener(eventType, prevProps[name]);
550 | });
551 |
552 | /**
553 | * 便利旧属性
554 | * 1、拿到非事件属性和非子节点的属性
555 | * 2、拿到被删除的属性
556 | * 3、删除属性
557 | */
558 | Object.keys(prevProps)
559 | .filter(isProperty)
560 | .filter(key => !(key in nextProps))
561 | .forEach(key => delete dom[key]);
562 |
563 | /**
564 | * 便利新属性
565 | * 1、拿到非事件属性和非子节点的属性
566 | * 2、拿到前后改变的属性
567 | * 3、添加属性
568 | */
569 | Object.keys(nextProps)
570 | .filter(isProperty)
571 | .filter(isNew(prevProps, nextProps))
572 | .forEach(name => {
573 | dom[name] = nextProps[name];
574 | });
575 |
576 | /**
577 | * 便利新属性
578 | * 1、拿到on开头的事件属性
579 | * 2、拿到前后改变的事件属性
580 | * 3、为新增的事件属性添加监听
581 | */
582 | Object.keys(nextProps)
583 | .filter(isEvent)
584 | .filter(isNew(prevProps, nextProps))
585 | .forEach(name => {
586 | const eventType = name.toLowerCase().substring(2);
587 | dom.addEventListener(eventType, nextProps[name]);
588 | });
589 | }
590 | ```
591 |
592 | 完成了一系列对 dom 的操作,我们将新改变的 dom 渲染到页面,当 input 事件执行时,页面又会进行渲染,但此时会进入更新 fiber 树的逻辑,
593 | alternate 指向之前的 fiber 节点进行复用,更快的执行 Update 操作,如图:
594 |
595 | 
596 |
597 | 大功告成!
598 |
599 | 完整代码可以看我[github](https://github.com/blazer233/init-react/tree/main/miniReact)。
600 |
601 | ## 结论与总结 💢
602 |
603 | 结论
604 |
605 | - 我们写的 JSX 代码被 babel 转化成了 React.createElement。
606 | - React.createElement 返回的其实就是虚拟 DOM 结构。
607 | - 虚拟 DOM 的调和和渲染可以简单粗暴的递归,但是这个过程是同步的,如果需要处理的节点过多,可能会阻塞用户输入和动画播放,造成卡顿。
608 | - Fiber 是 16.x 引入的新特性,用处是将同步的调和变成异步的。
609 | - Fiber 改造了虚拟 DOM 的结构,具有 父->第一个子, 子->兄, 子->父这几个指针,有了这几个指针,可以从任意一个 Fiber 节点找到其他节点。
610 | - Fiber 将整棵树的同步任务拆分成了每个节点可以单独执行的异步执行结构。
611 | - Fiber 可以从任意一个节点开始遍历,遍历是深度优先遍历,顺序是 父->子->兄->父,也就是从上往下,从左往右。
612 | - Fiber 的调和阶段可以是异步的小任务,但是提交阶段( commit)必须是同步的。因为异步的 commit 可能让用户看到节点一个一个接连出现,体验不好。
613 |
614 | 总结
615 |
616 | - react hook 实现 ✖
617 | - react 合成事件 ✖
618 | - 还有很多没有实现 😤...
619 |
620 | 至此,谢谢各位在百忙之中点开这篇文章,希望对你们能有所帮助,如有问题欢迎各位大佬指正。工作原因这篇文章大概断断续续写了有一个月,工作上在忙一个基于 `腾讯云TRTC`+`websocket` 的小程序电话功能,有时间也会写成文章分享一下,当然 react 的实现文章也会继续
621 |
622 | 👋:[跳转 github](https://github.com/blazer233/init-react/tree/main/miniReact) 欢迎给个 star,谢谢大家了
623 |
624 | 参考文献
625 |
626 | - 🍑:[手写系列-实现一个铂金段位的 React](https://juejin.cn/post/6978654109893132318?utm_source=gold_browser_extension#heading-14)
627 | - 🍑:[build-your-own-react(强烈推荐)](https://pomb.us/build-your-own-react/)
628 | - 🍑:[手写 React 的 Fiber 架构,深入理解其原理](https://mp.weixin.qq.com/s/wGSUdQJxOiyPTRbrBBs1Zg)
629 | - 🍑:[手写一个简单的 React](https://jelly.jd.com/article/60aceb6b27393b0169c85231#)
630 | - 🍑:[妙味课堂大圣老师 手写 react 的 fiber 和 hooks 架构](https://study.miaov.com/v_show/4227)
631 | - 🍑:[React Fiber 架构](https://zhuanlan.zhihu.com/p/37095662)
632 | - 🍑:[手写一个简单的 React](https://jelly.jd.com/article/60aceb6b27393b0169c85231#)
633 |
--------------------------------------------------------------------------------
/miniReact/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/miniReact/index.js:
--------------------------------------------------------------------------------
1 | import React from "./mini/index.js";
2 | const updateValue = (e) => rerender(e.target.value);
3 | const rerender = (value = "World") => {
4 | const element = React.createElement(
5 | "div",
6 | null,
7 | React.createElement("input", {
8 | onInput: updateValue,
9 | value: value,
10 | }),
11 | React.createElement("h2", null, "Hello ", value),
12 | React.createElement("hr", null)
13 | );
14 | React.render(element, document.getElementById("root"));
15 | };
16 |
17 | rerender();
18 |
--------------------------------------------------------------------------------
/miniReact/mini/createElement.js:
--------------------------------------------------------------------------------
1 | // 核心逻辑不复杂,将参数都塞到一个对象上返回就行
2 | // children也要放到props里面去,这样我们在组件里面就能通过this.props.children拿到子元素
3 | const createElement = (type, props, ...children) => ({
4 | type,
5 | props: {
6 | ...props,
7 | children: children.map(child =>
8 | typeof child === "object"
9 | ? child
10 | : {
11 | type: "TEXT_ELEMENT",
12 | props: {
13 | nodeValue: child,
14 | children: [],
15 | },
16 | }
17 | ),
18 | },
19 | });
20 |
21 | export default createElement;
22 |
--------------------------------------------------------------------------------
/miniReact/mini/index.js:
--------------------------------------------------------------------------------
1 | import createElement from "./createElement.js";
2 | import render from "./render.js";
3 | export default {
4 | createElement,
5 | render,
6 | };
7 |
8 |
--------------------------------------------------------------------------------
/miniReact/mini/render.js:
--------------------------------------------------------------------------------
1 | let nextUnitOfWork = null;
2 | let currentRoot = null;
3 | let wipRoot = null;
4 | let deletions = null;
5 |
6 | function render(element, container) {
7 | //渲染开始的入口
8 | wipRoot = {
9 | dom: container,
10 | props: { children: [element] },
11 | alternate: currentRoot,
12 | };
13 | deletions = [];
14 | nextUnitOfWork = wipRoot; //fiber
15 | }
16 |
17 | requestIdleCallback(workLoop);
18 | /**
19 | * 被requestIdleCallback调用时会把IdleDeadline传入函数作为参数
20 | * 它提供了一个方法, 可以让你判断浏览器还剩余多少闲置时间可以用来执行耗时任务
21 | * 即timeRemaining(),由于fiber是以链表的形式,nextUnitOfWork代表自上而下的每一个节点
22 | * 的fiber,即保存了type、child、effectTag等属性,当为 mount时 alternate 为空,
23 | * 即没有可复用的元素,在 deadline 剩余时间还充裕时 nextUnitOfWork逐级互相交换,当 deadline
24 | * 的剩余时间小于1时,且元素深度便利结束后 跳出循环 进行渲染,当空闲时再次进行调度
25 | *
26 | * 当没有下一个 工作单元时 进行渲染操作
27 | */
28 | function workLoop(deadline) {
29 | let shouldYield = false;
30 | while (nextUnitOfWork && !shouldYield) {
31 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
32 | shouldYield = deadline.timeRemaining() < 1;
33 | }
34 | if (!nextUnitOfWork && wipRoot) {
35 | commitRoot();
36 | }
37 | requestIdleCallback(workLoop);
38 | }
39 |
40 | function performUnitOfWork(fiber) {
41 | /**
42 | * 第一次fiber为div#root节点,props.children只有一个为 div 节点
43 | * 第二次fiber为div节点,props.children节点有三个分别为input、h1、hr
44 | * ...
45 | */
46 | if (!fiber.dom) fiber.dom = createDom(fiber); //根据type创建dom节点
47 | const elements = fiber.props.children; //当前元素下的所有同级节点
48 | /**
49 | * 计算是否变化给 fiber 打上 effectTag
50 | * 遍历如果有子节点则返回,进行下一次遍历
51 | * 遍历如果没有子节点,fiber 指向其父节点寻找兄弟节点进行返回,进行下一次遍历
52 | *
53 | // 这个函数的返回值是下一个任务,这其实是一个深度优先遍历
54 | // 先找子元素,没有子元素了就找兄弟元素
55 | // 兄弟元素也没有了就返回父元素
56 | // 然后再找这个父元素的兄弟元素
57 | // 最后到根节点结束
58 | // 这个遍历的顺序其实就是从上到下,从左到右
59 | */
60 | reconcileChildren(fiber, elements);
61 | if (fiber.child) {
62 | return fiber.child;
63 | } else {
64 | let nextFiber = fiber;
65 | while (nextFiber) {
66 | if (nextFiber.sibling) {
67 | return nextFiber.sibling;
68 | }
69 | nextFiber = nextFiber.parent;
70 | }
71 | }
72 | }
73 | function reconcileChildren(wipFiber, elements) {
74 | // reconcile 节点
75 | let index = 0;
76 | let oldFiber = wipFiber.alternate && wipFiber.alternate.child; //更新时才会产生
77 | let prevSibling;
78 | console.error("-----------ele", wipFiber, elements);
79 | while (index < elements.length || oldFiber) {
80 | /**
81 | * 每次 while 拿到的是同一层的元素
82 | * elements 即 child 为数组,
83 | * oldFiber判断是更新触发还是首次触发,更新触发时为元素下所有节点
84 | */
85 | let newFiber;
86 | const element = elements[index];
87 | const sameType = oldFiber && element && element.type == oldFiber.type; //同一个标签节点
88 | console.log(`更新节点是否相同:${sameType}`, oldFiber, element);
89 | /**
90 | * 更新时
91 | * 同标签不同属性,更新属性
92 | */
93 | if (sameType) {
94 | newFiber = {
95 | type: oldFiber.type,
96 | props: element.props, //只更新属性
97 | dom: oldFiber.dom,
98 | parent: wipFiber,
99 | alternate: oldFiber,
100 | effectTag: "UPDATE",
101 | };
102 | }
103 | /**
104 | * 不同标签,即替换了标签 or 创建新标签
105 | */
106 | if (element && !sameType) {
107 | newFiber = {
108 | type: element.type,
109 | props: element.props,
110 | dom: null,
111 | parent: wipFiber,
112 | alternate: null,
113 | effectTag: "PLACEMENT",
114 | };
115 | }
116 | /**
117 | * 标签不存在了
118 | */
119 | if (oldFiber && !sameType) {
120 | oldFiber.effectTag = "DELETION";
121 | deletions.push(oldFiber);
122 | }
123 |
124 | if (oldFiber) oldFiber = oldFiber.sibling;
125 | // 父级的child指向第一个子元素
126 | if (index === 0) {
127 | wipFiber.child = newFiber;
128 | } else {
129 | // 每个子元素拥有指向下一个子元素的指针
130 | prevSibling.sibling = newFiber;
131 | }
132 |
133 | prevSibling = newFiber;
134 | index++;
135 | }
136 | }
137 | function createDom(fiber) {
138 | //创建dom
139 | const dom =
140 | fiber.type == "TEXT_ELEMENT"
141 | ? document.createTextNode("")
142 | : document.createElement(fiber.type);
143 | /**
144 | * 在 createTextElement 时,元素节点type为其标签
145 | * 文本节点type为"TEXT_ELEMENT",第一次updateDom时
146 | * 全为添加属性,故 prevProps 置为空对象
147 | */
148 | updateDom(dom, {}, fiber.props);
149 | return dom;
150 | }
151 | const isEvent = key => key.startsWith("on");
152 | const isProperty = key => key !== "children" && !isEvent(key);
153 | const isNew = (prev, next) => key => prev[key] !== next[key];
154 |
155 | function updateDom(dom, prevProps, nextProps) {
156 | //更新节点属性
157 | /**
158 | * 1、拿到on开头的事件属性
159 | * 2、拿到被删除的事件
160 | * 3、已删除的事件取消监听
161 | */
162 | Object.keys(prevProps)
163 | .filter(isEvent)
164 | .filter(key => !(key in nextProps))
165 | .forEach(name => {
166 | const eventType = name.toLowerCase().substring(2);
167 | dom.removeEventListener(eventType, prevProps[name]);
168 | });
169 |
170 | /**
171 | * 便利旧属性
172 | * 1、拿到非事件属性和非子节点的属性
173 | * 2、拿到被删除的属性
174 | * 3、删除属性
175 | */
176 | Object.keys(prevProps)
177 | .filter(isProperty)
178 | .filter(key => !(key in nextProps))
179 | .forEach(key => delete dom[key]);
180 |
181 | /**
182 | * 便利新属性
183 | * 1、拿到非事件属性和非子节点的属性
184 | * 2、拿到前后改变的属性
185 | * 3、添加属性
186 | */
187 | Object.keys(nextProps)
188 | .filter(isProperty)
189 | .filter(isNew(prevProps, nextProps))
190 | .forEach(name => {
191 | dom[name] = nextProps[name];
192 | });
193 |
194 | /**
195 | * 1、拿到on开头的事件属性
196 | * 2、拿到前后改变的事件属性
197 | * 3、为新增的事件属性添加监听
198 | */
199 | Object.keys(nextProps)
200 | .filter(isEvent)
201 | .filter(isNew(prevProps, nextProps))
202 | .forEach(name => {
203 | const eventType = name.toLowerCase().substring(2);
204 | dom.addEventListener(eventType, nextProps[name]);
205 | });
206 | }
207 | /**
208 | * 拿到 fiber 树根据EffectTag的值进行不同操作的渲染
209 | * 最终'双缓存'中页面的树指向重新渲染完成的树,
210 | * 内存的树清空,等待下一次更新
211 | */
212 | function commitRoot() {
213 | //commit阶段
214 | deletions.forEach(commitWork);
215 | commitWork(wipRoot.child);
216 | currentRoot = wipRoot;
217 | wipRoot = null;
218 | }
219 |
220 | function commitWork(fiber) {
221 | //操作真实dom
222 | if (!fiber) return;
223 | const domParent = fiber.parent.dom;
224 | if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
225 | domParent.appendChild(fiber.dom);
226 | } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
227 | updateDom(fiber.dom, fiber.alternate.props, fiber.props);
228 | } else if (fiber.effectTag === "DELETION") {
229 | domParent.removeChild(fiber.dom);
230 | }
231 | commitWork(fiber.child);
232 | commitWork(fiber.sibling);
233 | }
234 | export default render;
235 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "classnames": "^2.3.1",
10 | "lodash": "^4.17.21",
11 | "node-sass": "4.14.1",
12 | "prop-types": "^15.7.2",
13 | "react": "^17.0.2",
14 | "react-dom": "^17.0.2",
15 | "react-scripts": "4.0.3",
16 | "web-vitals": "^1.0.1"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject"
23 | },
24 | "eslintConfig": {
25 | "extends": [
26 | "react-app",
27 | "react-app/jest"
28 | ]
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blazer233/init-react/e655bffc93e159c9d912b873c324e361048d2ec6/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blazer233/init-react/e655bffc93e159c9d912b873c324e361048d2ec6/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blazer233/init-react/e655bffc93e159c9d912b873c324e361048d2ec6/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/UI/Popover/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/UI/Popover/index.css:
--------------------------------------------------------------------------------
1 | .sdw-popover__wrap {
2 | position: absolute;
3 | background: black;
4 | opacity: 0.6;
5 | color: rgb(255, 255, 255);
6 | padding: 6px 20px;
7 | border-radius: 4px;
8 | transform: translateX(-50%) translateY(-100%);
9 | z-index: 9999;
10 | }
11 |
12 | .sdw-popover__wrap::before {
13 | content: "";
14 | display: inline-block;
15 | width: 8px;
16 | height: 8px;
17 | background-color: #000;
18 | transform: rotate(45deg);
19 | position: absolute;
20 | bottom: -3px;
21 | left: 50%;
22 | margin-left: -4px;
23 | }
24 |
--------------------------------------------------------------------------------
/src/UI/Popover/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 |
5 | export default class Popover extends React.Component {
6 | render() {
7 | let { tipString, position } = this.props;
8 |
9 | let clientX = position.x || -99999;
10 | let clientY = position.y || -99999;
11 |
12 | return ReactDOM.createPortal(
13 | !!tipString && (
14 |
21 | {tipString}
22 |
23 | ),
24 | document.body
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/UI/copy/example.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import CopyText from "./index";
3 | import Input from "../input/index";
4 |
5 | class App extends React.PureComponent {
6 | state = { copyText: "点击我,可以复制我,不信可以试试!!" };
7 | render() {
8 | let { copyText } = this.state;
9 | return (
10 | <>
11 | {copyText}
12 |
13 |
14 | 验证下
15 | this.setState({ inputValue: val })}
22 | />
23 | >
24 | );
25 | }
26 | }
27 |
28 | export default App;
29 |
--------------------------------------------------------------------------------
/src/UI/copy/img/icon_12_copy@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blazer233/init-react/e655bffc93e159c9d912b873c324e361048d2ec6/src/UI/copy/img/icon_12_copy@2x.png
--------------------------------------------------------------------------------
/src/UI/copy/index.css:
--------------------------------------------------------------------------------
1 | .monitor-eye-detail__user-img {
2 | height: 16px;
3 | vertical-align: text-top;
4 | cursor: pointer;
5 | margin-left: 8px;
6 | }
7 | .main {
8 | display: flex;
9 | width: 300px;
10 | justify-content: flex-end;
11 | flex-direction: row;
12 | }
13 | .input {
14 | width: 300px;
15 | }
16 |
--------------------------------------------------------------------------------
/src/UI/copy/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import USERIMG from "./img/icon_12_copy@2x.png";
4 | import "./index.css";
5 |
6 | export default props => {
7 | let [isCopy, setIsCopy] = useState(false);
8 | let [isShowTip, setIsShowTip] = useState(false);
9 |
10 | function handleCopy() {
11 | !!props.copyText && copyToClipboard(props.copyText);
12 | setIsCopy(true);
13 | }
14 |
15 | function copyToClipboard(value) {
16 | const input = document.createElement("input");
17 | input.style.position = "absolute";
18 | input.style.left = "10000px";
19 | input.style.bottom = "10000px";
20 | input.value = value;
21 | document.body.append(input);
22 | input.select();
23 | document.execCommand("copy");
24 | input.parentNode.removeChild(input);
25 | }
26 |
27 | return (
28 |
35 |

handleCopy()}
39 | onMouseLeave={() => {
40 | setIsCopy(false);
41 | setIsShowTip(false);
42 | }}
43 | onMouseEnter={() => setIsShowTip(true)}
44 | >
45 | {isShowTip && (
46 |
47 |
48 | {isCopy ? "复制成功" : !!props.copyTip ? props.copyTip : "点击复制"}
49 |
50 |
51 | )}
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/src/UI/drawer/example.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Drawer from "./index";
3 |
4 | export default () => {
5 | let [visible, setVisible] = useState(false);
6 | return (
7 | <>
8 | setVisible(true)}>点我打开抽屉
9 | setVisible(false)}
13 | onCancelClick={() => setVisible(false)}
14 | onSureClick={() => setVisible(false)}
15 | >
16 |
17 | 这里可以自定义内容
18 |
19 |
20 | >
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/UI/drawer/img/cancel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blazer233/init-react/e655bffc93e159c9d912b873c324e361048d2ec6/src/UI/drawer/img/cancel.png
--------------------------------------------------------------------------------
/src/UI/drawer/img/cancel_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blazer233/init-react/e655bffc93e159c9d912b873c324e361048d2ec6/src/UI/drawer/img/cancel_hover.png
--------------------------------------------------------------------------------
/src/UI/drawer/index.css:
--------------------------------------------------------------------------------
1 | @keyframes changeBackgroundColor {
2 | 0% {
3 | background: rgba(1, 1, 1, .01);
4 | }
5 | 100% {
6 | background: rgba(1, 1, 1, .5);
7 | }
8 | }
9 |
10 | @keyframes hideBackgroundColor {
11 | 0% {
12 | background: rgba(1, 1, 1, .5);
13 | }
14 | 100% {
15 | background: rgba(1, 1, 1, .01);
16 | }
17 | }
18 |
19 | .sdw-drawer__wrap {
20 | height: 100%;
21 | width: 100%;
22 | position: absolute;
23 | left: 0;
24 | top: 0;
25 | background: rgba(1, 1, 1, .5);
26 | animation: changeBackgroundColor .3s;
27 | z-index: 1000; /* dataPicker 的z-index为1050,这里不能大于它 */
28 | }
29 |
30 | .sdw-drawer__wrap.is_hide_win {
31 | background: transparent;
32 | animation: hideBackgroundColor .3s;
33 | }
34 |
35 | @keyframes changeWidth {
36 | 0% {
37 | width: 0;
38 | min-width: 0;
39 | }
40 | 100% {
41 | width: 50%;
42 | min-width: 920px;
43 | }
44 | }
45 |
46 | @keyframes hideWin {
47 | 0% {
48 | width: 50%;
49 | min-width: 920px;
50 | }
51 | 100% {
52 | width: 0;
53 | min-width: 0;
54 | }
55 | }
56 |
57 | .sdw-drawer__wrap .sdw-drawer__content-wrap {
58 | height: 100%;
59 | width: 50%;
60 | position: absolute;
61 | right: 0;
62 | top: 0;
63 | background: #fff;
64 | animation: changeWidth .3s;
65 | min-width: 920px;
66 | }
67 |
68 | .sdw-drawer__content-wrap.is_hide_win {
69 | width: 0;
70 | animation: hideWin .3s;
71 | }
72 |
73 | .sdw-drawer__left-wrap {
74 | height: 100%;
75 | width: 50%;
76 | position: absolute;
77 | left: 0;
78 | top: 0;
79 | background: transparent;
80 | }
81 |
82 | .sdw-drawer__content-header {
83 | align-items: center;
84 | color: #000;
85 | display: flex;
86 | font-weight: 600;
87 | font-size: 18px;
88 | padding: 20px 20px 20px 40px;
89 | border-bottom: 1px solid #eee;
90 | position: relative;
91 | }
92 |
93 | .sdw-drawer__content-header-cancel-img {
94 | position: absolute;
95 | right: 20px;
96 | cursor: pointer;
97 | display: inline-block;
98 | width: 24px;
99 | height: 24px;
100 | background: url(./img/cancel.png) no-repeat;
101 | background-size: cover;
102 | }
103 |
104 | .sdw-drawer__content-header-cancel-img:hover {
105 | background: url(./img/cancel_hover.png) no-repeat;
106 | background-size: cover;
107 | }
108 |
109 | .sdw-drawer__content-body {
110 | height: calc(100vh - 166px);
111 | padding: 20px 20px 20px 40px;
112 | overflow: auto;
113 | }
114 |
115 | .sdw-drawer__content-footer {
116 | padding: 10px 20px;
117 | border-top: 1px solid #eee;
118 | text-align: right;
119 | }
120 |
121 | .sdw-drawer__content-footer > button {
122 | display: inline-block;
123 | line-height: 1;
124 | white-space: nowrap;
125 | cursor: pointer;
126 | background: #FFF;
127 | border: 1px solid #DCDFE6;
128 | color: #666;
129 | -webkit-appearance: none;
130 | text-align: center;
131 | box-sizing: border-box;
132 | outline: 0;
133 | margin: 0;
134 | transition: .1s;
135 | font-weight: 500;
136 | padding: 12px 20px;
137 | font-size: 14px;
138 | border-radius: 4px;
139 | margin-left: 8px;
140 | }
141 |
142 | .sdw-drawer__content-footer >
143 | button.sdw-drawer__content-footer-button--primary {
144 | color: #FFF;
145 | background-color: #265CF0;
146 | border-color: #265CF0;
147 | }
148 |
149 | .sdw-drawer__content-footer >
150 | button.sdw-drawer__content-footer-button--primary:hover {
151 | background: #517df3;
152 | border-color: #517df3;
153 | color: #FFF;
154 | }
155 |
156 | .sdw-drawer__content-footer >
157 | button.sdw-drawer__content-footer-button--cancel:hover {
158 | color: #265CF0;
159 | border-color: #becefb;
160 | background-color: #e9effe;
161 | }
162 |
--------------------------------------------------------------------------------
/src/UI/drawer/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 |
5 | const DrawerBody = ({ children }) => children;
6 |
7 | class SdwDrawer extends Component {
8 | state = {
9 | isHide: false,
10 | isShowWin: false,
11 | };
12 |
13 | componentDidMount() {
14 | this.setState({
15 | isShowWin: this.props.visible,
16 | });
17 | }
18 |
19 | componentDidUpdate(prevProps, prevStates) {
20 | if (prevProps.visible !== this.props.visible) {
21 | if (!this.props.visible) {
22 | this.hanldeClick();
23 | } else {
24 | this.setState({
25 | isShowWin: this.props.visible,
26 | });
27 | }
28 | }
29 | }
30 |
31 | hanldeClick = () => {
32 | this.setState({
33 | isHide: true,
34 | });
35 |
36 | setTimeout(() => {
37 | this.setState({
38 | isHide: false,
39 | isShowWin: false,
40 | });
41 | }, 150);
42 | };
43 |
44 | render() {
45 | let { isHide, isShowWin } = this.state;
46 | let contentClassName = isHide ? "is_hide_win" : "";
47 | return ReactDOM.createPortal(
48 | isShowWin && (
49 |
50 |
54 |
55 |
56 | {this.props.title}
57 |
61 |
62 |
63 | {this.props.children}
64 |
65 |
66 | {!this.props.hideCancelButton && (
67 |
73 | )}
74 | {!this.props.hideSureButton && (
75 |
81 | )}
82 |
83 |
84 |
85 | ),
86 | document.body
87 | );
88 | }
89 | }
90 |
91 | SdwDrawer.Body = DrawerBody;
92 |
93 | export default SdwDrawer;
94 |
--------------------------------------------------------------------------------
/src/UI/input/img/cancel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blazer233/init-react/e655bffc93e159c9d912b873c324e361048d2ec6/src/UI/input/img/cancel.png
--------------------------------------------------------------------------------
/src/UI/input/img/clear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blazer233/init-react/e655bffc93e159c9d912b873c324e361048d2ec6/src/UI/input/img/clear.png
--------------------------------------------------------------------------------
/src/UI/input/index.css:
--------------------------------------------------------------------------------
1 | .sdw-input__div-wrap {
2 | display: inline-table;
3 | position: relative;
4 | margin-right: 38px;
5 | vertical-align: bottom;
6 | }
7 |
8 | .sdw-input__wrap,
9 | .sdw-textarea-input__wrap {
10 | border: 1px solid #eee;
11 | border-radius: 4px;
12 | }
13 |
14 | .sdw-input__wrap:focus,
15 | .sdw-textarea-input__wrap {
16 | outline: none;
17 | }
18 |
19 | .sdw-textarea-input__wrap {
20 | padding: 10px 18px 0;
21 | min-height: 60px;
22 | }
23 |
24 | .sdw-input__wrap {
25 | padding: 0 22px 0 14px;
26 | }
27 |
28 | .sdw-input__wrap:disabled {
29 | color: #ccc;
30 | cursor: not-allowed;
31 | }
32 |
33 | /* type=number */
34 | /* .sdw-input__wrap[type=number] {
35 | -moz-appearance: textfield;
36 | }
37 | .sdw-input__wrap[type=number]::-webkit-inner-spin-button,
38 | .sdw-input__wrap[type=number]::-webkit-outer-spin-button {
39 | -webkit-appearance: none;
40 | margin: 0;
41 | } */
42 |
43 | .sdw-error-input {
44 | border: 1px solid #ff5e5e;
45 | }
46 |
47 | .sdw-error-input__tip {
48 | position: absolute;
49 | color: #ff5e5e;
50 | font-size: 12px;
51 | background-color: #fff;
52 | line-height: 14px;
53 | width: calc(100% + 40px);
54 | bottom: -16px;
55 | }
56 |
57 | .sdw-input-clearable {
58 | display: inline-block;
59 | width: 16px;
60 | height: 16px;
61 | position: absolute;
62 | /* 等视觉给好icon后调整位置 */
63 | top: 12px;
64 | right: 8px;
65 | cursor: pointer;
66 | background-image: url('./img/clear.png');
67 | }
68 |
69 | /* 鼠标聚焦边框颜色蓝起来 */
70 | .sdw-input__on-focus .operation-label-title,
71 | .sdw-input__on-focus .sdw-input__wrap,
72 | .sdw-input__on-focus .sdw-textarea-input__wrap {
73 | border: 1px solid #265cf0;
74 | }
75 |
76 | .operation-label-title {
77 | display: inline-block;
78 | line-height: 40px;
79 | height: 40px;
80 | position: relative;
81 | z-index: 1;
82 | padding: 0 12px;
83 | margin-right: -4px;
84 | background: #fff;
85 | border: 1px solid #eee;
86 | font-size: 14px;
87 | color: #262626;
88 | border-radius: 4px 0 0 4px;
89 | box-sizing: border-box;
90 | }
91 |
92 | .sdw-error-input__wrap .operation-label-title {
93 | border: 1px solid #ff5e5e;
94 | }
95 |
96 | .sdw-nultiple-input-wrap {
97 | display: inline-block;
98 | border-radius: 4px;
99 | width: calc(100% + 36px);
100 | border: 1px solid #eee;
101 | }
102 |
103 | .sdw-input__multiple-choice-div-wrap {
104 | width: calc(100% + 36px);
105 | height: 120px;
106 | overflow-y: auto;
107 | position: absolute;
108 | border: 1px solid #eee;
109 | z-index: 9;
110 | background-color: #fff;
111 | }
112 |
113 | .sdw-input__multiple-choice-item {
114 | padding: 0 10px;
115 | line-height: 36px;
116 | }
117 |
118 | .sdw-input__multiple-choice-item:hover {
119 | background-color: #f5f7fa;
120 | }
121 |
122 | .sdw-input-tags.tag {
123 | display: inline-block;
124 | background-color: #f0f2f5;
125 | color: #909399;
126 | border-radius: 4px;
127 | padding: 8px;
128 | margin: 4px 0 4px 8px;
129 | line-height: 14px;
130 | border: 1px solid #eee;
131 | }
132 |
133 | .sdw-input-tags.search-input,
134 | .sdw-input-tags.search-input:hover {
135 | border: none;
136 | outline: none;
137 | }
138 |
139 | .sdw-input-tags__clear-icon {
140 | display: inline-block;
141 | width: 14px;
142 | height: 14px;
143 | background: url(./img/cancel.png) no-repeat;
144 | background-size: cover;
145 | vertical-align: middle;
146 | cursor: pointer;
147 | }
148 |
149 | .sdw-input-tags:hover {
150 | border: 1px solid #265cf0;
151 | }
152 |
153 | .sdw-input__multiple-choice-item.ellipsis {
154 | max-width: 100%;
155 | }
156 |
--------------------------------------------------------------------------------
/src/UI/input/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import "./index.css";
5 |
6 | class SdwInput extends Component {
7 | inputRef = React.createRef();
8 |
9 | static defaultProps = {
10 | width: null,
11 | height: 40,
12 | rows: 3,
13 | cols: 28,
14 | placeholder: "请输入",
15 | clearable: true,
16 | value: "",
17 | label: "",
18 | type: "text",
19 | disabled: false,
20 | sholdCheckValidate: false,
21 | multipleValue: [],
22 | multipleOption: [],
23 | };
24 |
25 | state = {
26 | isValidatePass: true,
27 | validateText: "",
28 | showClearIcon: false,
29 | isOnFocus: false,
30 | hideSearchField: true,
31 | searchValue: "",
32 | };
33 |
34 | componentDidUpdate(prevProps, prevState) {
35 | if (
36 | prevProps.sholdCheckValidate !== this.props.sholdCheckValidate ||
37 | prevProps.multipleValue !== this.props.multipleValue
38 | ) {
39 | this.validate(this.props.value);
40 | }
41 | }
42 |
43 | handleChange = e => {
44 | if (this.props.disabled) return;
45 | let value = e.target.value;
46 |
47 | if (typeof this.props.onChange === "function") {
48 | this.props.onChange(value);
49 | }
50 |
51 | if (!this.state.isValidatePass) {
52 | this.validate(value);
53 | }
54 |
55 | if (!value.length) {
56 | this.validate("");
57 | }
58 | };
59 |
60 | handleBlur = e => {
61 | this.validate(e.target.value);
62 |
63 | if (typeof this.props.onBlur === "function") {
64 | this.props.onBlur(e.target.value);
65 | }
66 | };
67 |
68 | handleClearInput = e => {
69 | e.preventDefault();
70 |
71 | if (typeof this.props.onChange === "function") {
72 | this.props.onChange("");
73 | }
74 |
75 | this.validate("");
76 | this.inputRef.current.focus();
77 | };
78 |
79 | handleKeyDown = record => {
80 | if (
81 | record.keyCode === 13 &&
82 | typeof this.props.onEnterKeyDown === "function"
83 | ) {
84 | this.props.onEnterKeyDown();
85 | this.inputRef.current.blur();
86 | }
87 | };
88 |
89 | validate = value => {
90 | if (this.props.isMultipleChoice) {
91 | value = this.props.multipleValue;
92 | }
93 |
94 | // 如果没有传入valiateFun进行校验,直接跳过
95 | if (typeof this.props.validateFun !== "function") {
96 | return;
97 | }
98 |
99 | let res = this.props.validateFun(value);
100 |
101 | // validateFun只有返回true,才会校验通过
102 | if (res === true) {
103 | this.setState({
104 | isValidatePass: res,
105 | validateText: "",
106 | });
107 | } else {
108 | this.setState({
109 | isValidatePass: false,
110 | validateText: res,
111 | });
112 | }
113 | };
114 |
115 | onChangeMultipleValue = (obj, type) => {
116 | let { multipleValue } = this.props;
117 | let list = [...multipleValue].filter(i => !!i);
118 |
119 | this.setState({
120 | searchValue: "",
121 | });
122 |
123 | if (!type || !obj || typeof this.props.changeMultipleValue !== "function")
124 | return;
125 |
126 | if (type === "add" && multipleValue.indexOf(obj.value) === -1) {
127 | list.push(obj.value);
128 | }
129 |
130 | if (type === "del" && multipleValue.indexOf(obj.value) !== -1) {
131 | let index = list.findIndex(i => i === obj.value);
132 | list.splice(index, 1);
133 | }
134 |
135 | this.props.changeMultipleValue(list);
136 | };
137 |
138 | render() {
139 | let {
140 | value,
141 | type,
142 | placeholder,
143 | disabled,
144 | width,
145 | height,
146 | rows,
147 | cols,
148 | label,
149 | clearable,
150 | isMultipleChoice,
151 | multipleValue,
152 | multipleOption,
153 | } = this.props;
154 |
155 | let {
156 | isValidatePass,
157 | validateText,
158 | showClearIcon,
159 | isOnFocus,
160 | hideSearchField,
161 | searchValue,
162 | } = this.state;
163 |
164 | let textareaClassName = isValidatePass
165 | ? "sdw-textarea-input__wrap"
166 | : "sdw-textarea-input__wrap sdw-error-input";
167 | let inputClassName = isValidatePass
168 | ? "sdw-input__wrap"
169 | : "sdw-input__wrap sdw-error-input";
170 |
171 | let filterSearchFieldsArr = [];
172 | let selectedFields = [];
173 | if (isMultipleChoice) {
174 | filterSearchFieldsArr = multipleOption.filter(
175 | i =>
176 | i.name.indexOf(searchValue) !== -1 && !multipleValue.includes(i.value)
177 | );
178 | selectedFields = multipleOption.filter(i =>
179 | multipleValue.some(j => j === i.value)
180 | );
181 | }
182 |
183 | return (
184 |
193 | {!!label && {label}}
194 | this.setState({ showClearIcon: true })}
196 | onMouseLeave={() => this.setState({ showClearIcon: false })}
197 | className="sdw-input__div-wrap"
198 | style={{ width: width === null ? "80%" : width }}
199 | >
200 | {type === "textarea" ? (
201 |
330 |
331 | );
332 | }
333 | }
334 |
335 | SdwInput.propTypes = {
336 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
337 | height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
338 | placeholder: PropTypes.string,
339 | clearable: PropTypes.bool,
340 | value: PropTypes.string,
341 | type: PropTypes.string,
342 | label: PropTypes.string,
343 | disabled: PropTypes.bool,
344 | rows: PropTypes.number,
345 | cols: PropTypes.number,
346 | onChange: PropTypes.func,
347 | validateFun: PropTypes.func,
348 | onFocus: PropTypes.func,
349 | onBlur: PropTypes.func,
350 | onEnterKeyDown: PropTypes.func, // 按下enter键触发的事件
351 | sholdCheckValidate: PropTypes.bool, // true: 手动触发必填项的检查
352 | isMultipleChoice: PropTypes.bool, // true: 支持'多选'
353 | multipleValue: PropTypes.array, // 多选绑定的数组value
354 | multipleOption: PropTypes.array, // 多选绑定的数组候选项
355 | changeMultipleValue: PropTypes.func, // 改变multipleValue函数
356 | };
357 |
358 | export default SdwInput;
359 |
--------------------------------------------------------------------------------
/src/UI/layout/example.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Layout from "./index";
3 |
4 | import "./index.css";
5 |
6 | export default () => {
7 | return (
8 | <>
9 | 没有上下左右的间隔:(红色边框是最外边的容器边框)
10 |
11 |
12 | col1
13 |
14 |
15 |
16 | col1
17 | col2
18 |
19 |
20 |
21 | col1
22 | col2
23 | col3
24 |
25 |
26 |
27 | col1
28 | col2
29 | col2
30 | col3
31 |
32 |
33 |
34 | 设置上下左右的间隔:上下:8px;左右:10px (红色边框是最外边的容器边框)
35 |
36 |
37 |
38 | col1
39 |
40 |
41 |
42 | col1
43 |
44 | col2
45 | col2
46 |
47 |
48 |
49 |
50 |
51 | col1
52 | col1
53 | col1
54 |
55 | col2
56 | col3
57 |
58 |
59 |
60 | col1
61 | col2
62 | col3
63 |
64 | col4
65 | col4
66 | col4
67 | col4
68 |
69 |
70 |
71 |
72 | col1
73 | col2
74 | col3
75 |
76 |
77 | >
78 | );
79 | };
80 |
--------------------------------------------------------------------------------
/src/UI/layout/index.css:
--------------------------------------------------------------------------------
1 | .sdw__col-wrap {
2 | box-sizing: border-box;
3 | }
--------------------------------------------------------------------------------
/src/UI/layout/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./index.css";
3 |
4 | const ROW = ({ gutter, justigy, marginTop, marginBottom, children }) => {
5 | Layout.customObj.gutter = gutter;
6 | return (
7 |
17 | {children}
18 |
19 | );
20 | };
21 |
22 | const COL = ({ span, children }) => (
23 |
32 | {children}
33 |
34 | );
35 |
36 | const Layout = ({ children }) => {children}
;
37 | Layout.Row = ROW;
38 | Layout.Col = COL;
39 | Layout.customObj = {};
40 |
41 | export default Layout;
42 |
--------------------------------------------------------------------------------
/src/UI/table/config.js:
--------------------------------------------------------------------------------
1 | export const dataList = [
2 | {
3 | ID: "10001",
4 | name: "中文名测试",
5 | lavel: "等级2",
6 | pro: "属性1",
7 | upTime: "2021-01-14 15:15:00",
8 | },
9 | {
10 | ID: "10002",
11 | name: "中文",
12 | lavel: "等级3",
13 | pro: "属性2",
14 | upTime: "2021-01-14 15:15:00",
15 | },
16 | {
17 | ID: "10003",
18 | name: "中文",
19 | lavel: "等级3",
20 | pro: "属性2",
21 | upTime: "2021-01-14 15:15:00",
22 | },
23 | {
24 | ID: "10004",
25 | name: "中文",
26 | lavel: "等级3",
27 | pro: "属性2",
28 | upTime: "2021-01-14 15:15:00",
29 | },
30 | {
31 | ID: "10005",
32 | name: "中文",
33 | lavel: "等级3",
34 | pro: "属性2",
35 | upTime: "2021-01-14 15:15:00",
36 | },
37 | ];
38 | export const columns1 = [
39 | {
40 | title: "ID",
41 | dataIndex: "ID",
42 | width: "100",
43 | getCellClick: (colData, data) => console.log(colData, data),
44 | },
45 | {
46 | title: "维度中文名",
47 | dataIndex: "name",
48 | },
49 | {
50 | title: "维度等级",
51 | dataIndex: "lavel",
52 | },
53 | {
54 | title: "属性类型",
55 | dataIndex: "pro",
56 | },
57 | {
58 | title: "修改时间",
59 | dataIndex: "upTime",
60 | },
61 | ];
62 | export const columns2 = [
63 | {
64 | title: "checkTd",
65 | dataIndex: "ID",
66 | checkTd: 1, // checkTd: 1 就是可选单元格按钮, 需要配合 showCheck={true} 一起才有用 // 展示可选单元格
67 | },
68 | {
69 | title: "ID",
70 | dataIndex: "ID",
71 | width: "100",
72 | getCellClick: (colData, data) => console.log(colData, data),
73 | },
74 | {
75 | title: "维度中文名",
76 | dataIndex: "name",
77 | },
78 | {
79 | title: "维度等级",
80 | dataIndex: "lavel",
81 | },
82 | {
83 | title: "属性类型",
84 | dataIndex: "pro",
85 | },
86 | {
87 | title: "修改时间",
88 | dataIndex: "upTime",
89 | },
90 | ];
91 | export const Deepclone = (target, map = new WeakMap()) => {
92 | if (typeof target === "object") {
93 | let cloneTarget = Array.isArray(target) ? [] : {};
94 | if (map.get(target)) {
95 | return map.get(target);
96 | }
97 | map.set(target, cloneTarget);
98 | for (const key in target) {
99 | cloneTarget[key] = Deepclone(target[key], map);
100 | }
101 | return cloneTarget;
102 | } else {
103 | return target;
104 | }
105 | };
106 |
--------------------------------------------------------------------------------
/src/UI/table/example.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { dataList, columns1, columns2 } from "./config";
3 | import Table from "./index";
4 | export default () => {
5 | let [asynclist, setAsync] = useState([]);
6 | useEffect(() => {
7 | setTimeout(() => setAsync(["10002"]), 3000);
8 | }, []);
9 | let onRowClick = (data, event) => console.log("行", data, event);
10 | let onClickGetOneData = (data, event) => console.log("格", data, event);
11 | let checkboxChange = data => setAsync(data);
12 | let handle = {
13 | title: "操作",
14 | render: (data, record) => (
15 | onClickGetOneData(data)}>编辑
16 | ),
17 | };
18 | return (
19 |
20 |
未配置可勾选行
21 |
22 |
23 |
配置可勾选行: {asynclist.join()}
24 |
模拟异步:默认勾选 10002
25 |
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/UI/table/img/empty@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blazer233/init-react/e655bffc93e159c9d912b873c324e361048d2ec6/src/UI/table/img/empty@2x.png
--------------------------------------------------------------------------------
/src/UI/table/img/right@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blazer233/init-react/e655bffc93e159c9d912b873c324e361048d2ec6/src/UI/table/img/right@2x.png
--------------------------------------------------------------------------------
/src/UI/table/index copy.js:
--------------------------------------------------------------------------------
1 | // API
2 | // columns为表格定义数据格式,title字段为表格标题,dataIndex为传入的数据源中需要显示的字段一致,可以通过render函数来渲染当前列的数据 -> Array// dataSource为数据源 -> Array
3 | // onRowClick为单行点击事件 返回该行的数据
4 | // checkTd:1 表示该单元格是可选单元格
5 | import _ from "lodash";
6 | import React, { Component } from "react";
7 | import PropTypes from "prop-types";
8 | import "./index.css";
9 | class Table extends Component {
10 | constructor(props) {
11 | console.log(props);
12 | super(props);
13 | }
14 |
15 | state = {
16 | isCheckClass: "is-checked", // 选中后的样式变化
17 | checkBoxIdList: [],
18 | allCheckState: 0,
19 | tipStyleClass: { display: "none" },
20 | tipContent: "",
21 | positionParams: { x: -99999, y: -99999 },
22 | showPopover: false,
23 | dividLineOnMouseDown: false,
24 | mouseDownClientX: 0,
25 | widthList: [],
26 | showDividLine: false,
27 | dividLineIndex: null,
28 | oprShowBoxShadow: false,
29 | hasChildrenList: [], // 拥有孩子节点的父节点的id
30 | openChildrenList: [], // 存储当前需要展开的子行的父行id
31 | };
32 |
33 | // 属性默认值
34 | static defaultProps = {
35 | tableStyle: {},
36 | className: "",
37 | dataSource: [],
38 | columns: [],
39 | emptyText: "暂无数据",
40 | tbodyMinHeight: "230px",
41 | onRowClick: () => {},
42 | checkboxChange: () => {},
43 | checkBoxIdList: [],
44 | tdEmptyText: "-",
45 | tdWhiteSpace: false,
46 | classTableHeadName: "",
47 | isUserSelect: "none",
48 | childrenString: "children",
49 | };
50 | /**
51 | * 返回选中数据,计算行高
52 | */
53 | componentDidMount() {
54 | if (this.props.checkBoxIdList.length) {
55 | this.initData();
56 | }
57 | if (Array.isArray(this.props.columns) && this.props.columns.length) {
58 | this.initColWidth();
59 | }
60 | }
61 |
62 | /**
63 | * 重新计算选数据、行高
64 | */
65 | componentDidUpdate(prevProps) {
66 | if (prevProps.checkBoxIdList.length !== this.props.checkBoxIdList.length) {
67 | this.initData();
68 | }
69 | if (
70 | (prevProps.columns !== this.props.columns ||
71 | prevProps.columns.length !== this.props.columns.length) &&
72 | Array.isArray(this.props.columns) &&
73 | this.props.columns.length
74 | ) {
75 | this.initColWidth();
76 | }
77 | if (
78 | prevProps.dataSource.length !== this.props.dataSource.length ||
79 | _.differenceWith(prevProps.dataSource, this.props.dataSource, _.isEqual)
80 | .length
81 | ) {
82 | this.inithasChildrenList(this.props.dataSource);
83 | }
84 | }
85 |
86 | inithasChildrenList = (list = []) => {
87 | let { childrenString } = this.props;
88 | let idList = [];
89 | list.forEach(item => {
90 | if (_.isArray(item[childrenString]) && item[childrenString].length) {
91 | idList.push(item.id);
92 | }
93 | });
94 | this.setState({
95 | hasChildrenList: idList,
96 | });
97 | };
98 |
99 | initData = () => {
100 | this.setState({
101 | checkBoxIdList: this.props.checkBoxIdList || [],
102 | });
103 | };
104 |
105 | /**
106 | * 计算行高
107 | */
108 | initColWidth = () => {
109 | let defaultWidth = 70;
110 | let widthList = this.props.columns.reduce((prev, col) => {
111 | if (!!col.width) {
112 | if (typeof col.width === "string" && col.width.indexOf("px") !== -1) {
113 | let width = +col.width.slice(0, -2);
114 | if (typeof width === "number" && !isNaN(width)) {
115 | prev.push(width);
116 | } else {
117 | prev.push(defaultWidth);
118 | }
119 | } else if (typeof +col.width === "number") {
120 | prev.push(+col.width);
121 | } else {
122 | prev.push(defaultWidth);
123 | }
124 | } else {
125 | prev.push(defaultWidth);
126 | }
127 |
128 | return prev;
129 | }, []);
130 |
131 | this.setState({ widthList: widthList });
132 | };
133 |
134 | // 单元格变化事件
135 | selectboxChange = (e = window.event, data, checkId) => {
136 | let checkBoxIdList = [...this.props.checkBoxIdList];
137 | let index = checkBoxIdList.indexOf(checkId);
138 |
139 | if (index >= 0) {
140 | checkBoxIdList.splice(index, 1);
141 | } else {
142 | checkBoxIdList.push(checkId);
143 | }
144 |
145 | this.setState({
146 | checkBoxIdList: checkBoxIdList,
147 | allCheckState: checkBoxIdList.length === this.props.dataSource.length,
148 | });
149 |
150 | this.props.checkboxChange(checkBoxIdList);
151 |
152 | // 阻止事件冒泡
153 | e.stopPropagation();
154 | e.preventDefault();
155 | };
156 |
157 | // 全选单元格事件
158 | selectAllChange = e => {
159 | let checkBoxIdList = [];
160 | let allCheckState = 0;
161 |
162 | if (this.state.allCheckState == 0) {
163 | let dataKey = "";
164 | for (let i = 0; i < this.props.columns.length; i++) {
165 | let checkTd = this.props.columns[i].checkTd
166 | ? this.props.columns[i].checkTd
167 | : 0;
168 | if (checkTd == 1) {
169 | dataKey = this.props.columns[i].dataIndex;
170 | break;
171 | }
172 | }
173 | if (dataKey != "") {
174 | for (let i = 0; i < this.props.dataSource.length; i++) {
175 | let id = this.props.dataSource[i][dataKey];
176 | checkBoxIdList.push(id);
177 | }
178 | }
179 | allCheckState = 1;
180 | }
181 |
182 | this.setState({
183 | checkBoxIdList: checkBoxIdList,
184 | allCheckState: allCheckState,
185 | });
186 |
187 | this.props.checkboxChange(checkBoxIdList);
188 |
189 | // 阻止事件冒泡
190 | var e = e || window.event;
191 | e.stopPropagation();
192 | e.preventDefault();
193 | };
194 |
195 | // 单元格tip渲染
196 | showTdTip = (e, showState, content, contentNullState) => {
197 | if (contentNullState === 1) return;
198 |
199 | let styleClass = {
200 | position: "absolute",
201 | top: e.target.offsetTop + 10,
202 | left: e.target.offsetLeft,
203 | background: "#262626",
204 | color: "#fff",
205 | display: "block",
206 | padding: "8px",
207 | borderRadius: "3px",
208 | };
209 |
210 | this.setState({
211 | tipStyleClass: styleClass,
212 | tipContent: content,
213 | });
214 | };
215 |
216 | hideTdTip = e => {
217 | let styleClass = {
218 | display: "none",
219 | };
220 |
221 | this.setState({
222 | tipStyleClass: styleClass,
223 | tipContent: "",
224 | });
225 | };
226 |
227 | getTdClassName = (tdWhiteSpace, fiexd) => {
228 | let classNames = "";
229 | if (tdWhiteSpace) {
230 | classNames += "tdWhiteSpace ";
231 | }
232 | if (fiexd && this.state.oprShowBoxShadow) {
233 | classNames += "fiexdTdClass";
234 | }
235 | return classNames;
236 | };
237 |
238 | // 表体渲染(tbody)
239 | retRows = (columns, data, index, isLastNoOp, length) => {
240 | let tdEmptyText = this.props.tdEmptyText;
241 | let { widthList } = this.state;
242 | return columns.map((col, i) => {
243 | // 是否是可选单元格
244 | let checkTd = col.checkTd ? col.checkTd : 0;
245 |
246 | // 可选单元格渲染
247 | if (checkTd == 1 && this.props.showCheck) {
248 | return (
249 |
250 |
251 |
276 |
277 | |
278 | );
279 | }
280 |
281 | // 非选中单元格渲染 className={row.fiexd ? "fiexdClass" : ""} {this.props.tdWhiteSpace && col.fiexd ? 'tdWhiteSpace':''}
282 | if (checkTd === 0) {
283 | if (col.dataIndex !== undefined) {
284 | if (data[col.dataIndex] !== undefined) {
285 | let showVal = data[col.dataIndex];
286 | let contentNullState = 0;
287 | if (
288 | typeof data[col.dataIndex] === "object" ||
289 | typeof data[col.dataIndex] === "array"
290 | ) {
291 | showVal = JSON.stringify(data[col.dataIndex]);
292 | }
293 | if (showVal === "" || showVal === "null" || showVal === null) {
294 | contentNullState = 1;
295 | showVal = tdEmptyText;
296 | }
297 | return (
298 | {
300 | col.showTip && this.hideTdTip(e, col.showTip, showVal);
301 | }}
302 | onMouseEnter={e => {
303 | col.showTip &&
304 | this.showTdTip(e, col.showTip, showVal, contentNullState);
305 | }}
306 | className={this.getTdClassName(
307 | this.props.tdWhiteSpace,
308 | col.fiexd
309 | )}
310 | onClick={() => {
311 | col.getCellClick && col.getCellClick(this, col, data);
312 | }}
313 | key={i}
314 | width={widthList[i]}
315 | >
316 | {
326 | if (!col.showTip) return;
327 | this.setState({
328 | showPopover: false,
329 | });
330 | }}
331 | onMouseEnter={e => {
332 | if (!col.showTip) return;
333 | this.setState({
334 | showPopover: true,
335 | positionParams: {
336 | x: e.clientX,
337 | y: e.clientY,
338 | },
339 | });
340 | }}
341 | >
342 | {col.checkChildren && (
343 | i === data.id)
346 | ? "sdw-table__td-has-children is-open"
347 | : this.state.hasChildrenList.some(i => i === data.id)
348 | ? "sdw-table__td-has-children"
349 | : "no-children"
350 | }
351 | onClick={() => this.handCheckChildren(data)}
352 | >
353 | )}
354 | {col.render ? col.render(showVal, data, index) : showVal}
355 | {col.tip ? col.tip(showVal, data, index) : ""}
356 |
357 | |
358 | );
359 | } else {
360 | return (
361 | {
367 | col.getCellClick && col.getCellClick(this, col, data);
368 | }}
369 | key={i}
370 | width={widthList[i]}
371 | >
372 | {tdEmptyText}
373 | |
374 | );
375 | }
376 | } else {
377 | // 自定义 render 函数渲染
378 | let renderEle = "";
379 | if (col.render) {
380 | renderEle = col.render(data, index);
381 | if (isLastNoOp && index === length - 1) {
382 | renderEle = "";
383 | }
384 | }
385 |
386 | // 自定义 tip 函数渲染
387 | let renderTip = "";
388 | if (col.tip) {
389 | renderTip = col.tip(data, index);
390 | }
391 | return (
392 |
400 | {
402 | if (!col.tip) return;
403 | this.setState({
404 | showPopover: false,
405 | });
406 | }}
407 | onMouseEnter={e => {
408 | if (!col.tip) return;
409 | this.setState({
410 | tipContent: renderTip,
411 | showPopover: true,
412 | positionParams: {
413 | x: e.clientX,
414 | y: e.clientY,
415 | },
416 | });
417 | }}
418 | >
419 | {col.render && renderEle}
420 |
421 | |
422 | );
423 | }
424 | }
425 | });
426 | };
427 |
428 | handCheckChildren = data => {
429 | let curArr = [...this.state.openChildrenList];
430 | let pID = data.id;
431 | let flagIndex = curArr.findIndex(i => i === pID);
432 |
433 | if (flagIndex === -1) {
434 | curArr.push(pID);
435 | } else {
436 | curArr.splice(flagIndex, 1);
437 | }
438 |
439 | this.setState({
440 | openChildrenList: curArr,
441 | });
442 | };
443 |
444 | // 表体空内容的展示
445 | retEmptyText = emptyText => {
446 | return (
447 |
448 | {emptyText}
449 |
450 | );
451 | };
452 |
453 | // 行点击事件 如果存在则触发该事件
454 | trClick = (data, event) => {
455 | this.props.onRowClick(data, event);
456 | var e = e || window.event;
457 | e.stopPropagation();
458 | };
459 |
460 | // 单元格点击事件
461 | tableTdClick = (data, event) => {
462 | this.props.tableTdClick(data, event);
463 | var e = e || window.event;
464 | e.stopPropagation();
465 | };
466 |
467 | onMouseDown = (e, k) => {
468 | this.setState({
469 | dividLineOnMouseDown: true,
470 | mouseDownClientX: e.clientX,
471 | dividLineIndex: k,
472 | });
473 | };
474 |
475 | getCurWidth = curWidth => {
476 | return curWidth < 80 ? 80 : curWidth;
477 | };
478 |
479 | onMouseUp = e => {
480 | let {
481 | dividLineIndex,
482 | mouseDownClientX,
483 | widthList,
484 | dividLineOnMouseDown,
485 | } = this.state;
486 |
487 | if (dividLineOnMouseDown) {
488 | let moveX = e.clientX - mouseDownClientX;
489 | let curWidthArr = [...widthList];
490 | curWidthArr[dividLineIndex - 1] = this.getCurWidth(
491 | +curWidthArr[dividLineIndex - 1] + moveX
492 | );
493 | curWidthArr[dividLineIndex] = this.getCurWidth(
494 | +curWidthArr[dividLineIndex] - moveX
495 | );
496 |
497 | this.setState({
498 | widthList: curWidthArr,
499 | dividLineOnMouseDown: false,
500 | oprShowBoxShadow: true,
501 | });
502 | }
503 | };
504 |
505 | render() {
506 | const {
507 | className,
508 | classTableHeadName,
509 | columns,
510 | dataSource,
511 | emptyText,
512 | isLastNoOp,
513 | tableStyle,
514 | tbodyMinHeight,
515 | tbodyHeight,
516 | childrenString,
517 | } = this.props;
518 |
519 | let { tipContent, widthList, showDividLine, openChildrenList } = this.state;
520 |
521 | let curDataSource = _.cloneDeep(dataSource);
522 | let newDataSource = curDataSource.reduce((prev, item) => {
523 | prev.push(item);
524 |
525 | let childList = _.cloneDeep(item[childrenString]);
526 | if (openChildrenList.includes(item.id) && childList && childList.length) {
527 | childList = childList.map(i => {
528 | return _.assign({}, i, {
529 | customClass: "children",
530 | });
531 | });
532 | prev = prev.concat(childList);
533 | }
534 |
535 | return prev;
536 | }, []);
537 |
538 | return (
539 |
540 |
653 | {/* 表体空内容展示 */}
654 | {newDataSource &&
655 | newDataSource.length == 0 &&
656 | this.retEmptyText(emptyText)}
657 |
658 | );
659 | }
660 | }
661 |
662 | Table.propTypes = {
663 | columns: PropTypes.array.isRequired, //表头名称
664 | dataSource: PropTypes.array.isRequired, //数据列表
665 | emptyText: PropTypes.string, //列表为空时, 空内容展示的html
666 | isLastNoOp: PropTypes.bool, //表格最后一行不需要渲染操作样式
667 | className: PropTypes.string, //表格的自定义的样式名,
668 | classTableHeadName: PropTypes.string, // 表格的表头的自定义样式名
669 | tableStyle: PropTypes.object,
670 | tbodyMinHeight: PropTypes.string,
671 | tbodyHeight: PropTypes.string,
672 | showCheck: PropTypes.bool, // 是否展示选中单元格
673 | checkBoxIdList: PropTypes.array, // 选中的单元格数据
674 | tdEmptyText: PropTypes.string, // 表格缺省值
675 | tdWhiteSpace: PropTypes.bool, // 单元格的内容是否需要一行展示
676 | isUserSelect: PropTypes.string,
677 | childrenString: PropTypes.string, // 子节点对应的字段,默认为 children
678 | };
679 |
680 | export default Table;
681 |
--------------------------------------------------------------------------------
/src/UI/table/index.css:
--------------------------------------------------------------------------------
1 | .table-box {
2 | width: 100%;
3 | font-size: 14px;
4 | position: relative;
5 | table-layout: fixed;
6 | overflow-y: auto;
7 | }
8 |
9 | .table-box .table-box-table {
10 | width: 100%;
11 | border-bottom: none;
12 | background-color: #fff;
13 | position: relative;
14 | }
15 |
16 | .table-box tr.sdw-table__tr {
17 | border-bottom: 1px solid #ebeef5;
18 | }
19 |
20 | .table-box tr.sdw-table__tr:nth-child(even) {
21 | background-color: #fbfbfb;
22 | }
23 |
24 | .table-box .table-box-table .table-box-table-thead th {
25 | color: #999999;
26 | padding: 15px 20px;
27 | text-align: left;
28 | background-color: #eef2fe;
29 | word-break: break-all;
30 | }
31 |
32 | .table-box .table-box-table .table-box-table-thead th.checkInput,
33 | .table-box .table-box-table td.checkInput {
34 | width: 20px;
35 | }
36 |
37 | .table-box .table-box-table td {
38 | height: 28px;
39 | padding: 10px 20px;
40 | font-size: 14px;
41 | color: #262626;
42 | word-break: break-all;
43 | position: relative;
44 | text-align: left;
45 | }
46 |
47 | .table-box .table-box-table td .tdContent {
48 | overflow: hidden;
49 | }
50 |
51 | .table-box .table-box-table td .tdContentTip {
52 | cursor: pointer;
53 | }
54 |
55 | .table-box .emptyTextClass {
56 | width: 136px;
57 | height: 136px;
58 | position: absolute;
59 | top: 20%;
60 | right: 0;
61 | bottom: 0;
62 | left: 0;
63 | margin: auto;
64 | }
65 |
66 | .table-box .emptyTextClass {
67 | background-image: url(./img/empty@2x.png);
68 | background-size: cover;
69 | }
70 |
71 | .table-box .emptyTextClass span.text {
72 | position: absolute;
73 | bottom: -18px;
74 | text-align: center;
75 | width: 100%;
76 | color: #999;
77 | font-size: 14px;
78 | display: block;
79 | }
80 |
81 | /* 给table的body设置定高 */
82 | table tbody.sdw-table__tbody {
83 | display: table;
84 | width: 100%;
85 | table-layout: fixed;
86 | }
87 |
88 | table.table-box-table thead,
89 | table.table-box-table tbody tr.sdw-table__tr {
90 | display: table;
91 | width: 100%;
92 | table-layout: fixed; /*重要 表格固定算法*/
93 | }
94 |
95 | table thead th {
96 | background: #ccc;
97 | }
98 |
99 | .el-checkbox__input.is-checked .el-checkbox__inner {
100 | background-color: #265cf0 !important;
101 | border-color: #265cf0 !important;
102 | }
103 |
104 | .el-checkbox__input.is-checked .el-checkbox__inner:after {
105 | transform: rotate(45deg) scaleY(1);
106 | }
107 | .el-checkbox__input.is-checked .el-checkbox__inner:after {
108 | transform: rotate(45deg) scaleY(1);
109 | }
110 | .el-checkbox__inner:after {
111 | box-sizing: content-box;
112 | content: "";
113 | border: 1px solid #fff;
114 | border-left: 0;
115 | border-top: 0;
116 | height: 7px;
117 | left: 4px;
118 | position: absolute;
119 | top: 1px;
120 | transform: rotate(45deg) scaleY(0);
121 | width: 3px;
122 | transition: transform 0.15s ease-in 0.05s;
123 | transform-origin: center;
124 | }
125 |
126 | .el-checkbox,
127 | .el-checkbox-button__inner,
128 | .el-radio {
129 | font-weight: 500;
130 | -webkit-user-select: none;
131 | -moz-user-select: none;
132 | -ms-user-select: none;
133 | }
134 | .el-checkbox {
135 | color: #666;
136 | font-size: 14px;
137 | cursor: pointer;
138 | user-select: none;
139 | margin-right: 30px;
140 | }
141 | .el-checkbox,
142 | .el-checkbox__input {
143 | display: inline-block;
144 | position: relative;
145 | white-space: nowrap;
146 | }
147 |
148 | .el-checkbox__input {
149 | cursor: pointer;
150 | outline: 0;
151 | line-height: 1;
152 | vertical-align: middle;
153 | }
154 |
155 | .el-checkbox,
156 | .el-checkbox__input {
157 | display: inline-block;
158 | position: relative;
159 | white-space: nowrap;
160 | }
161 |
162 | .el-checkbox__inner {
163 | display: inline-block;
164 | position: relative;
165 | border: 1px solid #dcdfe6;
166 | border-radius: 2px;
167 | box-sizing: border-box;
168 | width: 14px;
169 | height: 14px;
170 | background-color: #fff;
171 | z-index: 1;
172 | transition: border-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46),
173 | background-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46);
174 | }
175 |
176 | .btn-group .batch .el-button--default {
177 | display: inline-block;
178 | line-height: 1;
179 | white-space: nowrap;
180 | cursor: pointer;
181 | background: #fff;
182 | border: 1px solid #dcdfe6;
183 | color: #666;
184 | -webkit-appearance: none;
185 | text-align: center;
186 | box-sizing: border-box;
187 | outline: 0;
188 | margin: 0;
189 | transition: 0.1s;
190 | font-weight: 500;
191 | padding: 12px 20px;
192 | font-size: 14px;
193 | border-radius: 4px;
194 | margin-right: 10px;
195 | padding-right: 90px;
196 | }
197 |
198 | .btn-group .batch .el-button--default:hover {
199 | color: #265cf0;
200 | border-color: #becefb;
201 | background-color: #e9effe;
202 | background: #ffffff;
203 | border: 1px solid #265cf0;
204 | }
205 |
206 | .reRunData button {
207 | height: 40px;
208 | width: 98px;
209 | margin-right: 10px;
210 | box-sizing: border-box;
211 | line-height: 0 !important;
212 | }
213 |
214 | .reRunData {
215 | position: relative;
216 | top: -36px;
217 | display: inline-block;
218 | }
219 |
220 | .el-button-span .is-disabled {
221 | color: #fff !important;
222 | background-color: #93aef8 !important;
223 | border-color: #93aef8 !important;
224 | }
225 |
226 | .table-box .sdw-table__tbody .sdw-table__tr .tdWhiteSpace .tdContent {
227 | white-space: nowrap;
228 | text-overflow: ellipsis;
229 | font-size: 14px;
230 | }
231 |
232 | .table-box .sdw-table__tbody .sdw-table__tr button {
233 | font-size: 14px !important;
234 | }
235 |
236 | .table-box .tdTip:before {
237 | content: "";
238 | width: 0px;
239 | height: 0px;
240 | border-top: 10px solid #262626;
241 | border-left: 10px solid transparent;
242 | border-right: 10px solid transparent;
243 | position: absolute;
244 | bottom: -7px;
245 | left: 5px;
246 | }
247 |
248 | .table-box .fiexdClass {
249 | position: sticky !important;
250 | right: 0;
251 | box-shadow: -7px 0 15px -2px rgb(0 0 0 / 10%);
252 | }
253 |
254 | .table-box .fiexdTdClass {
255 | position: sticky !important;
256 | right: 0;
257 | box-shadow: -7px 0 15px -2px rgb(0 0 0 / 10%);
258 | background: #fff;
259 | }
260 |
261 | .table-box table {
262 | border-spacing: 0;
263 | }
264 |
265 | .table-box .sdw__table-th-dividing-line {
266 | display: inline-block;
267 | width: 3px;
268 | height: 32px;
269 | border-right: 1px solid #ccc;
270 | position: absolute;
271 | left: 0;
272 | top: 50%;
273 | transform: translateY(-50%);
274 | cursor: col-resize;
275 | }
276 |
277 | .table-box .el-checkbox__original {
278 | display: none;
279 | }
280 |
281 | .table-box .sdw-table__td-has-children {
282 | margin-right: 8px;
283 | display: inline-block;
284 | width: 16px;
285 | height: 16px;
286 | background: url(./img/right@2x.png) no-repeat;
287 | background-size: cover;
288 | vertical-align: middle;
289 | transition: 0.1s linear;
290 | }
291 |
292 | .table-box .sdw-table__td-has-children.is-open {
293 | transform: rotate(90deg);
294 | }
295 |
296 | .table-box .sdw-table__td-has-children:hover {
297 | cursor: pointer;
298 | }
299 |
300 | .tdContent.children {
301 | margin-left: 16px;
302 | }
303 |
304 | .tdContent .no-children {
305 | display: inline-block;
306 | margin-left: 24px;
307 | }
308 |
--------------------------------------------------------------------------------
/src/UI/table/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import "./index.css";
3 | import { Deepclone } from "./config";
4 | const isCheckClass = "is-checked";
5 |
6 | const App = ({
7 | tableStyle = {},
8 | className = "",
9 | dataSource = [],
10 | columns = [],
11 | emptyText = "暂无数据",
12 | tbodyMinHeight = "230px",
13 | onRowClick = () => {},
14 | checkboxChange = () => {},
15 | checkBoxIdList: checkIdList = [],
16 | tdEmptyText = "-",
17 | tdWhiteSpace = false,
18 | classTableHeadName = "",
19 | isUserSelect = "none",
20 | childrenString = "children",
21 | showCheck = false,
22 | tbodyHeight = "",
23 | isLastNoOp = "",
24 | }) => {
25 | let [checkBoxIdList, setList] = useState([]);
26 | let [allCheckState, setCheck] = useState(0);
27 | let [dividLineOnMouseDown, setLine] = useState(false);
28 | let [mouseDownClientX, setClientX] = useState(0);
29 | let [widthList, setWL] = useState([]);
30 | let [showDividLine, setSD] = useState(false);
31 | let [dividLineIndex, setDL] = useState(null);
32 | let [oprShowBoxShadow, setSB] = useState(false);
33 | let [hasChildrenList, setHC] = useState([]);
34 | let [openChildrenList, setOC] = useState([]);
35 | useEffect(() => initData(), [checkBoxIdList.length]);
36 | useEffect(() => initColWidth(), [columns.length]);
37 | useEffect(() => inithasChildrenList(dataSource), [dataSource.length]);
38 | let initData = () => setList(checkIdList);
39 | let initColWidth = () => {
40 | let defaultWidth = 70;
41 | let widthList = columns.reduce((prev, col) => {
42 | if (!!col.width) {
43 | if (typeof col.width === "string" && col.width.indexOf("px") !== -1) {
44 | let width = +col.width.slice(0, -2);
45 | if (typeof width === "number" && !isNaN(width)) {
46 | prev.push(width);
47 | } else {
48 | prev.push(defaultWidth);
49 | }
50 | } else if (typeof +col.width === "number") {
51 | prev.push(+col.width);
52 | } else {
53 | prev.push(defaultWidth);
54 | }
55 | } else {
56 | prev.push(defaultWidth);
57 | }
58 | return prev;
59 | }, []);
60 | setWL(widthList);
61 | };
62 | let inithasChildrenList = list => {
63 | let idList = [];
64 | list.forEach(item => {
65 | if (Array.isArray(item[childrenString]) && item[childrenString].length) {
66 | idList.push(item.id);
67 | }
68 | });
69 | setHC(idList);
70 | };
71 | // 全选单元格事件
72 | let selectAllChange = (e = window.event) => {
73 | let checkBoxIdList = [];
74 | let allCheckState = 0;
75 | if (allCheckState == 0) {
76 | let dataKey = "";
77 | for (let i = 0; i < columns.length; i++) {
78 | let checkTd = columns[i].checkTd ? columns[i].checkTd : 0;
79 | if (checkTd == 1) {
80 | dataKey = columns[i].dataIndex;
81 | break;
82 | }
83 | }
84 | if (dataKey != "") {
85 | for (let i = 0; i < dataSource.length; i++) {
86 | let id = dataSource[i][dataKey];
87 | checkBoxIdList.push(id);
88 | }
89 | }
90 | allCheckState = 1;
91 | }
92 | setList(checkBoxIdList);
93 | setCheck(allCheckState);
94 | checkboxChange(checkBoxIdList);
95 | e.stopPropagation();
96 | e.preventDefault();
97 | };
98 | // 单元格tip渲染
99 | let showTdTip = (e, showState, content, contentNullState) => {
100 | if (contentNullState === 1) return;
101 | let styleClass = {
102 | position: "absolute",
103 | top: e.target.offsetTop + 10,
104 | left: e.target.offsetLeft,
105 | background: "#262626",
106 | color: "#fff",
107 | display: "block",
108 | padding: "8px",
109 | borderRadius: "3px",
110 | };
111 | };
112 | let hideTdTip = () => {};
113 | // 行点击事件 如果存在则触发该事件
114 | let trClick = (data, event) => {
115 | onRowClick(data, event);
116 | var e = e || window.event;
117 | e.stopPropagation();
118 | };
119 | // 单元格点击事件
120 | let tableTdClick = (data, event) => {
121 | tableTdClick(data, event);
122 | var e = e || window.event;
123 | e.stopPropagation();
124 | };
125 |
126 | let onMouseDown = (e, k) => {
127 | setLine(true);
128 | setClientX(e.clientX);
129 | setDL(k);
130 | };
131 |
132 | let getCurWidth = curWidth => {
133 | return curWidth < 80 ? 80 : curWidth;
134 | };
135 |
136 | let onMouseUp = e => {
137 | if (dividLineOnMouseDown) {
138 | let moveX = e.clientX - mouseDownClientX;
139 | let curWidthArr = [...widthList];
140 | curWidthArr[dividLineIndex - 1] = getCurWidth(
141 | +curWidthArr[dividLineIndex - 1] + moveX
142 | );
143 | curWidthArr[dividLineIndex] = getCurWidth(
144 | +curWidthArr[dividLineIndex] - moveX
145 | );
146 | setWL(curWidthArr);
147 | setLine(false);
148 | setSB(true);
149 | }
150 | };
151 | let getTdClassName = (tdWhiteSpace, fiexd) => {
152 | let classNames = "";
153 | if (tdWhiteSpace) {
154 | classNames += "tdWhiteSpace ";
155 | }
156 | if (fiexd && oprShowBoxShadow) {
157 | classNames += "fiexdTdClass";
158 | }
159 | return classNames;
160 | };
161 | let handCheckChildren = data => {
162 | let curArr = [...openChildrenList];
163 | let pID = data.id;
164 | let flagIndex = curArr.findIndex(i => i === pID);
165 | if (flagIndex === -1) {
166 | curArr.push(pID);
167 | } else {
168 | curArr.splice(flagIndex, 1);
169 | }
170 | setOC(curArr);
171 | };
172 | // 表体渲染(tbody)
173 | let retRows = (columns, data, index, isLastNoOp, length) => {
174 | return columns.map((col, i) => {
175 | // 是否是可选单元格
176 | let checkTd = col.checkTd ? col.checkTd : 0;
177 | // 可选单元格渲染
178 | if (checkTd == 1 && showCheck) {
179 | return (
180 |
181 |
182 |
205 |
206 | |
207 | );
208 | }
209 | // 非选中单元格渲染 className={row.fiexd ? "fiexdClass" : ""} {tdWhiteSpace && col.fiexd ? 'tdWhiteSpace':''}
210 | if (checkTd === 0) {
211 | if (col.dataIndex !== undefined) {
212 | if (data[col.dataIndex] !== undefined) {
213 | let showVal = data[col.dataIndex];
214 | let contentNullState = 0;
215 | if (
216 | typeof data[col.dataIndex] === "object" ||
217 | typeof data[col.dataIndex] === "array"
218 | ) {
219 | showVal = JSON.stringify(data[col.dataIndex]);
220 | }
221 | if (showVal === "" || showVal === "null" || showVal === null) {
222 | contentNullState = 1;
223 | showVal = tdEmptyText;
224 | }
225 | return (
226 |
228 | col.showTip && hideTdTip(e, col.showTip, showVal)
229 | }
230 | onMouseEnter={e =>
231 | col.showTip &&
232 | showTdTip(e, col.showTip, showVal, contentNullState)
233 | }
234 | className={getTdClassName(tdWhiteSpace, col.fiexd)}
235 | onClick={() => {
236 | col.getCellClick && col.getCellClick(col, data);
237 | }}
238 | key={i}
239 | width={widthList[i]}
240 | >
241 | {
251 | if (!col.showTip) return;
252 | }}
253 | onMouseEnter={e => {
254 | if (!col.showTip) return;
255 | }}
256 | >
257 | {col.checkChildren && (
258 | i === data.id)
261 | ? "sdw-table__td-has-children is-open"
262 | : hasChildrenList.some(i => i === data.id)
263 | ? "sdw-table__td-has-children"
264 | : "no-children"
265 | }
266 | onClick={() => handCheckChildren(data)}
267 | >
268 | )}
269 | {col.render ? col.render(showVal, data, index) : showVal}
270 | {col.tip ? col.tip(showVal, data, index) : ""}
271 |
272 | |
273 | );
274 | } else {
275 | return (
276 | {
279 | col.getCellClick && col.getCellClick(col, data);
280 | }}
281 | key={i}
282 | width={widthList[i]}
283 | >
284 | {tdEmptyText}
285 | |
286 | );
287 | }
288 | } else {
289 | // 自定义 render 函数渲染
290 | let renderEle = "";
291 | if (col.render) {
292 | renderEle = col.render(data, index);
293 | if (isLastNoOp && index === length - 1) {
294 | renderEle = "";
295 | }
296 | }
297 | // 自定义 tip 函数渲染
298 | let renderTip = "";
299 | if (col.tip) {
300 | renderTip = col.tip(data, index);
301 | }
302 | return (
303 |
308 | {
310 | if (!col.tip) return;
311 | }}
312 | onMouseEnter={e => {
313 | if (!col.tip) return;
314 | }}
315 | >
316 | {col.render && renderEle}
317 |
318 | |
319 | );
320 | }
321 | }
322 | });
323 | };
324 | // 单元格变化事件
325 | let selectboxChange = (e = window.event, data, checkId) => {
326 | let checkBoxIdList = [...checkBoxIdList];
327 | let index = checkBoxIdList.indexOf(checkId);
328 | if (index >= 0) {
329 | checkBoxIdList.splice(index, 1);
330 | } else {
331 | checkBoxIdList.push(checkId);
332 | }
333 | setList(checkBoxIdList);
334 | setCheck(checkBoxIdList.length == dataSource.length);
335 | checkboxChange(checkBoxIdList);
336 | e.stopPropagation();
337 | e.preventDefault();
338 | };
339 | // 表体空内容的展示
340 | let retEmptyText = emptyText => {
341 | return (
342 |
343 | {emptyText}
344 |
345 | );
346 | };
347 | let curDataSource = Deepclone(dataSource);
348 | let newDataSource = curDataSource.reduce((prev, item) => {
349 | prev.push(item);
350 | let childList = Deepclone(item[childrenString]);
351 | if (openChildrenList.includes(item.id) && childList && childList.length) {
352 | childList = childList.map(i => {
353 | return Object.assign({}, i, {
354 | customClass: "children",
355 | });
356 | });
357 | prev = prev.concat(childList);
358 | }
359 | return prev;
360 | }, []);
361 | return (
362 |
363 |
467 | {/* 表体空内容展示 */}
468 | {newDataSource && newDataSource.length == 0 && retEmptyText(emptyText)}
469 |
470 | );
471 | };
472 |
473 | // Table.propTypes = {
474 | // columns: PropTypes.array.isRequired, //表头名称
475 | // dataSource: PropTypes.array.isRequired, //数据列表
476 | // emptyText: PropTypes.string, //列表为空时, 空内容展示的html
477 | // isLastNoOp: PropTypes.bool, //表格最后一行不需要渲染操作样式
478 | // className: PropTypes.string, //表格的自定义的样式名,
479 | // classTableHeadName: PropTypes.string, // 表格的表头的自定义样式名
480 | // tableStyle: PropTypes.object,
481 | // tbodyMinHeight: PropTypes.string,
482 | // tbodyHeight: PropTypes.string,
483 | // showCheck: PropTypes.bool, // 是否展示选中单元格
484 | // checkBoxIdList: PropTypes.array, // 选中的单元格数据
485 | // tdEmptyText: PropTypes.string, // 表格缺省值
486 | // tdWhiteSpace: PropTypes.bool, // 单元格的内容是否需要一行展示
487 | // isUserSelect: PropTypes.string,
488 | // childrenString: PropTypes.string, // 子节点对应的字段,默认为 children
489 | // };
490 |
491 | export default App;
492 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import reportWebVitals from "./reportWebVitals";
4 | import ChildrenProblems from "./problems/children";
5 | import MemoProblems from "./problems/memo";
6 | import CallbackProblems from "./problems/callback";
7 | import UseCloneElement from "./problems/cloneElement";
8 | import TableApp from "./UI/table/example";
9 | import CopyApp from "./UI/copy/example";
10 | import LayoutApp from "./UI/layout/example";
11 | import DrawerApp from "./UI/drawer/example";
12 | //unstable_createRoot
13 | ReactDOM.render(
14 |
15 | {/**将子组件以props.children代替解决父组件变化子组件渲染问题 */}
16 | {/* */}
17 |
18 | {/**将子组件放到useMemo中解决父组件变化子组件渲染问题 */}
19 | {/* */}
20 |
21 | {/* */}
22 |
23 |
24 |
25 | {/* */}
26 |
27 | {/* */}
28 |
29 | {/* */}
30 |
31 | {/* */}
32 | ,
33 | document.getElementById("root")
34 | );
35 |
36 | // If you want to start measuring performance in your app, pass a function
37 | // to log results (for example: reportWebVitals(console.log))
38 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
39 | reportWebVitals();
40 |
--------------------------------------------------------------------------------
/src/problems/callback/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState, useEffect } from "react";
2 |
3 | /* 用react.memo */
4 | const DemoChildren = React.memo(props => {
5 | /* 只有初始化的时候打印了 子组件更新 */
6 | console.log("子组件更新");
7 | useEffect(() => props.getInfo("子组件"), []);
8 | return 子组件
;
9 | });
10 |
11 | const DemoUseCallback = ({ id }) => {
12 | const [number, setNumber] = useState(1);
13 | /* 此时usecallback的第一参数 (sonName)=>{ console.log(sonName) }
14 | 经过处理赋值给 getInfo */
15 | const getInfo = useCallback(sonName => console.log(sonName), [id]);
16 | return (
17 |
18 | {/* 点击按钮触发父组件更新 ,但是子组件没有更新 */}
19 |
20 |
21 |
22 | );
23 | };
24 | export default DemoUseCallback;
25 |
--------------------------------------------------------------------------------
/src/problems/children/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | function Son() {
3 | console.log("child render!");
4 | return Son
;
5 | }
6 |
7 | function Parent(props) {
8 | const [count, setCount] = React.useState(0);
9 | return (
10 | setCount(count + 1)}>
11 | count:{count}
12 | {props.children}
13 |
14 | );
15 | }
16 |
17 | export default Parent;
18 |
--------------------------------------------------------------------------------
/src/problems/cloneElement/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | function FatherComponent({ children }) {
3 | const newChildren = React.cloneElement(children, { age: 18 });
4 | return {newChildren}
;
5 | }
6 |
7 | function SonComponent(props) {
8 | console.error(props);
9 | return hello,world
;
10 | }
11 |
12 | class Index extends React.Component {
13 | render() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 | }
23 | export default Index;
24 |
--------------------------------------------------------------------------------
/src/problems/memo/index.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | const Tree = ({ theme }) => {
3 | return useMemo(() => {
4 | console.log("useMemo中组件仅被渲染1次");
5 | return {theme};
6 | }, [theme]);
7 | };
8 |
9 | function App() {
10 | const [istrue, setTrue] = React.useState(false);
11 | const [num, setCount] = React.useState(0);
12 | return (
13 | {
15 | setTrue(true);
16 | setCount(num + 1);
17 | }}
18 | >
19 | 点击{num}
20 |
21 |
22 | );
23 | }
24 | export default App;
25 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/useState/_index.js:
--------------------------------------------------------------------------------
1 | let isMount = true;
2 | let workInProgressHook = null;
3 | let fiber = {
4 | startNode: App,
5 | memoizedState: null,
6 | };
7 | const useState = init => {
8 | let hook;
9 | if (isMount) {
10 | hook = {
11 | memoizedState: init,
12 | next: null,
13 | queue: { pending: null },
14 | };
15 | if (fiber.memoizedState == null) {
16 | fiber.memoizedState = hook;
17 | } else {
18 | workInProgressHook.next = hook;
19 | }
20 | workInProgressHook = hook;
21 | } else {
22 | hook = workInProgressHook;
23 | workInProgressHook = workInProgressHook.next;
24 | }
25 | let baseState = hook.memoizedState;
26 | if (hook.queue.pending) {
27 | let firstUpdate = hook.queue.pending.next;
28 | do {
29 | let action = firstUpdate.action;
30 | baseState = action(baseState);
31 | let firstUpdate = firstUpdate.next;
32 | } while (firstUpdate !== hook.queue.pending.next);
33 | hook.queue.pending = null;
34 | }
35 | hook.memoizedState = baseState;
36 | return [baseState, dispatchAction.bind(null, hook.queue)];
37 | };
38 |
39 | let dispatchAction = (queue, action) => {
40 | let update = {
41 | action,
42 | next: null,
43 | };
44 |
45 | if (queue.pending == null) {
46 | update.next = update;
47 | } else {
48 | update.next = queue.pending.next;
49 | queue.pending.next = update;
50 | }
51 |
52 | queue.pending = update;
53 | schedule();
54 | };
55 |
56 | let schedule = () => {
57 | workInProgressHook = fiber.memoizedState;
58 | let app = fiber.startNode();
59 | isMount = false;
60 | return app;
61 | };
62 |
63 | function App() {
64 | let [num, setNum] = useState(5);
65 | let [count, setCount] = useState(15);
66 | console.log(isMount, num, count);
67 | let changeNum = setNum(num => num + 1);
68 | let changeCount = setCount(count => count - 1);
69 | return {
70 | changeNum,
71 | changeCount,
72 | };
73 | }
74 | window.app = schedule();
75 |
--------------------------------------------------------------------------------
/useState/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/useState/index.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * workInProgressHook为单线链表,记录每次工作中useState调用 ,next指向下一次的useState
4 | */
5 | let isMount = true; //Mount还是Update
6 | let workInProgressHook = null; //保存当前的hook
7 | /**
8 | * 保存当前节点/操作的hook/...
9 | */
10 | const fiber = {
11 | startNode: App, //当前组件(当前函数)
12 | memoizedState: null, //保存hooks的数据(链表即 workInProgressHook ),如果是Class则保存this.state
13 | };
14 | /**
15 | *
16 | * @param {any} init 初始值
17 | */
18 | const useState = init => {
19 | let hook; //确定当前执行的hook
20 | if (isMount) {
21 | //首次渲染需要自创建一个hook
22 | hook = {
23 | memoizedState: init, //保存当前执行hook初始的值
24 | next: null, //指向下一个hook
25 | queue: { pending: null }, //保存改变之后新的状态(环状链表)
26 | };
27 | //
28 | if (!fiber.memoizedState) {
29 | //只有一个useState的情况
30 | /**
31 | * let [count, setCount] = useState(0);
32 | */
33 | fiber.memoizedState = hook; //更新当前fiber节点信息
34 | } else {
35 | //多个useState的情况
36 | /**
37 | * let [count, setCount] = useState(0);
38 | * let [num, setNum] = useState(0);
39 | * 第一次在 schedule 中 workInProgressHook = fiber.memoizedState
40 | * 所以处理多个时,将后续挂到next上
41 | */
42 | workInProgressHook.next = hook; //将hook插入workInProgressHook链表的下一个,链接之前创建的useState和刚创建的useState
43 | }
44 | workInProgressHook = hook; //全局指针指向当前创建的hook
45 | } else {
46 | /**
47 | * 更新时, hook无需自创建,直接取全局hook
48 | * 移动workInProgressHook指针
49 | * 1、当前hook指向全局hook
50 | * 2、全局hook指向下一个hook
51 | */
52 | //
53 | hook = workInProgressHook;
54 | workInProgressHook = workInProgressHook.next;
55 | }
56 |
57 | /**
58 | * 以上逻辑可以取到当前useState保存的数据
59 | *
60 | *
61 | *
62 | *
63 | *
64 | * 处理 dispatchAction 之后再来进行
65 | */
66 | let baseState = hook.memoizedState; //update执行前的初始state
67 | if (hook.queue.pending) {
68 | /**
69 | * 调用时触发
70 | * 获取update环状单向链表中第一个update
71 | */
72 | let firstUpdate = hook.queue.pending.next; //拿到第一个update
73 | do {
74 | // 执行update action
75 | const action = firstUpdate.action; //每一次执行的函数,即更新state的具体操作例如: setCount(count => count + 1)
76 | baseState = action(baseState); //拿到函数执行之后的新state,新的状态又被作为老的状态存储,即下一次操作的目标state
77 | firstUpdate = firstUpdate.next; //更新执行下一个setCount,firstUpdate指向他的next
78 | } while (firstUpdate !== hook.queue.pending.next); //每次触发的Update只要不是第一个Update就跳出,否则循环执行
79 | hook.queue.pending = null; // 清空queue.pending链表
80 | hook.memoizedState = baseState; //将update action执行完后的state作为memoizedState
81 | }
82 | /**
83 | * bind() 的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind() 的参数写在 this 后面。
84 | * 当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。
85 | */
86 | //偏函数 :bind将hook.queue提前作为第一个参数进行传入
87 | return [baseState, dispatchAction.bind(null, hook.queue)];
88 | };
89 |
90 | //useState 返回的第二个函数
91 | /**
92 | *
93 | * @param {Object} queue 当前处理的hook
94 | * @param {Function} action 改变state的 函数/值
95 | */
96 | const dispatchAction = (queue, action) => {
97 | /**
98 | * 也会存在多个setCount进行调用,也需要next形成链表,
99 | * 但是保存为环状链表(优先级不一样),每次创建的update即最后一个调用
100 | */
101 | const update = {
102 | //记一次更新
103 | // 环状链表 真实react,计算新的state,每次update更新是有优先级,点击更新高于ajax更新
104 | //创建的update即最后一个update
105 | action,
106 | next: null, //指向下一个update
107 | };
108 | /**
109 | * 函数调用时:
110 | * setData() {
111 | setCount(count => count + 1); queue.pending == null 时触发
112 | setCount(count => count + 1); else 时触发
113 | setCount(count => count + 1); else 时触发
114 | }
115 | */
116 | if (queue.pending == null) {
117 | /**
118 | * 第一次触发更新,queue.pending为初始值null
119 | * u0->u0->u0
120 | */
121 | update.next = update;
122 | } else {
123 | /**
124 | * 多次调用
125 | * 类似一个函数中多次执行
126 | *
127 | * 目标:
128 | * u1->u0->u1
129 | * u2->u0->u1->u2
130 | * u3->u0->u1->u2->u3
131 | *queue.pending 保存最后一个 update
132 | *queue.pending.next 保存第一个 update
133 | *
134 | * 实现:
135 | *
136 | * 1、此时update为正在处理的update
137 | * 此时queue.pending即上一次更新的update,因为update.next = update所以queue.pending.next也为update
138 | *
139 | * 1、调整queue.pending.next方向,指向当前的update,作为下一次的上一次更新,即达成环状链表
140 | */
141 | update.next = queue.pending.next;
142 | queue.pending.next = update;
143 | }
144 | /**
145 | * 第一次:queue.pending = u0
146 | * 多次:queue.pending = u1,保存每次的update到queue.pending
147 | */
148 | queue.pending = update;
149 | schedule();
150 | };
151 | //调度
152 | const schedule = () => {
153 | // 更新前将workInProgressHook重置为fiber保存的第一个Hook
154 | workInProgressHook = fiber.memoizedState;
155 | const app = fiber.startNode();
156 | isMount = false;
157 | return app;
158 | };
159 | /*********************************************************
160 | */
161 | function App() {
162 | let [count, setCount] = useState(5);
163 | let [num, setNum] = useState(10);
164 | console.log("isMount?:", isMount, "count:", count, "num:", num);
165 | let setDate1 = () => {
166 | setCount(count => count + 1);
167 | setCount(count => count + 1);
168 | setNum(num => num - 1);
169 | };
170 | let setDate2 = () => {
171 | setNum(count => count + 1);
172 | setNum(count => count + 1);
173 | setCount(count => count - 1);
174 | };
175 | return {
176 | setDate1,
177 | setDate2,
178 | };
179 | }
180 | window.app = schedule();
181 |
--------------------------------------------------------------------------------