電影是由一個個幀構成,播放的過程就是幀的替換。react
官方文檔元素渲染模塊有句這樣的話:React 元素都是不可變的。當元素被建立以後,你是沒法改變其內容或屬性的。一個元素就好像是動畫裏的一幀,它表明應用界面在某一時間點的樣子。node
結合實際去理解,就是當頁面構建,網頁就造成了初始幀,網頁上的展示內容是由state
去控制,react
能夠經過setState經過改變狀態去作幀的切換,由此實現頁面展示效果的變化。爲了提高性能,並儘量快且正確的切換幀,react作了以下優化:react
//咱們能夠在 /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和一些複雜的條件渲染。若是把他們都放在同一個函數下會形成組件代碼太長,不利於維護,所以拆分就頗有必要了。我經常用到如下幾種拆分方式: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>
}
複製代碼
在手機端業務中當有長列表,經常須要逐步加載相應的須要展示的內容。頁面滾動至底部加載就是其中一個策略。接下來咱們就來說這個功能進行抽離實現組件化。小程序
滾動至底部加載咱們能夠把這個邏輯進行拆分一下。後端
作過微信小程序的同窗可能記得它提供一個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
操做,這裏就不細說了。因而我毅然決然的選擇了第二種方式。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
組件封裝相關的的思路就結束啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦。。。。