首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >腾讯面试官:如何从0到1实现一个高性能Collapse折叠组件,直到现在我还实现不出来

腾讯面试官:如何从0到1实现一个高性能Collapse折叠组件,直到现在我还实现不出来

作者头像
linwu
发布2023-07-27 19:31:26
发布2023-07-27 19:31:26
3.4K0
举报
文章被收录于专栏:编程时光编程时光

现代JavaScript高级小册

深入浅出Dart

现代TypeScript高级小册

大家好,我是linwu,之前面腾讯某个部门的时候,面试官曾经给了我一道手写题,题目大概就是从0到1实现一个Collapse折叠组件,然后我根据提供接口属性,我大概实现出来类似下面组件的形态,然后面试官问动画除了height形式,还有其他它方式么,因为height的变化会触发重排,另外折叠面板panel如果是大量数据,打开的时候会卡顿,该如何处理,这个我到时候解决了,提前渲染隐藏就行,但是重排的问题直到现在我都没有解决,发出来问问大家,如果是你们,你们会如何思考🤔

jcode

我们先从最基本的实现开始,然后逐步添加更多的功能,如手风琴模式、自定义箭头、禁用状态、隐藏时是否渲染DOM结构

组件接口定义

Collapse

属性

说明

类型

默认值

accordion

是否开启手风琴模式

boolean

false

activeKey

当前展开面板的 key

手风琴模式:string | null 非手风琴模式:string[]

-

arrow

自定义箭头,如果是 ReactNode,那么 会自动为你增加旋转动画效果

ReactNode | ((active: boolean) => React.ReactNode)

-

defaultActiveKey

默认展开面板的 key

手风琴模式:string | null 非手风琴模式:string[]

-

onChange

切换面板时触发

手风琴模式:(activeKey: string | null) => void 非手风琴模式:(activeKey: string[]) => void

-

Collapse.Panel

属性

说明

类型

默认值

arrow

自定义箭头

ReactNode | ((active: boolean) => React.ReactNode)

-

destroyOnClose

不可见时卸载内容

boolean

false

disabled

是否为禁用状态

boolean

false

forceRender

被隐藏时是否渲染 DOM 结构

boolean

false

key

唯一标识符

string

-

onClick

标题栏的点击事件

(event: React.MouseEvent<Element, MouseEvent>) => void

-

title

标题栏左侧内容

ReactNode

-

创建基础Collapse组件

我们创建一个基础的Collapse组件。这个组件需要有一个状态来追踪它是否被展开

代码语言:javascript
复制
import React, { useState } from 'react';

const Collapse = ({ children }) => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? 'Collapse' : 'Expand'}
      </button>
      {isOpen &amp;&amp; <div>{children}</div>}
    </div>
  );
};

export default Collapse;

拓展Collapse组件其它属性

  • accordion:如果设置为true,我们将启用手风琴模式。在这种模式下,只有一个面板可以被展开。当一个新的面板被展开时,之前展开的面板将被关闭。
  • activeKey:当前展开面板的key。如果我们处于手风琴模式,这将是一个字符串或null。如果我们不在手风琴模式,这将是一个字符串数组。
  • arrow:自定义的箭头。如果是一个React节点,将自动为你添加旋转动画效果。如果是一个函数,它将接收一个参数,表示面板是否被展开,并返回一个React节点。
  • defaultActiveKey:默认展开面板的key。它的类型与activeKey相同。
  • onChange:它在面板切换时被触发。它接收一个参数,表示当前展开面板的key。它的类型与activeKey相同。
代码语言:javascript
复制
import React, { useState, useEffect } from 'react';

const Collapse = ({ children, forceRender, accordion, activeKey, arrow, defaultActiveKey, onChange }) => {
  const [isOpen, setIsOpen] = useState(false);
  const [currentActiveKey, setCurrentActiveKey] = useState(defaultActiveKey);

  useEffect(() => {
    setCurrentActiveKey(activeKey);
  }, [activeKey]);

  const handleClick = () => {
    setIsOpen(!isOpen);
    if (accordion) {
      setCurrentActiveKey(isOpen ? null : activeKey);
    }
    onChange &amp;&amp; onChange(isOpen ? null : activeKey);
  };

  const renderArrow = () => {
    if (typeof arrow === 'function') {
      return arrow(isOpen);
    }
    return arrow;
  };

  return (
    <div>
      <button onClick={handleClick}>
        {isOpen ? 'Collapse' : 'Expand'}
        {renderArrow()}
      </button>
      <div style={{ display: isOpen || forceRender ? 'block' : 'none' }}>
        {children}
      </div>
    </div>
  );
};

export default Collapse;

实现Panel

