深入探讨:Hooks 是如何工作的(译文)

252 阅读4分钟

(本文内容是参考此篇英文原版,整理了里面的主要内容,属于精简版。)

hooks 是利用了闭包原理,在内部声明了变量,return 出来,在外部可是访问。 实现个基础版 useState hook。

    // demo1
    function useState(initialValue) {
        // 创建一个内部变量,用来存状态,相当于 getter
        var _val = initialValue
        function state(){
            return _val
        }
        // 设置状态的函数, 相当于 setter
        function setState(newVal){
            _val = newVal
        }
        return [state, setState]
    }
    var [foo, setFoo] = useState(0)
    console.log(foo()) // 0
    // 对它的作用域的访问,该引用称为“闭包”
    setFoo(1)
    console.log(foo()) // 1

    // 在组件中使用
    function Counter(){
        const [count, setCount] = useState(0)
        return {
            click: ()=>setState(count()+1),
            render: console.log('render:',{count:count()})
        }
    }
    // 调用组件
    const C = Counter()
    C.render() // render: {count: 0}
    C.click()
    C.render() // render: {count: 1}

上面虽然实现了useState的功能,但是获取状态时候,使用的函数调用,我们想要匹配真是的ReactApi,状态必须是变量,下面来进一步修整。

    // demo2
    function useState(initialVal){
        var _val = initialVal
        function setState(newVal){
            _val = newVal
        }
        return [_val, setState]
    }
    var [foo, setFoo] = useState(0)
    console.log(foo) // logs 0
    setFoo(1)
    console.log(foo) // logs 0

上面直接返回_val会出现状态不更新的问题。

当 foo 从 useState 中解构出来时,将始终指向初始状态,并且不会被改变。

显然这种做法不是我们期望的...

我们希望组件状态始终指向当前的状态,是个变量而不是函数调用。

继续修整...

    // demo3
    const MyReact = (function(){
        let _val
        return {
            render(Component){
                const Comp = Component()
                Comp.render()
                return Comp
            },
            useState(initialValue) {
                _val = _val || initialValue
                function setState(newVal) {
                    _val = newVal
                }
                return [_val, setState]
            }
        }
    })()

    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}

上面将闭包写在模块(Module)中,模拟微型React,跟踪组件状态,可解决demo2中的问题。

目前为止,我们已经介绍了useState,这是第一个基本的ReactHook。下一个最重要的Hook是 useEffect。

与 useState 不同,useEffect 是异步执行,意味着有更多机会遇到闭包。

下面对demo3中建立的React微型模型进行扩展。

    // demo4
    const MyReact = (function(){
        // 声明状态和依赖栈
        var _val, _deps
        return {
            render(Component){
                const Comp = Component()
                Comp.render()
                return Comp
            },
            useState(initialValue){
                _val = _val || initialValue
                function setState(newVal){
                    _val = newVal
                }
                return [_val, setState]
            },
            useEffect(callback, depArray){
                // 没有添加执行依赖
                const hasNoDeps = !depArray
                const hasChangedDeps = _deps ? !depArray.every((el,i)=>el===_deps[i]):true
                // 如果没有依赖或依赖有变更都执行callback
                if(hasNoDeps || hasChangedDeps) {
                    callback()
                    _deps = depArray
                }
            }
        }
    })()

    // 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)
    // // no effect run
    // render {count: 1}
    App.click()
    App = MyReact.render(Counter)
    // effect 2
    // render {count: 2}

上面虽然实现了useState和useEffect,但两者都是单例。

为了跟踪依赖关系(因为当依赖改变后effect便会再次执行),我们再增加一个变量进一步跟踪。

hooks 本质是个数组~

    // demo5
    const MyReact = (function(){
        var hooks = [],
        currentHook = 0
        return {
            render(Component){
                const Comp = Component()
                Comp.render()
                return Comp
            },
            useState(initialValue){
                Hooks[currentHook] = Hooks[currentHook] || initialValue
                const setStateHookIndex = currentHook // for setState's closure!
                const setState = (newVal) => (Hooks[setStateHookIndex] = newVal)
                return [Hooks[currentHook++], setState]
            },
            useEffect(callback, depArray){
                const hasNoDeps = !depArray
                const deps = Hooks[currentHook]
                const hasChangeDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
                if (hasNoDeps || hasChangedDeps) {
                    callback()
                    hooks[currentHook] = depArray
                }
                currentHook++ // done with this hook
            }
        }
    })()

注意在setStateHookIndex这里的用法,该用法似乎没有做任何事情,但用于防止setState关闭currentHook变量!

如果您将其取出,则setState由于已关闭currentHook而过期,因此再次停止工作。往下看

    function Counter() {
        const [count, setCount] = MyReact.useState(0)
        const [text, setText] = MyReact.useState('foo') // 2nd state hook!
        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)
    // // no effect run
    // render {count: 1, text: 'bar'}
    App.click()
    App = MyReact.render(Counter)
    // effect 2 bar
    // render {count: 2, text: 'bar'}

因此,基本直觉是拥有一个数组hooks和一个索引,该索引在调用每个钩子时递增,并在呈现组件时重置。

我们可以在单个组件中使用多个 State Hook 或 Effect Hook

那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。

Hook 的调用顺序在每次渲染中都是相同的

只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。

Hook 需要在我们组件的最顶层调用。如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部,否则 Hook 的调用顺序就容易变更。

参考:Hooks规则