奇葩说框架之 react 组件及逻辑复用

代码逻辑复用 是我们开发人员减少代码重复度,进行代码优化的一个重要因素,上期我们的同学分享了关于 Vue 框架的相关逻辑复用的方法及原理,本期我们一起来了解下 React 框架中的逻辑复用,涉及mixinsHOCrender propsHook 四个部分,由浅入深,欢迎大家交流学习。

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流程,简单理解就是:

  1. 通过React.createClass(spec)接收到spec参数,判断spec对象中是否有mixins属性,
  2. 如果没有,那太棒了,直接遍历spec把propTypes、componentWillMount等生命周期函数、事件方法等挂载到Constructor.prototype上,这个Constructor就是react组件实例的构造函数;
  3. 如果有,就需要判断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相关的类图及调用关系。

简单理解:

  1. 代码执行完 const EnhancedComponent = Hoc(Demo)可以通过浏览器看到EnhancedComponent变成了class HOC类,并且继承了React component
  2. 当代码执行遇到jsx语法const comp = <EnhancedComponent /> 时,会调用React.createElement()方法将其转成react element对象节点,如下图所示:
  3. 当代码执行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 源码解析

简单理解:

  1. 当代码执行遇到jsx语法 const comp = <Demo func={(name) => { return <h1>{name}</h1> }} />, 会调用React.js的createElement方法,其实调用的是ReactElement.js中createElement方法转成react element节点。
  2. 当代码执行 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,接下来会重点分析这两个阶段核心的方法里mountStateupdateReducer都做了啥。

声明阶段-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框架在实现逻辑复用思想上的雷同之处,很是惊喜。文章有不足之处,欢迎大家多多指出,共同分享学习!

猜你喜欢

转载自juejin.im/post/7018369014007070733