关于前端:当谈论-React-hook我们究竟说的是什么

33次阅读

共计 20216 个字符,预计需要花费 51 分钟才能阅读完成。

这个题目很大,然而落点很小,只是我,一个开发者在学习和应用 hooks 中的一点感触和总结。

React hook 的由来

React hook 的由来,其实也能够看作是前端技术一直演进的后果。

在 world wide web 刚刚诞生的洪荒时代,还没有 js,Web 页面也都是动态的,更没有所谓的前端工程师,页面的内容与更新齐全由后端生成。这就使得页面的任意一点更新,都要刷新页面由后端从新生成,体验十分蹩脚。随后就有了 Brendan 十天创世、网景微软浏览器之争、HTML 的改良、W3C 小组的建设等等。

起初 ajax 技术被逐步器重,页面按钮提交 / 获取信息终于不必再刷新页面了,交互体验降级。随后又迎来了 jquery 的时代,能够不便地操作 DOM 和实现各种成果,大大降低了前端门槛。前端队伍的壮大,也催生了越发简单的交互,Web page 也逐步向着 Web App 的方向进化。

jquery 能够将一大段的 HTML 构造的字符串通过 $.html、$.append、$.before 的形式插入到页面上,尽管能够帮忙咱们以更难受的形式来操作 DOM,但不能从根本上解决当 DOM 操作量过多时的前端侧压力大的问题。

随着页面内容和交互越来越简单,如何解决这些繁琐、巨量的 DOM 操作带来的前端侧压力的问题,并可能把各个 HTML 扩散到不同的文件中,而后依据理论状况渲染出相应的内容呢?

这时候的前端们借鉴了后端技术,演绎出一个公式:html = template(data),也就带来了模板引擎计划。模板引擎计划偏向于点对点地解决繁琐 DOM 操作问题,它并没有也不打算替换掉 jquery,两者是共存的。

随后陆续诞生了不少模板引擎,像 handlebars、Mustache 等等。无论是选用了哪种模板引擎,都离不开 html = template(data) 的模式,模板引擎的实质都是简化了拼接字符串的过程,通过类 HTML 的语法疾速搭建起各种页面构造,通过变更数据源 data 来对同一个模板渲染出不同的成果。

这成就了模板引擎,但也限制住了它,它立足于「实现高效的字符串拼接」,但也局限于此,你不能指望模板引擎去做太简单的事件。晚期的模板引擎在性能上也不如人意,因为不够智能,它更新 DOM 的形式是将曾经渲染好的 DOM 登记,而后从新渲染,如果操作 DOM 频繁,体验和性能都会有问题。

尽管模板引擎有其局限性,然而 html=template(data) 的模式还是很有启发性,一批前端先驱兴许是从中吸取了灵感,决定要持续在「数据驱动视图」的方向上深刻摸索。模板引擎的问题在于对实在 DOM 的批改过于粗犷,导致了 DOM 操作的范畴太大,进而影响了性能。

既然实在的 DOM 性能消耗太大了,那操作假的 DOM 好了。既然批改的范畴太大,那每次批改的范畴变小。

于是,模板引擎计划中的原本是“数据 + 模板”造成实在 DOM 的过程中退出了虚构 DOM 这一层。

留神上图,右侧的“模板”是打引号的,因为这里不肯定是真的模板,起到相似模板的作用即可。比方 JSX 并不是模板,只是一种相似模板语法的 js 拓展,它领有齐全的 js 能力。退出了虚构 DOM 这一层后,能做的事件就很多了,首先是能够 diff,也能够实现同一套代码跨平台了。

当初假的 DOM 有了,通过 diff(找不同)+ patch(使统一)的协调过程,也能够一种较为准确地形式批改 DOM 了。咱们来到了 15.x 版本的 React。

咱们这时候有了 class 组件,有了函数式组件,然而为什么还须要退出 hook 呢?

官网的说法是这样的:

  • 在组件之间复用状态逻辑很难
  • 简单组件变得难以了解
  • 难以了解的 class

诚然这些都是 class 的痛点所在,咱们在应用 class 编写一个简略组件会遇到哪些问题:

  • 难以推敲的 this
  • 关联的逻辑被拆分
  • 纯熟记忆泛滥的生命周期,在适合的生命周期里做适当的事件
  • 代码量绝对更多,尤其是写简略组件时
