React Hooks

2021-03-02  本文已影响0人  左冬的博客

React Hooks

Hook是React v16.8的新特性,可以用函数的形式代替原来的继承类的形式,可以在不编写class的情况下使用state以及其他React特性

React 设计原理

我们所熟悉的React组件长这样

import React, { Component } from "react";
// React基于Class设计组件
export default class Button extends Component {
    constructor() {
        super();
        // 组件自身数据
        this.state = { buttonText: "Click me, please" };
        this.handleClick = this.handleClick.bind(this);
    }
    // 响应数据变更
    handleClick() {
        this.setState({ buttonText: "Thanks, been clicked!" });
    }
    // 编排数据呈现UI
    render() {
        const { buttonText } = this.state;
        return <button onClick={this.handleClick}>{buttonText}</button>;
    }
}

组件类的缺点

上面实例代码只是一个按钮组件,但是可以看到,它的代码已经很重了。真实的React App由多个类按照层级,一层层构成,复杂度成倍增长。再加入 Redux + React Router,就变得更复杂

很可能随便一个组件最后export出去就是酱紫的:

export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))

一个4层嵌套HOC,嵌套地狱

同时,如果你的组件内事件多,那么你的constructor就是酱紫的

class MyComponent extends React.Component {
  constructor() {
    // initiallize
    this.handler1 = this.handler1.bind(this)
    this.handler2 = this.handler2.bind(this)
    this.handler3 = this.handler3.bind(this)
    this.handler4 = this.handler4.bind(this)
    this.handler5 = this.handler5.bind(this)
    // ...more
  }
}

而Function Component编译后就是一个普通的function,function对js引擎是友好的,而Class Component在React内部是当做Javascript Function类来处理的,代码很难被压缩,比如方法名称

还有this啦,稍微不注意就会出现因this指向报错的问题等。。。

总结一下就是:

Hooks

State Hook

Hook是什么?
可以先通过一个例子来看看,在class中,我们通过在构造函数中设置this.state初始化组件的state:

this.state = {
    n: 0
}

而在函数组件中,我们没有this,所以我们不能分配或读取this.state,但是可以在组件中调用useStateHook

import React, {useState} from 'react';
function xxx() {
    const [n, setN] = useState(0);
}

在上面代码中,useState就是Hook

Hook是一个特殊的函数,它可以让你“钩入”React的特性。例如useState是允许你在React函数组件中添加state的Hook。
如果你在编写函数组件并意识到需要向其添加一些state,以前的做法是必须将其转化为Class。现在你可以在现有的函数组件中使用Hook
让函数组件自身具备状态处理能力,且自身能够通过某种机制触发状态的变更并引起re-render,这种机制就是Hooks

走进useState

示例代码:

import React, { useState } from 'react';

function App() {
    // 声明一个叫 "n" 的 state 变量
    // useState接收一个参数作为初始值
    // useState返回一个数组,[state, setState]
    const [n, setN] = useState(0);

    return (
        <div>
        {/* 读取n,等同于this.state.n */}
        <p>{n}</p>
        {/* 通过setN更新n,等同于this.setN(n: this.state.n + 1) */}
        <button onClick={() => setN(n + 1)}>
            +1
        </button>
        </div>
    );
}

