React Hooks

React Hooks

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

React 设计原理

  • React认为,UI视图是数据的一种视觉映射,UI = F(Data),这里的F主要负责对输入数据进行加工,对数据变更做出相应
  • 公式里的F在React里抽象成组件,React是以组件为粒度编排应用的,组件是代码复用的最小单元
  • 在设计上,React采用props属性来接收外部数据,使用state属性来管理组件自身产生的数据,而为了实现(运行时)对数据变更做出相应需要,React采用基于类的组件设计
  • 除此之外,React认为组件是有生命周期的,因此提供了一系列API供开发者使用

我们所熟悉的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指向报错的问题等。。。

总结一下就是:

  • 很难复用逻辑,会导致组件树层级很深
  • 会产生巨大的组件(很多代码必须写在类里面)
  • 类组件很难理解,比如方法需要bindthis的指向不明确
  • 编译size,性能问题

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的时候做了什么?

分析:

  • setN
    • setN一定会修改数据x,将n+1存入x
    • setN一定会触发<App />重新渲染(re-render)
  • useState
    • useState肯定会从x读取n的最新值
  • x
    • 每个组件都有自己的数据x,我们将其命名为state

尝试实现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)
    ...
}

解决:

  • 把_state做成对象
    • 不可行,没有key,useState(0)只传入了一个参数0,并不知道是n还是m
  • 把_state做成数组
    • 可行,_state = [0, 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调用顺序必须一致!

  • 如果第一次渲染时n是第一个,m是第二个,k是第三个
  • 则第二次渲染时必须保证顺序一致,因为数组根据调用顺序存储值
  • re-render时会从第一行代码开始重新执行整个组件
  • 所以React不允许出现如下代码

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
}

总结:

  • 每个函数组件对应一个React节点(FiberNode)
  • 每个节点保存着_state(memorizedState)和index(实际是链表)
  • useState会读取对应节点的state[index]
  • index是由useState的调用顺序决定
  • setState会修改_state,并触发更新

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

  • 先+1,后log => 1
  • 先log,后+1 => 0
  • 为什么log出了旧数据

分析:

  • 先点击log,log(0)三秒后执行,此时n0n不会变
  • 再点击+1,此时调用的是一个新的函数,生成了新的n,re-render
  • n=0n=1同时存在内存中

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

注意事项:

  • 不可局部更新(代码4
  • 地址要变:setState(obj)如果obj地址不变,那么React就认为数据没有变化
  • useState接受函数:函数返回初始state,且只执行一次
  • setState接收函数:setN(i => i + 1),优先使用这种形式

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基本用法

  • 接受Reducer函数和一个初始值作为参数
  • 返回一个数组,数组[0]位是状态当前值,第[1]位是dispatch函数,用来发送action

似曾相识的感觉

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>
        
    )
}

注意事项:

  • 使用useContext时,在一个模块改变数据,另一个模块是感知不到的
  • setN会重新渲染<App />,自上而下逐级通知更新,并不是响应式,因为响应式是监听数据变化通知对应组件进行更新

useEffect

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

应用:

  • 作为componentDidMount使用,[]作第二个参数
  • 作为componentDidUpdate使用,可指定依赖
  • 作为componentWillUnmount使用,通过return
  • 以上三种可同时存在
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

  • useEffect在浏览器渲染之后执行,useLayoutEffect在渲染前执行(代码7
  • useLayoutEffect在渲染前执行,使用它来读取 DOM 布局并同步触发重渲染
// 伪代码
App() -> 执行 -> VDOM -> DOM -> useLayoutEffect -> render -> useEffect

特点:

  • useLayoutEffect性能更好,但是会影响用户看到页面变化的时间(代码7
  • useLayoutEffect总是比useEffect先执行
  • useLayoutEffect里的任务最好是影响了layout
  • 还是推荐优先使用useEffect(如果不涉及操作dom的操作)

为什么建议将修改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第一个参数是() => value(value可以是函数、对象之类的),第二个参数是依赖数组[m]
  • 只有当依赖变化时,才会重新计算新的value
  • 如果依赖没有变化,就重用之前的value
  • 这不就是vue中的computed吗?

注意:

  • 如果你的value是个函数,那么你要写成useMemo(() => x => console.log(x))
  • 这是一个返回函数的函数
  • 这么难用的话,用用useCallback
// 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是同一个值,只有引用才能做到
  • useRef存储的实际上是一个对象{currnt: 0},对象对应的是同一个地址(内存)
  • 每次改变只是改变对象中的值,而不是改变对象,新旧组件必须引用同一个对象

讲了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)
代码

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容