class FriendStatus extends React.Component {constructor(props) {super(props);
    this.state = {isOnline: null};
    this.handleStatusChange = this.handleStatusChange.bind(this); // 要手动绑定 this
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus( // 订阅和勾销订阅逻辑的扩散
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() { // 要纯熟记忆并应用各种生命周期,在适当的生命周期里做适当的事件
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({isOnline: status.isOnline});
  }

  render() {if (this.state.isOnline === null) {return 'Loading...';}
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

React 的理念表白 UI = f(data) 中,React 组件的定位也更像是函数,像下面的示例其实就是 UI = render(state),要害的是 render 办法,其余的都是些通过 this 传参和一些外围反对函数。

针对上述问题,React 决定给函数组件带来新的能力。

过来的函数组件都是无状态组件,它们只能被动从内部承受数据。我心愿组件在更新后仍旧保留我上次输出的值或者选中与否的状态,这些值或者状态最终会反馈到宿主实例上(对浏览器来说就是 DOM)展现给用户,这是太常见的需要了。为了实现这种保护本身部分状态的性能,React 给函数组件绑定了一个部分状态。

这种绑定部分状态的形式,就是 React hook useState。

React 为了让函数可能更好地构建组件,还应用了很多个性来加强函数,绑定部分状态就是这些加强函数的个性之一。

所谓 React hook,就是这些加强函数组件能力个性的钩子,把这些个性「钩」进纯函数。 纯函数组件能够通过 useState 取得绑定部分状态的能力,通过 useEffect 来取得页面更新后执行副作用的能力,甚至通过你的自定义 hook useCounter 来取得加、减数值、设置数值、重置计数器等一整套治理计数器的逻辑。这些都是 hook 所赋予的能力。

React 官网说没有打算将 Class 从 React 中移除,但当初重心在加强函数式组件上。作为开发者的咱们,只有还在应用 React,就无奈齐全回绝 hooks。

尽管 hooks 并不完满,也有很多人吐槽,咱们尝试去拥抱它吧。

React hook 的实现

后面咱们提到了,React hook 是有益于构建 UI 的一系列个性,是用来加强函数式组件的。更具体的来说,hook 是一类非凡的函数。那么这类加强一般无状态组件的非凡函数到底是何方神圣?咱们既然要探寻 hook,就绕不开它的实现原理。

咱们拿 useState 为例,它是怎么做到在一次次函数组件被执行时,放弃住过来的 state 呢?

尽管源码是简单的,但原理可能是简略的。简略地说,之所以能放弃住 state, 是在一个函数组件之外的中央,保留了一个「对象」,这个对象里记录了之前的状态。

那到底如何实现?咱们从一段简略版的 useState 的实现里来一窥到底。

首先咱们是这么用 useState 的:

function App () {const [num, setNum] = useState(0);
  const [age, setAge] = useState(18);
  const clickNum = () => {setNum(num =>  num + 1);
    // setNum(num =>  num + 1);  // 是可能调用屡次的
  }
  const clickAage = () => {setNum(age =>  age + 3);
    // setNum(num =>  num + 1);  // 是可能调用屡次的
  }
  return <div>
    <button onClick={clickNum}>num: {num}</button>
    <button onClick={clickAage}>age:{age}</button>
  </div>
}

因为 jsx 须要 babel 的反对,咱们的简略版 demo 为了 UI 无关更简略的展现、应用 useState,咱们将上述常见应用形式稍稍革新为:

当执行了 App() 时,将返回一个对象,咱们调用对象的办法,就是模仿点击。

function App () {const [num, setNum] = useState(0);
    const [age, setAge] = useState(10);
    console.log(isMount ? '首次渲染' : '更新');
    console.log('num:', num);
    console.log('age:', age);
    const clickNum = () => {setNum(num =>  num + 1);
    //   setNum(num =>  num + 1);  // 是可能调用屡次的
    }
    const clickAge = () => {setAge(age =>  age + 3);
      // setNum(num =>  num + 1);  // 是可能调用屡次的
    }
    return {
      clickNum,
      clickAge
    }
  }

那咱们就从这个函数开始。

首先,组件要挂载到页面上,App 函数必定是须要执行的。执行一开始,咱们就遇到了 useState 这个函数。当初请临时遗记 useState 是个 React hook,它只是一个函数,跟其余函数没有任何不同。

在开始 useState 函数之前,先简略理解下链表 这种数据结构。

u1 -> u2 -> u3 -> u1,这是环状链表。

u1 -> u2 -> u3 -> null,这是单向链表。

咱们应用的链表,所须要的准备常识只有这些,它能保障咱们依照肯定程序不便的读取数据。

在之前表述的 useState 原理中,咱们提到:

之所以能放弃住 state,是在一个函数组件之外的中央,保留了一个「对象」,这个对象里记录了之前的状态。

那咱们在函数之外,先申明一些必要的货色:

// 组件是分首次渲染和后续更新的,那么就须要一个货色来判断这两个不同阶段,简略起见,咱们是应用这个变量好了。let isMount = true;  // 最开始必定是 true

// 咱们在组件中,常常是应用多个 useState 的,那么须要一个变量,来记录咱们以后切实解决那个 hook。let workInProgressHook = null; // 指向以后正在解决的那个 hook

// 针对 App 这个组件,咱们须要一种数据结构来记录 App 内所应用的 hook 都有哪些,以及记录 App 函数自身。这种构造咱们就命名为 fiber
const fiber = {
  stateNode: App, // 对函组件来说,stateNode 就是函数自身
  memorizedState: null // 链表构造。用来记录 App 里所应用的 hook 的。}

// 应用 setNum 是会更新组件的,那么咱们也须要一种能够更新组件的办法。这个办法就叫做 schedule
function schedule () {
  // 每次执行更新组件时,都须要从头开始执行各个 useState,而 fiber.memorizedState 记录着链表的终点。即 workInProgressHook 重置为 hook 链表的终点
  workInProgressHook = fiber.memorizedState;
  // 执行 App()
  const app = fiber.stateNode(); 
  // 执行完 App 函数了,意味着首次渲染曾经完结了,这时候标记位该扭转了。isMount = false;
  return app;
}

里面的货色筹备好了,开始 useState 这个函数的外部。

在开始之前,咱们对 useState 有几个疑难:

  • useState 到底怎么放弃住之前的状态的?
  • 如果屡次调用 setNum 这类更新状态的函数,该怎么解决这些函数呢?
  • 如果这个 useState 执行完了,怎么晓得下一个 hook 该去哪里找呢?

带着这些疑难,咱们进入 useState 的外部:

// 计算新状态,返回扭转状态的办法
function useState(initialState) {
    // 申明一个 hook 对象,hook 对象里将有三个属性,别离用来记录一些货色,这些货色跟咱们上述的三个疑难相干
    // 1. memorizedState,记录着 state 的初始状态(疑难 1 相干)// 2. queue, queue.pending 也是个链表,像下面所说,setNum 是可能被调用屡次的,这里的链表,就是记录这些 setNum。(疑难 2 相干)// 3. next, 链表构造,示意在 App 函数中所应用的下一个 useState(疑难 3 相干)let hook; 
    if (isMount) {
      // 首次渲染,也就是第一次进入到本 useState 外部,每一个 useState 对应一个本人的 hook 对象,所以这时候本 useState 还没有本人的的 hook 数据结构,创立一个
      hook = {
        memorizedState: initialState,
        queue: {pending: null // 此时还是 null 的,当咱们当前调用 setNum 时,这里才会被扭转},
        next: null
      }
      // 尽管当初是在首次渲染阶段,然而,却不肯定是进入的第一个 useState,须要判断
      if (!fiber.memorizedState) {
        // 这时候才是首次渲染的第一个 useState. 将以后 hook 赋值给 fiber.memorizedState
        fiber.memorizedState = hook; 
      } else {
        // 首次渲染进入的第 2、3、4...N 个 useState
        // 后面咱们提到过,workInProgressHook 的用途是,记录以后正在解决的 hook (即 useState),当进入第 N(N>1)个 useState 时,workInProgressHook 曾经存在了,并且指向了上一个 hook
        // 这时候咱们须要把本 hook,增加到这个链表的结尾
        workInProgressHook.next = hook;
      }
      // workInProgressHook 指向以后的 hook
      workInProgressHook = hook;
    } else {
      // 非首次渲染的更新阶段
      // 只有不是首次渲染,workInProgressHook 所在的这条记录 hook 程序的链表必定曾经建设好了。而且 fiber.memorizedState 记录着这条链表的终点。// 组件更新,也就是至多经验了一次 schedule 办法,在 schedule 办法里,有两个步骤:// 1. workInProgressHook = fiber.memorizedState,将 workInProgressHook 置为 hook 链表的终点。首次渲染阶段建设好了 hook 链表,所以更新时,workInProgressHook 必定是存在的
      // 2. 执行 App 函数,意味着 App 函数里所有的 hook 也会被从新执行一遍
      hook = workInProgressHook; // 更新阶段此时的 hook,是首次渲染时曾经建设好的 hook,取出来即可。所以,这就是为什么不能在条件语句中应用 React hook。// 将 workInProgressHook 往后挪动一位,下次进来时的 workInProgressHook 就是下一个以后的 hook
      workInProgressHook = workInProgressHook.next;
    }
    // 上述都是在建设、操作 hook 链表,useState 还要解决 state。let state = hook.memorizedState; // 可能是传参的初始值,也可能是记录的上一个状态值。新的状态,都是在上一个状态的根底上解决的。if (hook.queue.pending) {
      let firstUpdate = hook.queue.pending.next; // hook.queue.pending 是个环装链表,记录着屡次调用 setNum 的程序,并且指向着链表的最初一个,那么 hook.queue.pending.next 就指向了第一个
      do {
        const action = firstUpdate.action;
        state = action(state); // 所以,屡次调用 setNum,state 是这么被计算出来的
        firstUpdate.next = firstUpdate.next
      } while (firstUpdate !== hook.queue.pending.next) // 始终解决 action,直到回到环状链表第一位,阐明曾经齐全解决了
      hook.queue.pending = null;
    }
    hook.memorizedState = state; // 这就是 useState 能放弃住过来的 state 的起因
    return [state, dispatchAction.bind(null, hook.queue)]
  }

在 useState 中,次要是做了两件事:

  • 建设 hook 的链表。将所有应用过的 hook 有序连贯在一起,并通过挪动指针,使链表里记录的 hook 和以后真正被解决的 hook 可能一一对应。
  • 解决 state。在上一个 state 的根底上,通过 hook.queue.pending 链表来一直调用 action 函数,直到计算出最新的 state。

在最初,返回了 diapatchAction.bind(null, hook.queue),这才是 setNum 的真正本体,可见在 setNum 函数中,是暗藏携带着 hook.queue 的。

接下来咱们来看看 dispatchAction 的实现。

function dispatchAction(queue, action) {
    // 每次 dispatchAction 触发的更新,都是用一个 update 对象来表述
    const update = {
      action,
      next: null // 记录屡次调用该 dispatchAction 的程序的链表
    }
    if (queue.pending === null) {
      // 阐明此时,是这个 hook 的第一次调用 dispatchAction
      // 建设一个环状链表
      update.next = update;
    } else {
      // 非第一调用 dispatchAction
      // 将以后的 update 的下一个 update 指向 queue.pending.next 
      update.next = queue.pending.next;        
      // 将以后 update 增加到 queue.pending 链表的最初一位
      queue.pending.next = update;
      }
    queue.pending = update; // 把每次 dispatchAction 都把 update 赋值给 queue.pending,queue.pending 会在下一次 dispatchAction 中被应用,用来代表上一个 update,从而建设起链表
    // 每次 dispatchAction 都触发更新
    schedule();}
  

下面这段代码里,7 -18 行不太好了解,我来简略解释一下。

假如咱们调用了 3 次 setNum 函数,产生了 3 个 update,A、B、C。

当产生第一个 update A 时:

A:此时 queue.pending === null,

执行 update.next = update, 即 A.next = A;

而后 queue.pending = A;

建设 A -> A 的环状链表

B:此时 queue.pending 曾经存在了,

update.next = queue.pending.next 即 B.next = A.next 也就是 B.next = A

queue.pending.next = update; 即 A.next = B, 破除了 A ->A 的链条,将 A ->B

queue.pending = update 即 queue.pending = B

建设 B -> A -> B 的环状链表

C: 此时 queue.pending 也曾经存在了

update.next = queue.pending.next,即 C.next = B.next, 而 B.next = A,C.next = A

queue.pending.next = update, 即 B.next = C

queue.pending = update, 即 queue.pending = C

因为 C -> A,B -> C,而第二步中,A 是指向 B 的,即

建设起 C -> A -> B -> C 环状链表

当初,咱们曾经实现了简略 useState 的代码了,能够操作试试看,全副代码如下:

let isMount = true;
let workInProgressHook = null;
const fiber = {
  stateNode: App,
  memorizedState: null
}

function schedule () {
  workInProgressHook = fiber.memorizedState;
  const app = fiber.stateNode(); 
  isMount = false;
  return app;
}

function useState(initialState) {
      let hook; 
    if (isMount) {
      hook = {
        memorizedState: initialState,
        queue: {pending: null},
        next: null
      }
      if (!fiber.memorizedState) {fiber.memorizedState = hook;} else {workInProgressHook.next = hook;}
      workInProgressHook = hook;
    } else {
      hook = workInProgressHook;
      workInProgressHook = workInProgressHook.next;
    }
    let state = hook.memorizedState;
    if (hook.queue.pending) {
        let firstUpdate = hook.queue.pending.next
        do {
            const action = firstUpdate.action;
            state = action(state);
            firstUpdate.next = firstUpdate.next
        } while (firstUpdate !== hook.queue.pending.next)
      hook.queue.pending = null;
    }
    hook.memorizedState = state;
    return [state, dispatchAction.bind(null, hook.queue)]
  }

  function dispatchAction(queue, action) {
    const update = {
      action,
      next: null
    }
    if (queue.pending === null) {update.next = update;} else {
      update.next = queue.pending.next;        
      queue.pending.next = update;
      }
    queue.pending = update;
    schedule();}

  function App () {const [num, setNum] = useState(0);
    const [age, setAge] = useState(10);
    console.log(isMount ? '首次渲染' : '更新');
    console.log('num:', num);
    console.log('age:', age);
    const clickNum = () => {setNum(num =>  num + 1);
    //   setNum(num =>  num + 1);  // 是可能调用屡次的
    }
    const clickAge = () => {setAge(age =>  age + 3);
      // setNum(num =>  num + 1);  // 是可能调用屡次的
    }
    return {
      clickNum,
      clickAge
    }
  }

  window.App = schedule();

复制而后浏览器控制台粘贴,试试 App.clickNum() , App.clickAge() 吧。

因为咱们是每次更新都调用了 schedule,所以 hook.queue.pending 只有存在就会被执行,而后将 hook.queue.pending = null,所以在咱们的简略版 useState 里,queue.pending 所建设的环状链表没有被应用到。而在实在的 React 中,batchedUpdates 会将屡次 dispatchAction 执行完后,再触发一次更新。这时候就须要环状链表了。

置信通过下面具体的代码正文解说,对于后面咱们对 useState 的 3 个疑难,应该曾经有了答案。

– useState 到底怎么放弃住之前的状态?

每个 hook 都记录了上一次的 state,而后依据 queue.pending 链表中保留的 action,从新计算一遍,失去新的 state 返回。并记录此时的 state 供下一次状态更新应用。

– 如果我屡次调用 setNum 这类 dispatch 函数,该怎么解决这些函数呢?

屡次调用 dispatchAction 函数,会被存储在 hook.queue.pending 中,作为更新根据。每次组件更新完,hook.queue.pending 置 null。如果在之后再有 dispatchAction,则持续增加到 hook.queue.pending,并在 useState 函数中被顺次执行 action,而后再次置 null。

– 如果这个 useState 执行完了,下一个 hook 该去哪里找呢?

在首次渲染的时候,所有组件内应用的 hook 都按程序以链表的模式存在于组件对应的 fiber.memorizedState 中,并用一个 workInProgress 来标记以后正在解决的 hook。每解决完一个,workInProgress 将挪动到 hook 的下一位,保障解决的 hook 的程序和首次渲染时收集的程序严格对应。

React hook 的理念

依据 Dan 的博客(https://overreacted.io/zh-han…),React hook 是在践行代数效应(algebraic effects)。我对代数效应不太懂,只能含糊的将「代数效应」了解为「应用某种形式(表达式 / 语法)来取得某种成果」,就像通过 useState 这种形式,让组件取得状态,对使用者来说,不用再关怀到底是如何实现的,React 会帮咱们解决,而咱们能够专一于用这些效应来做什么。

但为什么 hook 抉择了函数式组件?是什么让函数式组件和类组件这么不同?

从写法上,过来的 class 组件的写法,根本是命令式的,当满足某个条件,去做一些事件。

class Box extends React.components {componentDidMount () {// fetch data}
  componentWillReceiveProps (props, nextProps) {if (nextProps.id !== props.id) {// this.setState}
  }
}

而 hook 的写法,则变成了申明式,先申明一些依赖,当依赖变动,主动执行某些逻辑。

function Box () {useEffect(() => {// fetch data}, [])
  useEffect(() => {// setState}, [id])
}

这两种哪种更好,可能因人而异。但我感觉对于第一次接触 React 的人来说,funciton 必定是更亲切的。

两者更大的差异是更新形式不同,class 组件是通过扭转组件实例的 this 中的 props、state 内的值,而后从新执行 render 来拿到最新的 props、state,组件是不会再次实例化的。

而函数式组件,则在更新时从新执行了函数自身。每一次执行的函数,都能够看做互相独立的。

我感觉这种更新形式的区别,是不习惯函数式组件的次要起因。函数组件捕捉了渲染所须要的值,所以有些状况下后果总是出乎意料。

就像这个例子(https://codesandbox.io/s/reac…),当咱们点击加号,而后尝试拖动窗口,看看控制台打印了什么?

没错,count is 0。
只管你点击了很屡次按钮,只管最新的 count 曾经变成了 N,然而每一次 App 的 context 都是独立的,在 handleWindowResize 被定义的时候,它看到的 count 是 0,而后被绑定事件。尔后 count 变动,跟这一个 App 世界里的 handleWindowResize 曾经没有关系了。会有新的 handleWindowResize 看到了新的 count,然而被绑定事件的,只有最后那个。

而 Class 版本是这样的:

class App extends Component {constructor(props) {super(props);
    this.state = {count: 0};
    this.handleWindowResize = this.handleWindowResize.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }
  handleWindowResize() {console.log(`count is ${this.state.count}`);
  }
  handleClick() {
    this.setState({count: this.state.count + 1});
  }
  componentDidMount() {window.addEventListener("resize", this.handleWindowResize);
  }
  componentWillUnmount () {window.removeEventListener('resize', this.handleWindowResize)
  }
  render() {const { count} = this.state;
    return (
      <div className="App">
        <button onClick={this.handleClick}>+</button>
        <h1>{count}</h1>
      </div>
    );
  }
}

在 Class 的版本里,App 的实例并不会再次创立,无论更新多少次 this.handleWindowResize、this.handleClick 还是一样,只是外面的 this.state 被 React 批改了。

从这个例子,咱们多少可能看出「函数捕捉了渲染所须要的值」的意思,这也是函数组件和类组件的次要不同。

但为什么是给函数式组件减少 hook?

给函数式组件减少 hook,除了是要解决 class 的几个缺点:

  • 在组件之间复用状态逻辑很难
  • 简单组件变得难以了解
  • 难以了解的 class

之外,从 React 理念的角度,UI = f(state) 也阐明了,组件应该只是数据的通道而已,组件实质上更贴近函数。从集体应用角度,其实很多状况下,咱们本不须要那么重的 class,只是苦于无状态组件没法 setState 而已。

React hook 的意义

后面说了这么多 hook,都是在说它如何实现,和 Class 组件有何不同,但还有一个最基本的问题没有答复,hook 给咱们开发者带来了什么?

React 官网里提到了,Hook 能够让组件之间状态可用逻辑更简略,并用高阶组件来比照,然而首要问题是,咱们为什么要用 HOC?

先看这个例子(https://codesandbox.io/s/mode…):

import React from "react";

function Count({count, add, minus}) {
  return (<div style={{ flex: 1, alignItems: "center", justifyContent: "center"}}>
      <div>You clicked {count} times</div>
      <button
        onClick={add}
        title={"add"}
        style={{minHeight: 20, minWidth: 100}}
      >
        +1
      </button>
      <button
        onClick={minus}
        title={"minus"}
        style={{minHeight: 20, minWidth: 100}}
      >
        -1
      </button>
    </div>
  );
}

const countNumber = (initNumber) => (WrappedComponent) =>
  class CountNumber extends React.Component {state = { count: initNumber};
    add = () => this.setState({ count: this.state.count + 1});
    minus = () => this.setState({ count: this.state.count - 1});
    render() {
      return (
        <WrappedComponent
          {...this.props}
          count={this.state.count}
          add={this.add.bind(this)}
          minus={this.minus.bind(this)}
        />
      );
    }
  };
export default countNumber(0)(Count);

成果就是展现以后数值,点击产生加减成果。

之所以要应用这种形式,是为了在被包裹组件内部提供一套可复用的状态和办法。本例中即 state、add、minus,从而使前面如果有其余性能类似的但款式不同的 WrappedComponent,能够间接用 countNumber 包裹一下就行。

从这个例子中,其实能够看到咱们应用 HOC 实质是想做两件事, 传值和同步。

传值是将里面失去的值传给咱们被包裹的组件。而同步,则是让被包裹的组件利用新值从新渲染。

从这里咱们大抵能够看到 HOC 的两个弊病:

  • 因为咱们想让子组件从新渲染的形式无限,要么高阶组件 setState,要么 forceUpdate,而这类办法都是 React 组件内的,无奈独立于 React 组件应用,所以 add\minus 这种业务逻辑和展现的 UI 逻辑,不得不粘合在一起。
  • 应用 HOC 时,咱们往往是多个 HOC 嵌套应用的。而 HOC 遵循透传与本身无关的 props 的约定,导致最终达到咱们的组件时,有太多与组件并不太相干的 props,调试也相当简单。咱们没有一种很好的办法来解决多层 HOC 嵌套所带来的麻烦。

基于这两点,咱们能力说 hooks 带来比 HOC 更能解决逻辑复用难、嵌套天堂等问题所谓的“优雅”形式。

HOC 里的业务逻辑不能抽到组件外的某个函数,只有组件内才有方法触发子组件从新渲染。而当初自定义 hook 带来了在组件外触发组件从新渲染的能力,那么难题就迎刃而解。

应用 hook,再也不必把业务逻辑和 UI 组件夹杂在一起了,下面的例子(https://codesandbox.io/s/mode…),用 hook 的形式是这样实现的:


// 业务逻辑拆分到这里了
import {useState} from "react";

function useCounter() {const [count, setCount] = useState(0);
  const add = () => setCount((count) => count + 1);
  const minus = () => setCount((count) => count - 1);
  return {
    count,
    add,
    minus
  };
}
export default useCounter;
// 纯 UI 展现组件
import React from "react";
import useCounter from "./counterHook";

function Count() {const { count, add, minus} = useCounter();
  return (<div style={{ flex: 1, alignItems: "center", justifyContent: "center"}}>
      <div>You clicked {count} times</div>
      <button
        onClick={add}
        title={"add"}
        style={{minHeight: 20, minWidth: 100}}
      >
        +1
      </button>
      <button
        onClick={minus}
        title={"minus"}
        style={{minHeight: 20, minWidth: 100}}
      >
        -1
      </button>
    </div>
  );
}
export default Count;

这种拆分,让咱们终于能够把业务和 UI 分来到了。如果想获取相似之前嵌套 HOC 那样的能力,只须要再引入一行 hook 就行了。


function Count() {const { count, add, minus} = useCounter();
  const {loading} = useLoading();
  return loading ? (<div>loading...please wait...</div>) : (<div style={{ flex: 1, alignItems: "center", justifyContent: "center"}}>
      ...
    </div>
  );
}
export default Count;

useCounter、useLoading 各自保护各的,而咱们所引入的的货色,高深莫测。

在这个计数器的例子上,咱们能够想的更远一些。既然当初逻辑和 UI 是能够拆分的,那如果提取出一个计数器的所有逻辑,是不是就能够套用任何 UI 库了?

从这个假如登程,如果我让 hook 提供这些能力:

  • 能够设置计数器的初始值、每次加减值、最大值最小值、精度
  • 能够通过返回的办法,间接取得超出最大最小值时按钮变灰无奈点击等等成果。
  • 能够通过返回的办法,间接获取两头输入框只能输出数字,不能输出文字等等性能。

而开发人员要做的,就是将这些放在任何 UI 库或原生的 button 与 input 之上,仅此而已。


function HookUsage() {const { getInputProps, getIncrementButtonProps, getDecrementButtonProps} =
    useNumberInput({
      step: 0.01,
      defaultValue: 1.53,
      min: 1,
      max: 6,
      precision: 2,
    })

  const inc = getIncrementButtonProps()
  const dec = getDecrementButtonProps()
  const input = getInputProps()

  return (
    <HStack maxW='320px'>
      <Button {...inc}>+</Button>
      <Input {...input} />
      <Button {...dec}>-</Button>
    </HStack>
  )
}

只须要这么几行代码,就能够失去这样的成果:

咱们能够瞻望一个与 Antd、elementUI 这些齐全不同的组件库。它能够只提供提炼后的业务组件逻辑,而不用提供 UI,你能够把这些业务逻辑利用在任何 UI 库上。

Hooks 的呈现能够让 UI library 转向 logic library,而这种将组件状态、逻辑与 UI 展现关注点拆散的设计理念,也叫做 Headless UI。

Headless UI 着力于提供组件的交互逻辑,而 UI 局部让容许使用者自由选择,在满足 UI 定制拓展的前提下,还能实现逻辑的交互逻辑的复用,业界曾经有一些这方面的摸索,比方组件库 chakra。后面的例子,其实就是 chakra 提供的能力。chakra 底层是对 Headless UI 的实际,提供对外的 hook,下层则提供了自带 UI 的组件,总之就是让开发者既能够疾速复用人家曾经提炼好的而且是最难写的交互逻辑局部,又不至于真的从头去给 button、input 写款式。另外 chakra 提供了组成一个大组件的原子组件,比方 Table 组件,chakra 会提供:

Table,
  Thead,
  Tbody,
  Tfoot,
  Tr,
  Th,
  Td,
  TableCaption,
  TableContainer,

这让使用者能够依据理论须要自由组合,提供原子组件的还有 Material-UI,对于用习惯了 Antd、element UI 的这种整体组件的咱们来说,提供了一种不同的形式,非常值得尝试。

在理论开发时,基于 hooks 甚至能够让经验丰富的人负责写逻辑,而老手来写 UI,分工合作,大大晋升开发效率。

React hook 的局限

React hook 的提出,在前端里是意义不凡的,但世界上没有完满的货色,hook 依然存在着一些问题。当然这只是我集体在应用中的一点感触与困惑。

被强制的程序

第一次接触 hook 时,对不能在嵌套或者条件里应用 hook 感到很奇怪,这十分反直觉。

useState、useEffect 明明就一个函数而已,竟然限度我在什么中央应用?只有我传入的参数是正确的,你管我在哪儿用呢?

咱们平时在我的项目里写一些工具函数时,会限度他人在什么中央应用么?顶多是判断一下宿主环境,然而对应用程序,必定是没有限度的。一个好的函数,应该像纯函数,同样的输出带来同样的输入,没有副作用,不须要使用者去思考在什么环境、什么层级、什么程序下调用,对使用者的认知累赘最小。

起初晓得了,React 是通过调用时序来保障组件内状态正确的。

当然这并不是什么大问题,已经 jsx 刚出的时候,js 夹杂 html 的语法同样被人诟病,当初也都真香了。开发者只有记住并恪守这些规定,就没什么累赘了。

简单的 useEffct

置信很多同学在遇到这个 API 的时候,第一个问题就是被其形容「执行副作用」所蛊惑。

啥是执行副作用?

React 说数据获取,设置订阅、手动更改 DOM 这些就是副作用。

为什么这些叫副作用?

因为现实状态下,Class 组件也好,函数组件也好,最好都是无任何副作用的纯函数,同样的输出永远是同样的输入,稳固牢靠能预测。然而实际上在组件渲染实现后去执行一些逻辑是很常见的需要,就像组件渲染完了要批改 DOM、要申请接口。所以为了满足需要,只管 Class 组件里的 render 是纯函数,还是将 Class 组件的副作用放在了 componentDidMount 生命周期中。只管已经的无状态组件是纯函数,还是减少了 useEffect 来在页面渲染之后做一些事件。所谓副作用,就是 componentDidMount 让 Class 的 render 变的不纯。useEffect 让无状态函组件变的不纯而已。

咱们即便了解了副作用,接下来要了解 useEffect 自身。

先接触 React 生命周期的同学,在学习 useEffct 的时候,或多或少会用 componentDidMount 来类比 useEffct。如果你相熟 Vue,可能会感觉 useEffct 相似 watcher,然而当用多了当前,会发现 useEffect 都似是而非。

首先 useEffect 每次渲染实现后都会执行,只是依据依赖数组去判断是否要执行你的 effect。它并不是 componentDidMount,但为了实现 componentDidMount 的成果,咱们须要应用空数组来模仿。这时候 useEffect 能够看做 componentDidMount。当依赖数组为空时,effect 里返回的革除办法,等同于 componentWillUnmount。

useEffect 除了实现 componentDidMount、componentWillUnmount 之外,还能够在依赖数组里设置须要监听的变量,这时看起来又像是 Vue 的 watcher。然而 useEffect 实际上是在页面更新后才会执行的。举个例子:


function App () {
  let varibaleCannotReRender; // 一般变量,扭转它并不会触发组件从新渲染
  useEffect(() => {// some code}, [varibaleCannotReRender])
  // 比方在一次点击事件中扭转了 varibaleCannotReRender
  varibaleCannotReRender = '123'
}

页面不会渲染,effect 必定不会执行,也不会有任何提醒。所以 useEffect 也不是一个变量的 watcher。事实上只有页面从新渲染了,你的 useEffect 的依赖数组里即便有非 props/state 的本地变量也能够触发 effect。

像这样,每次点击后,effect 都会执行,只管我没有监听 num,b 也只是个一般变量。


function App() {const [num, setNum] = useState(0);
  let b = 1;
  useEffect(() => {console.log('effefct', b);
  }, [b]);
  const click = () => {b = Math.random();
    set((num) => num + 1);
  };

  return <div onClick={click}>App {get}</div>;
}

所以在了解 useEffect 上,过来的 React 生命周期,Vue 的 watcher 的教训都不能很好地迁徙过去,可能最好的形式反而是遗记过来的那些教训,从头开始学习。

当然,即便你曾经分明了不同状况下 useEffect 都能带来什么成果,也不意味着就能够用好它。对于已经重度应用 Class 组件的开发人员来说尤其如此,摒弃掉生命周期的还不够。

在 Class 组件中,UI 渲染是 props 或者 state 在 render 函数中确定的,render 能够是个无副作用的纯函数,每次调用了 this.setState,新的 props、state 渲染出新的 UI,UI 与 props、state 之间放弃了一致性。而 componentDidMount 里的那些副作用,是不参加更新过程的,失去了与更新的同步。这是 Class 的思维模式。
而在 useEffect 思维模式中,useEffect 是与每一次更新同步的,这里没有 mount 与 update,第一次渲染和第十次渲染厚此薄彼。你违心的话,每一次渲染都能够去执行你的 effect。

这种思维模式上的区别,我认为是 useEffect 让人感觉困惑的起因。

useEffect 与更新同步,而理论业务中并不一定每一次更新都去执行 effect,所以须要用依赖数组来决定什么时候执行副作用,什么时候不执行。而依赖数组填写不当,又可能造成有限执行 effect、或者 effect 里拿到过期数值等状况。

在写业务的时候,咱们总是情不自禁的把性能、组件越写越大,useEffect 越来越简单。当依赖数组越来越长的时候,就该思考是不是设计上出了问题。咱们应该尽量遵循单一性准则,让每个 useEffect,只做一件尽可能简略的事件。当然,这也并不容易。

函数的纯正性

在 hook 呈现之前的函数式组件,没有部分状态,信息都是内部传入的,它自身就是像个纯函数,一旦函数从新执行,你在组件里申明的变量、办法全都是新的,简略纯正。那时候咱们还是把这类组件叫做 SFC 的(staless function component)。

但引入了 hook 之后,无状态函数组件领有了部分状态的能力,成了 FC 了。严格说这时候领有了部分状态的函数, 是不能看做是纯函数了,但为了加重一点思维上的累赘,能够把 useState 了解成相似函数组件之外中央所申明的一种数据,这个数据每次变动了都传给你的函数组件。

// 把这种
function YourComponent () {const [num, setNum] = useState(0);
  return <span>{num}</span>
}


// 了解成这种模式,应用了 useState,React 就主动给你生成 AutoContainer 包裹你的函数。这样你的组件仍能够看成是纯函数。function AutoContainer () {const [num, setNum] = useState(0);
   return <YourComponent num={num} />
 }
function YourComponent (props) {return <span>{props.num}</span>
}

每一次函数组件更新,就像一次快照一样捕捉了过后环境,每次更新都领有独一份的 context,更新之间互不烦扰,充分利用了闭包个性,仿佛也很纯正。

如果始终是这样,也还好,一次次更新就像一个个平行宇宙,类似但不雷同。props、state 等等 context 决定渲染,渲染之后是新的 context,各过各的,互不打搅。

但实际上,useRef 的呈现,突破了这种纯正性,useRef 让组件在每次渲染时都返回了同一个对象,就像一个空间宝石,在平行宇宙之间穿梭。而 useRef 的这一个性,使之成为了 hooks 的闭包救星,也造成了“遇事不决,useRef”的场面。说好的每次渲染都是独一份的 context,怎么还能拿到几次前更新的数据呢?

去掉 useRef 行不行?还真不行,有些场景就是须要一些值可能穿梭屡次渲染。

然而这样不相当于在 class 组件的 this 里加了个字段用来存数据吗?看得出来 React 是想拥抱函数式编程,然而,useRef 却让它变得不那么“函数式”了。

写在最初

我真正开始应用 hooks 曾经比拟晚了,这篇啰嗦了 1 万多字的文章,其实是我在学习和应用中已经问过本人的问题,在此我尝试给出这些问题的答复。能力无限,只能从一些部分角度来形容一些我所了解的货色,不全面也不够粗浅,随着持续学习,可能会在将来有不一样的感悟。

hooks 很弱小,把握一门弱小的技能素来都是须要一直磨难和工夫积淀的,心愿这篇文章,能稍稍解答一些你已经有过的疑难。

参考文档:
https://overreacted.io/
https://zh-hans.reactjs.org/d…
https://zh-hans.reactjs.org/d…
https://react.iamkasong.com/
https://juejin.cn/post/694486…

正文完
 0