这样配置属性,P8看了都流泪

在上一章,我们知道了把组件当做属性传递,可以很好的提升应用的性能。但是,这个模式不仅仅能提升组件的性能,这个模式还有很多不为人知的优点。这种模式所解决的最大用处,是提升组件的灵活性和配置方面的能力。

让我们一起研究React的工作机制。这次,我们要构建一个"有图标的按钮"组件。构建组件的过程可能有点复杂,但是在这个过程中,你将会学到:

  • 把组件当做元素传递模式将如何大幅提升组件的可配置性。
  • 组件的条件渲染将如何影响组件性能。
  • 何时把组件当属性传递,才能正确渲染
  • 如何使用cloneElement函数为作为属性传递的组件设置默认属性,以及这样做存在哪些弊端。

问题

想象一下,你要实现一个按钮组件。而其中一个要求是,这个按钮可以在加载资源时展示加载图标。

这是很容易实现的。我们只要实现一个button控件,再传递一个isLoading属性来控制loading图标的展示与隐藏即可。

js 复制代码
const Button = ({ isLoading }) => {
    return (
        <button>Submit {isLoading ? <Loading /> : null}</button>
    );
};

第二天,这个按钮需要支持展示样式库里面所有的图标,而不仅仅是loading图标。这是不难实现的,为Button组件拓展一个iconName属性即可。没过几天,有需求希望图标的颜色也可以自定义,所以又拓展了一个iconColor属性。后来,又实现了iconSize属性来控制图标的大小,等等。

最终,有一半的属性是用来控制图标的,没有人知道这些属性如何发挥作用,而且每个属性的变化都有可能导致不可预料的后果。

js 复制代码
const Button = ({
        isLoading,
        iconLeftName,
        iconLeftColor,
        iconLeftSize,
        isIconLeftAvatar,
        ...
    }) => {
        // no one knows what's happening here and how all those props
        work
        return ...
}

这个场景看起来很熟悉吧?

元素作为属性

好在我们可以用一个简单的方法高效的解决这个问题。我们只要把icon元素当做组件传给Button组件即可:

js 复制代码
const Button = ({ icon }) => {
    return <button>Submit {icon}</button>;
};

剩下的配置工作,让组件传递方按照他们的意愿传递即可:

js 复制代码
// default Loading icon
<Button icon={<Loading />} />

// red Error icon
<Button icon={<Error color="red" />} />

// yellow large Warning icon
<Button icon={<Warning color="yellow" size="large" />} />

// avatar instead of icon
<Button icon={<Avatar />} />

代码示例: advanced-react.com/examples/03...

当然,对一个按钮做这样的事情(上文提及的相关操作,比如通过某种方式设置属性之类的操作,具体需结合前文确定)有时候是有争议的。这在很大程度上取决于你的设计要求有多严格,以及在实现产品功能时它能允许多大程度的偏差。

想象一下,你需要实现一个由header、content和有一些按钮的footer组成的兑换框。

除非你的设计师要求非常严格且很有话语权,否则很可能你需要在不同的对话框中对那些按钮进行不同的配置:一个、两个、三个按钮的情况,其中一个按钮是链接,一个按钮是 "主要按钮",当然它们上面的文字不同,图标不同,提示信息也不同等等。想象一下要通过配置属性来传递所有这些内容吧!

但如果把元素当做属性来传递,事情就简单很多了,我们为对话框组件拓展一个footer组件即可:

js 复制代码
const ModalDialog = ({ content, footer }) => {
    return (
        <div className="modal-dialog">
            <div className="content">{content}</div>
            <div className="footer">{footer}</div>
        </div>
    );
};

之后,我们按需传递即可:

js 复制代码
// just one button in footer
<ModalDialog content={<SomeFormHere />} footer={<SubmitButton />}
/>

// two buttons
<ModalDialog
 content={<SomeFormHere />}
 footer={<><SubmitButton /><CancelButton /></>}
/>

代码示例: advanced-react.com/examples/03...

