React Hooks彻底上手指南

做者:RichLab 衍良javascript

编者按:这是一篇关于 React Hooks 的深度好文,阅读 & 理解须要必定时间,你们能够先收藏,再慢慢看。更多精彩互动欢迎移步 知乎

1 Why Hook?

1.1 从React组件设计理论提及

React以一种全新的编程范式定义了前端开发约束,它为视图开发带来了一种全新的心智模型:html

  • React认为,UI视图是数据的一种视觉映射,即UI = F(DATA),这里的F须要负责对输入数据进行加工、并对数据的变动作出响应
  • 公式里的F在React里抽象成组件,React是以组件(Component-Based)为粒度编排应用的,组件是代码复用的最小单元
  • 在设计上,React采用props属性来接收外部的数据,使用state属性来管理组件自身产生的数据(状态),而为了实现(运行时)对数据变动作出响应须要,React采用基于类(Class)的组件设计
  • 除此以外,React认为组件是有生命周期的,所以开创性地将生命周期的概念引入到了组件设计,从组件的create到destory提供了一系列的API供开发者使用

这就是React组件设计的理论基础,咱们最熟悉的React组件通常长这样:前端

// React基于Class设计组件
class MyConponent extends React.Component {
  // 组件自身产生的数据
  state = {
    counts: 0
  }

  // 响应数据变动
  clickHandle = () => {
    this.setState({ counts: this.state.counts++ });
    if (this.props.onClick) this.props.onClick();
  }

  // lifecycle API
  componentWillUnmount() {
    console.log('Will mouned!');
  }

    // lifecycle API
  componentDidMount() {
    console.log('Did mouned!');
  }

  // 接收外来数据(或加工处理),并编排数据在视觉上的呈现
  render(props) {
    return (
      <>
        <div>Input content: {props.content}, btn click counts: {this.state.counts}</div>
        <button onClick={this.clickHandle}>Add</button>
      </>
    );
  }
}
复制代码

1.2 Class Component的问题

1.2.1 组件复用困局

组件并非单纯的信息孤岛,组件之间是可能会产生联系的,一方面是数据的共享,另外一个是功能的复用:java

  • 对于组件之间的数据共享问题,React官方采用单向数据流(Flux)来解决
  • 对于(有状态)组件的复用,React团队给出过许多的方案,早期使用CreateClass + Mixins,在使用Class Component取代CreateClass以后又设计了Render PropsHigher Order Component,直到再后来的Function Component+ Hooks设计,React团队对于组件复用的探索一直没有中止

HOC使用(老生常谈)的问题:react

  • 嵌套地狱,每一次HOC调用都会产生一个组件实例
  • 可使用类装饰器缓解组件嵌套带来的可维护性问题,但装饰器本质上仍是HOC
  • 包裹太多层级以后,可能会带来props属性的覆盖问题

Render Props:git

  • 数据流向更直观了,子孙组件能够很明确地看到数据来源
  • 但本质上Render Props是基于闭包实现的,大量地用于组件的复用将不可避免地引入了callback hell问题
  • 丢失了组件的上下文,所以没有this.props属性,不能像HOC那样访问this.props.children

1.2.2 Javascript Class的缺陷

一、this的指向(语言缺陷)github

class People extends Component {
  state = {
    name: 'dm',
    age: 18,
  }

  handleClick(e) {
    // 报错!
    console.log(this.state);
  }

  render() {
    const { name, age } = this.state;
    return (<div onClick={this.handleClick}>My name is {name}, i am {age} years old.</div>);
  }
}
复制代码

createClass不须要处理this的指向,到了Class Component稍微不慎就会出现因this的指向报错。算法

二、编译size(还有性能)问题:编程

// Class Component
class App extends Component {
  state = {
    count: 0
  }

  componentDidMount() {
    console.log('Did mount!');
  }

  increaseCount = () => {
    this.setState({ count: this.state.count + 1 });
  }

  decreaseCount = () => {
    this.setState({ count: this.state.count - 1 });
  }

  render() {
    return (
      <>
        <h1>Counter</h1>
        <div>Current count: {this.state.count}</div>
        <p>
          <button onClick={this.increaseCount}>Increase</button>
          <button onClick={this.decreaseCount}>Decrease</button>
        </p>
      </>
    );
  }
}

// Function Component
function App() {
  const [ count, setCount ] = useState(0);
  const increaseCount = () => setCount(count + 1);
  const decreaseCount = () => setCount(count - 1);

  useEffect(() => {
    console.log('Did mount!');
  }, []);

  return (
    <>
      <h1>Counter</h1>
      <div>Current count: {count}</div>
      <p>
        <button onClick={increaseCount}>Increase</button>
        <button onClick={decreaseCount}>Decrease</button>
      </p>
    </>
  );
}
复制代码

Class Component编译结果(Webpack):redux

var App_App = function (_Component) {
  Object(inherits["a"])(App, _Component);

  function App() {
    var _getPrototypeOf2;
    var _this;
    Object(classCallCheck["a"])(this, App);
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }
    _this = Object(possibleConstructorReturn["a"])(this, (_getPrototypeOf2 = Object(getPrototypeOf["a"])(App)).call.apply(_getPrototypeOf2, [this].concat(args)));
    _this.state = {
      count: 0
    };
    _this.increaseCount = function () {
      _this.setState({
        count: _this.state.count + 1
      });
    };
    _this.decreaseCount = function () {
      _this.setState({
        count: _this.state.count - 1
      });
    };
    return _this;
  }
  Object(createClass["a"])(App, [{
    key: "componentDidMount",
    value: function componentDidMount() {
      console.log('Did mount!');
    }
  }, {
    key: "render",
    value: function render() {
      return react_default.a.createElement(/*...*/);
    }
  }]);
  return App;
}(react["Component"]);
复制代码

Function Component编译结果(Webpack):

function App() {
  var _useState = Object(react["useState"])(0),
    _useState2 = Object(slicedToArray["a" /* default */ ])(_useState, 2),
    count = _useState2[0],
    setCount = _useState2[1];
  var increaseCount = function increaseCount() {
    return setCount(count + 1);
  };
  var decreaseCount = function decreaseCount() {
    return setCount(count - 1);
  };
  Object(react["useEffect"])(function () {
    console.log('Did mount!');
  }, []);
  return react_default.a.createElement();
}
复制代码
  • Javascript实现的类自己比较鸡肋,没有相似Java/C++多继承的概念,类的逻辑复用是个问题
  • Class Component在React内部是当作Javascript Function类来处理的
  • Function Component编译后就是一个普通的function,function对js引擎是友好的
🤔问题:React是如何识别纯函数组件和类组件的?

1.3 Function Component缺失的功能

不是全部组件都须要处理生命周期,在React发布之初Function Component被设计了出来,用于简化只有render时Class Component的写法。

  • Function Component是纯函数,利于组件复用和测试
  • Function Component的问题是只是单纯地接收props、绑定事件、返回jsx,自己是无状态的组件,依赖props传入的handle来响应数据(状态)的变动,因此Function Component不能脱离Class Comnent来存在!
function Child(props) {
  const handleClick = () => {
    this.props.setCounts(this.props.counts);
  };

  // UI的变动只能经过Parent Component更新props来作到!!
  return (
    <>
      <div>{this.props.counts}</div>
      <button onClick={handleClick}>increase counts</button>
    </>
  );
}

class Parent extends Component() {
  // 状态管理仍是得依赖Class Component
  counts = 0

  render () {
    const counts = this.state.counts;
    return (
      <>
        <div>sth...</div>
        <Child counts={counts} setCounts={(x) => this.setState({counts: counts++})} />
      </>
    );
  }
}
复制代码

因此,Function Comonent是否能脱离Class Component独立存在,关键在于让Function Comonent自身具有状态处理能力,即在组件首次render以后,“组件自身可以经过某种机制再触发状态的变动而且引发re-render”,而这种“机制”就是Hooks!

Hooks的出现弥补了Function Component相对于Class Component的不足,让Function Component取代Class Component成为可能。

1.4 Function Component + Hooks组合

一、功能相对独立、和render无关的部分,能够直接抽离到hook实现,好比请求库、登陆态、用户核身、埋点等等,理论上装饰器均可以改用hook实现(如react-use,提供了大量从UI、动画、事件等经常使用功能的hook实现)。

case:Popup组件依赖视窗宽度适配自身显示宽度、相册组件依赖视窗宽度作单/多栏布局适配

