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

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

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

组件接口定义

Collapse

屏幕截图 2023-07-31 154411.png

创建基础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>
  );
};
// ...

完整效果:

<html>
<head>
  <meta charset="UTF-8">
  <title>Collapse Component</title>
  <style>
    .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: #fff;
      overflow: hidden;
      max-height: 0;
      transition: max-height 0.2s ease-out;
    }
    .collapse-panel-content.open {
      max-height: 100vh;
    }
  </style>
  <script src="https://unpkg.com/react/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/babel-standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
const Panel = ({ children, arrow, destroyOnClose, disabled, forceRender, id, onClick, title, isOpen }) => {
  const contentRef = useRef(null);
  const handleClick = () => {
    if (disabled) return;
    onClick && onClick(id);
  };
  useEffect(() => {
    contentRef.current.style.maxHeight = isOpen ? `${contentRef.current.scrollHeight}px` : '0';
  }, [isOpen]);
  return (
    <div id={id} className="collapse-panel">
      <button onClick={handleClick} className="collapse-panel-button">
        {title}
        {arrow}
      </button>
      <div ref={contentRef} className={`collapse-panel-content ${isOpen ? 'open' : ''}`}>{children}</div>
    </div>
  );
};
const Collapse = ({ children, accordion, activeKey, defaultActiveKey, onChange }) => {
  const [currentActiveKey, setCurrentActiveKey] = useState(defaultActiveKey);
  useEffect(() => {
    setCurrentActiveKey(activeKey || defaultActiveKey);
  }, [activeKey, defaultActiveKey]);
  const handleClick = (key) => {
    setCurrentActiveKey(accordion ? (currentActiveKey === key ? null : key) : key);
    onChange && onChange(key);
  };
  return (
    <div>
      {React.Children.map(children, (child) => {
        const { id } = child.props;
        const isActive = id === currentActiveKey;
        const arrow = isActive ? <span>&#9660;</span>:<span>&#9658;</span> ;
        return React.cloneElement(child, { onClick: handleClick, id, isOpen: isActive, arrow });
      })}
    </div>
  );
};
Collapse.Panel = Panel;
const App = () => (
  <Collapse defaultActiveKey='panel1' onChange={(key) => console.log(key)} accordion>
    <Panel id='panel1' title='Panel 1'>
      <p>内容1</p>
      <p>Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件</p>
      <p>Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件Collapse组件</p>
    </Panel>
    <Panel id='panel2' title='Panel 2'>
      <p>内容2</p>
      <p>答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来</p>
      <p>答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来答不出来</p>
    </Panel>
  </Collapse>
);
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
</html>

其它方式

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

  • 基于scaleY? 感觉不现实
  • 使用FLIP技术添加动画优化? 搜了一圈,更难实现?

目录
相关文章
|
2月前
|
算法 前端开发 Java
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
这篇文章总结了单链表的常见面试题,并提供了详细的问题分析、思路分析以及Java代码实现,包括求单链表中有效节点的个数、查找单链表中的倒数第k个节点、单链表的反转以及从尾到头打印单链表等题目。
34 1
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
|
7月前
|
Android开发 缓存 双11
android的基础ui组件,Android开发社招面试经验
android的基础ui组件,Android开发社招面试经验
android的基础ui组件,Android开发社招面试经验
|
2月前
|
负载均衡 算法 Java
腾讯面试:说说6大Nginx负载均衡?手写一下权重轮询策略?
尼恩,一位资深架构师,分享了关于负载均衡及其策略的深入解析,特别是基于权重的负载均衡策略。文章不仅介绍了Nginx的五大负载均衡策略,如轮询、加权轮询、IP哈希、最少连接数等,还提供了手写加权轮询算法的Java实现示例。通过这些内容,尼恩帮助读者系统化理解负载均衡技术,提升面试竞争力,实现技术上的“肌肉展示”。此外,他还提供了丰富的技术资料和面试指导,助力求职者在大厂面试中脱颖而出。
腾讯面试:说说6大Nginx负载均衡?手写一下权重轮询策略?
|
7月前
|
存储 运维 关系型数据库
2024年最全ceph的功能组件和架构概述(2),Linux运维工程面试问题
2024年最全ceph的功能组件和架构概述(2),Linux运维工程面试问题
2024年最全ceph的功能组件和架构概述(2),Linux运维工程面试问题
|
7月前
|
数据采集 数据挖掘 关系型数据库
2024年5分钟就能完成的5个Python小项目,赶紧拿去玩玩吧(2),2024年最新腾讯面试题
2024年5分钟就能完成的5个Python小项目,赶紧拿去玩玩吧(2),2024年最新腾讯面试题
2024年5分钟就能完成的5个Python小项目,赶紧拿去玩玩吧(2),2024年最新腾讯面试题
|
4月前
|
JavaScript API
【Vue面试题十】、Vue中组件和插件有什么区别?
这篇文章阐述了Vue中组件和插件的区别,指出组件主要用于构建应用程序的业务模块,而插件用于增强Vue本身的功能,两者在编写形式、注册方式和使用场景上有所不同。
【Vue面试题十】、Vue中组件和插件有什么区别?
|
4月前
|
Java
面试官:OpenFeign十大可扩展组件你知道哪些?
这篇文章是关于OpenFeign框架的可扩展组件的讨论,作者分享了自己在面试中遇到的相关问题,并回顾了OpenFeign源码,列出了十大组件,包括日志、解码器、重试组件等,并展示了如何使用FeignClient注解和@EnableFeignClients注解来实现远程RPC调用。
面试官:OpenFeign十大可扩展组件你知道哪些?
|
4月前
|
负载均衡 监控 Java
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
|
4月前
|
存储 JavaScript 容器
【Vue面试题十一】、Vue组件之间的通信方式都有哪些?
这篇文章介绍了Vue中组件间通信的8种方式,包括`props`传递、`$emit`事件触发、`ref`、`EventBus`、`$parent`或`$root`、`attrs`与`listeners`、`provide`与`inject`以及`Vuex`,以解决不同关系组件间的数据共享问题。
|
4月前
|
JavaScript 前端开发
面试题分享之封装一个弹框组件
面试题分享之封装一个弹框组件
37 0