Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >[译] 更可靠的 React 组件:提纯

[译] 更可靠的 React 组件:提纯

作者头像
江米小枣
发布于 2020-06-15 14:18:56
发布于 2020-06-15 14:18:56
1.1K00
代码可运行
举报
文章被收录于专栏:云前端云前端
运行总次数:0
代码可运行

原文摘自:https://dmitripavlutin.com/7-architectural-attributes-of-a-reliable-react-component/

pure、almost-pure 和 impure

一个 纯组件(pure componnet) 总是针对同样的 prop 值渲染出同样的元素; 一个 几乎纯的组件(almost-pure compoent) 总是针对同样的 prop 值渲染同样的元素,并且会产生一个 副作用(side effect)

在函数式编程的术语里,一个 纯函数(pure function) 总是根据某些给定的输入返回相同的输出。让我们看一个简单的纯函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function sum(a, b) {  
 return a + b;
}
sum(5, 10); // => 15  

对于给定的两个数字,sum() 函数总是返回同样的相加值。

一旦对相同的输入返回不同的输出了,一个函数就变成 非纯(impure) 的了。这种情况可能发生在函数依赖了全局状态的时候。举个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let said = false;

function sayOnce(message) {  
  if (said) {
    return null;
  }
  said = true;
  return message;
}

sayOnce('Hello World!'); // => 'Hello World!'  
sayOnce('Hello World!'); // => null  

即便是使用了同样的参数 'Hello World!',两次的调用返回值也是不同的。就是因为非纯函数依赖了全局状态: 变量 said

sayOnce() 的函数体中的 said = true 语句修改了全局状态。这产生了副作用,这是非纯的另一个特征。

因此可以说,纯函数没有副作用,也不依赖全局状态。 其单一数据源就是参数。所以纯函数是可以预测并可判断的,从而可重用并可以直接测试。

React 组件应该从纯函数特性中受益。给定同样的 prop 值,一个纯组件(不要和 React.PureComponent 弄混)总是会渲染同样的元素。来看一看:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function Message({ text }) {  
  return <div className="message">{text}</div>;
}

<Message text="Hello World!" />  
// => <div class="message">Hello World</div>

可以肯定的是 <Message> 接受相同的 prop 值后会渲染出相同的元素。

