React专题:组件

栏目: 服务器 · 发布时间: 5年前

内容简介:本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出来我的来我的个人博客 获得无与伦比的阅读体验

本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出

来我的 GitHub repo 阅读完整的专题文章

来我的个人博客 获得无与伦比的阅读体验

刀耕火种时期的前端,HTML描述页面结构,CSS描述样式,JavaScript描述功能。它们彼此是分离的。

然而这种方式却满足不了开发者对代码复用的需求。

近几年各大前端框架做了很多探索,其中组件化就是最璀璨的成果之一。

一个组件就是一个功能模块,所有的前端元素都封装在组件内部,对外只暴露有限的接口。这样开发者拿来就能用,通过接口与组件交互而不必知道组件的内部细节。

而React是前端框架里面组件化思想贯彻的最彻底的。

class组件和函数组件

在React中,构造一个组件既可以用class类,也可以仅用一个普通函数。

两者有什么区别呢?

class类不仅允许内部状态的存在,还有完整的生命周期钩子。

这让一个组件变的有生命力而且可以管理。

代价就是它的性能稍逊。

还有一点,class类组件必须继承React内置的组件类。因为那些生命周期钩子都是继承自React内置的组件类。

那你说,我不用生命周期钩子,可不可以不继承呢?

不可以,因为你必须要用生命周期钩子。

在一个class类组件中,render方法是必须的,没有了它就不可能返回UI,也就不能称其为一个组件。而render方法就是生命周期钩子之一。

import React, { Component } from 'react';

class App extends Component {
    state = { star: 1 };
    
    render() {
        return (
            <div>{this.state.star}</div>
        );
    }
}

export default App;
复制代码

函数组件没有办法实例化,除了一些逻辑判断之外,它的功能只是返回UI。

所以函数组件常常被称为无状态组件。

然而这正是React强大之处,它构建组件可以如此随意,只需要一个普通函数返回一段JSX元素。在React中,有的时候函数和组件的界限会非常模糊。

有一种 设计模式 把React组件分为容器组件和展示组件,容器组件管理数据、状态和业务逻辑,展示组件仅仅负责接收props展示UI。

所以函数组件的使命肯定是做好展示组件咯。

import React from 'react';

function App(props) {
    return (
        <div>{props.star}</div>
    );
}

export default App;
复制代码

那么在React中函数和组件的区别到底在哪?

组件必须返回一段JSX元素,class类组件和函数组件都一样。

Component和PureComponent

前面说到class类组件有完整的生命周期钩子。这些生命周期钩子是从哪来的呢?毕竟class类组件就是原生的class类写法。

其实React内置了一个 Component 类,生命周期钩子都是从它这里来的,麻烦的地方就是每次都要继承。

PureComponent 类又是干嘛用的?

凭名字猜测,它是一个纯纯的组件类。

怎么个纯法子?

它自动帮开发者做了一些优化工作,使得组件看起来更加纯粹。

而组件性能优化的主要手段就是通过 shouldComponentUpdate 生命周期钩子。

我们来看看它是如何自动优化的。

从下面的例子看,React专门有一个方法来判断组件该不该更新。如果 typeof instance.shouldComponentUpdate === 'function' ,那这就是一个继承了 Component 类的组件,直接执行 shouldComponentUpdate ,返回true则更新,返回false则不更新。

如果 ctor.prototype.isPureReactComponent ,那这就是一个继承了 PureComponent 类的组件,这时React会将 oldPropsnewProps 做一层浅比较,同时将 oldStatenewState 做一层浅比较,只要有一个浅比较不相等,则返回true更新,否则返回false不更新。

function checkShouldComponentUpdate(
    workInProgress,
    oldProps,
    newProps,
    oldState,
    newState,
    newContext,
) {
    const instance = workInProgress.stateNode;
    const ctor = workInProgress.type;
    if (typeof instance.shouldComponentUpdate === 'function') {
        startPhaseTimer(workInProgress, 'shouldComponentUpdate');
        const shouldUpdate = instance.shouldComponentUpdate(
            newProps,
            newState,
            newContext,
        );
        stopPhaseTimer();
        return shouldUpdate;
    }
    if (ctor.prototype && ctor.prototype.isPureReactComponent) {
        return (
            !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
        );
    }
    return true;
}
复制代码

shallowEqual 又干了什么呢?

  • 首先用Object.is判断是否相等(React自己写的polyfill)。
  • 如果Object.is判断不相等,再作引用类型的比较,比较属性key的长度,比较属性key的一一对应,比较属性value的引用。

