react项目总结

本文章主要是源自实际项目开发项目的总结,一些思考是参考了看过的文章,做了一篇总结,demo是跑过的,可以放心食用。

目录

       一.组件通信

1.父组件向子组件通信

2.子组件向父组件通信

3.跨级组件通信

4.兄弟组件通信

5.无嵌套关系的组件通信

二、避免重复渲染

1.隔离独立渲染的子组件

2.与渲染无关的变量不用state来管理数据

3.批量更新state、合并state

4.class组件中使用shouldComponentUpdate

5.绑定事件尽量不使用箭头函数

三、代码优化

1.大量的props

2.不兼容的props

3.props经过处理变为state

4.使用枚举管理状态

5.自定义Hook

四、其他优化

1.使用React.Fragment减少额外标签

2.避免使用内联样式属性

3.优化条件渲染

五、懒加载

1.懒加载React组件、第三方依赖组件

2.不用React.lazy懒加载


一.组件通信

1.父组件向子组件通信

  • 向子组件传递props
<ChildList list={this.state.list} />
<ChildSettings {...this.state.options}  />
  • 父组件调用子组件中的方法

1)函数组件:父组件useRef()创建一个ref,通过ref属性附加到子组件上,子组件用forwardRef来获取传递给它的ref,配合useImperativeHandle使用可以自定义暴露给父组件的方法

// 父组件
import { useRef } from "react";
const childRef = useRef();

useEffect(() => {
    childRef.current.childFunc();
}, []);

return (
    <div>
      <Child ref={childRef} />
    </div>
);

// 子组件
import { forwardRef, useImperativeHandle } from "react";

const Child = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    childFunc() {
      return logChild();
    },
  }));
  const logChild = () => {
    console.log("---child-----");
  };
  return <div>child</div>;
});

export default Child;

2)class组件:子组件通过调用父组件的props方法,将子组件本身暴露出去,父组件可直接使用子组件内的方法,不想暴露给父组件的方法可以用static关键字定义静态方法

// 父组件
export default class Parent extends React.Component {
  onClick = () => {
    this.childRef.childFunc();
  };

  render() {
    return (
      <div>
        <Child
          onRef={(ref) => {
            this.childRef = ref;
          }}
        />
        <button onClick={this.onClick}>click</button>
      </div>
    );
  }
}

// 子组件
class Child extends React.Component {
  componentDidMount() {
    this.props.onRef(this);
  }
   
  static myFunc = () => {
  	console.log('本方法不暴露给父组件');
  };

  childFunc = () => {
    console.log("---child-----");
  };

  render() {
    return <div>child</div>;
  }
}
export default Child;

2.子组件向父组件通信

父组件向子组件传递props方法,子组件主动调用该方法,将需要的信息作为参数传递到父组件的作用域中

// 父组件
<ChildSelector 
    value={this.state.selectedValue} 
    onSelect={(selectedValue) => {
        this.setState({
        	selectedValue
        })
    }} 
/>

// 子组件
onSelect = (value) => {
	this.props.onSelect(value);
}

3.跨级组件通信

使用Context共享组件树上能够全局访问的数据。注:Child1为Child的子组件

1)React.createContext(defaultValue)创建Context对象,订阅了Context对象的组件会从组件树中找到离自身最近的Provider中读取到当前的context,Context对象会返回Provider组件,允许Consumer消费组件订阅context的变化

// context/AppContext.js
import { createContext } from "react";

export default createContext({
  name: "",
  changeName: () => {},
});

2)函数组件:子组件使用useContext(),接收一个context对象,返回该context的当前值

// 根组件
import { useState } from "react";
import Child from "./Child";
import Context from "./context/AppContext";

const Parent = () => {
  const [name, setName] = useState("defaultName");

  const changeName = (name) => {
    setName(name);
  };

  return (
    <Context.Provider value={{ name, changeName }}>
      <Child />
    </Context.Provider>
  );
};

