电影是由一个个帧构成,播放的过程就是帧的替换。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
组件封装相关的的思路就结束啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦。。。。