• 一步步实现React-Hooks核心原理


    React Hooks已经推出一段时间,大家应该比较熟悉,或者多多少少在项目中用过。写这篇文章简单分析一下Hooks的原理,并带大家实现一个简易版的Hooks。

    这篇写的比较细,相关的知识点都会解释,给大家刷新一下记忆。

    Hooks

    Hooks是React 16.8推出的新功能。以这种更简单的方式进行逻辑复用。之前函数组件被认为是无状态的。但是通过Hooks,函数组件也可以有状态,以及类组件的生命周期方法。

    useState用法示例:

    import React, { useState } from 'react';
    
    function Example() {
      // count是组件的状态
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>        Click me      </button>
        </div>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    闭包

    开始之前,我们来简单回顾一下闭包的概念,因为Hooks的实现是高度依赖闭包的。

    闭包(Closure),Kyle Simpson在《你不知道的Javascript》中总结闭包是:

    Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.

    闭包就是,函数可以访问到它所在的词法作用域,即使是在定义以外的位置调用。

    闭包的一个重要应用就是,实现内部变量/私有数据。

    var counter = 0;
    
    // 给计数器加1
    function add() {
      counter += 1;
    }
    
    // 调用 add() 3次
    add(); // 1
    add(); // 2
    counter = 1000;
    add(); // 1003
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这里因为counter不是内部变量,所以谁都能修改它的值。我们不想让人随意修改counter怎么办?这时候就可以用闭包:参考 前端手写面试题详细解答

    function getAdd() {
      var counter = 0;
      return function add() {counter += 1;}
    }
    var add = getAdd();
    add(); // 1
    add(); // 2
    add(); // 3
    counter = 1000 // error! 当前位置无法访问counter
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们还可以把函数的定义挪到调用的位置,用一个立即执行函数表达式IIFE(Immediately Invoked Function Expression):

    var add = (function getAdd() {
      var counter = 0;
      return function add() {counter += 1;}
    })();
    add(); // 1
    add(); // 2
    add(); // 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这种通过IIFE创建闭包的方式也叫做模块模式(Module Pattern),它创建了一个封闭的作用域,只有通过返回的对象/方法来操纵作用域中的值。这个模式由来已久了,之前很多Javascript的库,比如jQuery,就是用它来导出自己的实例的。

    开始动手实现

    理清闭包的概念后可以着手写了。从简单的入手,先来实现setState。

    function useState(initialValue) {
      var _val = initialValue; // _val是useState的变量
      function state() {
        // state是一个内部函数,是闭包
        return _val;
      }
      function setState(newVal) {
        _val = newVal;
      }
      return [state, setState];
    }
    var [foo, setFoo] = useState(0);
    console.log(foo()); // 0
    setFoo(1);
    console.log(foo()) // 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    根据useState的定义来实现。比较简单不需要多解释。

    将useState应用到组件中

    现在我们将这个简易版的useState应用到一个Counter组件中:

    function Counter() {
      const [count, setCount] = useState(0);
      return {
        click: () => setCount(count() + 1),
        render: () => console.log('render:', { count: count() })
      }
    }
    const C = Counter();
    C.render(); // render: { count: 0 }
    C.click();
    C.render(); // render: { count: 1 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里简单起见,就不render真实DOM了,因为我们只关心组件的状态,所以每次render的时候打印count的值。

    这里点击click之后,counter的值加一,useState的基本功能实现了。但现在state是一个函数而不是一个变量,这和React的API不一致,接下来我们就来改正这一点。

    过期闭包

    function useState(initialValue) {
      var _val = initialValue
      // 去掉了state()函数
      function setState(newVal) {
        _val = newVal
      }
      return [_val, setState] //直接返回_val
    }
    var [foo, setFoo] = useState(0)
    console.log(foo) // 0
    setFoo(1) // 更新_val
    console.log(foo) // 0 - BUG!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    如果我们直接把state从函数改成变量,问题就出现了,state不更新了。无论点击几次,Counter的值始终不变。这个是过期闭包问题(Stale Closure Problem)。因为在useState返回的时候,state就指向了初始值,所以后面即使counter的值改变了,打印出来的仍然就旧值。我们想要的是,返回一个变量的同时,还能让这个变量和真实状态同步。那如何来实现呢?

    模块模式

    解决办法就是将闭包放在另一个闭包中。

    const MyReact = (function() {
      let _val //将_val提升到外层闭包
      return {
        render(Component) {
          const Comp = Component()
          Comp.render()
          return Comp
        },
        useState(initialValue) {
          _val = _val || initialValue //每次刷新
          function setState(newVal) {
            _val = newVal
          }
          return [_val, setState]
        }
      }
    })()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们运用之前提到的模块模式,创建一个MyReact模块(第一层闭包),返回的对象中包含useState方法(第二层闭包)。useState返回值中的state,指向的是useState闭包中的_val,而每次调用useState,_val都会重新绑定到上层的_val上,保证返回的state的值是最新的。解决了过期闭包的问题。

    MyReact还提供了另外一个方法render,方法中调用组件的render方法来“渲染”组件,也是为了不渲染DOM的情况下进行测试。

    function Counter() {
      const [count, setCount] = MyReact.useState(0)
      return {
        click: () => setCount(count + 1),
        render: () => console.log('render:', { count })
      }
    }
    let App
    App = MyReact.render(Counter) // render: { count: 0 }
    App.click()
    App = MyReact.render(Counter) // render: { count: 1 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里每次调用MyReact.render(Counter),都会生成新的Counter实例,调用实例的render方法。render方法中调用了MyReact.useState()。MyReact.useState()在多次执行之间,外层闭包中的_val值保持不变,所以count会绑定到当前的_val上,这样就可以打印出正确的count值了。

    实现useEffect

    实现了useState之后,接下来实现useEffect。

    const MyReact = (function() {
      let _val, _deps // 将状态和依赖数组保存到外层的闭包中
      return {
        render(Component) {
          const Comp = Component()
          Comp.render()
          return Comp
        },
        useEffect(callback, depArray) {
          const hasNoDeps = !depArray
          const hasChangedDeps = _deps ? !depArray.every((el, i) => el === _deps[i]) : true
          if (hasNoDeps || hasChangedDeps) {
            callback()
            _deps = depArray
          }
        },
        useState(initialValue) {
          _val = _val || initialValue
          function setState(newVal) {
            _val = newVal
          }
          return [_val, setState]
        }
      }
    })()
    
    // usage
    function Counter() {
      const [count, setCount] = MyReact.useState(0)
      MyReact.useEffect(() => {
        console.log('effect', count)
      }, [count])
      return {
        click: () => setCount(count + 1),
        noop: () => setCount(count),
        render: () => console.log('render', { count })
      }
    }
    let App
    App = MyReact.render(Counter)
    // effect 0
    // render {count: 0}
    App.click()
    App = MyReact.render(Counter)
    // effect 1
    // render {count: 1}
    App.noop()
    App = MyReact.render(Counter)
    // // 没有执行effect
    // render {count: 1}
    App.click()
    App = MyReact.render(Counter)
    // effect 2
    // render {count: 2}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    在MyReact.useEffect中,我们将依赖数组保存到_deps,每次调用,都和前一次的依赖数组进行比对。发生变化才触发回调。

    注意这里在比较依赖时用的是Object.is, React在比较state变化时也是用它。注意Object.is在比较时不会做类型转换(和==不同)。另外NaN === NaN返回false,但是Object.is(NaN, NaN)会返回true。

    (简单起见,我们实现的useEffect,回调函数是同步执行的,所以打印出来的log是effect先执行,然后才是render。实际React中useEffect的回调函数应该是异步执行的)

    支持多个Hooks

    到此为止我们已经简单实现了useState和useEffect。但还有一个问题,就是useState和useEffect每个组件中只能用一次。

    那么怎么才能支持使用多次hooks呢,我们可以将hooks保存到一个数组中。

    const MyReact = (function() {
      let hooks = [],
        currentHook = 0 // 存储hooks的数组,和数组指针
      return {
        render(Component) {
          const Comp = Component() // 执行effect
          Comp.render()
          currentHook = 0 // 每次render后,hooks的指针清零
          return Comp
        },
        useEffect(callback, depArray) {
          const hasNoDeps = !depArray
          const deps = hooks[currentHook]
          const hasChangedDeps = deps ? !depArray.some((el, i) => !Object.is(el, deps[i])) : true
          if (hasNoDeps || hasChangedDeps) {
            callback()
            hooks[currentHook] = depArray
          }
          currentHook++ // 每调用一次指针加一
        },
        useState(initialValue) {
          hooks[currentHook] = hooks[currentHook] || initialValue
          const setStateHookIndex = currentHook // 注意⚠️这句不是没用。是避免过期闭包问题。
          const setState = newState => (hooks[setStateHookIndex] = newState)
          return [hooks[currentHook++], setState]
        }
      }
    })()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    注意这里用了一个新的变量setStateHookIndex来保存currentHook的值。这是为了避免useState闭包包住旧的currentHook的值。

    将改动应用到组件中:

    function Counter() {
      const [count, setCount] = MyReact.useState(0)
      const [text, setText] = MyReact.useState('foo') // 第二次用了useState
      MyReact.useEffect(() => {
        console.log('effect', count, text)
      }, [count, text])
      return {
        click: () => setCount(count + 1),
        type: txt => setText(txt),
        noop: () => setCount(count),
        render: () => console.log('render', { count, text })
      }
    }
    let App
    App = MyReact.render(Counter)
    // effect 0 foo
    // render {count: 0, text: 'foo'}
    App.click()
    App = MyReact.render(Counter)
    // effect 1 foo
    // render {count: 1, text: 'foo'}
    App.type('bar')
    App = MyReact.render(Counter)
    // effect 1 bar
    // render {count: 1, text: 'bar'}
    App.noop()
    App = MyReact.render(Counter)
    // // 不运行effect
    // render {count: 1, text: 'bar'}
    App.click()
    App = MyReact.render(Counter)
    // effect 2 bar
    // render {count: 2, text: 'bar'}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    实现多个hooks支持的基本思路,就是用一个数组存放hooks。每次使用hooks时,将hooks指针加1。每次render以后,将指针清零。

    Custom Hooks

    接下来,可以借助已经实现的hooks继续实现custom hooks:

    function Component() {
      const [text, setText] = useSplitURL('www.google.com')
      return {
        type: txt => setText(txt),
        render: () => console.log({ text })
      }
    }
    function useSplitURL(str) {
      const [text, setText] = MyReact.useState(str)
      const masked = text.split('.')
      return [masked, setText]
    }
    let App
    App = MyReact.render(Component)
    // { text: [ 'www', 'google', 'com' ] }
    App.type('www.reactjs.org')
    App = MyReact.render(Component)
    // { text: [ 'www', 'reactjs', 'org' ] }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    重新理解Hooks规则

    了解Hooks的实现可以帮助我们理解Hooks的使用规则。还记得使用Hooks的原则吗?hooks只能用到组件最外层的代码中,不能包裹在if或者循环里,原因是在React内部,通过数组来存储hooks。所以必须保证每次render,hooks的顺序不变,数量不变,才能做deps的比对。

  • 相关阅读:
    window小技巧---------电脑自动开关机/电脑自动开机后打开应用/打开浏览器后自动响应某个页面并且f12
    CentOS7 根目录100%
    网络安全 - ARP 欺骗原理+实验
    【I/O方式——程序中断】
    【MHA】MySQL高可用MHA介绍8-常见错误以及解决
    Eclipse中去掉javaResources文件夹
    Stream方法的介绍
    vue 实现 word 下载的方式
    C++面试八股文:std::string是如何实现的?
    第04章 第04章 队列
  • 原文地址:https://blog.csdn.net/helloworld1024fd/article/details/127682395