export default Parent;

// 子组件Child1
import { useContext } from "react";
import Context from "./context/AppContext";

const Child1 = () => {
  const context = useContext(Context);

  return <div onClick={() => context.changeName("Child")}>{context.name}</div>;
};

export default Child1;

3)class组件:子组件contextType属性会被重新赋值为一个由 React.createContext() 创建的 Context 对象

// 根组件
import Child from "./Child";
import Context from "./context/AppContext";

export default class Parent extends React.Component {
  state = {
    name: "defaultName",
  };

  changeName = (name) => {
    this.setState({
      name,
    });
  };

  render() {
    return (
      <Context.Provider
        value={{ name: this.state.name, changeName: this.changeName }}
      >
        <Child />
      </Context.Provider>
    );
  }
}

// 子组件Child1
import { Component } from "react";
import Context from "./context/AppContext";

class Child1 extends Component {
  static contextType = Context;

  render() {
    return (
      <div onClick={() => this.context.changeName("Child")}>
        {this.context.name}
      </div>
      // 16.x版本后可写成,不需要定义contextType
      <Context.Consumer>
        {({ name, changeName }) => {
          return <div onClick={() => changeName("Child")}>{name}</div>;
        }}
      </Context.Consumer>
    );
  }
}
export default Child1;

4.兄弟组件通信

利用useReducer和context实现,可模仿一个简单的redux,useReducer传入reducer和defaultState,返回当前state和dispatch,通过context传给各个子组件,让子组件共享state和修改state

// types.js
export const EXAMPLE_TEST = "EXAMPLE_TEST";

// reducer.js
import * as Types from "./types";

export const defaultState = {
  count: 0,
};

export default (state, action) => {
  switch (action.type) {
    case Types.EXAMPLE_TEST:
      return {
        ...state,
        count: action.count,
      };
    default: {
      return state;
    }
  }
};

// action.js
import * as Types from "./types";

export const onChangeCount = (count) => ({
  type: Types.EXAMPLE_TEST,
  count: count + 1,
});
// 根组件
import React, { useReducer } from "react";
import Context from "./context";
import reducer, { defaultState } from "./reducer";
import Child from "./Child";
import Child1 from "./Child1";

function ReducerCom() {
  const [state, dispatch] = useReducer(reducer, defaultState);

  return (
    <Context.Provider value={{ state, dispatch }}>
      <Child />
      <Child1 />
    </Context.Provider>
  );
}

export default ReducerCom;

// Child.js
import React, { useEffect, useContext } from "react";
import { onChangeCount } from "./action";
import Context from "./context";

const Child = () => {
  const context = useContext(Context);

  useEffect(() => {
    // 监听变化
    console.log("变化执行啦");
  }, [context.state.count]);

  return (
    <button
      onClick={() => context.dispatch(onChangeCount(context.state.count))}
    >
      点击加 1
    </button>
  );
};

export default Child;

// Child1.js
import React, { useContext } from "react";
import Context from "./context";

const Child1 = () => {
  const context = useContext(Context);

  return (
    <div>
      <p>{context.state.count}</p>
    </div>
  );
};

export default Child1;

5.无嵌套关系的组件通信

  • 使用redux、mobx、flux等状态管理器
  • 使用发布-订阅模式
// EventEmitter.js
class EventEmitter {
  constructor() {
    this.subscribers = {};
  }
  on(type, fn) {
    if (!this.subscribers[type]) {
      this.subscribers[type] = [];
    }

    this.subscribers[type].push(fn);
  }
  off(type, fn) {
    let listeners = this.subscribers[type];
    if (!listeners || !listeners.length) return;
    this.subscribers[type] = listeners.filter((v) => v !== fn);
  }
  emit(type, ...args) {
    let listeners = this.subscribers[type];
    if (!listeners || !listeners.length) return;
    listeners.forEach((fn) => fn(...args));
  }
}