总的来说,它只做了一层比较,所以才叫做浅比较。

聪明的你肯定要问了:为什么不递归呢(并发射一个傲娇脸)?

因为递归的深比较非常耗费性能。 PureComponent 类只是帮开发者适度优化性能,它还是要找到成本与收益的平衡点的。

PureComponent 类有其正确打开方式

例如像数组这样的数据结构,在不改变引用的情况下,使用数组的方法操作数组,然后再setState该数组,组件是不会更新的。

你说不对呀,老数组和新数组虽然是同一个引用,但是长度不一样了,浅比较是能识别出来的呀。

我们以上面的例子来看,pop方法会修改老数组,所以此时老数组和新数组是一模一样的,引用一样,长度一样。

React拿着这两份数据做浅比较,肯定返回true。

import React, { Component } from 'react';

class App extends Component {
    state = {
        list: [1, 2, 3],
    };
    
    render() {
        const { list } = this.state;
        return (
            <div>
                <button onClick={this.handleDelete}>Delete</button>
                <ul>{list.map(n => <li key={n}>{n}</li>)}</ul>
            </div>
        );
    }

    handleDelete = () => {
        this.state.list.pop();
        this.setState((prevState) => ({ list: prevState.list }));
    }
}

export default App;
复制代码

这就是会误导开发者的地方。

正确的做法是永远不修改原数据,生成新数据时依赖于原数据的浅拷贝,避免新数据和老数据指向同一个引用。

import React, { Component } from 'react';

class App extends Component {
    state = {
        list: [1, 2, 3],
    };
    
    render() {
        const { list } = this.state;
        return (
            <div>
                <button onClick={this.handleDelete}>Delete</button>
                <ul>{list.map(n => <li key={n}>{n}</li>)}</ul>
            </div>
        );
    }

    handleDelete = () => {
        this.setState((prevState) => ({ list: [...prevState.list].pop() }));
    }
}

export default App;
复制代码

当然还有一种情况,采用传入时定义的方式给子组件传递props。

结果就是子组件做props的浅比较时永远返回false,因为每次的值都是重新定义的,绝非同一个引用。

PureComponent 类就失去它的意义了。

import React from 'react';

function App() {
    return (
        <Child method={value => console.log(value)} />
    );
}

export default App;
复制代码

总之,要想使用 PureComponent 类,得时刻盯紧引用数据类型。

在继承了 PureComponent 类的组件里写 shouldComponentUpdate 生命周期钩子会怎么样?

小伙子果然骨骼清奇。

原则上,React并不推荐这种写法,并且在开发环境下会打印一个警告。

你要么偷个懒让React帮你做优化,要么自己做优化,这么干不是赤裸裸的挑衅么!

不过React早就预测到你会这么干的,所以它只能随你的脾气,只要组件里定义了 shouldComponentUpdate 生命周期钩子, PureComponent 类的自动优化就不再起作用了。

组件复用

话说代码复用还真不是因为开发者懒。

假如一个蓝色卡片组件,分别有十个地方要用到。而且这个开发者异常勤奋,把这段代码小心翼翼的复制到这十个地方。看起来大功告成了。但是有一天,产品经理说:“我希望卡片背景换成柠檬色,再删减掉一些信息。”

那么问题来了:该开发者是应该砍死产品经理还是应该考虑代码复用?

React世界中一切都是组件,组件思想的根本诉求又是什么呢?当然是代码复用。一个组件就好像一个集装箱,我不用知道里面装的什么货物,我只用知道它能上我的货轮。

集装箱是商业世界的伟大发明,组件也是前端世界的伟大发明。

React的代码复用都是以组件为单位的。

组件四海为家

最普通的组件复用的方式就是直接使用组件。

一个 <Card /> 组件,我可以将它放在任何地方。它对外可以提供一些接口,以保证填充所在上下文要求的内容。开发者可以很方便的一处修改,处处生效。

高阶组件

首先我们回忆一下,什么是高阶函数?

定义非常简单:一个函数,它的参数是函数,或者它的返回值是函数。

基本就是鸡吃鸡或者鸡生鸡的意思。

那么高阶组件就好理解了。它的定义是:一个函数,传入一个组件,并且返回一个组件。

区别就是,高阶函数只要满足一个条件,高阶组件要满足两个条件。