我们创建一个名为Collapse.Panel的子组件来支持这些新的属性。这个子组件将作为Collapse组件的一部分,用于表示一个可折叠的面板。

  • arrow:这是一个自定义的箭头。如果这是一个React节点,antd-mobile将自动为你添加旋转动画效果。如果这是一个函数,它将接收一个参数,表示面板是否被展开,并返回一个React节点。
  • destroyOnClose:如果设置为true,我们将在面板关闭时销毁它的内容。
  • disabled:如果设置为true,我们将禁用面板,使其不能被打开或关闭。
  • forceRender:如果设置为true,我们将在面板关闭时仍然渲染它的DOM结构。
  • key:panel的唯一标识符。
  • onClick:它在面板的标题栏被点击时被触发。它接收一个参数,表示点击事件。
  • title:panel标题栏的内容。
代码语言:javascript
复制
import React, { useState, useEffect } from 'react';

const Panel = ({ children, arrow, destroyOnClose, disabled, forceRender, key, onClick, title }) => {
  const [isOpen, setIsOpen] = useState(false);

  const handleClick = (event) => {
    if (disabled) return;
    setIsOpen(!isOpen);
    onClick &amp;&amp; onClick(event);
  };

  const renderArrow = () => {
    if (typeof arrow === 'function') {
      return arrow(isOpen);
    }
    return arrow;
  };

  useEffect(() => {
    if (destroyOnClose &amp;&amp; !isOpen) {
      children = null;
    }
  }, [isOpen]);

  return (
    <div key={key}>
      <button onClick={handleClick}>
        {title}
        {renderArrow()}
      </button>
      <div style={{ display: isOpen || forceRender ? 'block' : 'none' }}>
        {children}
      </div>
    </div>
  );
};

const Collapse = ({ children, accordion, activeKey, defaultActiveKey, onChange }) => {
};

Collapse.Panel = Panel;

export default Collapse;

forceRender属性

我们要添加一个名为forceRender的属性。如果这个属性被设置为true,我们会在组件隐藏时仍然渲染DOM结构,如果面板渲染的数据量比较大,这个属性特别有用,不会造成打开的时候会卡顿一下

代码语言:javascript
复制
import React, { useState } from 'react';

const Collapse = ({ children, forceRender }) => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? 'Collapse' : 'Expand'}
      </button>
      <div style={{ display: isOpen || forceRender ? 'block' : 'none' }}>
        {children}
      </div>
    </div>
  );
};

export default Collapse;

实现折叠面板动画

height方式实现

代码语言:javascript
复制
.collapse-panel {
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-bottom: 10px;
  overflow: hidden;
}

.collapse-panel-button {
  background-color: #f5f5f5;
  color: #333;
  cursor: pointer;
  padding: 10px 15px;
  width: 100%;
  text-align: left;
  border: none;
  outline: none;
}

.collapse-panel-content {
  padding: 10px 15px;
  background-color: white;
  overflow: hidden;
  max-height: 0;
  transition: max-height 0.2s ease-out;
}

.collapse-panel-content.open {
  max-height: 100vh;
}
代码语言:javascript
复制
import React, { useState, useEffect, useRef } from 'react';

const Panel = ({ children, arrow, destroyOnClose, disabled, forceRender, key, onClick, title }) => {
  const [isOpen, setIsOpen] = useState(false);
  const contentRef = useRef(null);

  const handleClick = (event) => {
    if (disabled) return;
    setIsOpen(!isOpen);
    onClick &amp;&amp; onClick(event);
  };

  const renderArrow = () => {
    if (typeof arrow === 'function') {
      return arrow(isOpen);
    }
    return arrow;
  };

  useEffect(() => {
    if (destroyOnClose &amp;&amp; !isOpen) {
      children = null;
    }
  }, [isOpen]);

  useEffect(() => {
    contentRef.current.style.maxHeight = isOpen ? `${contentRef.current.scrollHeight}px` : '0';
  }, [isOpen]);

  return (
    <div key={key} className="collapse-panel">
      <button onClick={handleClick} className="collapse-panel-button">
        {title}
        {renderArrow()}
      </button>
      <div ref={contentRef} className={`collapse-panel-content ${isOpen ? 'open' : ''}`}>
        {children}
      </div>
    </div>
  );
};

// ...

完整效果: jcode

其它方式

上面手风琴效果是利用height的实现,这种实现会触发重排,所以感兴趣的同学可以考虑其它方式优化一下

  • 基于scaleY? 感觉不现实
  • 使用FLIP技术添加动画优化? 搜了一圈,更难实现?
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-07-11,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 组件接口定义
    • Collapse
    • Collapse.Panel
  • 创建基础Collapse组件
  • 拓展Collapse组件其它属性
  • 实现Panel
  • forceRender属性
  • 实现折叠面板动画
    • height方式实现
    • 其它方式
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档