React Hook的发展史

·  阅读 499
React Hook的发展史

转载: Nolan 《React Hook + TS的实战》
原文链接地址:www.notion.so/Hook-fd2547…

1. Hook 用来解决什么问题

一句话,Hook 是用来让我们更好地复用 React 状态逻辑代码的。注意这里说的不是模板代码,模板代码可以用组件来复用;而单纯的状态逻辑代码没法用组件复用

有的同学可能会说,普通的函数不就可以实现逻辑代码复用吗?答案是:普通的函数可以复用逻辑代码,但是没法复用带状态的逻辑代码。

什么是React的状态?

举个例子:


const Comp = () => {
  const [id, setId] = useState(0)
  const [assets, setAssets] = useState()

    useEffect(() => {
      fetch(`https://google.com?id=${id}`).then(async response => {
       const data = await response.json();
        if (response.ok) {
          setAssets(data)
        } else {
          return Promise.reject(data);
        }
        })
      }, [])

  return <div>{assets.map(a => a.name)}</div>
}
复制代码

这里面的 id,assets就是状态,它的特征是它是由特定的API(useState)定义的,而且它改变的时候组件会做出相应的反应(比如重新render)

const sum = (a, b) => a + b
复制代码

这个普通的函数就没有状态,sum的返回值无论怎么变,都不会让任何组件重新render

React团队是非常注重React 状态代码复用性的,从React被创造出来,他们就一直在优化代码复用的解决方案,大概经历了:Mixin → HOC → Render Props,一直到现在的 Hook

所以 Hook 并不是一拍脑门横空出世的产物,不理解这段思路也是无法完全理解 Hook的

下面我会发很多代码截图,为了让大家跟上节奏,大家只需要结合我讲的话题大概浏览这些代码截图,不需要关注太多细节

1. Mixin

Mixin 是最早的 React 代码复用方案

var SubscriptionMixin = {
  getInitialState: function() {
    return {
      comments: DataSource.getComments()
    };
  },

  componentDidMount: function() {
    DataSource.addChangeListener(this.handleChange);
  },

  componentWillUnmount: function() {
    DataSource.removeChangeListener(this.handleChange);
  },

  handleChange: function() {
    this.setState({
      comments: DataSource.getComments()
    });
  }
};

var CommentList = React.createClass({
  mixins: [SubscriptionMixin],

  render: function() {
    // Reading comments from state managed by mixin.
    var comments = this.state.comments;
    return (
      <div>
        {comments.map(function(comment) {
          return <Comment comment={comment} key={comment.id} />
        })}
      </div>
    )
  }
});
复制代码

它的好处是简单粗暴,符合直觉,也确实起到了重用代码的作用;但是坏处也很明显,隐式依赖,名字冲突,不支持 class component,难以维护,总之,现在已经被完全淘汰了

给大家看一下官方判决书:reactjs.org/blog/2016/0…

2. HOC (higher-order component)

2015年,React团队判处Mixin死刑以后,推荐大家使用HOC模式,HOC是采用了设计模式里的装饰器模式

function withWindowWidth(BaseComponent) {
  class DerivedClass extends React.Component {
    state = {
      windowWidth: window.innerWidth,
    }

    onResize = () => {
      this.setState({
        windowWidth: window.innerWidth,
      })
    }

    componentDidMount() {
      window.addEventListener('resize', this.onResize)
    }

    componentWillUnmount() {
      window.removeEventListener('resize', this.onResize);
    }

    render() {
      return <BaseComponent {...this.props} {...this.state}/>
    }
  }
  return DerivedClass;
}

const MyComponent = (props) => {
  return <div>Window width is: {props.windowWidth}</div>
};
复制代码

经典的 容器组件与展示组件分离 (separation of container presidential) 就是从这里开始的

// components/AddTodo.js

import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'

class AddTodo extends React.Component {
  // ...

  handleAddTodo = () => {
    // dispatches actions to add todo
    this.props.addTodo(this.state.input)

    // sets state back to empty string
    this.setState({ input: '' })
  }

  render() {
    return (
      <div>
        <input
          onChange={(e) => this.updateInput(e.target.value)}
          value={this.state.input}
        />
        <button className="add-todo" onClick={this.handleAddTodo}>
          Add Todo
        </button>
      </div>
    )
  }
}

export default connect(null, { addTodo })(AddTodo)
复制代码

