乐趣区

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

点击在线浏览,体验更好 链接
古代 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 组件。这个组件须要有一个状态来追踪它是否被开展

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 技术增加动画优化?搜了一圈,更难实现?
退出移动版