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