www.jianshu.com/p/ddbbbb16f…

经典的 容器组件与展示组件分离 (separation of container presidential) 就是从这里开始的

一个很经典的HOC使用案例是react redux 中的 connect 方法,AddTodo组件像一只无辜的小白兔,它的addTodo方法是connect方法给它注入进去的

  1. 可以在任何组件包括 Class Component 中工作
  2. 它所倡导的 容器组件与展示组件分离 原则做到了:关注点分离

缺点:

  1. 不直观,难以阅读
  2. 名字冲突
  3. 组件层层层层层层嵌套

3. Render Props

2017年,render props流行起来

class WindowWidth extends React.Component {
  propTypes = {
    children: PropTypes.func.isRequired
  }

  state = {
    windowWidth: window.innerWidth,
  }

  onResize = () => {
    this.setState({
      windowWidth: window.innerWidth,
    })
  }

  componentDidMount() {
    window.addEventListener('resize', this.onResize)
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.onResize);
  }

  render() {
    return this.props.children(this.state.windowWidth);
  }
}

const MyComponent = () => {
  return (
    <WindowWidth>
      {width => <div>Window width is: {width}</div>}
    </WindowWidth>
  )
}
复制代码

2017年,render props流行起来,它的缺点是,难以阅读,难以理解,下面是一个使用案例

4. Hook

大家看到上面的两种方法,它们最终的目的是什么呢?就是为了向组件注入 windowWidth 这个状态,为了这一个目的它们用了复杂又不直观的方法,有没有办法直观呢?那就是我们的 Hook 了,

还是上面相同的需求,我用Hook再实现一遍

import { useState, useEffect } from "react";

const useWindowsWidth = () => {
  const [isScreenSmall, setIsScreenSmall] = useState(false);

  let checkScreenSize = () => {
    setIsScreenSmall(window.innerWidth < 600);
  };
  useEffect(() => {
    checkScreenSize();
    window.addEventListener("resize", checkScreenSize);

    return () => window.removeEventListener("resize", checkScreenSize);
  }, []);

  return isScreenSmall;
};

export default useWindowsWidth;
复制代码
import React from 'react'
import useWindowWidth from './useWindowWidth.js'

const MyComponent = () => {
  const onSmallScreen = useWindowWidth();

  return (
    // Return some elements
  )
}
复制代码

Hook相比其他方案的优点:

  1. 提取逻辑出来非常容易
  2. 非常易于组合
  3. 可读性非常强
  4. 没有名字冲突问题

Hook分两种,React自带Hook和自定义Hook,自定义Hook是有自带Hook组合而成的,所以我们先讲一下自带Hook

2. React 自带 Hook 详解

1. useState

useState 是最基础的一个Hook,为什么这么说呢,因为它是状态生产器。它产生的状态和普通变量有什么区别的?

const [count, setCount] = useState(initialCount);
---------
const count = 1
const setCount = (value) => count = value
复制代码

这两个有什么区别呢?区别就在于第一个useState产生的是状态,状态改变的时候组件会重新渲染,它是响应式的;而第二个,就是一个普通变量,它改变什么都不会发生,听起来是不是有点可怜呢

2. useEffect

有了useState产生的状态,我们就可以写一些简单的组件了,比如

const Count = () => {
  const [count, setCount] = useState(0)
  const add = setCount(count + 1)
  return <button onClick={add}>add</button>
}
复制代码

这样一个简单的计数组件

但是,这终归是自娱自乐,我们写的代码,要和这个组件外面的世界产生联系,我们的状态,要和外面的世界同步,才能产生工业的价值。我们将发生在外面的事情统称为副作用

比如说你想将count和服务器的代码同步,你想将count和手机的震动同步,这时候就需要用到useEffect了。要摒弃以前的生命周期的概念,useEffect的唯一作用就是同步副作用。

3. useContext

React 的组件化让我们可以将不同的业务代码分割开,但是也带来了一个问题,那就是组件间共享状态是非常不方便的。比如,你有个很多组件都会用到的状态,app 主题状态,如何让一个组件随时可以获取到这个状态呢?大家可能听过状态提升,它缓解但是并没有解决这个问题。而context就是为了解决这个问题,大家可以把它理解成是React自带的Redux,实际上Redux就是用context实现的

4. useReducer

大家知道useState是主要的状态生产器,这个useReducer就是另一个没那么常用的状态生产器。它适合状态逻辑很复杂的时候,或者下一个state值依赖于上一个state值,比如

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
复制代码

