├── .gitignore ├── .idea ├── HowToBuildMyReactHook.iml ├── misc.xml ├── modules.xml └── vcs.xml ├── README.md ├── doc ├── useCallback.js ├── useContext.js ├── useEffect.js ├── useLayoutEffect.js ├── useMemo.js ├── useReducer.js └── useState.js ├── main.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── index.css └── index.js /.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 | -------------------------------------------------------------------------------- /.idea/HowToBuildMyReactHook.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Hook原理 2 | @[toc] 3 | ## 基本准备工作 4 | 利用 **creact-react-app** 创建一个项目 5 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201031145322925.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzk2NDE0OA==,size_16,color_FFFFFF,t_70#pic_center) 6 | 7 | > 已经把项目放到 github:https://github.com/Sunny-lucking/howToBuildMyWebpack。 可以卑微地要个star吗 8 | 9 | ## 手写useState 10 | ### useState的使用 11 | 12 | useState可以在函数组件中,添加state Hook。 13 | 14 | 调用useState会返回一个state变量,以及更新state变量的方法。useState的参数是state变量的初始值,**初始值仅在初次渲染时有效**。 15 | 16 | 17 | **更新state变量的方法,并不会像this.setState一样,合并state。而是替换state变量。** 18 | 下面是一个简单的例子, 会在页面上渲染count的值,点击setCount的按钮会更新count的值。 19 | 20 | 21 | ```js 22 | function App(){ 23 | const [count, setCount] = useState(0); 24 | return ( 25 |
26 | {count} 27 | 34 |
35 | ); 36 | } 37 | ReactDOM.render( 38 | , 39 | document.getElementById('root') 40 | ); 41 | ``` 42 | ### 原理实现 43 | 44 | 45 | ```js 46 | let lastState 47 | function useState(initState) { 48 | lastState = lastState || initState; 49 | function setState(newState) { 50 | lastState = newState 51 | } 52 | return [lastState,setState] 53 | } 54 | function App(){ 55 | //。。。 56 | } 57 | ReactDOM.render( 58 | , 59 | document.getElementById('root') 60 | ); 61 | ``` 62 | 如代码所示,我们自己创建了一个useState方法 63 | 64 | 当我们使用这个方法时,如果是第一次使用,则取initState的值,否则就取上一次的值(laststate). 65 | 66 | 在内部,我们创建了一个setState方法,该方法用于更新state的值 67 | 68 | 然后返回一个lastSate属性和setState方法。 69 | 70 | 看似完美,但是我们其实忽略了一个问题:每次执行玩setState都应该重新渲染当前组件的。 71 | 72 | 所以我们需要在setState里面执行刷新操作 73 | 74 | 75 | ```js 76 | let lastState 77 | function useState(initState) { 78 | lastState = lastState || initState; 79 | function setState(newState) { 80 | lastState = newState 81 | render() 82 | } 83 | return [lastState,setState] 84 | } 85 | function App(){ 86 | const [count, setCount] = useState(0); 87 | return ( 88 |
89 | {count} 90 | 97 |
98 | ); 99 | } 100 | // 新增方法 101 | function render(){ 102 | ReactDOM.render( 103 | , 104 | document.getElementById('root') 105 | ); 106 | } 107 | render() 108 | ``` 109 | 110 | 如代码所示,我们在setState里添加了个render方法。 111 | render方法则会执行 112 | 113 | 114 | ```js 115 | ReactDOM.render( 116 | , 117 | document.getElementById('root') 118 | ); 119 | ``` 120 | 也就是重新渲染啦。 121 | 122 | 好了,现在是不是已经完整了呢? 123 | 124 | 不,还有个问题:就说我们这里只是用了一个useState,要是我们使用了很多个呢?难道要声明很多个全局变量吗? 125 | 126 | 这显然是不行的,所以,我们可以设计一个全局数组来保存这些state 127 | 128 | 129 | ```js 130 | let lastState = [] 131 | let stateIndex = 0 132 | function useState(initState) { 133 | lastState[stateIndex] = lastState[stateIndex] || initState; 134 | const currentIndex = stateIndex 135 | function setState(newState) { 136 | lastState[stateIndex] = newState 137 | render() 138 | } 139 | return [lastState[stateIndex++],setState] 140 | } 141 | ``` 142 | 143 | 这里的currentIndex是利用了闭包的思想,将某个state相应的index记录下来了。 144 | 145 | 好了,useState方法就到这里基本完成了。是不是so easy!!! 146 | 147 | ## React.memo介绍 148 | 149 | 看下面的代码!你发现什么问题? 150 | 151 | ```js 152 | import React ,{useState}from 'react'; 153 | import ReactDOM from 'react-dom'; 154 | import './index.css'; 155 | function Child({data}) { 156 | console.log("天啊,我怎么被渲染啦,我并不希望啊") 157 | return ( 158 |
child
159 | ) 160 | } 161 | function App(){ 162 | const [count, setCount] = useState(0); 163 | return ( 164 |
165 | 166 | 169 |
170 | ); 171 | } 172 | function render(){ 173 | ReactDOM.render( 174 | , 175 | document.getElementById('root') 176 | ); 177 | } 178 | render() 179 | ``` 180 | 没错,就是尽管我们传个子组件的props是固定的值,当父组件的数据更改时,子组件也被重新渲染了,我们是希望当传给子组件的props改变时,才重新渲染子组件。 181 | 182 | 所以引入了React.memo。 183 | 184 | **看看介绍** 185 | 186 | React.memo() 和 PureComponent 很相似,它帮助我们控制何时重新渲染组件。 187 | 188 | 组件仅在它的 props 发生改变的时候进行重新渲染。通常来说,在组件树中 React 组件,只要有变化就会走一遍渲染流程。但是通过 PureComponent 和 React.memo(),我们可以仅仅让某些组件进行渲染。 189 | 190 | ```js 191 | 192 | import React ,{useState,memo}from 'react'; 193 | import ReactDOM from 'react-dom'; 194 | import './index.css'; 195 | function Child({data}) { 196 | console.log("天啊,我怎么被渲染啦,我并不希望啊") 197 | return ( 198 |
child
199 | ) 200 | } 201 | Child = memo(Child) 202 | function App(){ 203 | const [count, setCount] = useState(0); 204 | return ( 205 |
206 | 207 | 210 |
211 | ); 212 | } 213 | function render(){ 214 | ReactDOM.render( 215 | , 216 | document.getElementById('root') 217 | ); 218 | } 219 | render() 220 | 221 | ``` 222 | 223 | 224 | 因此,当Child被memo包装后,就只会当props改变时才会重新渲染了。 225 | 226 | 当然,由于React.memo并不是react-hook的内容,所以这里并不会取讨论它是怎么实现的。 227 | 228 | ## 手写useCallback 229 | ### useCallback的使用 230 | 当我们试图给一个子组件传递一个方法的时候,如下代码所示 231 | 232 | 233 | ```javascript 234 | 235 | import React ,{useState,memo}from 'react'; 236 | import ReactDOM from 'react-dom'; 237 | function Child({data}) { 238 | console.log("天啊,我怎么被渲染啦,我并不希望啊") 239 | return ( 240 |
child
241 | ) 242 | } 243 | // eslint-disable-next-line 244 | Child = memo(Child) 245 | function App(){ 246 | const [count, setCount] = useState(0); 247 | const addClick = ()=>{console.log("addClick")} 248 | return ( 249 |
250 | 251 | 252 | 255 |
256 | ); 257 | } 258 | function render(){ 259 | ReactDOM.render( 260 | , 261 | document.getElementById('root') 262 | ); 263 | } 264 | render() 265 | ``` 266 | 发现我们传了一个addClick方法 是固定的,但是却每一次点击按钮子组件都会重新渲染。 267 | 268 | 这是因为你看似addClick方法没改变,其实旧的和新的addClick是不一样的,如图所示 269 | 270 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201019102340105.png#pic_center) 271 | 272 | 273 | 这时,如果想要,传入的都是同一个方法,就要用到useCallBack。 274 | 275 | 如代码所示 276 | 277 | ```javascript 278 | import React ,{useState,memo,useCallback}from 'react'; 279 | import ReactDOM from 'react-dom'; 280 | function Child({data}) { 281 | console.log("天啊,我怎么被渲染啦,我并不希望啊") 282 | return ( 283 |
child
284 | ) 285 | } 286 | // eslint-disable-next-line 287 | Child = memo(Child) 288 | function App(){ 289 | const [count, setCount] = useState(0); 290 | // eslint-disable-next-line 291 | const addClick = useCallback(()=>{console.log("addClick")},[]) 292 | return ( 293 |
294 | 295 | 296 | 299 |
300 | ); 301 | } 302 | function render(){ 303 | ReactDOM.render( 304 | , 305 | document.getElementById('root') 306 | ); 307 | } 308 | render() 309 | 310 | ``` 311 | useCallback钩子的第一个参数是我们要传递给子组件的方法,第二个参数是一个数组,用于监听数组里的元素变化的时候,才会返回一个新的方法。 312 | 313 | ### 原理实现 314 | 315 | 我们知道useCallback有两个参数,所以可以先写 316 | 317 | ```javascript 318 | function useCallback(callback,lastCallbackDependencies){ 319 | 320 | 321 | } 322 | ``` 323 | 跟useState一样,我们同样需要用全局变量把callback和dependencies保存下来。 324 | 325 | ```javascript 326 | let lastCallback 327 | let lastCallbackDependencies 328 | function useCallback(callback,dependencies){ 329 | 330 | } 331 | ``` 332 | 333 | 首先useCallback会判断我们是否传入了依赖项,如果没有传的话,说明要每一次执行useCallback都返回最新的callback 334 | 335 | 336 | ```javascript 337 | let lastCallback 338 | let lastCallbackDependencies 339 | function useCallback(callback,dependencies){ 340 | if(lastCallbackDependencies){ 341 | 342 | }else{ // 没有传入依赖项 343 | 344 | 345 | } 346 | return lastCallback 347 | } 348 | ``` 349 | 350 | 351 | 352 | 所以当我们没有传入依赖项的时候,实际上可以把它当作第一次执行,因此,要把lastCallback和lastCallbackDependencies重新赋值 353 | 354 | ```javascript 355 | let lastCallback 356 | let lastCallbackDependencies 357 | function useCallback(callback,dependencies){ 358 | if(lastCallbackDependencies){ 359 | 360 | }else{ // 没有传入依赖项 361 | 362 | lastCallback = callback 363 | lastCallbackDependencies = dependencies 364 | } 365 | return lastCallback 366 | } 367 | ``` 368 | 当有传入依赖项的时候,需要看看新的依赖数组的每一项和来的依赖数组的每一项的值是否相等 369 | 370 | ```javascript 371 | let lastCallback 372 | let lastCallbackDependencies 373 | function useCallback(callback,dependencies){ 374 | if(lastCallbackDependencies){ 375 | let changed = !dependencies.every((item,index)=>{ 376 | return item === lastCallbackDependencies[index] 377 | }) 378 | }else{ // 没有传入依赖项 379 | 380 | lastCallback = callback 381 | lastCallbackDependencies = dependencies 382 | } 383 | return lastCallback 384 | } 385 | function Child({data}) { 386 | console.log("天啊,我怎么被渲染啦,我并不希望啊") 387 | return ( 388 |
child
389 | ) 390 | } 391 | ``` 392 | 当依赖项有值改变的时候,我们需要对lastCallback和lastCallbackDependencies重新赋值 393 | 394 | ```javascript 395 | import React ,{useState,memo}from 'react'; 396 | import ReactDOM from 'react-dom'; 397 | let lastCallback 398 | // eslint-disable-next-line 399 | let lastCallbackDependencies 400 | function useCallback(callback,dependencies){ 401 | if(lastCallbackDependencies){ 402 | let changed = !dependencies.every((item,index)=>{ 403 | return item === lastCallbackDependencies[index] 404 | }) 405 | if(changed){ 406 | lastCallback = callback 407 | lastCallbackDependencies = dependencies 408 | } 409 | }else{ // 没有传入依赖项 410 | 411 | lastCallback = callback 412 | lastCallbackDependencies = dependencies 413 | } 414 | return lastCallback 415 | } 416 | function Child({data}) { 417 | console.log("天啊,我怎么被渲染啦,我并不希望啊") 418 | return ( 419 |
child
420 | ) 421 | } 422 | // eslint-disable-next-line 423 | Child = memo(Child) 424 | function App(){ 425 | const [count, setCount] = useState(0); 426 | // eslint-disable-next-line 427 | const addClick = useCallback(()=>{console.log("addClick")},[]) 428 | return ( 429 |
430 | 431 | 432 | 435 |
436 | ); 437 | } 438 | function render(){ 439 | ReactDOM.render( 440 | , 441 | document.getElementById('root') 442 | ); 443 | } 444 | render() 445 | 446 | ``` 447 | 448 | ## 手写useMemo 449 | ### 使用 450 | useMemo和useCallback类似,不过useCallback用于缓存函数,而useMemo用于缓存函数返回值 451 | 452 | ```javascript 453 | let data = useMemo(()=> ({number}),[number]) 454 | ``` 455 | 如代码所示,利用useMemo用于缓存函数的返回值number,并且当只有监听元素为[number],也就是说,当number的值发生改变的时候,才会重新执行 456 | 457 | ```javascript 458 | ()=> ({number}) 459 | ``` 460 | 然后返回新的number 461 | ### 原理 462 | 所以,useMemo的原理跟useCallback的差不多,仿写即可。 463 | ```javascript 464 | import React ,{useState,memo,}from 'react'; 465 | import ReactDOM from 'react-dom'; 466 | let lastMemo 467 | // eslint-disable-next-line 468 | let lastMemoDependencies 469 | function useMemo(callback,dependencies){ 470 | if(lastMemoDependencies){ 471 | let changed = !dependencies.every((item,index)=>{ 472 | return item === lastMemoDependencies[index] 473 | }) 474 | if(changed){ 475 | lastMemo = callback() 476 | lastMemoDependencies = dependencies 477 | } 478 | }else{ // 没有传入依赖项 479 | lastMemo = callback() 480 | lastMemoDependencies = dependencies 481 | } 482 | return lastMemo 483 | } 484 | function Child({data}) { 485 | console.log("天啊,我怎么被渲染啦,我并不希望啊") 486 | return ( 487 |
child
488 | ) 489 | } 490 | // eslint-disable-next-line 491 | Child = memo(Child) 492 | function App(){ 493 | const [count, setCount] = useState(0); 494 | // eslint-disable-next-line 495 | const [number, setNumber] = useState(20) 496 | let data = useMemo(()=> ({number}),[number]) 497 | return ( 498 |
499 | 500 | 501 | 504 |
505 | ); 506 | } 507 | function render(){ 508 | ReactDOM.render( 509 | , 510 | document.getElementById('root') 511 | ); 512 | } 513 | render() 514 | ``` 515 | 516 | ## 手写useReducer 517 | ### 使用 518 | 先简单介绍下useReducer。 519 | 520 | ```javascript 521 | const [state, dispatch] = useReducer(reducer, initState); 522 | ``` 523 | **useReducer接收两个参数:** 524 | 525 | 第一个参数:reducer函数,第二个参数:初始化的state。 526 | 527 | 返回值为最新的state和dispatch函数(用来触发reducer函数,计算对应的state)。 528 | 529 | 按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。 530 | 531 | 听起来比较抽象,我们先看一个简单的例子: 532 | 533 | ```javascript 534 | // 官方 useReducer Demo 535 | // 第一个参数:应用的初始化 536 | const initialState = {count: 0}; 537 | 538 | // 第二个参数:state的reducer处理函数 539 | function reducer(state, action) { 540 | switch (action.type) { 541 | case 'increment': 542 | return {count: state.count + 1}; 543 | case 'decrement': 544 | return {count: state.count - 1}; 545 | default: 546 | throw new Error(); 547 | } 548 | } 549 | 550 | function Counter() { 551 | // 返回值:最新的state和dispatch函数 552 | const [state, dispatch] = useReducer(reducer, initialState); 553 | return ( 554 | <> 555 | // useReducer会根据dispatch的action,返回最终的state,并触发rerender 556 | Count: {state.count} 557 | // dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态 558 | 559 | 560 | 561 | ); 562 | } 563 | ``` 564 | 其实意思可以简单的理解为,当state是基本数据类型的时候,可以用useState,当state是对象的时候,可以用reducer,当然这只是一种简单的想法。大家不必引以为意。具体情况视具体场景分析。 565 | 566 | ### 原理 567 | 看原理你会发现十分简单,简单到不用我说什么,不到十行代码,不信你直接看代码 568 | 569 | ```javascript 570 | import React from 'react'; 571 | import ReactDOM from 'react-dom'; 572 | 573 | let lastState 574 | // useReducer原理 575 | function useReducer(reducer,initialState){ 576 | lastState = lastState || initialState 577 | function dispatch(action){ 578 | lastState = reducer(lastState,action) 579 | render() 580 | } 581 | return [lastState,dispatch] 582 | } 583 | 584 | // 官方 useReducer Demo 585 | // 第一个参数:应用的初始化 586 | const initialState = {count: 0}; 587 | 588 | // 第二个参数:state的reducer处理函数 589 | function reducer(state, action) { 590 | switch (action.type) { 591 | case 'increment': 592 | return {count: state.count + 1}; 593 | case 'decrement': 594 | return {count: state.count - 1}; 595 | default: 596 | throw new Error(); 597 | } 598 | } 599 | 600 | function Counter() { 601 | // 返回值:最新的state和dispatch函数 602 | const [state, dispatch] = useReducer(reducer, initialState); 603 | return ( 604 | <> 605 | {/* // useReducer会根据dispatch的action,返回最终的state,并触发rerender */} 606 | Count: {state.count} 607 | {/* // dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态 */} 608 | 609 | 610 | 611 | ); 612 | } 613 | function render(){ 614 | ReactDOM.render( 615 | , 616 | document.getElementById('root') 617 | ); 618 | } 619 | render() 620 | ``` 621 | 622 | ## 手写useContext 623 | ### 使用 624 | createContext 能够创建一个 React 的 上下文(context),然后订阅了这个上下文的组件中,可以拿到上下文中提供的数据或者其他信息。 625 | 626 | 基本的使用方法: 627 | 628 | ```javascript 629 | const MyContext = React.createContext() 630 | ``` 631 | 如果要使用创建的上下文,需要通过 Context.Provider 最外层包装组件,并且需要显示的通过 `` 的方式传入 value,指定 context 要对外暴露的信息。 632 | 633 | 子组件在匹配过程中只会匹配最新的 Provider,也就是说如果有下面三个组件:`ContextA.Provider->A->ContexB.Provider->B->C` 634 | 635 | 如果 ContextA 和 ContextB 提供了相同的方法,则 C 组件只会选择 ContextB 提供的方法。 636 | 637 | 通过 React.createContext 创建出来的上下文,在子组件中可以通过 useContext 这个 Hook 获取 Provider 提供的内容 638 | 639 | ```javascript 640 | const {funcName} = useContext(MyContext); 641 | ``` 642 | 643 | 从上面代码可以发现,useContext 需要将 MyContext 这个 Context 实例传入,不是字符串,就是实例本身。 644 | 645 | 这种用法会存在一个比较尴尬的地方,父子组件不在一个目录中,如何共享 MyContext 这个 Context 实例呢? 646 | 647 | 一般这种情况下,我会通过 Context Manager 统一管理上下文的实例,然后通过 export 将实例导出,在子组件中在将实例 import 进来。 648 | 649 | 下面我们看看代码,使用起来非常简单 650 | 651 | ```javascript 652 | import React, { useState, useContext } from 'react'; 653 | import ReactDOM from 'react-dom'; 654 | let AppContext = React.createContext() 655 | function Counter() { 656 | let { state, setState } = useContext(AppContext) 657 | return ( 658 | <> 659 | Count: {state.count} 660 | 661 | 662 | 663 | ); 664 | } 665 | function App() { 666 | let [state, setState] = useState({ number: 0 }) 667 | return ( 668 | 669 |
670 |