又或者,你需要实现一个三行布局的组件。

js 复制代码
<ThreeColumnsLayout
 leftColumn={<Something />}
 middleColumn={<OtherThing />}
 rightColumn={<SomethingElse />}
/>

代码示例: advanced-react.com/examples/03...

就本质而言,把一个元素当做属性传给一个组件是告诉消费者:给我你想渲染的内容,我不关心这是什么,我只负责把它渲染在正确的地方。剩下的工作,都由属性传递方决定。

当然,上一章讲到的"子组件"作为属性的模式也可以处理这个问题。如果我们想把一些内容当做该组件的"主要"展示内容来传递,例如对话框的"content"内容,或者三列组件的中间列,我们可以使用jsx的嵌套语法。

js 复制代码
// before
<ModalDialog
 content={<SomeFormHere />}
 footer={<SubmitButton />}
/>

// after
<ModalDialog
 footer={<SubmitButton />}
>
 <SomeFormHere />
</ModalDialog

在ModalDialog组件要做的事情,就是把content字段命名为children:

js 复制代码
const ModalDialog = ({ children, footer }) => {
    return (
        <div className="dialog">
            <div className="content">{children}</div>
            <div className="footer">{footer}</div>
        </div>
    );
};

记住,在这段代码中,children字段不过是基于jsx嵌套语法的语法糖罢了。

条件渲染与性能

在采用这个模式时,有时遇到最大的挑战就是性能问题。这颇具讽刺意味,因为在上一章中,我们还讨论了如何利用它来提升性能。那么,这到底是怎么回事呢?

想象一个场景,有一个组件接收元素作为属性,但是这个属性是条件式渲染的。就像我们的ModalDialog组件,它只会在isDialogOpen为true时被渲染。

js 复制代码
const App = () => {
    const [isDialogOpen, setIsDialogOpen] = useState(false);
    
    // when is this one going to be rendered?
    const footer = <Footer />;
    
    return isDialogOpen ? (
        <ModalDialog footer={footer} />
    ) : null;
};

这段代码中困扰开发者的问题是:我们在一个未渲染(也许永远不会被渲染)的对话框中,声明了一个Footer。这是否意味着,Footer会永远被重新渲染,即使对话框并没有展示在屏幕上。这会给应用的性能带来什么影响。它会拖慢应用的运行速度吗?

幸运的是,我们不必为此感到担忧。记住,正如在第二章提及的,footer = <Footer />所产生的仅仅是一个元素。就React源代码的视角而言,这个元素不过是静静的位于内存中的没有做任何事的对象罢了。而且,生成一个对象的开销是很低的(至少比渲染组件低)。

这个Footer只有在作为一个组件的返回对象时,才会被渲染。至于
是不是在App中被调用的,其实并不重要。真正接收Footer和返回Footer的是ModalDialog组件。

js 复制代码
const ModalDialog = ({ children, footer }) => {
    return (
        <div className="dialog">
            <div className="content">{children}</div>
            {/* Whatever is coming from footer prop is going to be
            rendered only when this entire component renders */}
            {/* not sooner */}
            <div className="footer">{footer}</div>
        </div>
    );
};

Reac的router接收element属性时,背后也是这个原理。

js 复制代码
const App = () => {
    return (
        <>
            <Route path="/some/path" element={<Page />} />
            <Route path="/other/path" element={<OtherPage />} />
            ...
        </>
    );
};

这段代码中并没有任何条件判断,看起来App组件拥有并同时渲染了和。然而,事实上并非如此。 和 所产生的是描述对应页面的描述对象。对应的渲染操作,只有在对应的路由与URL匹配,且Route组件返回了element时,才会发生。

为来自属性的元素配置默认值

任何图标都没有额外的属性了,现在只有由按钮控制的默认属性!然后,如果有人确实需要覆盖默认值,他们仍然可以这样做:像往常一样传递该属性。

js 复制代码
// override the default black color with red icons
<Button
 appearance="secondary"
 icon={<Loading color="red" />}
