惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

Airing 的博客

月刊(第34期):创造的快乐 2015-2016 随笔选集 2025,全新旅途 月刊(第33期):推理阶梯 月刊(第32期):中大校友会的采访 月刊(第31期):基于 Claude 的阅读流 月刊(第30期):写在 30 岁这天 游戏组件的一生: 从加载到上屏 月刊(第29期):新生活 2024,告别盛夏 月刊(第28期):AI 没有体验世界的能力 AI 心理疗愈应用的探索与实现 Mac 小众软件推荐与工作流分享(2024) 月刊(第27期):旅居 再见了,盛夏 Airing 开通了一个咨询服务 月刊(第26期):旅途 个人工具箱与好物分享 月刊(第25期):爱具体的人 2023,逃离仿徨 月刊(第24期):十年编程之路 月刊(第23期):多任务中的时间管理 月刊(第22期):当下的快乐 月刊(第21期):快节奏时代下的短视频 月刊(第20期):重启人生 月刊(第19期):日记的意义 大厂自研跨端框架技术揭秘 月刊(第18期):逃离社交网络 2022,平安喜乐 月刊(第17期):心之所向,素履以往 月刊(第16期):个人信息流分享 Chromium 渲染流水线——字节码到像素的一生 周刊(第13期):玄学杠杆与异世界小组 前端开发中的大小写敏感问题 周刊(第12期):前端三大浪漫 周刊(第11期):筮法是如何进行的 周刊(第10期):那些我喜欢的游戏(第1辑) 周刊(第9期):高效率到高消耗的现象与反思 周刊(第8期):三幕剧与英雄旅程 周刊(第7期):即兴发言模型 周刊(第6期):沟通艺术中的知觉检核 周刊(第5期):我在 TME 工作的一天 周刊(第4期):个人博客演变史 周刊(第3期):花束般的恋爱 周刊(第2期):重拾失去的好奇心 周刊(第1期):开刊,为什么写周刊 引擎剖析:JS 中的字符串转数值 2021,自渡向前 谈谈存在的价值与人生体验 Flutter 产物分析与减包方案 Roam Research 最佳实践——知识管理与任务管理 如何有效沟通——从《齐物论》立场问题切入 Flutter 异常处理方案——灰度与降级 2020,追逐星火 《光·遇》之“禅” Flutter 核心原理与混合开发模式 Flutter 混合开发框架模式探索 Flutter Boost 混合开发实践与源码解析 2019,走走停停 React Hooks 源码解析(4):useEffect React Hooks 源码解析(3):useState React Hooks 源码解析(1):类组件、函数组件、纯组件 WWDC19 游记 双生日记开发经验分享 前端安全 | HPP 的攻击举例与防范 前端安全 | XST 的攻击原理与防御 前端安全 | CSRF 的攻击手段与防范 前端安全 | XSS 的攻击手段与防范 人工意识何以可能? 文献阅读与论文写作的方法之己见 2018,沉淀初心 「没有你,也很好。」 Web 模拟终端博客系统 在线学习资源智慧推送系统研究 再见了,我的大学 6 万部豆瓣电影数据分析 B 站 2000 万用户分析
React Hooks 源码解析(2): 组件逻辑复用与扩展
2019-09-11 · via Airing 的博客

React 源码版本: v16.9.0 源码注释笔记:airingursb/react

如何复用和扩展 React 组件的状态逻辑?具体而言,有以下五种方案:

  1. Mixins
  2. Class Inheritance
  3. Higher-Order Component
  4. Render Props
  5. React Hooks

下面,我们一一介绍五种方案的实现。

1. Mixins

Mixins

Mixins 混合,其将一个对象的属性拷贝到另一个对象上面去,其实就是对象的融合,它的出现主要就是为了解决代码复用问题。

扩展:说到对象融合,Object.assign 也是常用的方法,它跟 Mixins 有一个重大的区别在于 Mixins 会把原型链上的属性一并复制过去(因为for...in),而 Object.assign 则不会。

由于现在 React 已经不再支持 Mixin 了,所以本文不再赘述其如何使用。至于以前在 React 中如何使用 Mixin ,请参考这篇文章:React Mixin 的使用 | segmentfault