{state.number}

671 | 672 |
673 |
674 | ) 675 | } 676 | function render() { 677 | ReactDOM.render( 678 | , 679 | document.getElementById('root') 680 | ); 681 | } 682 | render() 683 | ``` 684 | 要是用过vue的同学,会发现,这个机制有点类似vue 中提供的provide和inject 685 | 686 | ### 原理 687 | 原理非常简单,由于createContext,Provider 不是ReactHook的内容, 688 | 所以这里值需要实现useContext,如代码所示,只需要一行代码 689 | 690 | ```javascript 691 | import React, { useState } from 'react'; 692 | import ReactDOM from 'react-dom'; 693 | let AppContext = React.createContext() 694 | function useContext(context){ 695 | return context._currentValue 696 | } 697 | function Counter() { 698 | let { state, setState } = useContext(AppContext) 699 | return ( 700 | <> 701 | 702 | 703 | ); 704 | } 705 | function App() { 706 | let [state, setState] = useState({ number: 0 }) 707 | return ( 708 | 709 |
710 |

{state.number}

711 | 712 |
713 |
714 | ) 715 | } 716 | function render() { 717 | ReactDOM.render( 718 | , 719 | document.getElementById('root') 720 | ); 721 | } 722 | render() 723 | ``` 724 | ## 手写useEffect 725 | ### 使用 726 | 它跟class组件中的componentDidMount,componentDidUpdate,componentWillUnmount具有相同的用途,只不过被合成了一个api。 727 | 728 | ```javascript 729 | import React, { useState, useEffect} from 'react'; 730 | import ReactDOM from 'react-dom'; 731 | 732 | function App() { 733 | let [number, setNumber] = useState(0) 734 | useEffect(()=>{ 735 | console.log(number); 736 | },[number]) 737 | return ( 738 | 739 |
740 |

{number}

741 | 742 |
743 | ) 744 | } 745 | function render() { 746 | ReactDOM.render( 747 | , 748 | document.getElementById('root') 749 | ); 750 | } 751 | render() 752 | ``` 753 | 如代码所示,支持两个参数,第二个参数也是用于监听的。 754 | 当监听数组中的元素有变化的时候再执行作为第一个参数的执行函数 755 | 756 | ### 原理 757 | 758 | 原理发现其实和useMemo,useCallback类似,只不过,前面前两个有返回值,而useEffect没有。(当然也有返回值,就是那个执行componentWillUnmount函功能的时候写的返回值,但是这里返回值跟前两个作用不一样,因为你不会写 759 | 760 | ```javascript 761 | let xxx = useEffect(()=>{ 762 | console.log(number); 763 | },[number]) 764 | ``` 765 | 来接收返回值。 766 | 767 | 所以,忽略返回值,你可以直接看代码,真的很类似,简直可以用一模一样来形容 768 | 769 | ```javascript 770 | import React, { useState} from 'react'; 771 | import ReactDOM from 'react-dom'; 772 | let lastEffectDependencies 773 | function useEffect(callback,dependencies){ 774 | if(lastEffectDependencies){ 775 | let changed = !dependencies.every((item,index)=>{ 776 | return item === lastEffectDependencies[index] 777 | }) 778 | if(changed){ 779 | callback() 780 | lastEffectDependencies = dependencies 781 | } 782 | }else{ 783 | callback() 784 | lastEffectDependencies = dependencies 785 | } 786 | } 787 | function App() { 788 | let [number, setNumber] = useState(0) 789 | useEffect(()=>{ 790 | console.log(number); 791 | },[number]) 792 | return ( 793 | 794 |
795 |

{number}

796 | 797 |
798 | ) 799 | } 800 | function render() { 801 | ReactDOM.render( 802 | , 803 | document.getElementById('root') 804 | ); 805 | } 806 | render() 807 | ``` 808 | 809 | 你以为这样就结束了,其实还没有,因为第一个参数的执行时机错了,实际上作为第一个参数的函数因为是在**浏览器渲染结束后执行**的。而这里我们是同步执行的。 810 | 811 | 所以需要改成异步执行callback 812 | 813 | ```javascript 814 | import React, { useState} from 'react'; 815 | import ReactDOM from 'react-dom'; 816 | let lastEffectDependencies 817 | function useEffect(callback,dependencies){ 818 | if(lastEffectDependencies){ 819 | let changed = !dependencies.every((item,index)=>{ 820 | return item === lastEffectDependencies[index] 821 | }) 822 | if(changed){ 823 | setTimeout(callback()) 824 | lastEffectDependencies = dependencies 825 | } 826 | }else{ 827 | setTimeout(callback()) 828 | lastEffectDependencies = dependencies 829 | } 830 | } 831 | function App() { 832 | let [number, setNumber] = useState(0) 833 | useEffect(()=>{ 834 | console.log(number); 835 | },[number]) 836 | return ( 837 | 838 |
839 |

{number}

840 | 841 |
842 | ) 843 | } 844 | function render() { 845 | ReactDOM.render( 846 | , 847 | document.getElementById('root') 848 | ); 849 | } 850 | render() 851 | ``` 852 | 853 | ## 手写useLayoutEffect 854 | ### 使用 855 | 官方解释,这两个hook基本相同,调用时机不同,请全部使用useEffect,除非遇到bug或者不可解决的问题,再考虑使用useLayoutEffect。 856 | ### 原理 857 | 原理跟useEffect一样,只是调用时机不同 858 | 859 | 上面说到useEffect的调用时机是**浏览器渲染结束后执行**的,而useLayoutEffect是在**DOM构建完成,浏览器渲染前执行**的。 860 | 861 | 所以这里需要把宏任务setTimeout改成微任务 862 | 863 | ```javascript 864 | import React, { useState} from 'react'; 865 | import ReactDOM from 'react-dom'; 866 | let lastEffectDependencies 867 | function useLayouyEffect(callback,dependencies){ 868 | if(lastEffectDependencies){ 869 | let changed = !dependencies.every((item,index)=>{ 870 | return item === lastEffectDependencies[index] 871 | }) 872 | if(changed){ 873 | Promise.resolve().then(callback()) 874 | lastEffectDependencies = dependencies 875 | } 876 | }else{ 877 | Promise.resolve().then(callback()) 878 | lastEffectDependencies = dependencies 879 | } 880 | } 881 | function App() { 882 | let [number, setNumber] = useState(0) 883 | useLayouyEffect(()=>{ 884 | console.log(number); 885 | },[number]) 886 | return ( 887 | 888 |
889 |

{number}

890 | 891 |
892 | ) 893 | } 894 | function render() { 895 | ReactDOM.render( 896 | , 897 | document.getElementById('root') 898 | ); 899 | } 900 | render() 901 | ``` 902 | > 恭喜你阅读到这里,又变强了有没有 903 | > 已经把项目放到 github:https://github.com/Sunny-lucking/howToBuildMyWebpack。 顺便可以卑微地要个star吗 904 | 905 | > 文章首发于公众号《前端阳光》 906 | -------------------------------------------------------------------------------- /doc/useCallback.js: -------------------------------------------------------------------------------- 1 | import React ,{useState,memo}from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | let lastCallback 4 | // eslint-disable-next-line 5 | let lastCallbackDependencies 6 | function useCallback(callback,dependencies){ 7 | if(lastCallbackDependencies){ 8 | let changed = !dependencies.every((item,index)=>{ 9 | return item === lastCallbackDependencies[index] 10 | }) 11 | if(changed){ 12 | lastCallback = callback 13 | lastCallbackDependencies = dependencies 14 | } 15 | }else{ // 没有传入依赖项 16 | 17 | lastCallback = callback 18 | lastCallbackDependencies = dependencies 19 | } 20 | return lastCallback 21 | } 22 | function Child({data}) { 23 | console.log("天啊,我怎么被渲染啦,我并不希望啊") 24 | return ( 25 |
child
26 | ) 27 | } 28 | // eslint-disable-next-line 29 | Child = memo(Child) 30 | function App(){ 31 | const [count, setCount] = useState(0); 32 | // eslint-disable-next-line 33 | const addClick = useCallback(()=>{console.log("addClick")},[]) 34 | return ( 35 |
36 | 37 | 38 | 41 |
42 | ); 43 | } 44 | function render(){ 45 | ReactDOM.render( 46 | , 47 | document.getElementById('root') 48 | ); 49 | } 50 | render() 51 | -------------------------------------------------------------------------------- /doc/useContext.js: -------------------------------------------------------------------------------- 1 | mport React, { useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | let AppContext = React.createContext() 4 | function useContext(context){ 5 | return context._currentValue 6 | } 7 | function Counter() { 8 | let { state, setState } = useContext(AppContext) 9 | return ( 10 | <> 11 | 12 | 13 | ); 14 | } 15 | function App() { 16 | let [state, setState] = useState({ number: 0 }) 17 | return ( 18 | 19 |
20 |

{state.number}

21 | 22 |
23 |
24 | ) 25 | } 26 | function render() { 27 | ReactDOM.render( 28 | , 29 | document.getElementById('root') 30 | ); 31 | } 32 | render() 33 | -------------------------------------------------------------------------------- /doc/useEffect.js: -------------------------------------------------------------------------------- 1 | import React, { useState} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | let lastEffectDependencies 4 | function useEffect(callback,dependencies){ 5 | if(lastEffectDependencies){ 6 | let changed = !dependencies.every((item,index)=>{ 7 | return item === lastEffectDependencies[index] 8 | }) 9 | if(changed){ 10 | setTimeout(callback()) 11 | lastEffectDependencies = dependencies 12 | } 13 | }else{ 14 | setTimeout(callback()) 15 | lastEffectDependencies = dependencies 16 | } 17 | } 18 | function App() { 19 | let [number, setNumber] = useState(0) 20 | useEffect(()=>{ 21 | console.log(number); 22 | },[number]) 23 | return ( 24 | 25 |
26 |

{number}

27 | 28 |
29 | ) 30 | } 31 | function render() { 32 | ReactDOM.render( 33 | , 34 | document.getElementById('root') 35 | ); 36 | } 37 | render() 38 | -------------------------------------------------------------------------------- /doc/useLayoutEffect.js: -------------------------------------------------------------------------------- 1 | import React, { useState} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | let lastEffectDependencies 4 | function useLayouyEffect(callback,dependencies){ 5 | if(lastEffectDependencies){ 6 | let changed = !dependencies.every((item,index)=>{ 7 | return item === lastEffectDependencies[index] 8 | }) 9 | if(changed){ 10 | Promise.resolve().then(callback()) 11 | lastEffectDependencies = dependencies 12 | } 13 | }else{ 14 | Promise.resolve().then(callback()) 15 | lastEffectDependencies = dependencies 16 | } 17 | } 18 | function App() { 19 | let [number, setNumber] = useState(0) 20 | useLayouyEffect(()=>{ 21 | console.log(number); 22 | },[number]) 23 | return ( 24 | 25 |
26 |

{number}

27 | 28 |
29 | ) 30 | } 31 | function render() { 32 | ReactDOM.render( 33 | , 34 | document.getElementById('root') 35 | ); 36 | } 37 | render() 38 | -------------------------------------------------------------------------------- /doc/useMemo.js: -------------------------------------------------------------------------------- 1 | import React ,{useState,memo,}from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | let lastMemo 4 | // eslint-disable-next-line 5 | let lastMemoDependencies 6 | function useMemo(callback,dependencies){ 7 | if(lastMemoDependencies){ 8 | let changed = !dependencies.every((item,index)=>{ 9 | return item === lastMemoDependencies[index] 10 | }) 11 | if(changed){ 12 | lastMemo = callback() 13 | lastMemoDependencies = dependencies 14 | } 15 | }else{ // 没有传入依赖项 16 | lastMemo = callback() 17 | lastMemoDependencies = dependencies 18 | } 19 | return lastMemo 20 | } 21 | function Child({data}) { 22 | console.log("天啊,我怎么被渲染啦,我并不希望啊") 23 | return ( 24 |
child
25 | ) 26 | } 27 | // eslint-disable-next-line 28 | Child = memo(Child) 29 | function App(){ 30 | const [count, setCount] = useState(0); 31 | // eslint-disable-next-line 32 | const [number, setNumber] = useState(20) 33 | let data = useMemo(()=> ({number}),[number]) 34 | return ( 35 |
36 | 37 | 38 | 41 |
42 | ); 43 | } 44 | function render(){ 45 | ReactDOM.render( 46 | , 47 | document.getElementById('root') 48 | ); 49 | } 50 | render() 51 | -------------------------------------------------------------------------------- /doc/useReducer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | let lastState 5 | // useReducer原理 6 | function useReducer(reducer,initialState){ 7 | lastState = lastState || initialState 8 | function dispatch(action){ 9 | lastState = reducer(lastState,action) 10 | render() 11 | } 12 | return [lastState,dispatch] 13 | } 14 | 15 | // 官方 useReducer Demo 16 | // 第一个参数:应用的初始化 17 | const initialState = {count: 0}; 18 | 19 | // 第二个参数:state的reducer处理函数 20 | function reducer(state, action) { 21 | switch (action.type) { 22 | case 'increment': 23 | return {count: state.count + 1}; 24 | case 'decrement': 25 | return {count: state.count - 1}; 26 | default: 27 | throw new Error(); 28 | } 29 | } 30 | 31 | function Counter() { 32 | // 返回值:最新的state和dispatch函数 33 | const [state, dispatch] = useReducer(reducer, initialState); 34 | return ( 35 | <> 36 | {/* // useReducer会根据dispatch的action,返回最终的state,并触发rerender */} 37 | Count: {state.count} 38 | {/* // dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态 */} 39 | 40 | 41 | 42 | ); 43 | } 44 | function render(){ 45 | ReactDOM.render( 46 | , 47 | document.getElementById('root') 48 | ); 49 | } 50 | render() 51 | -------------------------------------------------------------------------------- /doc/useState.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | let lastState = [] 5 | let stateIndex = 0 6 | function useState(initState) { 7 | lastState[stateIndex] = lastState[stateIndex] || initState; 8 | const currentIndex = stateIndex 9 | function setState(newState) { 10 | lastState[stateIndex] = newState 11 | render() 12 | } 13 | return [lastState[stateIndex++],setState] 14 | } 15 | function App(){ 16 | const [count, setCount] = useState(0); 17 | const [sum, setSum] = useState(10) 18 | return ( 19 |
20 | {count} 21 |
22 | {sum} 23 | 31 |
32 | ); 33 | } 34 | function render(){ 35 | ReactDOM.render( 36 | , 37 | document.getElementById('root') 38 | ); 39 | } 40 | render() 41 | 42 | 43 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const {app,BrowserWindow} = require('electron') 2 | app.on('ready',()=>{ 3 | let mainWindow = new BrowserWindow({ 4 | width: 800, 5 | height: 600, 6 | webPreferences: { 7 | nodeIntegration:true // 8 | } 9 | }) 10 | mainWindow.loadURL('http://localhost:3000') 11 | mainWindow.webContents.openDevTools() 12 | 13 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "bootstrap": "^4.5.2", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-scripts": "3.4.3" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject", 19 | "dev": "concurrently \"wait-on http://localhost:3000 && electron .\" \"cross-env BROWSER=none npm start\"" 20 | }, 21 | "main": "main.js", 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | }, 37 | "devDependencies": { 38 | "concurrently": "^5.3.0", 39 | "cross-env": "^7.0.2", 40 | "electron": "^10.1.2", 41 | "wait-on": "^5.2.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunny-lucking/HowToBuildMyReactHook/b670d602c6a9704d8590a62cfcbc544bb9e4d737/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/Sunny-lucking/HowToBuildMyReactHook/b670d602c6a9704d8590a62cfcbc544bb9e4d737/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunny-lucking/HowToBuildMyReactHook/b670d602c6a9704d8590a62cfcbc544bb9e4d737/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/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React ,{useState,memo}from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | function Child({data}) { 5 | console.log("天啊,我怎么被渲染啦,我并不希望啊") 6 | return ( 7 |
child
8 | ) 9 | } 10 | Child = memo(Child) 11 | function App(){ 12 | const [count, setCount] = useState(0); 13 | return ( 14 |
15 | 16 | 19 |
20 | ); 21 | } 22 | function render(){ 23 | ReactDOM.render( 24 | , 25 | document.getElementById('root') 26 | ); 27 | } 28 | render() 29 | 30 | 31 | --------------------------------------------------------------------------------