目錄
整體框架的理解
電影是由一個個幀構成,播放的過程就是幀的替換。react
官方文件元素渲染模組有句這樣的話:React 元素都是不可變的。當元素被建立之後,你是無法改變其內容或屬性的。一個元素就好像是動畫裡的一幀,它代表應用介面在某一時間點的樣子。
結合實際去理解,就是當頁面構建,網頁就形成了初始幀,網頁上的展現內容是由state
去控制,react
可以通過setState通過改變狀態去做幀的切換,由此實現頁面展現效果的變化。為了提升效能,並儘可能快且正確的切換幀,react做了如下優化:
- 非同步的setState。
- 採用diff演算法,儘可能快的進行元素比較,並找到相應元素替換。
- 引用fiber演算法,減少深層級元件對網頁響應的影響。
react中使用ts
- 元件定義
//我們可以在 /node_modules/@type/React 找到有關Component的定義,我們擷取部分
interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }
class Component<P, S> {
constructor(props: Readonly<P>);
state: Readonly<S>;
// ....
}
// 可以看出定義一個元件可以通過定義泛型去指定他的props和state型別
interface IDemoProps{
a: string,
b: string
}
interface IDemoState{
c: string,
d: string
}
// 當你定義一個元件時定義了State型別,你至少以下列一種方式指定state屬性,否則會報錯;
class Demo extends Component<IDemoProps, IDemoState>{
// 方式一
state={
c: 1,
d: 2
}
constructor(props: IDemoProps){
super(props);
// 方式二
this.state = {
c: 'c',
d: 'd'
}
props.c // 報錯,屬性不存在
}
render(){
return <>
<div>{this.state.c}</div>
<div>{this.props.a}</div>
</>
}
}
// 當元件內部不需要使用生命週期鉤子,或者元件內不儲存自身狀態時,可簡化成函式式元件
// 同樣我們找到定義方式
interface FunctionComponent<P = {}> {
(props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;
// ...
}
const Demo = (props: IDemoProps)=>{
return <>
<div>{this.state.c}</div>
<div>{this.props.a}</div>
</>
}
// pureComponent 在寫元件的過程中同學會用到 pureComponent 去提升元件的效能
// 從描述上來看它和普通的Component沒有區別
class PureComponent<P = {}, S = {}, SS = any> extends Component<P, S, SS> { }
// 擷取一段 pureComponent 原始碼進行比較發現他就是當元件不存在shouldComponetUpdate這個鉤子時,會自己新增一條規則即前後 props 和 state 進行淺比較判斷是否需要修改元件。
if (inst.shouldComponentUpdate) {
shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
} else {
if (this._compositeType === CompositeType.PureClass) {
// 用 shallowEqual 對比 props 和 state 的改動
// 如果都沒改變就不用更新
shouldUpdate =
!shallowEqual(prevProps, nextProps) ||
!shallowEqual(inst.state, nextState);
}
}
//在 /node_modules/@type/React 中還可以找到這些,我可以理解成這是component的介面宣告方式
interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
new (props: P, context?: any): Component<P, S>;
// ....
}
// sfc fc StatelessComponent 都是 FunctionComponent 的別名。
type ComponentState = any;
type SFC<P = {}> = FunctionComponent<P>;
type StatelessComponent<P = {}> = FunctionComponent<P>;
type FC<P = {}> = FunctionComponent<P>;
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
// 因此當我們以元件作為引數傳遞的時候可以使用ComponentType進行宣告
複製程式碼
ps: 當我們用第三方庫的時候,不妨看看他們的d.ts檔案,這個可能是最好的文件。
- 檢視的拆分
我們在元件的書寫過程中,由於展示檢視過於複雜,render函式的時候常常會遇到大段大段的dom和一些複雜的條件渲染。如果把他們都放在同一個函式下會造成元件程式碼太長,不利於維護,因此拆分就很有必要了。我常常用到以下幾種拆分方式:
// 拆分成元件,把大元件拆分成多個小元件,這時候不免一些方法的傳遞,為了使得 this 繫結當前元件,定義方法時可以採用下面switchItemHandler這種方式,避免再進行一個bind。
//...component dosomthing....
switchItemHandler = (checkItem:checkedItemData)=>{
this.setState...
}
render(){
return <>
<TabHead
switchItemHandler={this.switchItemHandler}
// ...
></TabHead>
<MiddleBar
//....
></MiddleBar>
<ScrollEndLoadComponent
//....
></ScrollEndLoadComponent>
</>
}
// 有時候逐個引數傳遞太麻煩了我們可以定義一個返回jsxElement的函式,然後通過call去呼叫,注意,使用bind、call、apply的函式,在ts中的定義,需要在形參中加入this的型別定義。
export const comfirm = function(this: GoodsBuy){
return <div className="flex-bottom">
<div className="button-fill-large" onClick={this.createOrder}>
確認兌換
</div>
</div>
}
// class GoodsBuy
render(){
return <div className="goodsBuy">
{goodsAndCount.call(this)}
{total.call(this)}
{comfirm.call(this)}
</div>
}
複製程式碼
一個元件說說HOC 、 props render
在手機端業務中當有長列表,常常需要逐步載入相應的需要展現的內容。頁面滾動至底部載入就是其中一個策略。接下來我們就來講這個功能進行抽離實現元件化。
滾動至底部載入我們可以把這個邏輯進行拆分一下。
- 監聽滾動條事件的監聽。
- 資料載入策略。
- 具體列表內容的展現。
做過微信小程式的同學可能記得它提供一個onReachBottom
上拉觸底的鉤子,參照這個設計思路我希望我定義元件時,加入一個鉤子,在滾動到底部的時候這個鉤子會被呼叫。
// component
scrollInBottom = ()=>{
// do something..
}
複製程式碼
通常情況下我們需要去做監聽呼叫。
scrollBottomHandler = ()=>{
if(document.body.scrollHeight - document.body.scrollTop < innerHeight + 10)
this.scrollInBottom();
}
componentDidMount(){
document.addEventListener('scroll',this.scrollBottomHandler)
}
componentWillUnmount(){
document.removeEventListener('scroll', this.scrollBottomHandler);
}
複製程式碼
我現在想把這個功能抽離,目前有兩種思路:
- 定義一個具有著三個方法的類,通過
extends
使得現有Component
也能具有這個三個方法。 - 定義一個接收一個
classComponent
(具備一個鉤子scrollInBottom
)的函式,返回一個元件,這個元件進行滾動監聽,當滾動到底部的時候呼叫classComponent
的component方法(這也就是我們們常說的HOC)。
第一種方法有個很吃癟的地方,就是當前元件如果定義了這三個方法時,會覆蓋extends
的方法,使得功能失效,需要額外的super
操作,這裡就不細說了。於是我毅然決然的選擇了第二種方式。
// 我們來根據第二種思路來描述這個方法
// 定義一個具有scrollInBottom:()=>void函式作為屬性的react元件
type IComponet = {scrollInBottom: ()=>void} & Component;
// 定義一個能獲取ref,例項化後能生成具有scrollInBottom的元件。
interface IHasScrollBottomClass<P = {}, S = ComponentState> extends ComponentClass<P, S>{
new (props: P, context?: any): IComponet
}
// 接下來就是上面思路2的描述了,就不贅述啦。
const scrollBottomLoad = <T extends object,S = {}>(WrapComponent: IHasScrollBottomClass<T, S>)=>{
return class extends Component<T>{
subRef: IComponet | null = null
scrollBottomHandler = ()=>{
if(!this.subRef)return;
if(document.body.scrollHeight - document.body.scrollTop < innerHeight + 10)
this.subRef.scrollInBottom();
}
componentDidMount(){
document.addEventListener('scroll',this.scrollBottomHandler)
}
componentWillUnmount(){
document.removeEventListener('scroll', this.scrollBottomHandler);
}
render(){
return <WrapComponent
ref={cp=> this.subRef = cp}
{...this.props}
></WrapComponent>
}
}
}
複製程式碼
至此,滾動事件監聽功能就已經抽離出來了。接下來我們要抽離載入和具體內容展示。我們碼上說話
interface QueryListModel{
start: number;
limit: number;
}
// 定義元件接收3個引數
interface ILoadDataAndCheckMoreProps<T, K> {
loadFuc: (queryCondition: T & QueryListModel)=>Promise<K[]>;//資料載入函式
queryCondition: T;// 除基礎模型歪的查詢條件
render: (props: K[])=>ReactElement<{ list: K[]}>; // 渲染列表的方法
}
// 本地相關state分別儲存
interface ILoadDataAndCheckMoreState<T, K>{
noMore: boolean // 是否還有資料
queryConditionCombin: T & QueryListModel // 條件列表
list: K[] // 資料列表
}
const ANYTIME_REQUEST_ITEMNUMBER = 10; // 每次請求他的條數
class LoadDataAndCheckMore<T extends object, K extends object> extends Component<ILoadDataAndCheckMoreProps<T, K>, ILoadDataAndCheckMoreState<T, K>> {
constructor(props:ILoadDataAndCheckMoreProps<T, K>){
super(props);
// 初始化3個狀態
this.state = {
noMore: false,
queryConditionCombin: this.initQueryCondition(props.queryCondition),
list: []
}
}
// 初始化狀態,為啥這裡要是負數呢?往下看。
initQueryCondition = (props: T)=>{return Object.assign({}, props, {start: -ANYTIME_REQUEST_ITEMNUMBER, limit: 10 })}
// 資料載入
loadMore = ()=>{
// 如果沒有資料了,不再載入
if(this.state.noMore)return;
// 這就是上面為什麼start要為負數
this.state.queryConditionCombin.start += ANYTIME_REQUEST_ITEMNUMBER;
// 每次請求之後,並檢查還沒有更多。
this.loadListAndCheckNoMore().then((data: K[])=>{
this.setState({
list: this.state.list.concat(data)
})
Toast.hide();
})
// loading相關
Toast.loading('資料載入中....', 3000)
}
loadListAndCheckNoMore = ()=>{
// 判斷條件是取得的資料數量,小於limit。為啥要這樣,因為後端沒有返回這個欄位給我,我就只能這樣判斷咯。
return this.props.loadFuc(this.state.queryConditionCombin).then((data:K[])=>{
this.setState({
noMore: data && data.length < this.state.queryConditionCombin.limit
})
return data;
})
}
// 你懂得,哈哈哈哈哈哈哈哈哈嗝。
scrollInBottom = ()=>{
!this.state.noMore && this.loadMore();
}
// 當搜尋條件變化之後,是不是要從0開始載入呢?
componentWillReceiveProps(nextProps:ILoadDataAndCheckMoreProps<T, K>){
this.setState({
queryConditionCombin: this.initQueryCondition(nextProps.queryCondition),
noMore: false,
list: []
},this.loadMore)
}
// 第一次載入資料喔。
componentDidMount(){
this.loadMore()
}
render(){
// 我期望渲染的方式交由業務層面
return <>
// 為什麼要用render作為函式傳遞呢?因為如果寫成元件你需要
// const Cp = this.props.render
// <Cp list={this.state.list}/>
// 這就有點狠難受了,因為props我就想傳個陣列,但是元件不支援啊,因為他要個物件。
// 然後你需要重新把他拎出來,根據元件的命名規範,才能重新使用
{this.props.render(this.state.list)}
// 提示提示提示咯
<p className="loadingTips">{this.state.noMore ? '這裡見底啦/(ㄒoㄒ)/~~...' : '資料載入中,請稍後...'}</p>
</>
}
}
// 最後是匯出,為啥要這麼寫呢?
export default <T, K>()=> scrollBottom<
ILoadDataAndCheckMoreProps<T, K>,
ILoadDataAndCheckMoreState<T, K>
>(LoadDataAndCheckMore)
// 我們回看一下scrollBottom方法。
<T extends object,S = {}>(WrapComponent: IHasScrollBottomClass<T, S>)
// 如果直接返回scrollBottom(LoadDataAndCheckMore),這個T和S會被當成簡單的{}。這樣就會造成ILoadDataAndCheckMoreProps、ILoadDataAndCheckMoreState的泛型T/K就是空物件,顯然是不正確的。
複製程式碼
就此個人對react
元件封裝相關的的思路就結束啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦。。。。