export default new EventEmitter();
// 组件1
const Child = () => {
  const [name, setName] = useState("defaultName");

  useEffect(() => {
    EventEmitter.on("changeName", (name) => {
      setName(name);
    });
    return EventEmitter.off();
  }, []);

  return <div>{name}</div>;
};

// 组件2
const Child1 = () => {
  const changeChildName = () => {
    EventEmitter.emit("changeName", "change name from child2");
  };

  return <button onClick={changeChildName}>click</button>;
};

二、避免重复渲染

1.隔离独立渲染的子组件

用React.memo隔离组件形成独立的渲染单元,避免父组件重新渲染造成子组件也重新渲染,可以用于不依赖于父组件状态渲染的子组件,函数组件和class也可以用useMemo和pureComponent实现。

export default React.memo(Child);

1)函数组件:useMemo的第二个参数是依赖项数组,某个依赖项改变时才会重新渲染子组件。

{useMemo(
	() => (
    	<Child />
	),
	[]
)}

2)class组件:用React.pureComponent

export default class Child extends React.PureComponent {}

2.与渲染无关的变量不用state来管理数据

触发this.setState或者useState,只要state改变就会触发渲染,与在render中是否引用无关,可以直接把数据绑定在this上,或者使用useRef做数据缓存。

1)函数组件

const App = ({name}) => {
	const nameRef = useRef('defaultName');
    
    useEffect(() => {
    	nameRef.current = name;
    })
    
    return <div>hello world!</div>;
}

2)class组件

class App extends React.Component{
	name = 'defaultName';

	componentDidMount() {
    	this.name =  this.props.name;
    }

	render () {
		return <div>hello world!</div>;
    }
}

3.批量更新state、合并state

class组件如果在一个函数中setState了三次,会触发三次setState,但是不会渲染三次,因为react会合并成一次做批量更新,但是在异步函数中会多次渲染,批量更新失效。

demo如下:

const Parent = () => {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);
  const [c, setC] = useState(0);

  const handleClick = () => {
    setA(a + 1);
    setB(b + 1);
    setC(c + 1);
  };

  console.log("----render----");

  return (
    <div>
      {a}---{b}---{c}
      <button onClick={handleClick}>click</button>
    </div>
  );
};

export default Parent;

效果如下:点击三次,才会render三次

如果是异步的更改state

const handleClick = () => {
    setTimeout(() => {
      setA(a + 1);
      setB(b + 1);
      setC(c + 1);
    }, 0);
  };

效果如下:点击一次,就会render三次,说明没有合并更新

解决方案:

1)手动批量更新可以用react-dom中的unstable_batchedUpdates,三次更新就会合并成一次

import { unstable_batchedUpdates } from 'react-dom';
const handleClick = () => {
    setTimeout(() => {
      unstable_batchedUpdates(() => {
        setA(a + 1);
        setB(b + 1);
        setC(c + 1);
      });
    }, 0);
  };

2)合并state:用一个setState改变多个state,或者一个useState保存多个state

4.class组件中使用shouldComponentUpdate

使用shouldComponentUpdate控制组件是否需要重新渲染

shouldComponentUpdate(nextProps, nextState) {
    if(nextState.id != this.state.id ) {
      return true;
    }
    return false;
}

5.绑定事件尽量不使用箭头函数

使用箭头函数每次渲染都会创建一个新的时间处理器,子组件每次都会被渲染。

1)函数组件:子组件使用React.memo配合父组件用useCallback包裹props方法,实现父组件渲染不影响子组件的渲染

<Child onClick={handleClick} />
// 如果用箭头函数绑定事件父组件还是会影响子组件的渲染
<Child onClick={(value) => handleClick(value)} />

const handleClick = useCallback((value) => {
    console.log(value);
 }, []);

2)class组件:不用箭头函数,子组件用React.memo包裹即可

三、代码优化

1.大量的props

        如果需要将大量的props传递到一个组件中,那么可以思考以下几点:

