这样配置属性,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来实现它。
  • 但是这个方法是很容易产生问题的,所以这个方法只适用于简单的情况。
相关推荐
热爱编程的小曾6 分钟前
sqli-labs靶场 less 8
前端·数据库·less
gongzemin18 分钟前
React 和 Vue3 在事件传递的区别
前端·vue.js·react.js
Apifox30 分钟前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
-代号952735 分钟前
【JavaScript】十四、轮播图
javascript·css·css3
树上有只程序猿1 小时前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼2 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
QTX187302 小时前
JavaScript 中的原型链与继承
开发语言·javascript·原型模式
黄毛火烧雪下2 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox2 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞2 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox