Reacthooks

659 阅读11分钟

文章分为三部分:

1:react-hooks的一些基础 常用hooks和hooks中得状态 文章来源---> https://juejin.cn/post/6844904013595410440#heading-1
2:react-hooks-redux的应用 文章来源---> https://www.jianshu.com/p/d3ee3b3cc8b7
3:自己之前出的demo的修改

传送门 章节一 章节二 章节三

章节一

常用Hooks

useState

我们可以把这里的 state 看做我们在 class component 中使用的 this.state。 我们的每一次 setState 操作,在改变了值之后,都会引发 rerender 操作,从而触发页面的更新(但是如果没有改变的话,则不会触发 rerender,我们在后面将会利用这一特性做一件有趣的事情)。 同时,setState 可以以函数作为参数,这个时候我们可以获取到最新的 state 值(在第一个回调参数)。

import React, { useState } from 'react';
function App() {
    const [ state, setState ] = useState(0);
    return (
        <span>{state}</span>
    )
}

useEffect

可以说是所有 hooks API 中最像是声明周期的钩子了,很容易让人理解成为,如果依赖数组为空,那么它等价为 componentDidMount,但是真的这样吗?我们可以这样去理解我们的函数组件,函数组件的每次运行都相当于 class component 中的一次 render,每轮都会保留它的闭包,所以,我们的 useEffect 实际保留了它运行轮次的 state 和 props 状态(如果依赖不更新,那么状态不更新),这也就是 useEffect 和 componentDidMount 生命周期的关系。

import React, { useEffect } from 'react';
function App() {
    useEffect(() => {
        console.log('I am mount');
        return () => {
            console.log('before next run, I am cleaned');
        }
}, []);

useLayoutEffect

useLayoutEffect 与 useEffect 的不同在于,useLayoutEffect 会在 DOM 渲染之前执行,而 useEffect 会在 DOM 渲染之后执行,所以我们可以利用这个特性,避免一些由于 DOM 渲染之后进行操作导致的白屏问题。

useCallback

useCallback 可以帮助我们缓存函数(useMemo同样可以做到,写法不同),通过手动控制依赖,做到减少因为函数的更新导致子组件的更新(带来的性能问题非常明显)

import React, { useCallback } from 'react';
function App() {
    const cb = useCallback(() => { console.log('callback') }, []);
    return (
        <button onClick={cb}></button>
    )
}

useMemo

useMemo 可以为我们的 function component 提供缓存的能力,在一些重计算的场景下,可以减少重复计算的次数,起到明显的性能提升。 当然,useMemo同样可以用来缓存组件,起到类似与 class component 中 shouldComponentUpdate 的作用,让我们手动通过管理依赖的方式做到控制子组件的更新(当然这个手动管理的成本是非常高的)

useRef

因为在 hooks 中,我们所声明的所有变量是只属于它的闭包的,所以,我们无法做到变量的一个共享。因为 immutable 的 state 并不适合我们存储一些不参与 UI 显示的变量。hooks 为我们提供了 useRef 去存储 mutable 的不参与 UI 显示的变量,并且可以在每一轮 render 中共享。

useRef 不仅可以用来存储对 Dom 元素的引用(它的本意),更可以用来存储我们需要在每轮 render 中共享的 mutable 的变量(可能非常常用)。

import React, { useRef } from 'react';
function App() {
  const td = useRef(1);
  console.log(td.current); // 1
  ...

useReducer

在当前版本中的 useReducer 事实上是对 useState 的一层封装,实现了 redux 的一套原理(之前的版本是 useState 是对 useReducer 的一层封装)

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}

useContext

假定我们已经有了一个 Context ,并且我们的子组件已经在 Provider 包裹下,我们可以直接使用 useContext 去获取值,而非使用回调去获取值。 同时,我们也可以对某些 Context 进行 useContext 的封装,让我们可以在不同的组件中方便的使用 Context 中的数据。

// 假定我们已经有 Context
function Child(props) {
    const { value } = useContext(Context);
    return (
      <div>
        {value}
      </div>
    )
}

我们可以将 Context 、useReducer 与 useContext 结合起来,打造我们自己的 Redux

const CTX = React.createContext(null);
const reducer = (state, action) => {
    switch(action.type) {
        default:
            reutrn state;
    }
}
const Context = function({ children }) {
    const [state, dispatch] = useReducer(reducer, {});
    return (
        <CTX.Provider value={ state, dispatch }>
            {children}
        </CTX.Provider>
    )
}

怎么理解Hooks中的状态

我们应该树立一个理念,在 function component 中,所有的状态,都是隶属于它的闭包的,所以导致了 我们每一轮的 Render 都会有自己的一个闭包,所有的 useEffect 与 useLayoutEffect 都在其最后一次更新的闭包中 Hooks处理请求

正确处理依赖

hooks 编程有些类似于响应式编程,同时,为了可以总是拿到最正确的值,正确的去书写hooks依赖是非常重要的,也就是所谓的对依赖诚实,这样才能保证我们最终发送请求之时,可以取到正确的 state 和 props。

放置依赖的请求于 useEffect 中

为了能够正确的处理请求,有一种想法是——将请求的函数放置于 useEffect 中,这样子就可以确保我们每时每刻都会去正确的处理其中的依赖问题。 处理竞态我们知道,在 hooks 里,每一次 Render 以及 每一次 useEffect 的执行都是在它自己所处轮次的闭包中,所以,我们处理竞态的一个思路就来源于这里。

我们的依赖变化会触发我们的 ajax 操作,所以当第二次请求发生时,实际上上一次 effect 已经到了清理副作用时期,所以执行了 return 中的函数,将我们的flag置为true,这样,当我们的请求返回之时,其effect 所在的闭包是可以感知到执行结束的状态的,从而抛弃旧值,达到对竞态的正确处理。

请求与触发分离

请求
我们可以将请求函数用普通函数的方法,放置于整个 function 中,这样足以确保我们这个函数能够拿到当前 render 轮次所依赖的
state 和 props,如果有性能方面的顾虑,可以考虑使用 useCallback 去进行包装(但此时一定要对依赖诚实)
function App() {
const [flag, setFlag] = useState(0);
    const ajax = () => {
       _ajax(props)
    };

    useEffect(() => {
        ajax();
    }, [flag]);

    return (
        ...
    )
}

触发

这个时候一定要注意的一点是,我们的触发 flag,一定要在最后修改(先进行预操作——其它的 state 修改),确定我们的 effect 更新时,
索引用的,是最新的 ajax 请求函数。
非渲染参数使用 ref 进行保存
因为我们在 effect 中,永远可以正确的获取到 ref 值,所以,当我们的参数不参与渲染时,我们可以用 useRef 生成的 ref 对其进行管理,
这样我们就可以不用去担心由于 ref 所引用参数的变化问题(同时,也不会触发页面的 rerender)
const name = useRef('小明')
const ajax = useCallback(() => {
    ajax({ name })
}, []);
// 修改 param 直接操作 ref
name.current = '123';

章节二

react-hooks 是 react 官方新的编写推荐,我们很容易在官方的 useReducer 钩子上进行一层很简单的封装以达到和以往 react-redux \ redux-thunk \ redux-logger 类似的功能,并且大幅度简化了声明。

安装

react-hooks-redux, 需要 react 版本 >= 16.7

yarn add react-hooks-redux

我们用了不到 35 行代码就声明了一个完整的 react-redux 的例子, 拥抱 hooks。

import React from 'react';
import ReactHookRedux from 'react-hooks-redux';
// 通过 ReactHookRedux 获得 Provider 组件和一个 sotre 对象
const { Provider, store } = ReactHookRedux({
    isDev: true, // 打印日志
    initialState: { name: 'dog', age: 0 },
});
function actionOfAdd() {
    return {
    type: 'add the count',
    reducer(state) {
    return { ...state, age: state.age + 1 }; // 每次需要返回一个新的 state
    },
};
}
function Button() {
    function handleAdd() {
    store.dispatch(actionOfAdd()); //dispatch
}
    return <button onClick={handleAdd}>add</button>;
}
function Page() {
    const state = store.useContext();
    return (
    <div>
        {state.age} <Button />{' '}
    </div>
    );
}
export default function App() {
return (
    <Provider>
        <Page />
    </Provider>
    );
}

总结一下:

准备工作

使用 ReactHookRedux 创建 Provider 组件 和 store 对象 使用 Provide r 包裹根组件

使用

在需要使用状态的地方 使用 store.useContext() 获取 store 中的 state 使用 store.dispatch(action()) 派发更新

我们阅读这个小例子会发现,没有对组件进行 connect, 没有编写 reducer 函数, 这么简化设计是为了迎合 hooks, hooks 极大的简化了我们编写千篇一律的类模板,但是如果我们还是需要对组件进行 connect, 我们又回到了编写模板代码的老路。

middleware 的编写

绝大部分情况,你不需要编写 middleware, 不过它也极其简单。middleware 是一个一维数组,数组中每个对象都是一个函数, 传入了参数并且如果返回的对象存在, 就会替换成 nextState 并且继续执行下一个 middleware。 我们可以使用 middleware 进行打印日志、编写 chrome 插件或者二次处理 state 等操作。 我们看看 middleware 的源码:
let nextState = reducer(lastState, action);
for (let i = 0; i < middleware.length; i++) {
    const newState = middleware[i](lastState, nextState, action, isDev);
    if (newState) {
    nextState = newState;
    }
}
return nextState;

性能和注意的事项 性能(和实现上)上最大的区别是,react-hooks-redux 使用 useContext 钩子代替 connect 高阶组件进行 dispatch 的派发。

在传统的 react-redux 中,如果一个组件被 connect 高阶函数进行处理,那么当 dispatch 时,这个组件相关的 mapStateToProps 
函数就会被执行,并且返回新的 props 以激活组件更新。

而在 hooks 风格中,当一个组件被声明了 useContext() 时,context 相关联的对象被变更了,这个组件会进行更新。

理论上性能和 react-redux 是一致的,由于 hooks 相对于 class 有着更少的声明,所以应该会更快一些。

所以,我们有节制的使用 useContext 可以减少一些组件被 dispatch 派发更新。

如果我们需要手动控制减少更新 可以参考 useMemo 钩子的使用方式进行配合。

以上都是理论分析,由于此库和此文档是一个深夜的产物,并没有去做性能上的基准测试,所以有人如果愿意非常欢迎帮忙做一些基准测试。

其他例子

异步 action 的例子

import React from 'react';
import ReactHookRedux, { reducerInAction, devLog } from 'react-hooks-redux';
// 通过 ReactHookRedux 获得 Provider 组件和一个 sotre 对象
const { Provider, store } = ReactHookRedux({
  isDev: true, // default is false
  initialState: { count: 0, asyncCount: 0 }, // default is {}
  reducer: reducerInAction, // default is reducerInAction 所以可省略
  middleware: [devLog], // default is [devLog] 所以可省略
  actions: {}, // default is {} 所以可省略
});
// 模拟异步操作
function timeOutAdd(a) {
  return new Promise(cb => setTimeout(() => cb(a + 1), 500));
}
const actions = {
  // 如果返回的是一个function,我们会把它当成类似 react-thunk 的处理方式,并且额外增加一个ownState的对象方便获取state
  asyncAdd: () => async (dispatch, ownState) => {
    const asyncCount = await timeOutAdd(ownState.asyncCount);
    dispatch({
      type: 'asyncAdd',
      // if use reducerInAction, we can add reducer Function repeat reducer
      reducer(state) {
        return {
          ...state,
          asyncCount,
        };
      },
    });
  },
};
function Item() {
  const state = store.useContext();
  return <div>async-count: {state.asyncCount}</div>;
}
function Button() {
  async function handleAdd() {
    // 使用 async dispatch
    await store.dispatch(actions.asyncAdd());
  }
  return <button onClick={handleAdd}>add</button>;
}
export default function App() {
  return (
    <Provider>
      <Item />
      <Button />
    </Provider>
  );
}

使用 immutableJS 配合 hooks 减少重渲染的例子

import React, { useCallback } from 'react';
import ReactHookRedux, { createDevLogFromImmutable } from 'react-hooks-redux';
import { Map } from 'immutable';
const { Provider, store } = ReactHookRedux({
  isDev: true, // 打印日志
  initialState: Map({ products: ['iPhone'] }),
  // createDevLogFromImmutable,提前声明getIn对象,可以有效的规避toJS的性能开销
  // 例如 createDevLogFromImmutable('user', ['data', 'local'], 'ui', 'products');
  middleware: [createDevLogFromImmutable('products')],
});
function actionAddProduct(product) {
  return {
    type: 'add the product',
    reducer(state) {
      return state.update('products', p => {
        p.push(product);
        return [...p];
      });
    },
  };
}
let num = 0;
function Button() {
  function handleAdd() {
    num += 1;
    store.dispatch(actionAddProduct('iPhone' + num)); //dispatch
  }
  return <button onClick={handleAdd}>add-product</button>;
}
function Page() {
  const state = store.useContext();
  // 从immutable获取对象,如果products未改变,会从堆中获取而不是重新生成新的数组
  const products = state.get('products');
  return useCallback(
    <div>
      <Button />
      {products.map(v => (
        <div>{v}</div>
      ))}
    </div>,
    [products], // 如果products未发生改变,不会进行进行重渲染
  );
}
export default function App() {
  return (
    <Provider>
      <Page />
    </Provider>
  );
}

章节三

之前作者做的是js+jsx的demo后续改成了纯jsx的

原文链接:传送门

father.js

import React, {useState, useReducer, useEffect} from 'react';
import Child from './child'
 function Father(props) {
  // 基本挂钩 
  // 在函数组件里声明state属性
  // const [ value, setValue ] = useState(value的初始值);
  const [value, setValue] = useState('');
  const [date, setDate ] = useState([{
    computed: false,
    val: '点击可划线'
  }])
  const clickHandle = () => {
    if ( value.length > 0 ) {
      setDate(date.concat({computed: false,val:value}))
    }
    // 把input内容清空
    setValue('');
  }
  useReducer(()=>{
    // useReducer是对useState的一层封装
  })
  useEffect(()=>{
    // 类似于componentDidMount和DidUpdate和WillUnMount的组合
  })
  return (
    <div className="wrapper">
      <input
       value={value} 
       //  在元素值改变时  
       onChange={(ev) =>{setValue(ev.target.value)}} 
       className="input"
       placeholder={".........别空"}
      />
      {/* 点击时-->增功能 */}
      <button className={'btn'} onClick={clickHandle}>
        增
      </button>
      {
        date.map((item,index)=>
          <Child key={index} item={item} />
      )}
    </div>
  );
}  
export default Father;

Child.jsx

import React, {useState} from 'react';

function Child(props) {
  // const [computed, setCompoted] = useState(computed的初始值);
  const [computed, setCompoted] = useState(props.item.computed);
  const clickHandle = () => {
    setCompoted(!computed);
  };
  return (
    // 点击事件
    <div className="item" onClick={clickHandle}>
      {
        // 三目运算符
        !computed ? 
        <p >{props.item.val}</p> 
        : 
        <s >{props.item.val}</s>
      }
    </div>
  );
}

export default Child;