typescript + react 項目開發體驗之 react

目錄

總體框架的理解

​ 電影是由一個個幀構成,播放的過程就是幀的替換。react 官方文檔元素渲染模塊有句這樣的話:React 元素都是不可變的。當元素被建立以後,你是沒法改變其內容或屬性的。一個元素就好像是動畫裏的一幀,它表明應用界面在某一時間點的樣子。node

​ 結合實際去理解,就是當頁面構建,網頁就造成了初始幀,網頁上的展示內容是由state去控制,react能夠經過setState經過改變狀態去作幀的切換,由此實現頁面展示效果的變化。爲了提高性能,並儘量快且正確的切換幀,react作了以下優化:react

  1. 異步的setState。
  2. 採用diff算法,儘量快的進行元素比較,並找到相應元素替換。
  3. 引用fiber算法,減小深層級組件對網頁響應的影響。

react中使用ts

  1. 組件定義
//咱們能夠在 /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文件,這個多是最好的文檔。算法

  1. 視圖的拆分

咱們在組件的書寫過程當中,因爲展現視圖過於複雜,render函數的時候經常會遇到大段大段的dom和一些複雜的條件渲染。若是把他們都放在同一個函數下會形成組件代碼太長,不利於維護,所以拆分就頗有必要了。我經常用到如下幾種拆分方式:typescript

// 拆分紅組件,把大組件拆分紅多個小組件,這時候難免一些方法的傳遞,爲了使得 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

在手機端業務中當有長列表,經常須要逐步加載相應的須要展示的內容。頁面滾動至底部加載就是其中一個策略。接下來咱們就來說這個功能進行抽離實現組件化。小程序

滾動至底部加載咱們能夠把這個邏輯進行拆分一下。後端

  1. 監聽滾動條事件的監聽。
  2. 數據加載策略。
  3. 具體列表內容的展示。

作過微信小程序的同窗可能記得它提供一個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);
}
複製代碼

我如今想把這個功能抽離,目前有兩種思路:微信

  1. 定義一個具備着三個方法的類,經過 extends 使得現有Component也能具備這個三個方法。
  2. 定義一個接收一個classComponent(具有一個鉤子scrollInBottom)的函數,返回一個組件,這個組件進行滾動監聽,當滾動到底部的時候調用classComponent 的component方法(這也就是我們常說的HOC)。

第一種方法有個很吃癟的地方,就是當前組件若是定義了這三個方法時,會覆蓋extends的方法,使得功能失效,須要額外的super操做,這裏就不細說了。因而我毅然決然的選擇了第二種方式。app

// 咱們來根據第二種思路來描述這個方法
// 定義一個具備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>()=> scrollBottomLoad<
    ILoadDataAndCheckMoreProps<T, K>,
    ILoadDataAndCheckMoreState<T, K>
    >(LoadDataAndCheckMore)
// 咱們回看一下scrollBottomLoad方法。
	<T extends object,S = {}>(WrapComponent: IHasScrollBottomClass<T, S>)
// 若是直接返回scrollBottomLoad(LoadDataAndCheckMore),這個T和S會被當成簡單的{}。這樣就會形成ILoadDataAndCheckMoreProps、ILoadDataAndCheckMoreState的泛型T/K就是空對象,顯然是不正確的。

複製代碼

就此我的對react組件封裝相關的的思路就結束啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦。。。。

相關文章
相關標籤/搜索