这种情况用useState当然也可以,但是用useReducer就显得代码干净漂亮

5. useCallback/useMemo

这里React官网文档讲的非常不清楚。

给大家出一个判断题:父组件刷新,所有的子组件都会跟着刷新,这句话对吗?

这句话是对的,父组件刷新,所有的子组件都会刷新,这样听起来很耗性能,但是对于绝大多数组件来说,性能都是没有问题的,应为React真的很快。

但是对于耗性能的组件来说,这样就有很大的问题了,耗性能的组件不希望被经常刷新,所以我们可以用 React.memo包裹住它们,这样只有在它们的props变化的时候它们才会刷新。

这样又有一个问题,比如:

const TestComp = () => {
  const value = {name: 'Jack'}
  return <MemoExpensiveList value={value}/>
}
复制代码

大家看MemoExpensiveList是被React.memo给处理过的,它的props变化它才会刷新。但是在上面这个案例里,TestComp一刷新MemoExpensiveList就会刷新,这是为什么呢?原因就是,onClick在每次TestComp刷新时都会生成一个新的实例,{name: 'Jack'} ≠= {name: 'Jack'}

这就是 useMemo派上用场的时候了,我们可以用useMemo包裹住:

const value = useMemo(() => {}, [])
复制代码

这样它只会生成一个实例,也就不会骚扰到MemoExpensiveList了

而useCallback就是一个特殊版本的useMemo,专门来处理函数的

6. useRef

上面详细给大家讲了状态的概念,有时候我们希望创建一种类型的值,它不是状态,但是又可以在不同的render之间以同一个实例的形式存在。它有点类似于在class component里的 this.xxx

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
复制代码

3. 自定义 Hook

自定义Hook是目前最好的重用React逻辑的方法,它和普通的函数很像很像,自定义Hook的特殊之处在于,它是有状态的,它返回的也是状态。所以在什么时候我们应该用到自定义Hook?那就是,我们想要抽象出处理状态的逻辑的时候

给大家举一个例子

const Comp = () => {
  const [arr, setArr] = useState([1, 2])
  return <button onClick={() => setArr([...arr, value])}>add</button>
}
复制代码

如果你发现你的app里有好几处这种数组处理,你可以

export const useArray = <T>(initialArray: T[]) => {
  const [value, setValue] = useState(initialArray);
  return {
    value,
    setValue,
    add: (item: T) => setValue([...value, item]),
    clear: () => setValue([]),
    removeIndex: (index: number) => {
      const copy = [...value];
      copy.splice(index, 1);
      setValue(copy);
    },
  };
};
复制代码

用这样一个自定义的Hook,不仅返回了状态,也返回了处理这个状态的方法

这个例子也展示了,自定义Hook可以以状态为核心,并将它和与它相关的东西封装在一起。这也符合我们编程的seperation of concert,也就是关注点分离的原则。关注点分离是大家写代码时一定要注意的事情,也就是说无关的代码不要放在一起,不然关注点混在一起,会让维护难度大大加大。

大家明天去看一下自己的代码,很可能会发现,有一些面条代码其实是可以用hook抽象出来的,给大家再举个例子

const Comp = () => {
  const [id, setId] = useState(0)
  const [assets, setAssets] = useState()

    useEffect(() => {
      fetch(`https://google.com?id=${id}`).then(async response => {
       const data = await response.json();
        if (response.ok) {
          setAssets(data)
        } else {
          return Promise.reject(data);
        }
        })
      }, [])

  return <div>{assets.map(a => a.name)}</div>
}
复制代码

这里的fetch的内容和这个组件关系大吗?不大,因为这个组件其实不怎么在乎fetch的细节,它只在乎拿到result.data,那么我们就可以用hook来抽象

// util.ts
const useAssets = (id) => {
    const [assets, setAssets] = useState()

    useEffect(() => {
      fetch(`https://google.com?id=${id}`).then(async response => {
       const data = await response.json();
        if (response.ok) {
          setAssets(data)
        } else {
          return Promise.reject(data);
        }
        })
      }, [])
    return assets
}

// comp.tsx
const Comp = () => {
  const [id, setId] = useState(0)
  const assets = useAssets(id)

  return <div>{assets.map(a => a.name)}</div>
}
复制代码

大家看,这样就实现了逻辑的分离

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改