有时也不总是能够把组件做成纯的。比如要像下面这样依赖一些环境信息:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class InputField extends Component {  
  constructor(props) {
    super(props);
    this.state = { value: '' };
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange({ target: { value } }) {
    this.setState({ value });
  }

  render() {
    return (
      <div>
         <input 
           type="text" 
           value={this.state.value} 
           onChange={this.handleChange} 
         />
         You typed: {this.state.value}
      </div>
    );
  }
}

带状态的 <InputField> 组件并不接受任何 props,但根据用户输入会渲染不同的输出。因为要通过 input 域访问环境信息,所以 <InputField> 只能是非纯的。

非纯代码虽然有害但不可或缺。大多数应用都需要全局状态、网络请求、本地存储等等。你能做的只是将非纯代码从纯代码中隔离出来,这一过程又成为提纯(purification)

孤立的非纯代码有明确的副作用,或对全局状态的依赖。在隔离状态下,非纯代码对系统中其余部分的不可预测性影响会降低很多。

来看一些提纯的例子。

案例学习1:从全局变量中提纯

我不喜欢全局变量。它们破坏了封装、造成了不可预测的行为,并使得测试困难重重。

全局变量可以作为可变(mutable)对象使用,也可以当成不可变的只读对象。

改变全局变量会造成组件的不可控行为。数据被随意注入和修改,将干扰一致性比较(reconciliation)过程,这是一个错误。

如果需要可变的全局状态,解决的办法是引入一个可预测的系统状态管理工具,比如 Redux。

全局中不可变的(或只读的)对象经常用于系统配置等。比如包含站点名称、已登录的用户名或其他配置信息等。

下面的语句定义了一个配置对象,其中保存了站点的名称:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export const globalConfig = {  
 siteName: 'Animals in Zoo'
};

随后,<Header> 组件渲染出系统的头部,其中显示了以上定义的站点名称:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { globalConfig } from './config';export default function Header({ children }) {  
 const heading =
   globalConfig.siteName ? <h1>{globalConfig.siteName}</h1> : null;
 return (
    <div>
      {heading}
      {children}
    </div>
 );
}

<Header>globalConfig.siteName 渲染到一个 <h1> 标签中。当站点名称没有定义(比如赋值为 null)时,头部就不显示。

首先要关注的是 <Header> 是非纯的。在给定相同 children 的情况下,组件会根据 globalConfig.siteName 返回不同的结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// globalConfig.siteName 为 'Animals in Zoo'
<Header>Some content</Header>  
// 渲染:
<div>  
 <h1>Animals in Zoo</h1>
 Some content
</div>

或是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// globalConfig.siteName 为 `null`
<Header>Some content</Header>  
// 渲染:
<div>  
 Some content
</div>  

第二个问题是难以测试。要测试组件如何处理 null 站点名,你得手动修改全局变量为 globalConfig.siteName = null

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import assert from 'assert';  
import { shallow } from 'enzyme';  
import { globalConfig } from './config';  
import Header from './Header';

describe('<Header />', function() {  
  it('should render the heading', function() {
    const wrapper = shallow(
      <Header>Some content</Header>
    );
    assert(wrapper.contains(<h1>Animals in Zoo</h1>));
  });

  it('should not render the heading', function() {
    //修改全局变量:
    globalConfig.siteName = null;
    const wrapper = shallow(
      <Header>Some content</Header>
    );
    assert(appWithHeading.find('h1').length === 0);
  });
});

为了测试而修改全局变量 globalConfig.siteName = null 既不规范又令人不安。 之所以如此是因为 <Heading> 紧依赖了全局环境。

为了解决这种非纯情况,最好是将全局变量注入组件的作用域,让全局变量作为组件的一个输入。

下面来修改 <Header>,让其再多接收一个 prop siteName。然后用 recompose 库提供的 defaultProps() 高阶组件包裹 <Header>,以确保缺失 prop 时填充默认值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { defaultProps } from 'recompose';  
import { globalConfig } from './config';

export function Header({ children, siteName }) {  
  const heading = siteName ? <h1>{siteName}</h1> : null;
  return (
     <div className="header">
       {heading}
       {children}
     </div>
  );
}

export default defaultProps({  
  siteName: globalConfig.siteName
})(Header);

<Header> 已经变为一个纯的函数式组件,也不再直接依赖 globalConfig 变量了。纯化版本是一个命名过的模块: export function Header() {...},这在测试时是很有用的。

与此同时,用 defaultProps({...}) 包装过的组件会在 siteName 属性缺失时将其设置为 globalConfig.siteName。正是这一步,非纯组件被分离和孤立出来。

让我们测试一下纯化版本的 <Header>

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import assert from 'assert';  
import { shallow } from 'enzyme';  
import { Header } from './Header';

describe('<Header />', function() {  
  it('should render the heading', function() {
    const wrapper = shallow(
      <Header siteName="Animals in Zoo">Some content</Header>
    );
    assert(wrapper.contains(<h1>Animals in Zoo</h1>));
  });

  it('should not render the heading', function() {
    const wrapper = shallow(
      <Header siteName={null}>Some content</Header>
    );
    assert(appWithHeading.find('h1').length === 0);
  });
});

棒极了。纯组件 <Header>单元测试非常简单。测试只做了一件事:检验组件是否针对给定的输入渲染出期望的输出。不需要引入、访问或修改全局变量,也没有什么摸不准的副作用了。

设计良好的组件易于测试,纯组件正是如此。

案例学习2:从网络请求中提纯

重温一下之前文章中提过的 <WeatherFetch> 组件,其加载后会发起一个查询天气信息的网络请求:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class WeatherFetch extends Component {  
   constructor(props) {
     super(props);
     this.state = { temperature: 'N/A', windSpeed: 'N/A' };
   }

   render() {
     const { temperature, windSpeed } = this.state;
     return (
       <WeatherInfo temperature={temperature} windSpeed={windSpeed} />
     );
   }

   componentDidMount() {
     axios.get('http://weather.com/api').then(function(response) {
       const { current } = response.data; 
       this.setState({
         temperature: current.temperature,
         windSpeed: current.windSpeed
       })
     });
   }
}

<WeatherFetch> 是非纯的,因为对于相同的输入,其产生了不同的输出。组件渲染什么取决于服务器端的响应。

麻烦的是,HTTP 请求副作用无法被消除。从服务器端请求数据是 <WeatherFetch> 的直接职责。

但可以让 <WeatherFetch> 针对相同 props 值渲染相同的输出。然后将副作用隔离到一个叫做 fetch() 的 prop 函数中。这样的组件类型可以称为 几乎纯(almost-pure) 的组件。

让我们来把非纯组件 <WeatherFetch> 转变为几乎纯的组件。Redux 在将副作用实现细节从组件中抽离出的方面是一把好手。

fetch() 这个 action creator 开启了服务器调用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export function fetch() {  
 return {
   type: 'FETCH'
 };
}

一个 saga (译注:Sage是一个可以用来处理复杂异步逻辑的中间件,并且由 redux 的 action 触发)拦截了 "FETCH" action,并发起真正的服务器请求。当请求完成后,"FETCH_SUCCESS" action 会被分发:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { call, put, takeEvery } from 'redux-saga/effects';

export default function* () {  
  yield takeEvery('FETCH', function* () {
    const response = yield call(axios.get, 'http://weather.com/api');
    const { temperature, windSpeed } = response.data.current;
    yield put({
      type: 'FETCH_SUCCESS',
      temperature,
      windSpeed
    });
  });
}

可响应的 reducer 负责更新应用的 state:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const initialState = { temperature: 'N/A', windSpeed: 'N/A' };

export default function(state = initialState, action) {  
  switch (action.type) {
    case 'FETCH_SUCCESS': 
      return {
        ...state,
        temperature: action.temperature,
        windSpeed: action.windSpeed
      };
    default:
      return state;
  }
}

(Redux store 和 sagas 的初始化过程在此被省略了)

即便考虑到使用了 Redux 后需要额外的构造器,如 actions、 reducers 和 sagas,这仍然将 <FetchWeather> 转化为了几乎纯的组件。

那么把 <WeatherFetch> 修改为可以适用于 Redux 的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { connect } from 'react-redux';  
import { fetch } from './action';

export class WeatherFetch extends Component {  
   render() {
     const { temperature, windSpeed } = this.props;
     return (
       <WeatherInfo temperature={temperature} windSpeed={windSpeed} />
     );
   }

   componentDidMount() {
     this.props.fetch();
   }
}

function mapStateToProps(state) {  
  return {
    temperature: state.temperate,
    windSpeed: state.windSpeed
  };
}
export default connect(mapStateToProps, { fetch });  

connect(mapStateToProps, { fetch }) HOC 包裹了 <WeatherFetch>.

当组件加载后,this.props.fetch() 这个 action creator 会被调用,触发一个服务器请求。当请求完成后,Redux 会更新系统状态并让 <WeatherFetch> 从 props 中获得 temperaturewindSpeed

this.props.fetch() 作为被孤立并扁平化的非纯代码,正是它产生了副作用。要感谢 Redux 的是,组件不会再被 axios 库的细节、服务端 URL,或是 promise 搞得混乱。此外,对于相同的 props 值,新版本的 <WeatherFetch> 总是会渲染相同的元素。组件变为了几乎纯的。

相比于非纯的版本,测试几乎纯的 <WeatherFetch> 就更简单了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import assert from 'assert';  
import { shallow, mount } from 'enzyme';  
import { spy } from 'sinon';  
import { WeatherFetch } from './WeatherFetch';  
import WeatherInfo from './WeatherInfo';

describe('<WeatherFetch />', function() {  
  it('should render the weather info', function() {
    function noop() {}
    const wrapper = shallow(
      <WeatherFetch temperature="30" windSpeed="10" fetch={noop} />
    );
    assert(wrapper.contains(
      <WeatherInfo temperature="30" windSpeed="10" />
    ));
  });

  it('should fetch weather when mounted', function() {
    const fetchSpy = spy();
    const wrapper = mount(
     <WeatherFetch temperature="30" windSpeed="10" fetch={fetchSpy}/>
    );
    assert(fetchSpy.calledOnce);
  });
});

要测试的是对于给定的 props, <WeatherFetch> 渲染出了符合期望的 <WeatherInfo>,以及加载后 fetch() 会被调用。简单又易行。

让“几乎纯”的“更纯”

实际上至此为止,你可能已经结束了隔离非纯的过程。几乎纯的组件在可预测性和易于测试方面已经表现不俗了。

但是... 让我们看看兔子洞到底有多深。几乎纯版本的 <WeatherFetch> 还可以被转化为一个更理想的纯组件。

让我们把 fetch() 的调用抽取到 recompose 库提供的 lifecycle() HOC 中:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { connect } from 'react-redux';  
import { compose, lifecycle } from 'recompose';  
import { fetch } from './action';

export function WeatherFetch({ temperature, windSpeed }) {  
   return (
     <WeatherInfo temperature={temperature} windSpeed={windSpeed} />
   );
}

function mapStateToProps(state) {  
  return {
    temperature: state.temperate,
    windSpeed: state.windSpeed
  };
}

export default compose(  
  connect(mapStateToProps, { fetch }),
  lifecycle({
    componentDidMount() {
      this.props.fetch();
    }
  })
)(WeatherFetch);

lifecycle() HOC 接受一个指定生命周期的对象。componentDidMount() 被 HOC 处理,也就是用来调用 this.props.fetch()。通过这种方式,副作用被从 <WeatherFetch> 中完全消除了。

现在 <WeatherFetch> 是一个纯组件了。没有副作用,且总是对于给定的相同 temperaturewindSpeed props 值渲染相同的输出。

纯化版本的 <WeatherFetch> 在可预测性和简单性方面无疑是很棒的。为了将非纯组件逐步提纯,虽然增加了引入 compose() 和 lifecycle() 等 HOC 的开销,通常这是很划算的买卖。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-06-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 云前端 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
[译] 更可靠的 React 组件:单一职责原则
原文摘自:https://dmitripavlutin.com/7-architectural-attributes-of-a-reliable-react-component/#6testableandtested
江米小枣
2020/06/15
1.2K0
[译] 更可靠的 React 组件:从"可测试的"到"测试通过的"
原文摘自:https://dmitripavlutin.com/7-architectural-attributes-of-a-reliable-react-component/#6testableandtested
江米小枣
2020/06/15
1K0
你要的 React 面试知识点,都在这了
React是流行的javascript框架之一,在2019年及以后将会更加流行。React于2013年首次发布,多年来广受欢迎。它是一个声明性的、基于组件的、用于构建用户界面的高效javascript库。
前端小智@大迁世界
2019/06/15
19K0
React Hooks
以前,React API 只有一套,现在有两套:类(class)API 和基于函数的钩子(hooks) API。
Leophen
2021/07/13
2.2K0
react全家桶包括哪些_react 自定义组件
对于现在比较流行的三大框架都有属于自己的脚手架(目前这些脚手架都是使用node编写的,并且都是基于webpack的):
全栈程序员站长
2022/11/18
6K0
react全家桶包括哪些_react 自定义组件
深入浅出 React Hooks
Hooks 顾名思义,字面意义上来说就是 React 钩子的概念。通过一个 case 我们对 React Hooks 先有一个第一印象。
桃翁
2019/06/12
2.5K0
react 同构初步(3)
后端ssr只是渲染了网页模板(ul),列表(li)的html都是异步请求加载出来的。再回看首页列表的代码:
一粒小麦
2019/12/19
1.6K0
【React】211- 2019 React Redux 完全指南
https://juejin.im/post/5cac8ccd6fb9a068530111c7
pingan8787
2019/07/23
4.4K0
【React】211- 2019 React Redux 完全指南
React 组件优化方案
如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作。该方法会在 重新渲染前 被触发,其默认实现总是返回 true。
多云转晴
2020/02/17
3.3K0
react 创建组件以及组件通信
关于React.createClass方法与class App extends Component方法的区别
念念不忘
2019/03/29
9870
React组件设计实践总结04 - 组件的思维
在 React 的世界里”一切都是组件“, 组件可以映射作函数式编程中的函数,React 的组件和函数一样的灵活的特性不仅仅可以用于绘制 UI,还可以用于封装业务状态和逻辑,或者非展示相关的副作用, 再通过组合方式组成复杂的应用. 本文尝试解释用 React 组件的思维来处理常见的业务开发场景.
_sx_
2019/08/07
2.4K0
React组件设计实践总结04 - 组件的思维
react hooks api
hooks API是 React 16.8的"新增"功能(16.8更新于2年前)。官网是这么说的:
一粒小麦
2020/06/16
2.9K0
react hooks api
用Jest来给React完成一次妙不可言的~单元测试
在2020的今天,构建一个 web 应用对于我们来说,并非什么难事。因为有很多足够多优秀的的前端框架(比如 React,Vue 和 Angular);以及一些易用且强大的UI库(比如 Ant Design)为我们保驾护航,极大地缩短了应用构建的周期。
用户1462769
2020/03/30
15.2K0
react面试如何回答才能让面试官满意
注意:batchingStrategy 对象可以理解为“锁管理器”。这里的“锁”,是指 React 全局唯一的 isBatchingUpdates 变量,isBatchingUpdates 的初始值是 false,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate 去执行更新动作时,会先把这个锁给“锁上”(置为 true),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队等候下一次的批量更新,而不能随意“插队”。此处体现的“任务锁”的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。
beifeng1996
2022/09/22
9760
你需要的react面试高频考察点总结
函数式组件(Functional component)根本没有实例instance。类组件(Class component)有实例instance,但是永远也不需要直接创建一个组件的实例,因为React帮我们做了这些。
beifeng1996
2022/11/08
3.7K0
React 进阶 - React Redux
应用初始化时候,只请求一次数据,然后通过状态管理把数据存起来,需要数据的组件只需要从状态管理中‘拿’就可以了。
Cellinlab
2023/05/17
1.1K0
React 进阶 - React Redux
【React】945- 你真的用对 useEffect 了吗?
useEffect 做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
pingan8787
2021/05/14
9.8K0
【React】945- 你真的用对 useEffect 了吗?
照方抓药 - 重构 React 组件的实用清单
本文尝试将相关的概念做一个总结,列出一张可用、实用的方法论清单,让我们每次新建组件、修改组件时有章可循,真诚是让一切变好的基础,但实用的套路也是必不可少的。
江米小枣
2020/06/15
1.5K0
React 组件性能优化——function component
函数式组件是一种非常简洁的数据驱动 UI 的实现方式。如果将 React 组件拆分成三个部分 —— 数据、计算和渲染,我们可以看到性能优化的几个方向。
Sneaker-前端公虾米
2021/09/09
1.6K0
React 组件性能优化——function component
[译] 更可靠的 React 组件:组合及可重用性
原文摘自:https://dmitripavlutin.com/7-architectural-attributes-of-a-reliable-react-component/
江米小枣
2020/06/15
2.9K2
相关推荐
[译] 更可靠的 React 组件:单一职责原则
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验