├── .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 | ![Alt](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5506b312f4ae415a89f10860c7e98bd8~tplv-k3u1fbpfcp-zoom-1.image) 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 | ![actionGif.gif](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0b299a5b1f494b84874588013d12d6ca~tplv-k3u1fbpfcp-watermark.image) 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 | ![Alt](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05322d7da1dc48c0a40be7bf7770e25d~tplv-k3u1fbpfcp-zoom-1.image) 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 | ![Alt](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/782ce851ab8a49e79ce56a287233d8a3~tplv-k3u1fbpfcp-zoom-1.image) 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 | ![actionGif.gif](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0b299a5b1f494b84874588013d12d6ca~tplv-k3u1fbpfcp-watermark.image) 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 |