React hooks是如何工作的

自从 React 16.8 推出的 hooks 特性,从刚开始的摸索到现在的顺心顺手也是过了很久,它是函数式组件的福音,也把组件复用玩出了新花样。。。等等,跑题了,这篇不是评价 hooks 的褒贬也不是介绍其用法,而是探究其原理及代码实现~ 事先提示:react 源码的类型检查基于自家的 flow,因此提前准备相关 typescript 基础阅读更佳。 基本法 function Person(props) { const [name, setName] = useState(''); const [age, setAge] = useState(0); useEffect(() => { setName('foo'); return () => {/* unsubscribe */}; }, []); useEffect(() => { setAge(25); }, [props.id]); return <div>{name}, {age} years old</div>; } 在 hooks 发布之前,函数式组件没有自己的 state,仅仅充当于 class 组件中的 render 角色,这意味着大量基于 state 的业务逻辑都无法复用。高阶组件和 render props 的写法虽然解决了大半组件复用的难题,但是其复杂的组件层次和松散的渲染逻辑导致项目代码变得难以阅读和维护。 事实上,hooks 建立在 react 组件的理念之上,初学者对于这样的 API 用法会感到困惑,为什么会这样用以及这样写法的作用。在 class 组件中,声明式 state、生命周期、react 如何渲染等这些概念在其中更好地被理解&介绍。 // 总不能在函数的原型上介绍生命周期函数 function Person(){} Person.prototype.getInitialState = function getInitialState(){}; Person.prototype.componentDidMount = function componentDidMount(){}; // es5 创建对象的写法也不太抽象 createReactClass({ mixins: [], getInitialState() { return {}; }, render(){}, }); 官网上的 Motivation 介绍 class 语法会使开发者困惑,,,并不苟同,任何一个原生 js 学习者应该都能很好理解,并且学习好 this 是基本功。现在,绝大部分的包的文档介绍上的写法都改写成了 hooks,有些甚至为了 hooks 而 hooks,诚然能社区广泛参与甚好,但个人看来这样存在一定的门槛。 回到 hooks 发布之后,函数式组件能与 state 挂钩,并且有能力访问生命周期等概念,看起来就像魔法一样!So 它是如何工作的呢: 如何使 API 定位到组件?如何在不依赖 this 的情况下保存组件的 state?如何用 useEffect 模拟生命周期?如何保证执行顺序? React 部分源码介绍 整个 React 库被拆分成了几个不同的包,每个包负责特定的功能,这些都建立在 yarn workspaces 上。 从 npm 7 开始,也支持了 workspaces 功能。文档 挑几个包介绍: react 抽象概念的实现以及一系列操纵抽象概念的接口react-reconciler diff 渲染机制与基于 Fiber 的更新执行队列react-dom 将组件等抽象实现渲染到浏览器真实 dom 粗略调试了一下(事实上基本捣鼓了一整个下午),感觉真复杂,就像刚入门那会看 jQuery 源码一样。。。而且总有种简单问题复杂化的感受,好比一个西瓜卖几块钱,非得建立一个多元非线性微积分方程来计算这个西瓜的价钱一样。。 React 中抽象概念组件的实现就是个普通的构造函数,元素(虚拟 dom)也只是通过工厂模式生成的纯对象: // packages/react/src/ReactBaseClasses.js function Component(props, context, updater) { this.props = props; this.context = context; this.updater = updater || ReactNoopUpdateQueue; } // 原型上可供更新组件的接口 Component.prototype.setState = function setState(partialState, callback) { this.updater.enqueueSetState(this, partialState, callback, 'setState'); }; // packages/react/src/ReactElement.js // ...createElement... const ReactElement = function (type, key, ref, self, source, owner, props){ const element = { $$typeof: REACT_ELEMENT_TYPE, type: type, // div props: props, _owner: owner, // 组件实例 }; // ... return element; }; 工作系列 Talk is cheap,直接开始 debug 吧~ Hooks API 与组件实例 Hooks 的“魔法”之一 useState,将状态 state 附加到正确的组件上,使得函数式组件存在自己的 state。但一般提供值的引用的 API 在任何一个地方修改了值,那另一个地方的值也将改变: /* 伪代码实现 */ let value = 0; function myAPI() { return [value, (v) => value = v]; } // 组件中的调用 function Foo() { const [value, setValue] = myAPI(1); } function Bar(){ const [value, setValue] = myAPI(2); } 这显然走不通,如果说利用可变的 this 来关联组件与 state,那倒是有可能。但众所周知,函数式组件根本没有 this,否则干脆直接赋值上去就行了,它只是单纯的一个渲染函数,过程式调用,不会实例化。纠结无益,看源码: // react/src/React.js import { useState, useEffect } from './ReactHooks'; const React = { useState, useEffect, }; export default React; /** * 跳转到 ReactHooks.js * * 不得不说 React 整个库的模块组织能力非常出色,是值得学习的对象, * 复杂的核心功能架分了几个包,而这种较简单的 API 则细分到了另一个模块文件中。 */ // 去除了 flow 的类型注解以及无关代码 // react/src/ReactHooks.js import ReactCurrentDispatcher from './ReactCurrentDispatcher'; export function useState(initialState) { const dispatcher = resolveDispatcher(); // ReactCurrentDispatcher.current return dispatcher.useState(initialState); } export function useEffect(create, inputs) { const dispatcher = resolveDispatcher(); return dispatcher.useEffect(create, inputs); } // react/src/ReactCurrentDispatcher.js const ReactCurrentDispatcher = { current: null, }; useState 很简单,只有 2 行代码,最终调用了 dispatcher.useState,那么重点就在于这个 dispatcher 上。但不难发现,dispatcher 只是一个引用地址,意味着所有的函数式组件引用的都是同一个 dispatcher,这几乎不可能实现 state 的功能。唯一的解释是在组件渲染之前,该引用地址被修改成了与之相关的 dispatcher。在耗无头绪的情况下,debug: 从调用堆栈来看,组件被渲染之前有一步 renderWithHooks。 // react-reconciler/src/ReactFiberHooks.js export function renderWithHooks(...) { ... // 组件的 dispatcher ReactCurrentDispatcher.current = nextCurrentHook === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; // 无实例化。例如:Foo() let children = Component(props, refOrContext); } const HooksDispatcherOnMount = { useEffect: mountEffect, useState: mountState, // 挂载阶段 }; const HooksDispatcherOnUpdate = { useEffect: updateEffect, useState: updateState, // 更新阶段 }; 实际上还是同一个引用地址(对象),之前的解释有误。有点区别就是 useState 这个 API 实际上由两个函数组成:组件挂载阶段为 mountState、组件更新阶段为 updateState。这似乎为 initState 提供可能。 总而言之,又进入死胡同了,函数式组件与 state 挂钩不在 dispatcher 这一层,而在 mountState 这层: // react-reconciler/src/ReactFiberHooks.js function mountState(initialState) { const hook = mountWorkInProgressHook(); if (typeof initialState === 'function') { initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; const queue = (hook.queue = { last: null, dispatch: null, eagerReducer: basicStateReducer, eagerState: initialState, }); // 不知道是不是因为 React 主力贡献者就是 redux 作者的关系, // 这里总是会联想到 redux 相关。。。 const dispatch = (queue.dispatch = dispatchAction.bind( null, currentlyRenderingFiber, queue, )); return [hook.memoizedState, dispatch]; }


JavaScript全屏阅读

下一篇:打酱油

上一篇:钢铁是怎样炼成的

Ctrl + Enter