1)该组件是否做了多件事,一个组件应该只做一件事,将该组件拆分成多个小组件是否会更合理;

2)组件是否可以被合成,如果组件中有很多不相干的逻辑,就可以考虑拆分再重新组合;

3)是否传递了很多配置有关的props,比如带分页配置的表格组件,可以将多个配置的props合成一个options,可以更好的控制组件选项,也更规范。

2.不兼容的props

        避免组件之间传递不兼容的props,例如有一个<Input/>组件功能是把输入的小写英文都转变成大写,过了一段时间,想将它用于电话号码的处理,虽然都是用的input元素,但是明显电话号码的处理用不上之前的功能,并且毫无关联,这时候也可以分割组件明确职责,如果有共享的逻辑可以放到hooks中

3.props经过处理变为state

        一般常规做法是,子组件内创建一个state,当props的值改变时再改变state,但是如果只是基于props通过计算得到新的state可以用useMemo来代替useState。

const Child = ({ count }) => {
  const formatCount = (value) => {
    return value + 10;
  };

  const formattedCount = useMemo(() => {
    return formatCount(count);
  }, [count]);
    
  //const [formattedCount, setFormattedCount] = useState(count);

  //useEffect(() => {
  //  setFormattedCount(formatCount(count));
  //}, [count]);

  return <div>{formattedCount}</div>;
};

export default Child;

4.使用枚举管理状态

在编写组件时,很容易用很多个布尔值来表示组件当前的状态,比如isLoading、isFinished等等,虽然技术可行,但是很难推断组件当前处于什么状态,不容易维护,可以用一个枚举的状态来表示。

function Component() {
  const [isLoading, setIsLoading] = useState(false)
  const [isFinished, setIsFinished] = useState(false)
  const [hasError, setHasError] = useState(false)

  const fetchSomething = () => {
    setIsLoading(true)

    fetch(url)
      .then(() => {
        setIsLoading(false)
        setIsFinished(true)
      })
      .catch(() => {
        setHasError(true)
      })
  }

  if (isLoading) return <Loader />
  if (hasError) return <Error />
  if (isFinished) return <Success />

  return <button onClick={fetchSomething} />
}

// 改造后
function Component() {
  const [status, setStatus] = useState('loading')

  const fetchSomething = () => {
    setStatus('loading')

    fetch(url)
      .then(() => {
        setStatus('finished')
      })
      .catch(() => {
        setStatus('error')
      })
  }

  if (status === 'loading') return <Loading />
  if (status === 'error') return <Error />
  if (status === 'finished') return <Success />

  return <button onClick={fetchSomething} />
}

5.自定义Hook

        组件具有相似逻辑、某个状态具有自己的复杂逻辑或者和生命周期有关的函数封装可以考虑自定义Hook,也可以使用封装好的Hook库,比如Umi Hooks--https://hooks.umijs.org/zh-CN/hooks/async

1)场景一:通用查询表格

        没有用自定义Hook提取可重用函数之前,每个页面的查询表格组件都要维护自己的loading/list/pageNo等状态,每个组件的逻辑都是默认请求列表,在请求列表前loading置为true,请求完毕置为false,设置list的数据,设置分页器,点击查询再根据查询条件请求列表,用自定义Hook可以将这些重复的逻辑封装起来,返回这些通用的state,减少重复的代码,让这些状态统一管理起来,比如默认分页数可以统一在一个地方控制。
const [list, setList] = useState([]);
const [loading, setLoading] = useState(false);
const [paginator, setPaginator] = useState({
    page: 1,
    pageSize: 20,
});

const fetchList = (fetchStatus) => {
    setLoading(true);
    getList({
      pageSize: paginator.pageSize,
      pageNo: paginator.page,
      name,
    }).then((res) => {
      setList(res.data);
      setLoading(false);
    }).catch(() => {
    	setLoading(false);
    });
}; 