/>

事实上,组件的使用者很有可能不知道Button组件有默认值逻辑。对于他们来说,icon组件的工作原理简直就是魔法。

为什么我不能滥用属性的默认值

说到神奇之处:设置默认值这件事看似神奇地起作用,但也可能存在很大的弊端。这里最大的问题在于,太容易出错并永久性地覆盖属性了。例如,如果我不使用实际的属性去覆盖默认属性,而是直接进行赋值的话:

js 复制代码
const Button = ({ appearance, size, icon }) => {
    // create default props
    const defaultIconProps = {
        size: size === 'large' ? 'large' : 'medium',
        color: appearance === 'primary' ? 'white' : 'black',
    };
    
    // clone the icon and assign the default props to it - don't do that!
    // it will override all the props that are passed to the icon from the outside!
    const clonedIcon = React.cloneElement(
        icon,
        defaultIconProps,
    );
    return <button>Submit {clonedIcon}</button>;
   };

这样做会摧毁icon属性。无论使用者为icon配置什么尺寸和颜色,都无法渲染出预期效果:

js 复制代码
// color "red" won't work here - I never passed those props to the
cloned icon
<Button appearance="secondary" icon={<Loading color="red" />} />

// but if I just render this icon outside the button, it will work
<Loading color="red" />

代码示例: advanced-react.com/examples/03...

祝任何试图弄明白为何在按钮外部设置图标的颜色能完美运行,但当图标作为这个属性传递时却行不通的人好运。

所以要格外小心这种模式,确保你总是用实际的属性去覆盖默认属性。如果你对此感到不放心 ------ 别担心。在 React 中,有无数种方法能达成完全相同的结果。有一种模式在这种情况下会非常有用:渲染属性(render props)。如果你需要根据按钮的状态来计算图标的属性,或者只是单纯地将该状态回传给图标,它也会非常有帮助。下一章将全是关于这种模式的内容。

知识概要

在我们前往渲染属性模式前,我们需要记住:

c

js 复制代码
const Button = ({ icon }) => {
    return <button>Submit {icon}</button>;
};

// large red Error icon
<Button icon={<Error color="red" size="large" />} />;
  • 如果一组件的元素属性是依赖组件内的状态条件式渲染的,那么即使这个元素的生成独立于这个条件,它们也只会在渲染条件成立时进行渲染。
js 复制代码
const App = () => {
    // footer will be rendered only when the dialog itself renders
    // after isDialogOpen is set to "true"
    const footer = <Footer />;
   
    return isDialogOpen ? (
        <ModalDialog footer={footer} />
    ) : null;
};
  • 如果我们要为元素属性配置默认属性,我们可以使用cloneElement来实现它。
  • 但是这个方法是很容易产生问题的,所以这个方法只适用于简单的情况。
相关推荐
cnsxjean2 小时前
Vue教程|搭建vue项目|Vue-CLI2.x 模板脚手架
javascript·vue.js·ui·前端框架·npm
web组态软件2 小时前
BY组态-低代码web可视化组件
前端·低代码
react_in2 小时前
webpack 题目
前端·webpack
MarisolHu2 小时前
前端学习笔记-Vue篇-02
前端·vue.js·笔记·学习
学前端的小朱2 小时前
Webpack的基础配置
前端·webpack·node.js
小小优化师 anny3 小时前
JS +CSS @keyframes fadeInUp 来定义载入动画
javascript·css·css3
小周同学_丶3 小时前
解决el-select数据量过大的3种方法
前端·vue.js·elementui
先知demons4 小时前
uniapp开发微信小程序笔记10-触底加载
前端·笔记·微信小程序·小程序·uni-app
每一天,每一步4 小时前
react antd不在form表单中提交表单数据,而是点查询按钮时才将form表单数据和其他查询条件一起触发一次查询,避免重复触发请求
前端·javascript·react.js
NoneCoder4 小时前
HTML5系列(9)-- Web Components
前端·html·html5