coppyC / blog

个人博客,用issue 分享,记录关于前端,js 各种经验和奇淫技巧。欢迎star, watch。
74 stars 2 forks source link

一口气学完 React Hook #11

Open coppyC opened 5 years ago

coppyC commented 5 years ago

Why React Hook?

在React 的世界中,组件有两种,一种是状态组件,另一种无脑组件。 通俗来讲,状态组件就是有自己的状态this.state,而无脑组件就是不能自己控制自己,没有状态,只能通过外部的 props 来改变自己。

相信大家在第一次学习 React 组件的时候,官方的第一个示例给人留下深刻的印象,一个函数就是一个组件,从来没有见过这么简单就能创建一个组件的方式。 相比vue,创建一个组件就要去建一个 .vue 文件。 angular。。。那就更不说了,一个组件少则两个,多则三个文件。

这么说来,React 创建最小化组件的方式倒是很快捷,甚至可以在一个文件内快速创建多个组件。 但是函数创建的组件属于无脑组件,如果需要状态,就需要改装成 React.Component ,修改方式十分繁琐,而且React.Component十分不利于代码压缩。

而恰巧的是,FaceBook的程序员也更喜欢函数式组件,他们绞尽脑汁,想让函数式组件不再无脑,于是React Hook就出来了,从此,函数式组件不再无脑,它也可以有状态,如此一来,从一个无脑组件修改为状态组件也没有额外的成本。 (官方的解释: 对class的不满而有hook, https://react.docschina.org/docs/hooks-intro.html#motivation)

而且,函数式组件拥有React.Component很多没有的优点,其中,易于创建与压缩保证了开发效率与线上生产包的大小。 除了函数式组件本身的优点,还有react hook的优点,对组件更加颗粒化的代码复用,可以复用无ui部分的组件逻辑,无需封装成组件。

不得不说,React是一个开创性的框架,很多理念都极具创新。

hook 的使用

hook 还是有点黑科技的感觉,但由于代码开源,这个技术也透明起来。 官方文档解释如下:

每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。

所以使用 hook 有着必须遵守的如下限制

其实hook最难的就是值的不稳定,js不好的同学可能会有很多bug不能理解。

核心 hook (重点学习,敲黑板)

react hook 在官方文档中有很多api,但实际上,最常用的就是两个useStateuseEffect 正所谓学会20%的知识就能解决80%的问题。 甚至可以说,这两个api就可以解决所有情况,其它的api都是用来优化的。 所以学会这两个api,畅游react hook也没什么问题,但是阅读开源项目就有点吃力。

useState

如果说有组件让react拥有灵魂,那useState就让函数式组件拥有状态 没错,这个api人如其名,就是在函数式组件中使用状态

function Example() {
  // 声明一个叫 “count” 的 state 变量。
  const [count, setCount] = useState(0)
  return (
    <div onClick={() => setCount(count + 1)}>
      {count}
    </div>
  )
}

useState的第一个参数是初始值,如果这个值是计算出来的,比较消耗性能,可以用函数返回值来优化,这样就只会在初始化时执行 init 函数

const [count, setCount] = useState(() => init(0))

使用了解构声明,第一个count就是值,对应class组件的this.state.xxx,第二个就是设置变量的函数,对应class组件的 this.setState

ps: 实际上,我不太喜欢这个hook,因为他过于繁琐,声明了count,然后还要写一个setCount,为了保持风格一致,这个setCount 还不能拼错,拼成什么setCout什么的,毕竟声明变量也没有提示,打起来挺费劲的。(身处外包公司的我,效率天下第一)目前比较满意的就是mobx的hook,但是mobx需要包裹一层observer,也挺繁琐的,而且有时候忘了很难排错。我自己封闭了一个,写起来快捷,但也有一些约定限制或性能需要优化,目前我还没有找到一个我满意的方式来创建状态。

useEffect

useState让函数组件有了状态,useEffect让函数组件有了生命周期。 有了状态,有了生命周期,嗯。。。 “ 那个,class组件,你可以下班了。 ” useEffect 是我最喜欢的一个hook,没有之一,它被设计得十分完美,一个api,承包了所有的生命周期。

举个栗子,写一个计时器

function Counter() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const id = setTimeout(() => setCount(count+1), 1000)
    return () => clearTimeout(id)
  })
  return <div>{count}</div>
}

useEffect 是 componentDidMount, componentDidUpdate, componentWillUnmount 的融合版本。参数是一个回调函数,会在componentDidMount和componentDidUpdate时执行,如果返回一个清理函数,那么下次componentDidUpdate或componentWillUnmount时,会执行这个清理函数。

这样,在组件卸载的时候,就会清理还在执行中的setTimeout,使用return的方式,让关注点融合,无需折分在两个生命周期中写,也不必在 state 中挂载setTimeoutId,妙啊~~妙啊~~

但是这里有些小问题,useEffect里面的函数是在componentDidMount这个时间点执行,也就是说,加上 unMount 到 didMount 这段时间,实际每计一秒都会超过一秒。那用setInterval啊,可是仔细想想,useEffect中包含了componentDidUpdate,想在didmount时setInterval,willunount时clear这个做法可能就办不到了。啊? 难道这个我刚才吹半天的api开始心有余力不足了? 别急。我这就来介绍它的第二个参数。

第二个参数是一个数组,传入的数组会和上一次的数组做一次浅比较,如果相同,则本次不会执行effect,如果不同则相反,一般用来做优化使用,但很多时候,我们可以传入一个空数组妙用,来保证每次比较结果都相同,从此componentDidUpdate是路人,只留下didmount和willunmount。

修改一下刚才的代码。

function Counter() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const id = setInterval(() => setCount(count => count + 1), 1000)
    return () => clearInterval(id)
  }, [])
  return <div>{count}</div>
}

如此一来,就十分完美,在组件卸载是clearInterval,避免内存泄漏。 值得一提的是,不能写成这样的代码

  useEffect(() => {
    // 不要这么写!!!
    const id = setInterval(() => setCount(count+1), 1000)
    return () => clearInterval(id)
  }, [])

因为count是不稳定的值,而useEffect使用了第二个参数[],导致effect中的count总是0, 而setCount可以接收一个回调函数,使得count的值是最新的。

而且由于生命周期的融合,思考的方式也和之前的不一样,需要适应一下,一但适应,就会发现这个思维模式十分优雅。这种模式,也使得基于订阅的rxjs在hook中如鱼得水,虽然我不用rxjs,但还是要说一句,rxjs + hook强无敌

rxjs + hook 有多强可以看这里,强得我差点入了rxjs的坑:https://jerryzou.com/posts/rxjs-hooks

自定义 hook

看完核心api后,其它的api可以放一边,开始diy了。

自定义hook也不是什么新的语法,就是一个函数,只是函数里面用了其它hook,所以它也就不得不成为一个新的hook,顾取名为自定义hook。React 设计巧妙的地方就在新的知识没有新的api(像之前的高阶组件),js超强,react就越强,把学习时间花在一本万利的js身上,而不是可能淘汰的react身上。

约定

所有的hook约定使用use开关,然后接驼峰写法,官方的hook都是如此,自定义hook也要如此,没有规矩,不成方圆。

实战

例如把之前的计时器进行封装成一个hook

function useCounter() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const id = setInterval(() => setCount(count => count + 1), 1000)
    return () => clearInterval(id)
  }, [])
  return count
}

function Counter() {
  const count = useCounter()
  return <div>{count}</div>
}

如此一来,就复用了这个计时器的逻辑,这样再封装成别的计时器成本也不大了,比如不return div了,要span了,还要红色的

function RedCounter() {
  const count = useCounter()
  return <span style={{color: 'red'}}>{count}</span>
}

其它hook

说实话,官方提供的其它hook基本就是语法糖,有的甚至能用核心hook来封装。

useContext

const MyContext = React.createContext(defaultValue)
function Consumer() {
  const value = useContext(MyContext)
  return <div>{value}</div>
}

这个明显就是context的语法糖,懂context的一看就知道,即使没有这个语法糖,依旧可以使用MyContext.Consumer,而且如果使用第三方库来管理全局状态,像redux,mobx这些,甚至都不用context。

useReducer

这也是语法糖,完全可以用 useState 自己封装一个,代码大概是这个样子

function useReducer(reducer, initialArg, init) {
  const initState = init ? () => init(initialArg) : initialArg
  const [state, setState] = useState(initState)
  const dispatch = (action) => setState(reducer(state, action))
  return [ state, dispatch ]
}

既然是可以用核心hook来shim的api,就不细讲了,至于怎么用,有兴趣看官方文档: https://react.docschina.org/docs/hooks-reference.html#usereducer

useCallback

这是一个做性能优化的hook,应用场景大概是这个样子

class AnOther extends PureComponent {
  render() {
    console.log('render')
    return <div>another: {this.props.renderChildren()}</div>
  }
}

export default function() {
  const [state, setState] = useState(0)
  const renderChildren = () => 'children'
  return (
    <div onClick={() => setState(state+1)}>
      <div>{state}</div>
      <AnOther renderChildren={renderChildren}></AnOther>
    </div>
  )
}

就像官方说的一样,做了优化的组件,会对props进行比较,决定是否跳过渲染来达成优化,像上面的AnOther就是做了优化的组件,如果renderChildren一样,则会路过更新。但像上面renderChildren,每次传的函数都不是同一个,使得AnOther无法做优化,表现为每次点击,AnOther都会打印 console.log('render')。这时就可以使用 useCallback来做优化,将const renderChildren = () => 'another'改为

  const renderChildren = useCallback(() => 'children', [])