const handlePageSelect = (page, pageSize) => {
    setPaginator({ ...paginator, page, pageSize });
};

useEffect(() => {
    fetchList();
}, [paginator.page, paginator.pageSize]);

<Table rowMax={list && list.length} loading={loading} data={list} column={columns} />
<Pagination  
    prev 
    next 
    first 
    last 
    recordCount={total} 
    pageSize={paginator.pageSize} 
    activePage={paginator.page} 
    onSelect={handlePageSelect} 
/>

自定义组件 useTable.js

输入:查询列表的接口、【除了分页器之外的参数】、【处理列表数据的函数】、【请求接口完毕的回调】

输出:list、loading、getData函数、分页的配置项

import { useState, useEffect } from 'react';

const useTable = (getListData, extraParam = {}, handleData, callback) => {
  const [paginator, setPaginator] = useState({
    recordCount: 0,
    pageSize: 20,
    pageNo: 1,
  });

  const [list, setList] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    getData();
  }, [paginator.pageNo, paginator.pageSize]);

  const onSelect = (pageNo, pageSize) => {
    if (!isNaN(pageNo)) {
      const paginator = { ...paginator, pageSize, pageNo };
      setPaginator(paginator);
    }
  };

  // 调用接口,获取数据
  const getData = (pageNoParam, newParam) => {
    // 搜索条件改变查询的时候,当前页数重置为1
    const pageNo = !isNaN(pageNoParam) ? pageNoParam : paginator.pageNo;
    const paginatorExtra = { pageNo, pageSize: paginator.pageSize };
    setLoading(true);
    const params = newParam || extraParam;
    getListData &&
      getListData({ ...params, ...paginatorExtra })
        .then((res = { data: [] }) => {
          const data = res.data;
          const recordCount = res.total || res.count;
          let list = [];
          if (handleData) {
            list = handleData(data);
          } else {
            list = data.map((item) => {
              // 默认以id为key
              return { key: item.id, ...item };
            });
          }
          // 因为使用这个hook的地方可以手动调用getData,所以分页器在这里赋值
          setPaginator({ ...paginatorExtra, recordCount });
          setList(list);
          setLoading(false);
          callback && callback(list);
        })
        .catch((err) => {
          setLoading(false);
        });
  };

  return {
    list,
    loading,
    getData,
    tableProps: {
      prev: true,
      next: true,
      first: true,
      last: true,
      recordCount: paginator.recordCount,
      pageSize: paginator.pageSize,
      activePage: paginator.pageNo,
      pageNo: paginator.pageNo,
      onSelect,
    },
  };
};

export default useTable;

使用useTable()

const { list, tableProps, loading, getData } = useTable(
  getList,
  { teamId, productName, status },
  (data) => {
    return data.map((item) => {
      return { ...item, id: item.uuid };
    });
  }
);

<Button type="primary" onClick={() => getData(1)}>查询</Button>
<Table border rowMax={list.length} data={list} loading={loading} column={columns} />
<Pagination {...tableProps} />

2)场景二:封装useInterval代替setInterval

为什么不直接使用setInterval?

        如下面的例子,在组件加载时定义一个定时器,卸载组件时也清空定时器,但是useEffect只会执行一次,setInterval中拿到的始终是第一次渲染时拿到的count为1,所以界面上始终上显示的是2。

        为了解决这个问题,把useEffect的第二个参数改成[count],这样就会每次拿到最新的count,但是每次count更改,定时器就会不停的新增和移除。

funtion Counter() {
  const [count, setCount] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // 此时的count始终是1
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <div>{count}</div>;
};

解决办法1:函数式更新,useState 中的set方法可接收函数,该函数将接收之前的state,返回一个更新后的值。这样定时器每次拿到的是最新的值。

setCount((count) => count + 1);

解决办法2:用useRef将定时器函数提取出来,每次定时器触发的时候,都能获取到最新的count。