Mixins 虽然能解决代码复用的问题,但是其会产生许多问题,甚至弊大于利,由此 React 现在已经不支持 Mixins 了。具体而言,有以下几个缺点:

  1. 代码过于耦合:Mixins 引入了隐藏的依赖关系,代码之间可能会相互依赖,相互耦合,不利于代码维护。
  2. 名称相同的 Mixin 不可以同时使用:比如 FluxListenerMixin 定义 handleChange()WindowSizeMixin 定义handleChange(),则不能同时使用它们,甚至我们也无法在自己的组件上定义具有此名称的方法。
  3. 雪球效应的复杂度:Mixins 数量比较多的时候,组件是可以感知到的,甚至组件代码中还要为其做相关处理增加 Hack 逻辑,这样会给代码造成滚雪球式的复杂性。

2. Class Inheritance

说到类组件的代码逻辑复用,熟悉 OOP 的同学肯定第一时间想到了类的继承,A 组件只要继承 B 组件就可以复用父类中的方法。但同样的,我也相信使用 React 的同学不会用继承的方法去复用组件的逻辑。

这里主要的考虑是代码质量问题,如果两个组件本身业务比较复杂,做成继承的方式就很不好,阅读子组件代码的时候,对于那么不明就里的、没有在该组件中声明的方法还需要跑到去父组件里去定位,而 React 希望一个组件只专注于一件事。

另外,如果重写子组件的生命周期,那父组件的生命周期会被覆盖,这也是我们在开发中不愿意看到的。

Facebook 对在 React 中使用继承这件事“深恶痛绝”,官网在 Composition vs Inheritance 一文中写到:“在 Facebook,我们在成百上千个组件中使用 React,我们并没有发现需要使用继承来构建组件层次的情况。”

的确,函数式编程和组件式编程思想某种意义上是一致的,它们都是“组合的艺术”,一个大的函数可以有多个职责单一的函数组合而成。同样的,组件也是如此。我们做 React 开发时,总是会不停规划组件,将大组件拆分成子组件,对组件做更细粒度的控制,从而保证组件的纯净性,使得组件的职责更单一、更独立。组合带来的好处就是可复用性、可测试性和可预测性。

因此,优先考虑组合,才去考虑继承,并且 Facebook 在官网的文章中推荐使用 HOC 去实现组件的逻辑复用(详见《Higher-Order Components》),那下面我们就来看一看 HOC 到底是什么。

3. HOC(Higher-Order Component)

HOC,Higher-Order Component,即高阶组件。虽然名字很高级,但其实和高阶函数一样并没有什么神奇的地方。

回顾一下高阶函数的定义:

  1. 函数可以作为参数被传递
  2. 函数可以作为返回值输出

其实高阶组件也就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。需要注意的是高阶组件是一个函数,并不是一个组件。可见 HOC 其实就是一个装饰器,因此也可以使用 ES 7 中的装饰器语法,而本文为了代码的直观性就不使用装饰器语法了。

扩展阅读:装饰器提案 proposal-decorators | GitHub

高阶组件也有两种实现:

  1. 继承式的 HOC:即反向继承 Inheritance Inversion
  2. 代理式的 HOC:即属性代理 Props Proxy

由于继承官方不推崇,继承式的 HOC 可能会原始组件的逻辑而并非简单的复用和扩展,因此继承式的 HOC 依然有许多弊端,我们这里就列一段代码展示一下,但就不展开讲了。

// 继承式 HOC

import React, { Component } from 'react'

export default const HOC = (WrappedComponent) => class NewComponent extends WrappedComponent {
    
    componentWillMount() {
        console.log('这里会修改原始组件的生命周期')
    }

    render() {
        const element = super.render()
        const newProps = { ...this.props, style: { color: 'red' }}
        return React.cloneElement(element, newProps, element.props.children)
    }
}

可以看到继承式的 HOC 也确实可以复用和扩展原始组件的逻辑。而代理式的 HOC 更加简单,接下来举个例子来看看,该案例具体的项目代码可以点下面按钮进入调试:

Edit HOC

这里有两个组件 Profile 和 Home,两个组件都被 Container 包裹,且每个 Container 的样式一样并且都有一个 title。这里我们希望 Profile 和 Home 都可以复用 Container 的样式和结构,现在我们用 HOC 实现一下:

// app.js

import React from "react";
import ReactDOM from "react-dom";
import Profile from "./components/Profile";
import Home from "./components/Home";
import "./styles.css";