这样可以保证renderChildren的引用一致,帮助AnOther完成优化。 这里useCallback第二个参数和useEffect的第二个一致,区别就是useCallback的第二个参数是必填参数。

但恕我直言,这是一个没有什么用的hook,因为在多时候,我们不知道哪些组件是有做优化的,是怎么优化的。js不好的也很难理解为什么一样功能的函数,引用会不一样,而且乱用可以还会降低性能,毕竟执行了一些没有必要的代码。还有就是第二个参数要填什么,有时候函数修改但忘记修改第二个参数,锁住了不稳定的变量,引发莫名bug。可能就会出现弊大于利的情况。

另外,这个函数也是可以使用核心hook来造轮子的,有兴趣的可以自己试试。

useMemo

这又是一个性能优化的函数,类比计算属性功能,直接拿官方的例子说事

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

当a和b一样,computeExpensiveValue函数就不会执行,而是直接返回上一次计算的值,大致性能优化

值得一提的是,useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

既然useCallback可以造轮子,useMemo也是可以用核心hook来造,有兴趣的课后自己实践一下。

useRef

看名字就知道是ref的hook,使用方法如下 (也没什么好说的,我就直接贴官方实例了)。

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

但其实useRef是一个稳定的值,它的用法不止是ref,你可以用它来放任何东西,像这样

export default function() {
  const count = useRef('')
  return (
    <input type="text" onChange={e => count.current = e.target.value} />
  )
}

值得注意的是,修改 ref.current 不会触发组件的更新,这对一些场景来说很实用,像非受控组件。

另外,这个hook也可以造轮子 (果然是万能的useState),代码也不多,而且很简单,我就现场造一个

function useRef(current) {
  const [state] = useState({ current })
  return state
}

因为没有返回 setState 的关系,所以修改current 组件不会更新也就很好理解了

useImperativeHandle

此hook 弥补了函数组件没有自己的ref的缺点,但使用过程极其繁琐,个人十分不喜欢,所以也没有细究,就直接贴官方文档

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用

const FancyInput = React.forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
});

在本例中,渲染 的父组件可以调用 fancyInputRef.current.focus()。

这里面还用了恶心的高阶组件, 期待官方能有更好的方式使用函数组件ref。 另外,ref这种模式我觉得都是迫不得已,其实可以设计更好的组件来避免ref的使用

useLayoutEffect

这是一个 功能类似 useEffect 的组件,参数也一致,唯一的区别就是effect 的执行时机,useLayoutEffect会在所有的 DOM 变更之后同步调用 effect ,调用阶段和componentDidMount, componentDidUpdate一致,而useEffect会在DOM 变更后异步调用。不能理解的话,就使用useEffect就可以了,它是异步的,不会阻塞视觉更新,当你需要用到useLayoutEffect的时候,你自然会知道这两者的区别。

例如,我写了一个库,就用 useLayoutEffect 取代 useEffect,避免dom更新后,css还没更新的尴尬情况,就是这个库 https://github.com/style-hook/style-hook (下期我再好好吹吹这个库[#滑稽])

useDebugValue

这是一个生产环境用不上的hook,功能就像它的名字一样,debug value。 可用于在 React 开发者工具中显示自定义 hook 的标签。 记住,是只对自定义hook有效,举个栗子,就拿上面的计时器hook来说,为了方便查看,我把代码也贴上

function useCounter() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    let count = 0
    const id = setInterval(() => setCount(++count), 1000)
    return () => clearInterval(id)
  }, [])
  return count
}

在React 开发者工具中显示如下(笔者使用的是chrome 插件) image 展开标签后效果 image 你可能想在未展开标签时显示 count 的值,这时,你可以这么做

function useCounter() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    let count = 0
    const id = setInterval(() => setCount(++count), 1000)
    return () => clearInterval(id)
  }, [])
  useDebugValue(count)
  return count
}

效果 image 展开标签 image

如果要显示的值需要经过复杂的计算,为了避免阻塞ui,可以使用第二个参数来延迟格式化 debug 值

useDebugValue(value, value => ( value / 10 * 24 * 60 * 60 * 1000 )>> 3)

但其实需要这个hook一般都是在开发环境使用,既然是开发环境,性能比生产环境差些也无可厚非。

hook F&Q

看到这里,恭喜,hook已经学完了,但你可能还有很多疑问,别急, 官方文档的FAQ很完整,并且通俗易懂,我就不丢人献眼了。 https://react.docschina.org/docs/hooks-faq.html

或者可以在这里留言,交流有关hook的问题。

xqdd commented 5 years ago

Hook好用不好用我不知道,mobx倒是比redux好用一百倍

coppyC commented 5 years ago

mobx虽好用,但不可贪杯。

ponyorange commented 5 years ago

大师牛逼

MikuBlog commented 5 years ago

大湿牛逼