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组件封装相关的的思路就结束啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦。。。。

相关文章
相关标签/搜索