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

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

jcode

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

组件接口定义

Collapse

属性说明类型默认值
accordion是否开启手风琴模式booleanfalse
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不可见时卸载内容booleanfalse
disabled是否为禁用状态booleanfalse
forceRender被隐藏时是否渲染 DOM 结构booleanfalse
key唯一标识符string-
onClick标题栏的点击事件(event: React.MouseEvent<Element, MouseEvent>) => void-
title标题栏左侧内容ReactNode-

创建基础Collapse组件

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

import React, { useState } from 'react';

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

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

export default Collapse;

拓展Collapse组件其它属性

  • accordion:如果设置为true,我们将启用手风琴模式。在这种模式下,只有一个面板可以被展开。当一个新的面板被展开时,之前展开的面板将被关闭。
  • activeKey:当前展开面板的key。如果我们处于手风琴模式,这将是一个字符串或null。如果我们不在手风琴模式,这将是一个字符串数组。
  • arrow:自定义的箭头。如果是一个React节点,将自动为你添加旋转动画效果。如果是一个函数,它将接收一个参数,表示面板是否被展开,并返回一个React节点。
  • defaultActiveKey:默认展开面板的key。它的类型与activeKey相同。
  • onChange:它在面板切换时被触发。它接收一个参数,表示当前展开面板的key。它的类型与activeKey相同。
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 && 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标题栏的内容。
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 && onClick(event);
  };

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

  useEffect(() => {
    if (destroyOnClose && !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结构,如果面板渲染的数据量比较大,这个属性特别有用,不会造成打开的时候会卡顿一下
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方式实现

.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;
}

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 && onClick(event);

};

const renderArrow = () => {

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

};

useEffect(() => {

if (destroyOnClose && !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](https://code.juejin.cn/pen/7254521650341740583)


### 其它方式

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

- 基于scaleY? 感觉不现实
- 使用FLIP技术添加动画优化? 搜了一圈,更难实现?
点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
3年前
List的Select 和Select().tolist()
List<PersondelpnewList<Person{newPerson{Id1,Name"小明1",Age11,Sign0},newPerson{Id2,Name"小明2",Age12,
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Jenkins管家
Jenkins管家
Lv1
走向大海,才知道珍惜港湾的宁静。
文章
2
粉丝
0
获赞
0