🤔:请自行脑补使用Class Component来如何实现
function useWinSize() {
  const html = document.documentElement;
  const [ size, setSize ] = useState({ width: html.clientWidth, height: html.clientHeight });

  useEffect(() => {
    const onSize = e => {
      setSize({ width: html.clientWidth, height: html.clientHeight });
    };

    window.addEventListener('resize', onSize);

    return () => {
      window.removeEventListener('resize', onSize);
    };
  }, [ html ]);

  return size;
}

// 依赖win宽度,适配图片布局
function Article(props) {
  const { width } = useWinSize();
  const cls = `layout-${width >= 540 ? 'muti' : 'single'}`;

  return (
    <>
      <article>{props.content}<article>
      <div className={cls}>recommended thumb list</div>
    </>
  );
}

// 弹层宽度根据win宽高作适配
function Popup(props) {
  const { width, height } = useWinSize();
  const style = {
    width: width - 200,
    height: height - 300,
  };
  return (<div style={style}>{props.content}</div>);
}
复制代码

二、有render相关的也能够对UI和功能(状态)作分离,将功能放到hook实现,将状态和UI分离

case:表单验证

function App() {
  const { waiting, errText, name, onChange } = useName();
  const handleSubmit = e => {
    console.log(`current name: ${name}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <>
        Name: <input onChange={onChange} />
        <span>{waiting ? "waiting..." : errText || ""}</span>
      </>
      <p>
        <button>submit</button>
      </p>
    </form>
  );
}
复制代码

2 Hooks的实现与使用

2.1 useState

useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>]
复制代码

做用:返回一个状态以及能修改这个状态的setter,在其余语言称为元组(tuple),一旦mount以后只能经过这个setter修改这个状态。

思考🤔:useState为啥不返回object而是返回tuple?
useState函数申明
  • 使用了Hooks API的函数组件,返回的setter能够改变组件的状态,而且引发组件re-render
  • 和通常意义上的hook(钩子)不同,这里的hook能够屡次调用且产生不一样的效果,且hook随Fiber Node一块儿生灭

2.1.1 为何只能在Function Component里调用Hooks API?

Hooks API的默认实现:

function throwInvalidHookError() {
  invariant(false, 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.');
}

var ContextOnlyDispatcher = {
    ...
  useEffect: throwInvalidHookError,
  useState: throwInvalidHookError,
  ...
};
复制代码

当在Function Component调用Hook:

function renderWithHooks(current, workInProgress, Component, props, refOrContext, nextRenderExpirationTime) {
  currentlyRenderingFiber$1 = workInProgress; // 指针指向当前正在render的fiber节点
  ....
  if (nextCurrentHook !== null) {
    // 数据更新
    ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
  } else {
    // 首次render
    ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
  }
}

/// hook api的实现
HooksDispatcherOnMountInDEV = {
    ...
  useState: function (initialState) {
    currentHookNameInDev = 'useState';
    ...
    return mountState(initialState);
  },
};
复制代码

2.1.2 为何必须在函数组件顶部做用域调用Hooks API?

在类组件中,state就是一个对象,对应FiberNode的memoizedState属性,在类组件中当调用setState()时更新memoizedState便可。可是在函数组件中,memoizedState被设计成一个链表(Hook对象):

// Hook类型定义
type Hook = {
  memoizedState: any, // 存储最新的state
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null, // 更新队列
  next: Hook | null, // 下一个hook
}

// 定义一次更新
type Update<S, A> = {
  ...
  action: A,
  eagerReducer: ((S, A) => S) | null,
  eagerState: S | null, // 待更新状态值
  next: Update<S, A> | null,
  ...
};

// 待更新队列定义
type UpdateQueue<S, A> = {
  last: Update<S, A> | null, // 最后一次更新操做
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null, // 最新处理处理state的reducer
  lastRenderedState: S | null, // 最新渲染后状态
};
复制代码

示例:

function App() {
  const [ n1, setN1 ] = useState(1);
  const [ n2, setN2 ] = useState(2);

  // if (sth) {
  //    const [ n4, setN4 ] = useState(4);
  // } else {
  //    const [ n5, setN5 ] = useState(5);
  // }

  const [ n3, setN3 ] = useState(3);
}
复制代码

Hook存储(链表)结构:

Fiber(Hook)链表结构
  • Hook API调用会产生一个对应的Hook实例(并追加到Hooks链),可是返回给组件的是state和对应的setter,re-render时框架并不知道这个setter对应哪一个Hooks实例(除非用HashMap来存储Hooks,但这就要求调用的时候把相应的key传给React,会增长Hooks使用的复杂度)。
  • re-render时会从第一行代码开始从新执行整个组件,即会按顺序执行整个Hooks链,若是re-render时sth不知足,则会执行useState(5)分支,相反useState(4)则不会执行到,致使useState(5)返回的值实际上是4,由于首次render以后,只能经过useState返回的dispatch修改对应Hook的memoizedState,所以必需要保证Hooks的顺序不变,因此不能在分支调用Hooks,只有在顶层调用才能保证各个Hooks的执行顺序!

2.1.3 useState hook如何更新数据?

useState() mount阶段(部分)源码实现:

// useState() 首次render时执行mountState
function mountState(initialState) {
  // 从当前Fiber生成一个新的hook对象,将此hook挂载到Fiber的hook链尾,并返回这个hook
  var hook = mountWorkInProgressHook();

  hook.memoizedState = hook.baseState = initialState;

  var queue = hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: (state, action) => isFn(state) ? action(state) : action,
    lastRenderedState: initialState
  };
  // currentlyRenderingFiber$1保存当前正在渲染的Fiber节点
  // 将返回的dispatch和调用hook的节点创建起了链接,同时在dispatch里边能够访问queue对象
  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

//// 功能至关于setState!
function dispatchAction(fiber, queue, action) {
  ...
  var update = {
    action, // 接受普通值,也能够是函数
    next: null,
  };
  var last = queue.last;

  if (last === null) {
    update.next = update;
  } else {
    last.next = update;
  }

  // 略去计算update的state过程
  queue.last = update;
  ...
  // 触发React的更新调度,scheduleWork是schedule阶段的起点
  scheduleWork(fiber, expirationTime);
}
复制代码
  • dispatchAction函数是更新state的关键,它会生成一个update挂载到Hooks队列上面,并提交一个React更新调度,后续的工做和类组件一致。
  • 理论上能够同时调用屡次dispatch,但只有最后一次会生效(queue的last指针指向最后一次update的state)
  • 注意useState更新数据和setState不一样的是,前者会与old state作merge,咱们只需把更改的部分传进去,可是useState则是直接覆盖!
schedule阶段介于reconcile和commit阶段之间,schedule的起点方法是scheduleWork。 ReactDOM.render, setState,forceUpdate, React Hooks的dispatchAction都要通过scheduleWork。 Ref: zhuanlan.zhihu.com/p/54042084

update阶段(state改变、父组件re-render等都会引发组件状态更新)useState()更新状态:

function updateState(initialState) {
  var hook = updateWorkInProgressHook();
  var queue = hook.queue;
  var newState;
  var update;

  if (numberOfReRenders > 0) {
    // 组件本身re-render
    newState = hook.memoizedState;
    // renderPhaseUpdates是一个全局变量,是一个的HashMap结构:HashMap<(Queue: Update)>
    update = renderPhaseUpdates.get(queue);
  } else {
    // update
    newState = hook.baseState;
    update = hook.baseUpdate || queue.last;
  }

  do {
    newState = update.action; // action多是函数,这里略去了细节
    update = update.next;
  } while(update !== null)

  hook.memoizedState = newState;
  return [hook.memoizedState, queue.dispatch];
}
复制代码
  • React会依次执行hook对象上的整个update queue以获取最新的state,因此useState()返回的tuple[0]始终会是最新的state!
  • 能够看到,在update阶段,initialState根本没有用到的!

2.1.4 useState hook更新过程

function App() {
  const [n1, setN1] = useState(1);
  const [n2, setN2] = useState(2);
  const [n3, setN3] = useState(3);

  useEffect(() => {
    setN1(10);
    setN1(100);
  }, []);

  return (<button onClick={() => setN2(20)}>click</button>);
}
复制代码

图解更新过程:

useState更新过程
  • setState返回的setter执行会致使re-render
  • 框架内部会对屡次setter操做进行合并(循环执行传入的setter,目的是保证useState拿到最新的状态)

2.2 useEffect

useEffect(effect: React.EffectCallback, deps?: ReadonlyArray<any> | undefined)
复制代码

做用:处理函数组件中的反作用,如异步操做、延迟操做等,能够替代Class Component的componentDidMountcomponentDidUpdatecomponentWillUnmount等生命周期。

2.2.1 useEffect实现剖析

HooksDispatcherOnMountInDEV = {
    useEffect: function() {
    currentHookNameInDev = 'useEffect';
    ...
    return mountEffectImpl(Update | Passive, UnmountPassive | MountPassive, create, deps);
  },
};

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  return hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}

function pushEffect(tag, create, destroy, deps) {
  var effect = {
    tag: tag,
    create: create, // 存储useEffect传入的callback
    destroy: destroy, // 存储useEffect传入的callback的返回函数,用于effect清理
    deps: deps,
    next: null
  };
  .....
  componentUpdateQueue = createFunctionComponentUpdateQueue();
  componentUpdateQueue.lastEffect = effect.next = effect;
  ....
  return effect;
}

function renderWithHooks() {
    ....
  currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
  ....
}
复制代码
  • 与useState传入的是具体state不一样,useEffect传入的是一个callback函数,与useState最大的不一样是执行时机,useEffect callback是在组件被渲染为真实DOM后执行(因此能够用于DOM操做)
  • useEffect调用也会在当前Fiber节点的Hooks链追加一个hook并返回,它的memoizedState存放一个effect对象,effect对象最终会被挂载到Fiber节点的updateQueue队列(当Fiber节点都渲染到页面上后,就会开始执行Fiber节点中的updateQueue中所保存的函数)

2.2.2 deps参数很重要

下面一段很常见的代码,🤔有什么问题?运行demo

// 用Hook写
function App() {
  const [data, setData] = useState('');

  useEffect(() => {
    setTimeout(() => {
      setData(`current data: ${Date.now()}`);
    }, 3000);
  });

  return <div>{data}</div>;
}
// 等价代码
class App extends Component {
  state = {data = ''}

  componentDidMount() {
    setTimeout(() => {
      this.setState({ data: `current data: ${Date.now()}` });
    }, 3000);
  }

  render() {
    return <div>{this.state.data}</div>;
  }
}
复制代码
  • 组件re-render时,函数组件是从新执行整个函数,其中也包括全部“注册”过的hooks,默认状况下useEffect callback也会被从新执行!
  • useEffect能够接受第二个参数deps,用于在re-render时判断是否从新执行callback,因此deps必需要按照实际依赖传入,不能少传也不要多传!
  • deps数组项必须是mutable的,好比不能也没必要传useRef、dispatch等进去
  • deps的比较实际上是浅比较(参阅源码),传入对象、函数进去是无心义
  • 做为最佳实践,使用useEffect时请尽量都传deps(不传入deps的场景笔者暂时没找到)

2.2.3 清理反作用

Hook接受useEffect传入的callback返回一个函数,在Fiber的清理阶段将会执行这个函数,从而达到清理effect的效果:

function App() {
  useEffect(() => {
    const timer = setTimeout(() => {
        console.log('print log after 1s!');
    }, 1000);
    window.addEventListener('load', loadHandle);

    return () => window.removeEventListener('load', loadHandle); // 执行清理
  }, []);
}

// 同等实现
class App extends Component {
  componentDidMount() {
    const timer = setTimeout(() => {
        console.log('print log after 1s!');
    }, 1000);
    window.addEventListener('load', loadHandle);
  }

  componentDidUnmount() {
    window.removeEventListener('load', loadHandle);
  }
}
复制代码

2.3 useContext

对于组件之间的状态共享,在类组件里边官方提供了Context相关的API:

  • 使用React.createContext API建立Context,因为支持在组件外部调用,所以能够实现状态共享
  • 使用Context.Provider API在上层组件挂载状态
  • 使用Context.Consumer API为具体的组件提供状态或者经过contextType属性指定组件对Context的引用

在消费context提供的状态时必需要使用contextType属性指定Context引用或者用<Context.Consumer>包裹组件,在使用起来很不方便(参见React Context官方示例)。

React团队为函数组件提供了useContext Hook API,用于在函数组件内部获取Context存储的状态:

useContext<T>(Context: ReactContext<T>, unstable_observedBits: void | number | boolean): T
复制代码

useContext的实现比较简单,只是读取挂载在context对象上的_currentValue值并返回:

function useContext(content, observedBits) {
  // 处理observedBits,暂时
  // 只有在React Native里边isPrimaryRenderer才会是false
  return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
复制代码
理解useContext的实现,首先要对Context源码实现有所了解,推荐《 React 源码系列 | React Context 详解

useContext极大地简化了消费Context的过程,为组件之间状态共享提供了一种可能,事实上,社区目前基于Hooks的状态管理方案很大一部分是基于useContext来实现的(另外一种是useState),关于状态管理方案的探索咱们放在后面的文章介绍。

2.4 useReducer

useReducer<S, I, A>(reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S, Dispatch<A>]
复制代码

做用:用于管理复杂的数据结构(useState通常用于管理扁平结构的状态),基本实现了redux的核心功能,事实上,基于Hooks Api能够很容易地实现一个useReducer Hook:

const useReducer = (reducer, initialArg, init) => {
  const [state, setState] = useState(
    init ? () => init(initialArg) : initialArg,
  );
  const dispatch = useCallback(
    action => setState(prev => reducer(prev, action)),
    [reducer],
  );
  return useMemo(() => [state, dispatch], [state, dispatch]);
};
复制代码

reducer提供了一种能够在组件外从新编排state的能力,而useReducer返回的dispatch对象又是“性能安全的”,能够直接放心地传递给子组件而不会引发子组件re-render。

function reducer(state, action) {
  // 这里可以拿到组件的所有state!!
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + state.step,
      };
    ...
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, {count: initialCount, step: 10});

  return (
    <>
      <div>{state.count}</div>
      // redux like diaptch
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <ChildComponent dispatch={dispatch} />
    </>
  );
}
复制代码

2.5 性能优化(Memoization)相关Hooks API

2.5.1 useCallback

useCallback<T>(callback: T, deps: Array<mixed> | void | null): T
复制代码

因为javascript函数的特殊性,当函数签名被做为deps传入useEffect时,仍是会引发re-render(即便函数体没有改变),这种现象在类组件里边也存在:

// 当Parent组件re-render时,Child组件也会re-render
class Parent extends Component {
  render() {
    const someFn = () => {}; // re-render时,someFn函数会从新实例化

    return (
      <>
        <Child someFn={someFn} />
        <Other />
      </>
    );
  }
}

class Child extends Component {
  componentShouldUpdate(prevProps, nextProps) {
    return prevProps.someFn !== nextProps.someFn; // 函数比较将永远返回false
  }
}
复制代码

Function Component(查看demo):

function App() {
  const [count, setCount] = useState(0);
  const [list, setList] = useState([]);
  const fetchData = async () => {
    setTimeout(() => {
      setList(initList);
    }, 3000);
  };

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return (
    <>
      <div>click {count} times</div>
      <button onClick={() => setCount(count + 1)}>Add count</button>
      <List list={list} />
    </>
  );
}
复制代码

解决方案:

  • 将函数移到组件外部(缺点是没法读取组件的状态了)
  • 条件容许的话,把函数体移到useEffect内部
  • 若是函数的调用不止是useEffect内部(如须要传递给子组件),可使用useCallback API包裹函数,useCallback的本质是对函数进行依赖分析,依赖变动时才从新执行

2.5.2 useMemo & memo

useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T
复制代码

useMemo用于缓存一些耗时的计算结果,只有当依赖参数改变时才从新执行计算:

function App(props) {
  const start = props.start;
  const list = props.list;
  const fibValue = useMemo(() => fibonacci(start), [start]); // 缓存耗时操做
  const MemoList = useMemo(() => <List list={list} />, [list]);

  return (
    <>
      <div>Do some expensive calculation: {fibValue}</div>
      {MemoList}
      <Other />
    </>
  );
}
复制代码
简单理解: useCallback(fn, deps) === useMemo(() => fn, deps)

在函数组件中,React提供了一个和类组件中和PureComponent相同功能的API React.memo,会在自身re-render时,对每个 props 项进行浅对比,若是引用没有变化,就不会触发重渲染。

// 只有列表项改变时组件才会re-render
const MemoList = React.memo(({ list }) => {
  return (
    <ul>
      {list.map(item => (
        <li key={item.id}>{item.content}</li>
      ))}
    </ul>
  );
});
复制代码

相比React.memouseMemo在组件内部调用,能够访问组件的props和state,因此它拥有更细粒度的依赖控制。

2.6 useRef

关于useRef其实官方文档已经说得很详细了,useRef Hook返回一个ref对象的可变引用,但useRef的用途比ref更普遍,它能够存储任意javascript值而不只仅是DOM引用。

useRef的实现比较简单:

// mount阶段
function mountRef(initialValue) {
  var hook = mountWorkInProgressHook();
  var ref = { current: initialValue };
  {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}

// update阶段
function updateRef(initialValue) {
  var hook = updateWorkInProgressHook();
  return hook.memoizedState;
}
复制代码

useRef是比较特殊:

  • useRef是全部Hooks API里边惟一一个返回mutable数据的
  • 修改useRef值的惟一方法是修改其current的值,且值的变动不会引发re-render
  • 每一次组件render时useRef都返回固定不变的值,不具备下文所说的Capture Values特性

2.7 其余Hooks API

  • useLayoutEffect:用法和useEffect一致,与useEffect的差异是执行时机,useLayoutEffect是在浏览器绘制节点以前执行(和componentDidMount以及componentDidUpdate执行时机相同)
  • useDebugValue:用于开发者工具调试
  • useImperativeHandle:配合forwardRef使用,用于自定义经过ref给父组件暴露的值

2.8 Capture Values特性

一、useState具备capture values,查看demo

二、useEffect具备capture values

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
    // 连续点击三次button,页面的title将依次改成一、二、3,而不是三、三、3
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
复制代码

三、event handle具备capture values,查看demo

四、。。。全部的Hooks API都具备capture values特性,除了useRef,查看demo(setTimeout始终能拿到state最新值),state是Immutable的,ref是mutable的。

function mountRef(initialValue) {
  var hook = mountWorkInProgressHook();
  var ref = { current: initialValue }; // ref就是一个普通object的引用,没有闭包
  {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}
复制代码

非useRef相关的Hooks API,本质上都造成了闭包,闭包有本身独立的状态,这就是Capture Values的本质

2.9 自定义组件:模拟一些经常使用的生命周期

  • componentDidMount:当deps为空时,re-render时再也不执行callback
// mount结束,已经更新到DOM
const onMount = function useDidMount(effect) => {
    useEffect(effect, []);
};
复制代码
  • componentDidUpdate
// layout结束,render DOM以前(会block rendering)
const onUpdate = function useUpdate(effect) => {
  useLayoutEffect(effect, []);
};
复制代码
  • componentWillUnMount
const unMount = function useWillUnMount(effect, deps = []) => {
  useEffect(() => effect, deps);
};
复制代码
  • shouldComponentUpdate(或React.PureComponent)
// 使用React.memo包裹组件
const MyComponent = React.memo(() => {
  return <Child prop={prop} />
}, [prop]);

// or
function A({ a, b }) {
  const B = useMemo(() => <B1 a={a} />, [a]);
  const C = useMemo(() => <C1 b={b} />, [b]);
  return (
    <>
      {B}
      {C}
    </>
  );
}
复制代码

3 Hooks的问题

一、Hooks能解决组件功能复用,但没有很好地解决JSX的复用问题,好比(1.4)表单验证的case:

function App() {
  const { waiting, errText, name, onChange } = useName();
  // ...

  return (
    <form>
      <div>{name}</div>
      <input onChange={onChange} />
      {waiting && <div>waiting<div>}
      {errText && <div>{errText}<div>}
    </form>
  );
}
复制代码

虽可以将用户的输入、校验等逻辑封装到useName hook,但DOM部分仍是有耦合,这不利于组件的复用,期待React团队拿出有效的解决方案来。

二、React Hooks模糊了(或者说是抛弃了)生命周期的概念,但也带来了更高门槛的学习心智(如Hooks生命周期的理解、Hooks Rules的理解、useEffect依赖项的判断等),相比Vue3.0即将推出的Hooks有较高的使用门槛。

三、类拥有比函数更丰富的表达能力(OOP),React采用Hooks+Function Component(函数式)的方式实际上是一种无奈的选择,试想一个挂载了十几个方法或属性的Class Component,用Function Component来写如何组织代码使得逻辑清晰?这背后实际上是函数式编程与面向对象编程两种编程范式的权衡。

4 Ref


最后,感谢你认真阅读这么长的一篇文章~

蚂蚁 RichLab 前端团队」致力于与你共享高质量的技术文章

欢迎关注咱们的专栏,将文章分享给你的好友,共同成长 :-)

咱们团队正在急招:互动图形技术、前端/全栈开发、前端架构、算法、大数据开发等方向任选,指望层级 P6+~P7,团队技术氛围好,上升空间大,简历能够直接砸给我哈 shudai.lyy@alibaba-inc.com

相关文章
相关标签/搜索