const myRef = useRef(null);
  myRef.current = () => {
    setCount(count + 1);
  };
  useEffect(() => {
    const id = setInterval(() => {
      myRef.current();
    }, 1000);
    return () => clearInterval(id);
  }, []);

定义useInterval.js

import { useEffect, useRef } from 'react';

const useInterval = (callback, delay) => {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback);

  useEffect(() => {
    let id;
    function tick() {
        savedCallback.current(() => {
          clearInterval(id);
        });
    }
    if (!isNaN(delay)) {
      id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
};
export default useInterval;

使用useInterval.js

useInterval((clear) => {
    setCount(count + 1);
}, 1000);

四、其他优化

1.使用React.Fragment减少额外标签

        每个组件都必须要唯一一个父标签,如果该标签只是为了当父标签,没有其他额外的用途,则可以用片段fragment包裹子元素,节省渲染器渲染额外的元素的工作量。

2.避免使用内联样式属性

        添加的内联样式是js对象不是真正的样式,需要花费更多的时间转换为等效的css样式属性,才会应用样式。

3.优化条件渲染

        安装和卸载 React 组件是昂贵的操作,所以用条件渲染减少安装和卸载组件。执行不同的if else语句切换渲染的组件,没有更改的部分不需要用条件控制,不必要每次改变state的时候都卸载并重新安装

五、懒加载

1.懒加载React组件、第三方依赖组件

    React16.6版本中,新增了React.lazy函数,可以动态加载React组件,配合webpack的code splitting,当用import()时,webpack监测到这个语法会自动进行代码分割,只有当组件被加载,对应的资源才会导入;Suspense组件可以指定在js加载完成之前的loading。适合路由懒加载、Tab切换、单个资源很大、第三方依赖组件很大的场景。

import React, { Suspense, useState } from "react";

const App = () => {
  const [showChild, setShowChild] = useState(false);

  const Child = React.lazy(() => import("./Child"));

  return (
    <div>
      <button
        onClick={() => {
          setTimeout(() => {
            setShowChild(true);
          }, 2000);
        }}
      >
        click
      </button>

      {showChild && (
        <Suspense fallback={<div>loading...</div>}>
          <Child />
        </Suspense>
      )}
    </div>
  );
};

效果:2s后加载2.chunk.js

我们也可以指定这个js的名字

const Child = React.lazy(() => import(/* webpackChunkName: "child" */"./Child"));

因为网络问题或者组件内部错误导致资源加载失败时,可能会导致页面白屏,可以用Error Boundaries组件来优雅降级。

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    console.log(error, errorInfo);
  }
  render() {
    if (this.state.hasError) {
      return <p>资源加载失败,请稍后重试</p>;
    }
    return this.props.children;
  }
}

// App组件
{showChild && (
    <ErrorBoundary>
      <Suspense fallback={<div>loading...</div>}>
        <Child />
      </Suspense>
    </ErrorBoundary>
)}

2.不用React.lazy懒加载

import React, { useState, Component } from "react";
// 异步按需加载component
const asyncComponent = (getComponent) => {
  return class AsyncComponent extends Component {
    static Component = null;
    state = { Component: AsyncComponent.Component };

    componentDidMount() {
      if (!this.state.Component) {
        getComponent().then(({ default: Component }) => {
          AsyncComponent.Component = Component;
          this.setState({ Component });
        });
      }
    }
    render() {
      const { Component } = this.state;
      if (Component) {
        return <Component {...this.props} />;
      }
      return <div>loading...</div>;
    }
  };
};

const App = () => {
  const [showChild, setShowChild] = useState(false);

  const Child = asyncComponent(() =>
    import(/* webpackChunkName: "child" */ "./Child")
  );

  return (
    <div>
      <button
        onClick={() => {
          setTimeout(() => {
            setShowChild(true);
          }, 2000);
        }}
      >
        click
      </button>

      {showChild && <Child />}
    </div>
  );
};

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值