1.什么是高阶组件?
参照高阶函数的定义:高阶函数是一个接受一个函数作为参数的,可以操作其他函数的函数,我们可以把高阶组件做这样一个定义:高阶组件是一个接受一个组件作为参数的,可以操作其他其他组件的组件。
2.如何实现一个高阶组件?
我们往往把高阶组件定义为一个函数组件,因为通过使用函数我们可以很方便接受一个组件作为原始组件,然后返回一个新的组件,在这个新组件中挂载渲染原始组件,同时传入所有的props和高阶组件中的state或者方法。此时,高阶组件作为原始组件的装饰器,在不修改原始组件的前提下添加了一些新的功能,给原始组件做了增强,这种模式往往叫做装饰器模式。
3.使用装饰器模式封装一个remoteInput组件
首先明确两个定义,木偶组件和智能组件,木偶组件是纯ui展示的组件没有任何逻辑,父组件传给它什么它就用什么,就像个提线木偶一样,智能组件负责数据的维护和逻辑的处理,往往会把一个行为相关的数据和方法封装到一个智能组件里,智能组件再把这些数据和方法通过props传入到下一个组件中,最后都传到木偶组件中,智能组件就像是操纵木偶的手。
我们会封装一下几个木偶组件:
FlodAble负责实现列表展开和收起的动画:
它的实现比较简单,通过改变transform的值来实现展开和收起。具体实现如下:
function FlodAble(props){
const {flod,children}=props
return (
<div
style={{transform:flod?'translate3d(0, 0, 0) scaleY(0)':'translate3d(0, 0, 0) scaleY(1)'}}
className={'flodAbleArea'}
>
{children}
</div>
)
}
export default FlodAble
使用translate3d(0, 0, 0)属性可以开启gpu加速,动画会更流畅。
flodAbleArea的样式为:
.flodAbleArea{
transition: all 0.3s;
transform-origin: 50% 0;
border-radius: 5px;
box-shadow:0 0 5px 1px grey;
padding: 10px;
position: absolute;
z-index: 99999 !important;
width: 100%;
background: #ffffff;
}
List组件负责列表呈现,主要分为两部分:
Item,接受text和onItemClick回调函数,负责呈现条目:
function Item (props) {
const {text,onItemClick}=props
return (
<li className={'list-item'} onClick={()=>{onItemClick(text)}}>{text}</li>
)
}
List:接受数据集合和管理Item:
function List(props){
const {dataList,onItemClick}=props
return (
<ul className={'list'}>
{
dataList.map((d,i)=>
<Item key={i} text={d} onItemClick={onItemClick}/>
)
}
</ul>
)
}
SearchInput负责响应用户的输入和调用接口:
Search直接使用antd的组件。
function SearchInput(props){
const {value,onChange,placeholder,onSearch}=props
return (
<Search value={value}
onChange={onChange}
placeholder={placeholder}
onSearch={onSearch}
/>
)
}
Selector用于管理木偶组件和分发props到相应的木偶组件:
由于Component已经是组件的实例且我们需要传递props所以使用React.cloneElement克隆实例的时候传入props。
function Selector(props){
let {value,onChange,placeholder, dataList,onItemClick,loading,width,flod,onSearch}=props
return (
<div style={{width}}>
{
props.children.map((Component,i)=>{
if(Component.type.name==='SearchInput'){
return React.cloneElement(Component,{
value,
onChange,
placeholder,
onSearch,
key:'SearchInput'
})
}
if(Component.type.name==='List'){
return <div className="ListWrap" key={'ListWrap'}>
<FlodAble flod={flod} >
<Spin spinning={loading} >
{
(function () {
if(dataList.length===0){
return loading?'拼命加载中。。。。。':'没有数据'
}else{
return React.cloneElement(Component,{
loading,
dataList,
onItemClick,
key:'List'
})
}
})()
}
</Spin>
</FlodAble>
</div>
}
}
)
}
</div>
)
}
接下来,我们根据业务封装几个智能组件:
AsnycDataDecorator用于执行获取数据的回调:
function AsnycDataDecorator(WrappedComponent){
function AsnycDataDecorator (props) {
const [dataList,setDataList]=useState([])
const [loading,setLoading]=useState(false)
function getData(value){
setLoading(true)
props.unfold()
new Promise((resolve,reject)=>{
props.fetchData(value,resolve,reject)
}).then((value)=>{setDataList(value);setLoading(false)})
.catch(((reason)=>{setLoading(false);setDataList([reason])}))
}
function clearData() {
setDataList([])
}
return (
<WrappedComponent
{ ...props}
loading={loading}
dataList={dataList}
getData={getData}
clearData={clearData}
/>
)
}
return AsnycDataDecorator
}
在获取数据的时候设置loading并且展开列表,请求完成后关闭loading并且把数据放到state中,
这个组件把loading、dataList、getData、clearData加入到数据流中。
FlodDecorator负责控制FlodAble的展开与收起:
function FlodDecorator(WrappedComponent){
function FlodDecorator(props) {
const [flod,setFlod]=useState(true)
useEffect(()=>{
window.addEventListener('click',()=>{
setFlod(true)
})
return function () {
window.removeEventListener('click')
}
},[])
function unfold(){
setFlod(false)
}
return (
<WrappedComponent
{ ...props}
flod={flod}
unfold={unfold}
/>
)
}
return FlodDecorator
}
我们在副作用函数中在window添加了一个事件监听,当用户点击页面时收起列表。
这个组件把flod、unfold加入到数据流中。
SearchInputDecorator负责控制input中的数据显示:
function SearchInputDecorator(WrappedComponent){
function SearchInputDecorator(props){
const [value,setValue]=useState('')
function onChange(e){
let value=e.target.value
setValue(value)
props.getData(value)
}
function onItemClick(value){
setValue(value)
props.clearData&&props.clearData()
}
return (
<WrappedComponent
{ ...props}
onChange={onChange}
onItemClick={onItemClick}
value={value}
/>
)
}
return SearchInputDecorator
}
在用户输入内容的时更新state并调用请求数据的回调函数,
在用户点击列表中的条目时更新state并且清除掉原理列表中的数据。
这个组件把onChange、onItemClick、value加入到数据流中。
组合木偶组件和智能组件:
const FinalComponent=compose(FlodDecorator,AsnycDataDecorator,SearchInputDecorator)(Selector)
function RemoteInput(props){
return (
<FinalComponent {...props}>
<SearchInput/>
<List/>
</FinalComponent>
)
}
我们使用compose函数把这些组件组合起来,关于compose可以看这里:简单实现compose
需要注意的是确保compose只会调用一次,如果写在了render等其他会多次调用场景时会实例化多次从而导致丢失实例,组件内的状态会丢失,而且会带来性能上的问题。
使用组件:
<RemoteInput
placeholder={'输入城市搜索'}
onSearch={(value) => { console.log(`onSearch: ${value}`) }}
fetchData={fetchData}
width={'500px'}
/>
我们在fetchData中使用timeout模拟向后台请求数据并做了一个简单的防抖:
function fetchData(value,resolve,reject){
let dataList=[]
clearTimeout(timeout)
timeout=setTimeout(()=>{
list.forEach((d)=>{
if(value.length>0&&d.indexOf(value)!==-1){
dataList.push(d)
}
})
resolve(dataList)
},1000)
}
到现在组件的封住就完成了。
4.总结
咋看一个这么简单组件搞出了这么多子组件出来,而且还一层套一层,头都大了,不如一把梭全写在一个组件里,省事直观方便。其实高阶组件的优点就如它的缺点一样直观:
1.增加代码的复用率,组件中的木偶组件和智能组件都可以复用到其他组件之中,一处修改四处生效。
2.增强了组件的可维护性,当要加入新的功能的时候只要添加新的智能组件就好了,不必对原来的组件 进行修改,如果一个组件被多个人多次修改之后会变得更加难以维护。
3.复杂组件可以根据用户的配置按需加载相应的功能和逻辑,避免一些用户不需要的代码加载进来。
最后放一下代码的地址:代码地址