开发过程中经常会有联级选择的场景,利用antd的组件可以方便的实现相关功能。知其然更要知其所以然,这边来分析一下相关源码,了解一下实现过程。
实现的功能如下:(应用也很方便,相关传参见antd官网https://3x.ant.design/components/cascader-cn/)
源码分析如下:
const Cascader = React.forwardRef((props: CascaderProps<any>, ref: React.Ref<CascaderRef>) => { return ( // 返回RcCascader组件 <RcCascader prefixCls={prefixCls} className={classNames( !customizePrefixCls && cascaderPrefixCls, { [`${prefixCls}-lg`]: mergedSize === 'large', [`${prefixCls}-sm`]: mergedSize === 'small', [`${prefixCls}-rtl`]: isRtl, [`${prefixCls}-borderless`]: !bordered, [`${prefixCls}-in-form-item`]: isFormItemInput, }, getStatusClassNames(prefixCls, mergedStatus, hasFeedback), className, )} ....... /> })
// import RcCascader from 'rc-cascader';
下载地址: https://github.com/react-component/cascader
// https://github.com/react-component/cascader/ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, ref) => { const { // MISC id, prefixCls = 'rc-cascader', fieldNames, // Value defaultValue, value, changeOnSelect, onChange, displayRender, checkable, // Search searchValue, onSearch, showSearch, // Trigger expandTrigger, // Options options, dropdownPrefixCls, loadData, // Open popupVisible, open, popupClassName, dropdownClassName, dropdownMenuColumnStyle, popupPlacement, placement, onDropdownVisibleChange, onPopupVisibleChange, // Icon expandIcon = '>', loadingIcon, // Children children, dropdownMatchSelectWidth = false, showCheckedStrategy = SHOW_PARENT, ...restProps } = props; .............. return ( <CascaderContext.Provider value={cascaderContext}> // 返回的BaseSelect的select类型 <BaseSelect {...restProps} // MISC ref={ref as any} id={mergedId} ............. getRawInputElement={() => children} /> </CascaderContext.Provider> }
其中的一些细节处理:
showSearch配置:
const [mergedShowSearch, searchConfig] = useSearchConfig(showSearch); // 返回的mergedShowSearch表示showSearch表示是否可查询 // showSearch为对象时,用于筛选后的选项配置 (filter, limit, matchInputWidth, render, sort) // useSearchConfig export default function useSearchConfig(showSearch?: CascaderProps['showSearch']) { return React.useMemo<[boolean, ShowSearchType]>(() => { if (!showSearch) { // 没配置时 返回显示默认值 return [false, {}]; } let searchConfig: ShowSearchType = { matchInputWidth: true, limit: 50, // }; if (showSearch && typeof showSearch === 'object') { searchConfig = { ...searchConfig, ...showSearch, }; } } // 返回配置 return [true, searchConfig]; }, [showSearch]); }
searchOptions选项配置:
const searchOptions = useSearchOptions( mergedSearchValue, mergedOptions, // 默认的options选项 mergedFieldNames, // 自定义options中label name children的字段{ label: 'label', value: 'value', children: 'children' } dropdownPrefixCls || prefixCls, searchConfig, // showSearch的配置选项 changeOnSelect, // props-> changeOnSelect ); // mergedFieldNames const mergedFieldNames = React.useMemo( () => fillFieldNames(fieldNames), [JSON.stringify(fieldNames)], ); export function fillFieldNames(fieldNames?: FieldNames): InternalFieldNames { const { label, value, children } = fieldNames || {}; const val = value || 'value'; return { // 替换原值 label: label || 'label', value: val, key: val, children: children || 'children', }; } // useSearchOptions return React.useMemo(() => { const filteredOptions: DefaultOptionType[] = []; if (!search) { return []; } function dig(list: DefaultOptionType[], pathOptions: DefaultOptionType[]) { list.forEach(option => { // Perf saving when `sort` is disabled and `limit` is provided if (!sort && limit > 0 && filteredOptions.length >= limit) { return; } const connectedPathOptions = [...pathOptions, option]; const children = option[fieldNames.children]; // If current option is filterable if ( // If is leaf option !children || children.length === 0 || // If is changeOnSelect changeOnSelect ) { //filter // const defaultFilter: ShowSearchType['filter'] = (search, options, { label }) => // options.some(opt => String(opt[label]).toLowerCase().includes(search.toLowerCase())); if (filter(search, connectedPathOptions, { label: fieldNames.label })) { filteredOptions.push({ ...option, [fieldNames.label as 'label']: render( //默认渲染 search, connectedPathOptions, prefixCls, fieldNames, ), [SEARCH_MARK]: connectedPathOptions, }); } } if (children) { //if children 递归循环 dig(option[fieldNames.children] as DefaultOptionType[], connectedPathOptions); } }); } dig(options, []); // Do sort if (sort) { filteredOptions.sort((a, b) => { return sort(a[SEARCH_MARK], b[SEARCH_MARK], search, fieldNames); }); } return limit > 0 ? filteredOptions.slice(0, limit as number) : filteredOptions; }, [search, options, fieldNames, prefixCls, render, changeOnSelect, filter, sort, limit]);
// https://github.com/react-component/select
<SelectContext.Provider value={selectContext}> <BaseSelect {...restProps} // >>> MISC id={mergedId} prefixCls={prefixCls} ref={ref} omitDomProps={OMIT_DOM_PROPS} mode={mode} // >>> Values displayValues={displayValues} onDisplayValuesChange={onDisplayValuesChange} // >>> Search searchValue={mergedSearchValue} onSearch={onInternalSearch} autoClearSearchValue={autoClearSearchValue} onSearchSplit={onInternalSearchSplit} dropdownMatchSelectWidth={dropdownMatchSelectWidth} // >>> OptionList OptionList={OptionList} emptyOptions={!displayOptions.length} // >>> Accessibility activeValue={activeValue} activeDescendantId={`${mergedId}_list_${accessibilityIndex}`} /> </SelectContext.Provider>
节点的渲染:
if (customizeRawInputElement) { renderNode = selectorNode; } else { renderNode = ( <div className={mergedClassName} {...domProps} ref={containerRef} onm ouseDown={onInternalMouseDown} onKeyDown={onInternalKeyDown} onKeyUp={onInternalKeyUp} onFocus={onContainerFocus} onBlur={onContainerBlur} > {mockFocused && !mergedOpen && ( <span style={{ width: 0, height: 0, position: 'absolute', overflow: 'hidden', opacity: 0, }} aria-live="polite" > {/* Merge into one string to make screen reader work as expect */} {`${displayValues .map(({ label, value }) => // select的值是number或string类型? ['number', 'string'].includes(typeof label) ? label : value, ) .join(', ')}`} </span> )} {selectorNode} {arrowNode} {clearNode} </div> ); } // >>> Selector (selectorNode ) const selectorNode = ( <SelectTrigger .......... > {customizeRawInputElement ? ( React.cloneElement(customizeRawInputElement, { ref: customizeRawInputRef, }) ) : ( <Selector {...props} .......... /> )} </SelectTrigger> ); // SelectTrigger <Trigger // import Trigger from 'rc-trigger'; {...restProps} ............ > {children} </Trigger> // Selector const selectNode = // 多选 mode === 'multiple' || mode === 'tags' ? ( <MultipleSelector {...props} {...sharedProps} /> ) : ( // 单选 <SingleSelector {...props} {...sharedProps} /> ); return ( <div ref={domRef} className={`${prefixCls}-selector`} onClick={onClick} onm ouseDown={onMouseDown} > {selectNode} </div> );
SingleSelector(单选选择框)
<> <span className={`${prefixCls}-selection-search`}> // input上的输入框 <Input // 另外的封装 ref={inputRef} prefixCls={prefixCls} id={id} open={open} inputElement={inputElement} disabled={disabled} //是否可编辑,mode === 'combobox' || showSearch autoFocus={autoFocus} autoComplete={autoComplete} editable={inputEditable} activeDescendantId={activeDescendantId} value={inputValue} onKeyDown={onInputKeyDown} onm ouseDown={onInputMouseDown} onChange={(e) => { setInputChanged(true); onInputChange(e as any); // props的onInputChange }} onPaste={onInputPaste} // onCompositionStart和onCompositionEnd 主要避免中英文输入问题 onCompositionStart={onInputCompositionStart} onCompositionEnd={onInputCompositionEnd} tabIndex={tabIndex} attrs={pickAttrs(props, true)} maxLength={combobox ? maxLength : undefined} /> </span> {/* Display value */} // 显示展示的值 {!combobox && item && !hasTextInput && ( <span className={`${prefixCls}-selection-item`} title={title}> {item.label} </span> )} {/* Display placeholder */} // 显示placeholder {renderPlaceholder()} </>
其中 SingleSelector 和 MultipleSelector都是上面的输入框
SelectTrigger是外面的包裹层(代码分析如下:)
<Trigger {...restProps} ................... > {children} </Trigger> // Trigger
// 分析下面的Trigger代码(https://github.com/react-component/trigger.git)
export function generateTrigger( // 入参: Portal PortalComponent: any, ): React.ComponentClass<TriggerProps> { class Trigger extends React.Component<TriggerProps, TriggerState> { constructor() { super(props); // 初始化popupVisible,popup的显示与否 let popupVisible: boolean; if ('popupVisible' in props) { popupVisible = !!props.popupVisible; } else { popupVisible = !!props.defaultPopupVisible; } this.state = { prevPopupVisible: popupVisible, popupVisible, }; // 注册事件 ALL_HANDLERS.forEach((h) => { this[`fire${h}`] = (e) => { this.fireEvents(h, e); }; }); //////////////////////////////////////////////////////////////////// componentDidUpdate() {} componentWillUnmount() { // 清除一些定时器操作 this.clearDelayTimer(); this.clearOutsideHandler(); clearTimeout(this.mouseDownTimeout); raf.cancel(this.attachId); } render() { const { popupVisible } = this.state; const { children, forceRender, alignPoint, className, autoDestroy } = this.props; const child = React.Children.only(children) as React.ReactElement; const newChildProps: HTMLAttributes<HTMLElement> & { key: string } = { key: 'trigger', }; // ============================== Visible Handlers ============================== // >>> ContextMenu if (this.isContextMenuToShow()) { newChildProps.onContextMenu = this.onContextMenu; } else { newChildProps.onContextMenu = this.createTwoChains('onContextMenu'); } // >>> Click if (this.isClickToHide() || this.isClickToShow()) { newChildProps.onClick = this.onClick; newChildProps.onMouseDown = this.onMouseDown; newChildProps.onTouchStart = this.onTouchStart; } else { newChildProps.onClick = this.createTwoChains('onClick'); newChildProps.onMouseDown = this.createTwoChains('onMouseDown'); newChildProps.onTouchStart = this.createTwoChains('onTouchStart'); } // >>> Hover(enter) if (this.isMouseEnterToShow()) { newChildProps.onMouseEnter = this.onMouseEnter; // Point align if (alignPoint) { newChildProps.onMouseMove = this.onMouseMove; } } else { newChildProps.onMouseEnter = this.createTwoChains('onMouseEnter'); } // >>> Hover(leave) if (this.isMouseLeaveToHide()) { newChildProps.onMouseLeave = this.onMouseLeave; } else { newChildProps.onMouseLeave = this.createTwoChains('onMouseLeave'); } // >>> Focus if (this.isFocusToShow() || this.isBlurToHide()) { newChildProps.onFocus = this.onFocus; newChildProps.onBlur = this.onBlur; } else { newChildProps.onFocus = this.createTwoChains('onFocus'); newChildProps.onBlur = this.createTwoChains('onBlur'); } // =================================== Render =================================== const childrenClassName = classNames( child && child.props && child.props.className, className, ); if (childrenClassName) { newChildProps.className = childrenClassName; } const cloneProps: any = { ...newChildProps, }; if (supportRef(child)) { cloneProps.ref = composeRef(this.triggerRef, (child as any).ref); } const trigger = React.cloneElement(child, cloneProps); let portal: React.ReactElement; // prevent unmounting after it's rendered if (popupVisible || this.popupRef.current || forceRender) { portal = ( //////import Popup from './Popup'; <PortalComponent key="portal" getContainer={this.getContainer} didUpdate={this.handlePortalUpdate} > {this.getComponent()} </PortalComponent> ); } if (!popupVisible && autoDestroy) { portal = null; } return ( <TriggerContext.Provider value={this.triggerContextValue}> {trigger} // React.cloneElement(child, cloneProps) {portal} // <PortalComponent //传入的参数 // key="portal" // getContainer={this.getContainer} // didUpdate={this.handlePortalUpdate} // > // {this.getComponent()} // </PortalComponent> </TriggerContext.Provider> ); } // {this.getComponent()} return ( <Popup ................. onClick={onPopupClick} > // 上面select的popup props // let popupNode = popupElement; // if (dropdownRender) { // popupNode = dropdownRender(popupElement); // } {typeof popup === 'function' ? popup() : popup} </Popup> } } } // Popup组件 const popupNode: React.ReactNode = inMobile ? ( <MobilePopupInner {...cloneProps} mobile={mobile} ref={ref} /> ) : ( <PopupInner {...cloneProps} ref={ref} /> ); // We can use fragment directly but this may failed some selector usage. Keep as origin logic return ( <div> <Mask {...cloneProps} /> {popupNode} // const popupNode: React.ReactNode = inMobile ? ( // <MobilePopupInner {...cloneProps} mobile={mobile} ref={ref} /> // ) : ( // <PopupInner {...cloneProps} ref={ref} /> // ); </div> ); //Mask (没扒了,盲猜就是遮罩框,应该就是点击外部就可以关闭) return ( <CSSMotion {...motion} visible={visible} removeOnLeave> {({ className }) => ( <div style={{ zIndex }} className={classNames(`${prefixCls}-mask`, className)} /> )} </CSSMotion> );
算了,就扒到这里吧,全是层层嵌套的引入,明天有时间自己来实现一下吧。
标签:const,newChildProps,label,React,源码,design,Cascader,return,children From: https://www.cnblogs.com/best-mll/p/16893332.html