相信大部分人都已经在使用 React hooks 了,但是在开发过程中,我们要 知其然知其所以然。整理了一下最近使用 React hooks 遇到的一些问题,并附上详细答案,如果想要更加深入了解,推荐阅读文章中推荐的一些文章。
没有hooks之前使用 render props
和 高阶组件
。render props
是接受一个组件作为props
, HOC
是一个函数,接受一个组件作为参数,返回另一个组件。使用这些开发的组件会形成“嵌套地狱”,调试困难。
很多组件在最开始写的时候都是很简单的,基本上就是只做一件事,当你的业务逻辑变得复杂之后,组件也会变得复杂起来。大多数情况下,我们不大可能把组件拆分的更小,因为可能有很多共用的状态逻辑,拆分后,组件之间的通信也会很复杂,甚至需要引用 Redux 来管理组件之间的状态。
要想用好 class 组件,你必须了解 ES6 中的class,理解 JavaSript 中 this
的工作方式,要注意绑定事件处理器,清楚当前this的指向。
详细查看react 官方文档 Hook 简介
我们在平常开发中经常会遇到很多的页面有一些公共的逻辑,我们不能每次遇到的时候都直接把原来的代码 copy 过来改扒改扒,改的时候又要全局搜索改掉(很难保证没有漏的,费时费力)所以要想办法去复用,mixin
、HOC
, render props
等都是实现逻辑复用的方式。
vue和react中都曾用过mixin(react目前已经抛弃) mixin(混入)本质上就是将对象复制到另一个对象上。
const mixin = function (obj, mixins) { const newObj = obj; newObj.prototype = Object.create(obj.prototype); for(let prop in mixins) { if(mixins.hasOwnProperty(prop)) { newObj.prototype[prop] = mixins[prop]; } } return newObj; } const obj = { sayHello() { console.log('hello'); } }; const otherObj = function() { console.log('otherObj'); } const Obj = mixin(otherObj, obj); const a = new Obj(); // otherObj a.sayHello(); // hello 复制代码
mixin存在的几个问题:
HOC是React社区提出的新的方式用来取代mixin的。 高阶函数是函数式编程中一个基本的概念,它描述了一种这样的函数:接受函数作为输入,或是返回一个函数,比如 map, reduce等都是高阶函数。 高阶组件( higher-order component),类似于高阶组件接受一个组件作为参数,返回另一个组件。
function getComponent(WrappedComponent) { return class extends React.Component { render() { return <WrappedComponent {...this.props}/>; } }; } 复制代码
HOC的优点为:
HOC的问题是:
render props: 通过props接受一个返回react element 的函数,来动态决定自己要渲染的结果
<DataProvider render={data => ( <h1>Hello {data.target}</h1> )}/> 复制代码
React Router中就用到了 Render Props
<Router> <Route path="/home" render={() => <div>Home</div>} /> </Router>, 复制代码
它有哪些问题呢
具体实现就是通过一个函数来封装跟状态有关的逻辑,将这些逻辑从组件中抽取出来。而这个函数中我们可以使用其他的Hooks,也可以单独进行测试,甚至将它贡献给社区。
import { useState, useEffect } from 'react'; function useCount() { const [count, setCount] = useState(0); useEffect(() = { document.title = `You clicked ${count} times`; }); return count } 复制代码
hooks的引入就是为了解决上面提到的这么问题,因为 使用函数式组件,我们在开发组件的时候,可以当做平常写函数一样自由。
函数复用是比较容易的,直接传不同的参数就可以渲染不同的组件,复杂组件实现,我们完全可以多封装几个函数,每个函数只单纯的负责一件事。而且很多公用的代码逻辑和一些场景我们可以抽出来,封装成自定义hooks使用,比如 Umi Hooks库封装了很多共用的逻辑,比如 useSearch,封装了异步搜索场景的逻辑;比如 useVirtualList,就封装了虚拟列表的逻辑。
在使用hooks的时候,你可能会对它的规则有很多疑问,比如:
我们先来看一下官方文档给出的解释
每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。
React中是通过类似单链表的形式来实现的,通过 next 按顺序串联所有的 hook。可以看下 源码部分
export type Hook = {| memoizedState: any, baseState: any, baseQueue: Update<any, any> | null, queue: UpdateQueue<any, any> | null, next: Hook | null, |}; export type Effect = {| tag: HookEffectTag, create: () => (() => void) | void, destroy: (() => void) | void, deps: Array<mixed> | null, next: Effect, |}; 复制代码
更详细的推荐查看 React Hooks 原理 和 Under the hood of React’s hooks system。
我们先来看一下使用 setState 的更新机制:
在React
的setState
函数实现中,会根据一个变量isBatchingUpdates
判断是直接更新this.state
还是放到 队列中回头再说。而isBatchingUpdates
默认是false
,也就表示setState
会同步更新this.state
。但是,有一个函数 batchedUpdates
, 这个函数会把isBatchingUpdates
修改为true
,而当React
在调用事件处理函数之前就会调用这个batchedUpdates
,造成的后果,就是由React控制的事件处理程序过程setState
不会同步更新this.state
。
知道这些,我们下面来看两个例子。
下面的代码输出什么?
class Example extends React.Component { constructor() { super(); this.state = { val: 0 }; } componentDidMount() { this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 1 次 log this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 2 次 log setTimeout(() => { this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 3 次 log 1 this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 4 次 log 2 }, 0); } render() { return null; } }; 复制代码
打印结果是: 0, 0, 2, 3。
dirtyComponents
,所以打印时获取的都是更新前的状态 0this.state.val
都是 0,所以执行时都是将0设置为1,在react内部会被合并掉,只执行一次。设置完成后 state.val
值为1。isBatchingUpdates为false
,所以能够直接进行更新,所以连着输出 2, 3上面代码改用react hooks的话
import React, { useEffect, useState } from 'react'; const MyComponent = () => { const [val, setVal] = useState(0); useEffect(() => { setVal(val+1); console.log(val); setVal(val+1); console.log(val); setTimeout(() => { setVal(val+1); console.log(val); setVal(val+1); console.log(val); }, 0) }, []); return null }; export default MyComponent; 复制代码
打印输出: 0, 0, 0, 0。
更新的方式没有改变。首先是因为 useEffect
函数只运行一次,其次setTimeout
是个闭包,内部获取到值val一直都是 初始化声明的那个值,所以访问到的值一直是0。以例子来看的话,并没有执行更新的操作。
在这种情况下,需要使用一个容器,你可以将更新后的状态值写入其中,并在以后的 setTimeout
中访问它,这是useRef
的一种用例。可以将状态值与ref
的current
属性同步,并在setTimeout
中读取当前值。
关于这部分详细内容可以查看 React useEffect的陷阱