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

201 阅读5分钟

theme: devui-blue

点击在线阅读,体验更好链接
现代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 (
    &lt;div&gt;
          &lt;button onClick={() =&gt; setIsOpen(!isOpen)}&gt;
                  {isOpen ? 'Collapse' : 'Expand'}
                        &lt;/button&gt;
                              {isOpen &amp;&amp; &lt;div&gt;{children}&lt;/div&gt;}
                                  &lt;/div&gt;
                                    );
                                    };
                                    
                                    export default Collapse;
                                    </code></pre><h2 id="item-0-6">拓展Collapse组件其它属性</h2><ul><li><code>accordion</code>:如果设置为true,我们将启用手风琴模式。在这种模式下,只有一个面板可以被展开。当一个新的面板被展开时,之前展开的面板将被关闭。</li><li><code>activeKey</code>:当前展开面板的key。如果我们处于手风琴模式,这将是一个字符串或null。如果我们不在手风琴模式,这将是一个字符串数组。</li><li><code>arrow</code>:自定义的箭头。如果是一个React节点,将自动为你添加旋转动画效果。如果是一个函数,它将接收一个参数,表示面板是否被展开,并返回一个React节点。</li><li><code>defaultActiveKey</code>:默认展开面板的key。它的类型与activeKey相同。</li><li><code>onChange</code>:它在面板切换时被触发。它接收一个参数,表示当前展开面板的key。它的类型与activeKey相同。</li></ul><pre><code class="jsx">import React, { useState, useEffect } from 'react';
                                    
                                    const Collapse = ({ children, forceRender, accordion, activeKey, arrow, defaultActiveKey, onChange }) =&gt; {
                                      const [isOpen, setIsOpen] = useState(false);
                                        const [currentActiveKey, setCurrentActiveKey] = useState(defaultActiveKey);
                                        
                                          useEffect(() =&gt; {
                                              setCurrentActiveKey(activeKey);
                                                }, [activeKey]);
                                                
                                                  const handleClick = () =&gt; {
                                                      setIsOpen(!isOpen);
                                                          if (accordion) {
                                                                setCurrentActiveKey(isOpen ? null : activeKey);
                                                                    }
                                                                        onChange &amp;&amp; onChange(isOpen ? null : activeKey);
                                                                          };
                                                                          
                                                                            const renderArrow = () =&gt; {
                                                                                if (typeof arrow === 'function') {
                                                                                      return arrow(isOpen);
                                                                                          }
                                                                                              return arrow;
                                                                                                };
                                                                                                
                                                                                                  return (
                                                                                                      &lt;div&gt;
                                                                                                            &lt;button onClick={handleClick}&gt;
                                                                                                                    {isOpen ? 'Collapse' : 'Expand'}
                                                                                                                            {renderArrow()}
                                                                                                                                  &lt;/button&gt;
                                                                                                                                        &lt;div style={{ display: isOpen || forceRender ? 'block' : 'none' }}&gt;
                                                                                                                                                {children}
                                                                                                                                                      &lt;/div&gt;
                                                                                                                                                          &lt;/div&gt;
                                                                                                                                                            );
                                                                                                                                                            };
                                                                                                                                                            
                                                                                                                                                            export default Collapse;</code></pre><h2 id="item-0-7">实现Panel</h2><p>我们创建一个名为<code>Collapse.Panel</code>的子组件来支持这些新的属性。这个子组件将作为<code>Collapse</code>组件的一部分,用于表示一个可折叠的面板。</p><ul><li><code>arrow</code>:这是一个自定义的箭头。如果这是一个React节点,antd-mobile将自动为你添加旋转动画效果。如果这是一个函数,它将接收一个参数,表示面板是否被展开,并返回一个React节点。</li><li><code>destroyOnClose</code>:如果设置为true,我们将在面板关闭时销毁它的内容。</li><li><code>disabled</code>:如果设置为true,我们将禁用面板,使其不能被打开或关闭。</li><li><code>forceRender</code>:如果设置为true,我们将在面板关闭时仍然渲染它的DOM结构。</li><li><code>key</code>:panel的唯一标识符。</li><li><code>onClick</code>:它在面板的标题栏被点击时被触发。它接收一个参数,表示点击事件。</li><li><code>title</code>:panel标题栏的内容。</li></ul><pre><code class="jsx">import React, { useState, useEffect } from 'react';
                                                                                                                                                            
                                                                                                                                                            const Panel = ({ children, arrow, destroyOnClose, disabled, forceRender, key, onClick, title }) =&gt; {
                                                                                                                                                              const [isOpen, setIsOpen] = useState(false);
                                                                                                                                                              
                                                                                                                                                                const handleClick = (event) =&gt; {
                                                                                                                                                                    if (disabled) return;
                                                                                                                                                                        setIsOpen(!isOpen);
                                                                                                                                                                            onClick &amp;&amp; onClick(event);
                                                                                                                                                                              };
                                                                                                                                                                              
                                                                                                                                                                                const renderArrow = () =&gt; {
                                                                                                                                                                                    if (typeof arrow === 'function') {
                                                                                                                                                                                          return arrow(isOpen);
                                                                                                                                                                                              }
                                                                                                                                                                                                  return arrow;
                                                                                                                                                                                                    };
                                                                                                                                                                                                    
                                                                                                                                                                                                      useEffect(() =&gt; {
                                                                                                                                                                                                          if (destroyOnClose &amp;&amp; !isOpen) {
                                                                                                                                                                                                                children = null;
                                                                                                                                                                                                                    }
                                                                                                                                                                                                                      }, [isOpen]);
                                                                                                                                                                                                                      
                                                                                                                                                                                                                        return (
                                                                                                                                                                                                                            &lt;div key={key}&gt;
                                                                                                                                                                                                                                  &lt;button onClick={handleClick}&gt;
                                                                                                                                                                                                                                          {title}
                                                                                                                                                                                                                                                  {renderArrow()}
                                                                                                                                                                                                                                                        &lt;/button&gt;
                                                                                                                                                                                                                                                              &lt;div style={{ display: isOpen || forceRender ? 'block' : 'none' }}&gt;
                                                                                                                                                                                                                                                                      {children}
                                                                                                                                                                                                                                                                            &lt;/div&gt;
                                                                                                                                                                                                                                                                                &lt;/div&gt;
                                                                                                                                                                                                                                                                                  );
                                                                                                                                                                                                                                                                                  };
                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                  const Collapse = ({ children, accordion, activeKey, defaultActiveKey, onChange }) =&gt; {
                                                                                                                                                                                                                                                                                  };
                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                  Collapse.Panel = Panel;
                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                  export default Collapse;</code></pre><h2 id="item-0-8">forceRender属性</h2><blockquote>我们要添加一个名为forceRender的属性。如果这个属性被设置为true,我们会在组件隐藏时仍然渲染DOM结构,如果面板渲染的数据量比较大,<code>这个属性特别有用,不会造成打开的时候会卡顿一下</code></blockquote><pre><code class="jsx">import React, { useState } from 'react';
                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                  const Collapse = ({ children, forceRender }) =&gt; {
                                                                                                                                                                                                                                                                                    const [isOpen, setIsOpen] = useState(false);
                                                                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                                                                      return (
                                                                                                                                                                                                                                                                                          &lt;div&gt;
                                                                                                                                                                                                                                                                                                &lt;button onClick={() =&gt; setIsOpen(!isOpen)}&gt;
                                                                                                                                                                                                                                                                                                        {isOpen ? 'Collapse' : 'Expand'}
                                                                                                                                                                                                                                                                                                              &lt;/button&gt;
                                                                                                                                                                                                                                                                                                                    &lt;div style={{ display: isOpen || forceRender ? 'block' : 'none' }}&gt;
                                                                                                                                                                                                                                                                                                                            {children}
                                                                                                                                                                                                                                                                                                                                  &lt;/div&gt;
                                                                                                                                                                                                                                                                                                                                      &lt;/div&gt;
                                                                                                                                                                                                                                                                                                                                        );
                                                                                                                                                                                                                                                                                                                                        };
                                                                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                                                        export default Collapse;
                                                                                                                                                                                                                                                                                                                                        ````
                                                                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                                                        ## 实现折叠面板动画
                                                                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                                                        ### height方式实现
                                                                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                                                        </code></pre><p>.collapse-panel {<br>  border: 1px solid #ddd;<br>  border-radius: 4px;<br>  margin-bottom: 10px;<br>  overflow: hidden;<br>}</p><p>.collapse-panel-button {<br>  background-color: #f5f5f5;<br>  color: #333;<br>  cursor: pointer;<br>  padding: 10px 15px;<br>  width: 100%;<br>  text-align: left;<br>  border: none;<br>  outline: none;<br>}</p><p>.collapse-panel-content {<br>  padding: 10px 15px;<br>  background-color: white;<br>  overflow: hidden;<br>  max-height: 0;<br>  transition: max-height 0.2s ease-out;<br>}</p><p>.collapse-panel-content.open {<br>  max-height: 100vh;<br>}</p><p>import React, { useState, useEffect, useRef } from 'react';</p><p>const Panel = ({ children, arrow, destroyOnClose, disabled, forceRender, key, onClick, title }) =&gt; {<br>  const [isOpen, setIsOpen] = useState(false);<br>  const contentRef = useRef(null);</p><p>const handleClick = (event) =&gt; {</p><pre><code>if (disabled) return;
                                                                                                                                                                                                                                                                                                                                        setIsOpen(!isOpen);
                                                                                                                                                                                                                                                                                                                                        onClick &amp;&amp; onClick(event);</code></pre><p>};</p><p>const renderArrow = () =&gt; {</p><pre><code>if (typeof arrow === 'function') {
                                                                                                                                                                                                                                                                                                                                          return arrow(isOpen);
                                                                                                                                                                                                                                                                                                                                          }
                                                                                                                                                                                                                                                                                                                                          return arrow;</code></pre><p>};</p><p>useEffect(() =&gt; {</p><pre><code>if (destroyOnClose &amp;&amp; !isOpen) {
                                                                                                                                                                                                                                                                                                                                            children = null;
                                                                                                                                                                                                                                                                                                                                            }</code></pre><p>}, [isOpen]);</p><p>useEffect(() =&gt; {</p><pre><code>contentRef.current.style.maxHeight = isOpen ? `${contentRef.current.scrollHeight}px` : '0';</code></pre><p>}, [isOpen]);</p><p>return (</p><pre><code>&lt;div key={key} className="collapse-panel"&gt;
                                                                                                                                                                                                                                                                                                                                              &lt;button onClick={handleClick} className="collapse-panel-button"&gt;
                                                                                                                                                                                                                                                                                                                                                  {title}
                                                                                                                                                                                                                                                                                                                                                      {renderArrow()}
                                                                                                                                                                                                                                                                                                                                                        &lt;/button&gt;
                                                                                                                                                                                                                                                                                                                                                          &lt;div ref={contentRef} className={`collapse-panel-content ${isOpen ? 'open' : ''}`}&gt;
                                                                                                                                                                                                                                                                                                                                                              {children}
                                                                                                                                                                                                                                                                                                                                                                &lt;/div&gt;
                                                                                                                                                                                                                                                                                                                                                                &lt;/div&gt;</code></pre><p>);<br>};</p><p>// ...</p><pre><code>
                                                                                                                                                                                                                                                                                                                                                                完整效果:
                                                                                                                                                                                                                                                                                                                                                                [jcode](https://code.juejin.cn/pen/7254521650341740583)
                                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                                ### 其它方式
                                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                                &gt; 上面手风琴效果是利用height的实现,这种实现会触发重排,所以感兴趣的同学可以考虑其它方式优化一下
                                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                                - 基于scaleY? 感觉不现实
                                                                                                                                                                                                                                                                                                                                                                - 使用FLIP技术添加动画优化? 搜了一圈,更难实现?
                                                                                                                                                                                                                                                                                                                                                                </code></pre>