运行一下(代码1

  1. 首次渲染 render <App />
  2. 调用App函数,得到虚拟DOM对象,创建真实DOM
  3. 点击buttno调用setN(n + 1),因为要更新页面的n,所以再次render<App />
  4. 重复第二步,从控制台打印看出每次执行setN都会触发App函数运行,得到一个新的虚拟DOM,DOM Diff更新真实DOM

那么问题来了,首次运行App函数和setN时都调用了App,两次运行useState是一样的吗?setN改变n的值了吗?为什么得到了不一样的nuseState的时候做了什么?

分析:

尝试实现React.useState(代码2

// 和useState一样,myUseState接收一个初始值,返回state和setState方法
const myUseState = initialValue => {
    let state = initialValue
    const setState = newValue => {
        state = newValue
        // 重新渲染
        render()
    }
    return [state, setState]
}

const render = () => {
    // 鸡贼暴力渲染法
    ReactDOM.render(<App />, rootElement)
}

function App() {
    const [n, setN] = myUseState(0)
    ...
}

点击button,n没有任何变化
原来每次state都变成了初始值0,因为myUseState会将state重置
我们需要一个不会被myUseState重置的变量,那么这个变量只要声明在myUseState外面即可

let _state;
const myUseState = initialValue => {
    // 如果state是undefined,则赋给初始值,否则就赋值为保存在外面的_state
    _state = _state === undefined ? initialValue : _state;
    const setState = newValue => {
        _state = newValue;
        render();
    };
    return [_state, setState];
};

还有问题,如果一个组件有俩state咋整?由于所有数据都放在_state,产生冲突:

function App() {
    const [n, setN] = myUseState(0)
    const [m, setM] = myUseState(0)
    ...
}

解决:

let _state = [];
let index = 0;
const myUseState = (initialValue) => {
    const currentIndex = index;
    _state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
    const setState = (newValue) => {
        _state[currentIndex] = newValue;
        render();
    };
    index += 1;
    return [_state[currentIndex], setState];
};

const render = () => {
    // 重新渲染要重置index
    index = 0;
    ReactDOM.render(<App />, rootElement);
};

解决了存在多个state的情况,但是还有问题,就是useState调用顺序必须一致!

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.

只在最顶层使用 Hook

最后一个问题:
App用了_state和index,那其他组件用什么?放在全局作用域重名怎么解决?

运行App后,React会维护一个虚拟DOM树,每个节点都有一个虚拟DOM对象(Fiber),将_state,index存储在对象上

额外扩展一下Fiber对象,它的数据结构如下:

function FiberNode(
    tag: WorkTag,
    pendingProps: mixed,
    key: null | string,
    mode: TypeOfMode,
    ) {
    // Instance 实例
    this.tag = tag;
    this.key = key;
    // JSX翻译过来之后是React.createElement,他最终返回的是一个ReactElement对象
    // 就是ReactElement的`?typeof`
    this.elementType = null;
    // 就是ReactElement的type,他的值就是<MyClassComponent />这个class,不是实例,实例是在render过程中创建
    this.type = null;
    this.stateNode = null;

    // Fiber
    this.return = null;
    this.child = null;
    this.sibling = null;
    this.index = 0;

    this.ref = null;

    this.pendingProps = pendingProps;
    this.memoizedProps = null;
    this.updateQueue = null;
    // 用来存储state
    // 记录useState应该返回的结果
    this.memoizedState = null;
    this.firstContextDependency = null;

    // ...others
}

总结:

搞清楚useState干了啥以后,回过头再看setN改变n了吗,为什么得到了不一样的n代码3

分析:

结论:因为有多个nsetN并不会改变n,React函数式编程决定了n的值不会被改变,只会被回收

注意事项:

useReducer

React本身不提供状态管理功能,通常需要使用外部库,最常用的库是Redux
Redux的核心概念是,将需要修改的state都存入到store里,发起一个action用来描述发生了什么,用reducers描述action如何改变state,真正能改变store中数据的是store.dispatch API
Reducer是一个纯函数,只承担计算 State 的功能,函数的形式是(state, action) => newState
Action是消息的载体,只能被别人操作,自己不能进行任何操作
useReducer()钩子用来引入Reducer功能(代码5

const [state, dispatch] = useReducer(reducer, initial)

上面是useReducer基本用法

似曾相识的感觉

const [n, setN] = useState(0)
//   n:读
//   setN:写

总的来说useReducer就是复杂版本的useState,那么什么时候使用useReducer,什么时候又使用useState呢?
看一个代码6
当你需要维护多个state,那么为什么不用一个对象来维护呢,对象是可以合并的

需要注意的是,由于Hooks可以提供状态管理和Reducer函数,所以在这方面可以取代Redux。但是,它没法儿提供中间件(midddleware)和时间旅行(time travel),如果你需要这两个功能,还是要用Redux。

中间件原理:封装改造store.dispatch,将其指向中间件,以实现在dispatch和reducer之间处理action数据的逻辑,也可以将中间件看成是dispatch方法的封装器

有没有代替Redux的方法呢?

Reducer + Context

useContext

什么是上下文?

使用方法:

// 创建上下文
const c = createContext(null)

function App() {
    const [n, setN] = useState(0)
    return (
        // 使用<c.Provider>圈定作用域
        <c.Provider value={n, setN}>
            <Father />
        </ c.Provider>
    )
}

function Father() {
    return (
        <div>我是爸爸
            <Son />
        </div>
    )
}

function Son() {
    // 在作用域中使用useContext(c)来获取并使用上下文
    // 要注意这里useContext返回的是对象,不是数组
    const {n, setN} = useContext(c)
    const onClick = () => {
        setN( i => i + 1)
    }
    return (
        <div>我是儿子,我可以拿到n:{n}
            <button onClick={onClick}>我也可以更新n</button>
        </div>
        
    )
}

注意事项:

useEffect

useEffect钩子会在每次render后运行
React保证了每次运行useEffect的同时,DOM 都已经更新完毕

应用:

function App() {
    const [n, setN] = useState(0)
    const onClick = () => {
        setN(i => i + 1)
    }

    const afterRender = useEffect;
    // componentDidMount
    useEffect(() => {
        console.log('第一次渲染之后执行这句话')
    }, [])
    // componentDidUpdate
    useEffect(() => {
        console.log('每次次都会执行这句话')
    })

    useEffect(() => {
        console.log('n变化就会执行这句话,包含第一次')
    }, [n])
    // componentWillUnmount
    useEffect(() => {
        const id = setInterval(() => {
            console.log('每一秒都打印这句话')
        }, 1000)
        return () =>{
            // 如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除
            console.log('当组件要挂掉了,打印这句话')
            window.clearInterval(id)
        }
    }, [])
    return (
        <div>
            n: {n}
            <button onClick={onClick}>+1</button>
        </div>
    )
}

Hook 允许我们按照代码的用途分离他们,而不是像生命周期函数那样
React将按照effect声明的顺序依次调用组件中的每一个effect

对应的,另一个effect钩子,useLayoutEffect

// 伪代码
App() -> 执行 -> VDOM -> DOM -> useLayoutEffect -> render -> useEffect

特点:

为什么建议将修改DOM的操作放到useLayoutEffect里,而不是useEffect呢,是因为当DOM被修改时,浏览器的线程处于被阻塞阶段(js线程和浏览器线程互斥),所以还没有发生回流、重绘。由于内存中的DOM已经被修改,通过useLayoutEffect可以拿到最新的DOM节点,并且在此时对DOM进行样式上的修改。这样修改一次性渲染到屏幕,依旧只有一次回流、重绘的代价。

注意:
由于useEffect是在render之后执行,浏览器完成布局和绘制后,不应在函数中执行阻塞浏览器更新屏幕的操作

useMemo

React默认有多余的render(修改n,但是依赖m的组件却自动刷新了),如果props不变就没有必要再执行一次函数组件,先从一个例子来理解memo(代码8

这里有一个问题,如果给子组件一个方法,即使prop没有变化,子组件还是会每一次都执行

const onClickChild = () => {}

<Child data={m} onClick={onClickChild} />

这是因为在App重新渲染时,生成了新的函数,就像一开始讲的多个n的道理一样,新旧函数虽然功能一样,但是地址不一样,这就导致props还是变化了

那么对于子组件的方法,如何重用?
使用useMemo钩子(代码9)

const onClickChild = useMemo(() => {
    return () => {
        console.log(m)
    }
}, [m])

特点:

注意:

// useMemo
const onClickChild = useMemo(() => {
    return () => {
        console.log(m)
    }
}, [m])

// useCallback
const onClickChild = useCallback(() => {
    console.log(m)
})

// 伪代码
useCallback(x => log(x), [m]) 等价于 useMemo(() => x => log(x), [m])

useMemouseCallback作用完全一样,语法糖而已

useRef

一直用到的这个例子,每点击一下就会重新渲染一下App

function App() {
    console.log('App 执行');
    const [n, setN] = useState(0)
    const onClick = () => {
        setN(i => i + 1)
    }

    return (
        <div>
            <button onClick={onClick}>update n {n}</button>
        </div>
    )
}

假如我要知道这个App执行了多少次,我怎么记录?
如果我需要一个值,在组件不断render的时候也能够保持不变怎么做?

function App() {
    // count的值通过useRef记录了下来
    // 初始化
    const count = useRef(0)

    useEffect(() => {
        // 读取 count.current
        count.current += 1
    })
}

同样的,useRef也是通过它所对应的fiberNode对象来保存

为什么需要current?

讲了useRef就不得不讲讲forwardRef

在函数组件中怎么使用ref,尝试一下(代码10

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

说明,props无法传递ref属性
所以,函数组件用ref的话,需要用forwardRef包装做一下转发,才能拿到ref

自定义Hook

通过自定义Hook,可以将组件逻辑提取到可重用的函数中
自定义Hook是一个函数,其名称以 “use” 开头(符合 Hook 的规则),函数内部可以调用其他的Hook
每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的(每次调用 Hook,它都会获取独立的 state)
代码

参考

上一篇 下一篇

猜你喜欢

热点阅读