function App() {
    return (
        <div className="App">
            <Profile name={"Airing"} />
            <Home />
        </div>
    );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
// Container.js

import React, { Component } from "react";
import "../styles.css";

export default title => WrappedComponent =>
    class Container extends Component {
        render() {
            return (
                <div className="container">
                    <header className="header">
                        {title}
                    </header>
                    <div>
                        <WrappedComponent url={"https://me.ursb.me"} {...this.props} />
                    </div>
                </div>
            );
        }
    };
// Profile.js

import React, { Component } from "react";
import WrappedComponent from "./WrappedComponent";

class Profile extends Component {
    render() {
        return (
            <>
                <p>Author: {this.props.name}</p>
                <p>Blog: {this.props.url}</p>
                <p>Component A</p>
            </>
        );
    }
}

export default WrappedComponent("Profile")(Profile);
// Home.js

import React, { Component } from "react";
import WrappedComponent from "./WrappedComponent";

class Home extends Component {
    render() {
        return (
            <>
                <p>Component B</p>
            </>
        );
    }
}

export default WrappedComponent("Home")(Home);

可以发现这里的 HOC 其实本质上是原始组件的一个代理,在新组件的 render 函数中,将被包裹组件渲染出来,除了 HOC 自己要做的工作,其余功能全都转手给了被包裹的组件。

而 Redux 的 connect 函数其实也是 HOC 的一个应用。

ConnectedComment = connect(mapStateToProps, mapDispatchToProps)(Component);

等同于

// connect是一个返回函数的函数(就是个高阶函数)
const enhance = connect(mapStateToProps, mapDispatchToProps);
// 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store
// 关联起来的新组件
const ConnectedComment = enhance(Component);

Redux connect

另外,还有 antd 的 Form 也是用 HOC 实现的。

const WrappedNormalLoginForm = Form.create()(NormalLoginForm);

虽然 HOC 在组件逻辑复用上提供了很多便利,也有许多项目会使用这种模式,但 HOC 还是存在一些缺点的:

  1. Wrapper Hell,组件层级嵌套过多(Debug 过 Redux 的必然深有体会),这让调试变得非常困难。
  2. 为了在 Debug 中显示组件名,需要显示声明组件的 displayName
  3. 对 Typescript 类型化不够友好
  4. 无法完美地使用 ref(注:React 16.3 中提供了 React.forwardRef 可以转发 ref,解决了这个问题)
  5. 静态属性需要手动拷贝:当我们应用 HOC 去增强另一个组件时,我们实际使用的组件已经不是原组件了,所以我们拿不到原组件的任何静态属性,我们可以在 HOC 的结尾手动拷贝它们。
  6. 透传了不相关的 props:HOC 可以劫持 props,在不遵守约定的情况下可以覆盖掉透传的 props。另外,这也导致中间组件也接受了不相关的 props,代码可读性变差。
/**
 * 使用高阶组件,我们可以代理所有的props,但往往特定的HOC只会用到其中的一个或几个props。
 * 我们需要把其他不相关的props透传给原组件
 */

function visible(WrappedComponent) {
  return class extends Component {
    render() {
      const { visible, ...props } = this.props;
      if (visible === false) return null;
      return <WrappedComponent {...props} />;
    }
  }
}

下图对比了 Mixin 和 HOC 的差异:(图源:【React深入】从Mixin到HOC再到Hook

4. Render Props

Render Props 其实很常见,比如 React Context API:

class App extends React.Component {
   render() {
     return (
       <ThemeProvider>
         <ThemeContext.Consumer>
           {val => <div>{val}</div>}
         </ThemeContext.Consumer>
       </ThemeProvider>
     )
   }
 }

React 的 props 并没有限定类型,它可以是一个函数,于是就有了 render props,这种模式也很常见。它的实现思路很简单,把原来该放组件的地方,换成了回调,这样当前组件里就可以拿到子组件的状态并使用。

但是,这会产生和 HOC 一样的 Wrapper Hell 问题。

5. React Hooks

而以上的问题,使用 Hooks 均可以得到解决,Hooks 可谓是组件逻辑复用扩展的完美方案。具体而言,有以下优点:

  1. 避免命名冲突:Hook 和 Mixin 在用法上有一定的相似之处,但是 Mixin 引入的逻辑和状态是可以相互覆盖的,而多个 Hook 之间互不影响。
  2. 避免 Wrapper Hell:原理类似于回调地狱之于 async + await。
  3. Hooks 拥有Functional Component 的所有优点(请阅读该系列第一篇文章),同时若使用 useState、useEffect、useRef 等 Hook 可以在 Functional Component 中使用 State、生命周期和 ref,规避了 Functional Component 固有的缺点。

至于 Hooks 的具体实现,我们下一篇文章中再谈。