高阶组件既是函数,也是组件,因为在React中一个函数只要返回JSX元素就是组件。但这个组件有点特殊,因为它不是用来堆砌UI的,而是一个组件装饰工厂。

话不多说,我们直接来看一下 react-redux 中的 connect 方法(极度简化的伪代码)。

connect 方法是用来将redux中的state作为props传给组件的。这就是一个典型的高阶组件,当然它还在外面套了一层函数,为了传值。

不使用 connect 方法的组件是这样导出的: export default App;

使用 connect 方法的组件是这样导出的: export default connect(mapState, mapDispatch)(App);

开发者首先传参执行connect,然后再传组件执行返回的函数,导出的组件就多了一些属性。

可以看出,这种类型的高阶组件主要是为了给目标组件传递一些props。

import React, { Component } from 'react';

function connect(mapStateToProps, mapDispatchToProps) {
    return function wrapWithConnect(WrappedComponent) {
        return class Connect extends Component {
            render() {
                return (
                    <WrappedComponent {...mapStateToProps} {...mapDispatchToProps} />
                );
            }
        }
    }
}

export default connect;
复制代码

说高阶组件是一个组件装饰工厂是有道理的,因为我们可以用装饰器的写法来部署高阶组件。

下面是 connect 方法的装饰器写法。

import React, { Component } from 'react';

@connect(mapStateToProps, mapDispatchToProps);
class App extends Component {
    render() {
        return (
            <div>app</div>
        );
    }
}

export default App;
复制代码

另一种类型的高阶组件更加巧妙,它使用到了继承的特性。

返回的组件可以获得传入组件的所有属性和方法,如果复用组件需要对传入组件做一些侵入性比较强的改动,那么这种方式非常具有灵活性。

import React from 'react';

function HOC(WrappedComponent) {
    return class WithComponent extends WrappedComponent {
        render() {
            return (
                <WrappedComponent />
            );
        }
    }
}

export default HOC;
复制代码

理解高阶组件的精髓是什么呢?我认为是理解复用的到底是什么。

假如我们造一个组件,然后用到不同的地方,那直接用就好了,组件本身就是可复用的。

关键在于组件之外的东西。我们需要重复的给一个组件传入props,或者我们需要重复的给组件增强一些功能。这些都可以理解为组件的装饰,高阶组件其实解决的是组件装饰的复用。

大多数时候,使用高阶组件的场景都有更简单的替代方案。

高阶组件之所以这么流行,是因为React生态系统中很多库都有高阶组件的身影,它们或许是为了提供一个优雅的API,或许是需要处理复杂的场景,或许是...炫技。

如果哪天你复用组件时产生了瓶颈,不妨来看看老朋友高阶组件,它有多强大完全取决于你。

忘了告诉你们,高阶组件简称HOC,高阶组件的第一种类型叫属性代理,第二种类型叫反向继承。不过让这些概念都见鬼去吧,它只会让初学者望而却步。

render props

高阶组件就是传入一个组件,然后返回一个组件。

那我能不能不通过函数的参数传递组件,比如说通过props传递呢?

这是一个绝好的思路。

它有一个名字叫 render props

当然, render props 也需要一个用来复用的容器。通过给这个容器传递render属性,它可以渲染不同的组件。

属性名是无所谓的,render或者rander都可以,关键它的值得是一个组件,或者说无状态组件。

import React, { Component } from 'react';

class WithRender extends Component {
    state = { star: 1 }

    render() {
        return (
            <div>{this.props.render(this.state)}</div>
        );
    }
}
复制代码

react-router 中的 withRouter 方法就是用的 render props

const withRouter = (Component) => {
    return (props) => {
        return (
            <Route children={routeComponentProps => <Component {...routeComponentProps} />} />
        );
    }
}
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

深入理解SPARK

深入理解SPARK

耿嘉安 / 机械工业出版社 / 2016-1-1 / 99

《深入理解SPARK:核心思想与源码分析》结合大量图和示例,对Spark的架构、部署模式和工作模块的设计理念、实现源码与使用技巧进行了深入的剖析与解读。 《深入理解SPARK:核心思想与源码分析》一书对Spark1.2.0版本的源代码进行了全面而深入的分析,旨在为Spark的优化、定制和扩展提供原理性的指导。阿里巴巴集团专家鼎力推荐、阿里巴巴资深Java开发和大数据专家撰写。 本书分为......一起来看看 《深入理解SPARK》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具