代码逻辑复用 是我们开发人员减少代码重复度,进行代码优化的一个重要因素,上期我们的同学分享了关于 Vue 框架的相关逻辑复用的方法及原理,本期我们一起来了解下 React 框架中的逻辑复用,涉及mixins,HOC,render props,Hook 四个部分,由浅入深,欢迎大家交流学习。
1 mixins
1.1 mixins简介
React在v15.3.0之前,通过React.createClass 创建组件,支持mixins; 在v15.3.0之后(包含),通过class 创建组件,废弃mixins。
1.2 mixins demo演示(基于v15.2.1)
demo比较简单,这里就不赘述了。
tips: 可以类比vue的mixins。
1.3 mixins 源码解析
基于1.2 demo,经过debugger调试,我画了如下基础的React相关的类图及调用关系。 蓝色部分就是核心的mixins流程,简单理解就是:
- 通过React.createClass(spec)接收到spec参数,判断spec对象中是否有mixins属性,
- 如果没有,那太棒了,直接遍历spec把propTypes、componentWillMount等生命周期函数、事件方法等挂载到Constructor.prototype上,这个Constructor就是react组件实例的构造函数;
- 如果有,就需要判断mixins对象中是否还有mixins属性(因为框架允许mixins嵌套mixins),没有再回到步骤2挂载到原型,有则继续步骤3遍历。
tips:步骤2挂载到原型,会经历react的合并策略,例如mixins、propTypes等规定我们可以定义多次,内部实现方法合并调用;render函数只被允许定义一次,超限会直接报错提示,更多可以查看源码了解
1.4 mixins 缺点
- 引入了隐式依赖
例如:嵌套mixins各自定义的属性可能被嵌套的多个mixins隐式引用着,删除其中一个会破坏另一个,不形成层次结构,被扁平化并在相同的命名空间中运行。
- 导致命名冲突
存在潜在的破坏性更改,因为在某些使用它的组件上可能已经存在具有相同名称的方法。
- 导致滚雪球般的复杂性
原本抽离公共的mixins是为了代码复用,但是随着业务需求的迭代,逐渐出现了差异化,就开始在上面增加if...else逻辑,代码耦合度、复杂度增高,随之带来的是理解成本和开发出错成本,重构代码也是个难题。
详细理解可参看官方博客:Mixins Considered Harmful
2 HOC
2.1 HOC 简介
官方介绍:
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
具体而言,高阶组件是参数为组件,返回值为新组件的函数。
2.2 HOC demo演示(基于v15.3.0)
高阶组件(HOC)常见的两个应用场景是属性代理和反向继承,因为对反向继承的应用理解不是太深入,所以这里仅演示对属性代理的操作。
Demo组件可理解为一个公共基础组件,Hoc函数定义返回了一个增强组件,抽象了props age属性,Hoc(Demo)的执行结果EnhancedComponent就是Demo的增强组件,当然可以将EnhancedComponent作为公共组件继续向上抽象公共属性和方法。
tips: 可以类比vue的高阶组件
2.3 HOC 源码解析
基于2.2 demo,画了如下基础的React相关的类图及调用关系。
简单理解:
- 代码执行完
const EnhancedComponent = Hoc(Demo)
可以通过浏览器看到EnhancedComponent变成了class HOC类,并且继承了React component - 当代码执行遇到jsx语法
const comp = <EnhancedComponent />
时,会调用React.createElement()方法将其转成react element对象节点,如下图所示: - 当代码执行
ReactDOM.render(comp, document.getElementById("root"))
,其实是调用了ReactMount.render并最终返回了React component类型的组件,ReactMount.render在执行过程中会执行HOC组件的render函数,在遇到jsx语法 return 会执行步骤2转成react element,接着执行Demo组件的render函数,遇到jsx语法执行步骤1转成react element,最终经过复杂的渲染过程渲染到页面。
2.4 HOC 缺点
- 很难复用逻辑,会导致组件树层级很深
如果使用HOC或者render props方案来实现组件之间复用状态逻辑,会很容易形成“嵌套地狱”。
- 业务逻辑分散在组件的各个方法中
- 难以理解的class
对于非前端专业的同学,理解class的成本会有点高
3 render props
3.1 render props 简介
官方介绍:
“render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术。
具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。
注意:render prop 是因为模式才被称为 render prop ,你不一定要用名为 render 的 prop 来使用这种模式。事实上, 任何被用于告知组件需要渲染什么内容的函数 prop 在技术上都可以被称为 “render prop”
3.2 render props demo演示(基于v15.3.0)
这里我定义了一个可以接收props func函数 的Demo组件,在外部使用Demo组件时传入func函数,告诉组件内部渲染成h1标签。
tips:这个用法简直像极了vue的slot插槽呀
3.3 render props 源码解析
简单理解:
- 当代码执行遇到jsx语法
const comp = <Demo func={(name) => { return <h1>{name}</h1> }} />
, 会调用React.js的createElement方法,其实调用的是ReactElement.js中createElement方法转成react element节点。 - 当代码执行
ReactDOM.render(comp, document.getElementById("root"))
调用的是ReactDOM.render,最终返回React component类型的组件,在执行render的过程中,会调用Demo组件的render函数,遇到jsx语法return (<div>{func(this.state.name)}</div>);
会执行步骤1生成element节点并返回,当执行到func(this.state.name),遇到jsx语法return <h1>{name}</h1>
, 会再次执行步骤1生成element节点并返回,最终经过复杂的渲染过程渲染到页面。
3.4 render props 缺点
缺点同高阶组件(HOC),嵌套地狱、业务逻辑分散在各个组件、难以理解的class。
上述三种逻辑复用策略都存在一定的问题,于是官方提出了Hook。
4 Hook
4.1 Hook 简介
官方介绍:
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
4.2 Hook demo演示(基于v16.8.0)
官方提供了好几个Hook方法,包括常用的useState、useEffect、useRef、useContext,还有高级的useReducer、useMemo、useLayoutEffect方法以及自定义Hook方法。
这里,我仅演示useState、useEffect
tips: 这个跟vue3 composition api神似
4.3 Hook 源码解析
在实际的debugger过程中会发现,useState的调用分为两个阶段:
声明阶段
:在首次渲染页面的过程中会触发一次useState的调用,也就是下图蓝色区域的render阶段Reconciler过程中会触发,这个蓝色区域不是本次重点,后续会有同学分享到这部分哦~
其实这一次调用是在执行Demo组件中的这行代码: const [count, setCount] = useState(0);
一路追踪useState,最终定位源码:
ReactCurrentDispatcher$1.current = nextCurrentHook === null ? HooksDispatcherOnMountInDEV : HooksDispatcherOnUpdateInDEV;
复制代码
详细的调用关系图如下图蓝色部分:
可以看到声明阶段:实际调用的HooksDispatcherOnMountInDEV对象的useState,然后useState又调用的最终的核心方法-mountState
调用阶段
:在更新count时,也就是调用setCount时会触发useState调用。
可以看到调用阶段:实际调用的HooksDispatcherOnUpdateInDEV对象的useState,然后useState又调用了updateState,最终调用了核心方法-updateReducer
,接下来会重点分析这两个阶段核心的方法里mountState
和updateReducer
都做了啥。
声明阶段-mountState
源码截图如下:
源码拆解如下图所示:
简单理解:
创建hook对象,用于存放初始值和dispatch函数,便于调用阶段的数据更新,当执行 useState(0),实际上是执行 mountState(0),initialState即0赋值给memoizedState,dispatch关联内部dispatchAction函数, 最终返回[hook.memoizedState, dispatch],也是正好对应了[count, setCount]的解构赋值,最终count被赋上了初始值,setCount也关联上了内部定义的dispatchAction。
调用阶段-updateReducer
源码截图如下:
当我们点击主动触发setCount(count + 1)时,可以看下面流程图理解:
最终执行的updateReducer,获取了声明阶段
定义的hook对象,判断非首次更新则进入优化策略,是首次更新则会计算结果_newState的值,然后更新hook对象的新计算值memoizedState,初始值baseState、队列中记录的上一次更新的dispatch函数以及计算值,并最终返回[hook.memoizedState, dispatch],此时count值就变更成了1。
4.4 Hook 缺点
摘了几条核心的官方回复:
- 没有计划移除class,推荐新代码尝试
- 不推荐用Hook重写已有的class
- 目标尽早覆盖class所有的使用场景
- 不会替代render props和高阶组件
那么是不是可以理解Hook的2个小缺点:
- 老的项目使用class不建议重构,实际问题就在于老项目
- 使用场景可能还没有完全覆盖class的所有场景
5 总结
本文通过简介、demo使用、源码分析以及缺点分析了react的4种逻辑复用策略,学习过程中也发现了react和vue框架在实现逻辑复用思想上的雷同之处,很是惊喜。文章有不足之处,欢迎大家多多指出,共同分享学习!