React框架解读(一)

第一章 React发展历程

迭代概述

React是Facebook于2013年在 GitHub 上开源的,至今已经发布了很多版本,版本更新的主旨在提升用户体验。

迭代特性

React 16.x

  • Time Slicing:控制任务执行的暂停和恢复,解决CPU速度问题;
  • Suspense:和lazy配合搭配使用,实现异步加载组件,解决网络IO问题;
  • componentDidCatch:生命周期钩子,用于捕获组件及内层组件的渲染错误;

React 16.8

  • useState:返回有状态值,以及更新这个状态值的函数,声明组件内部状态;
  • useEffect:接受包含命令式,可能有副作用代码的函数,模拟生命周期钩子;
  • useContext:接受上下文对象并返回当前上下文值;
  • useReducer:useState的替代方案,接受类型为(state,action) => newState的reducer,并返回与dispatch方法配对的当前状态;
  • useCallback:缓存函数地址,属于性能优化hook;
  • useMemo:缓存结果,属于性能优化hook;
  • useRef:返回一个可变的ref对象,其.current属性被初始化为传递的参数,返回的 ref 对象在组件的整个生命周期内保持不变;
  • useImperativeMethods:自定义使用ref时公开给父组件的实例值;
  • useMutationEffect:更新兄弟组件之前,它在React执行其DOM改变的同一阶段同步触发;
  • useLayoutEffect:DOM改变后同步触发,使用它来从DOM读取布局并同步重新渲染;

React 17

  • 稳定的事件系统:利用稳定的事件系统代替合成事件的事件委托机制,可以直接在事件目标上注册事件监听器;
  • 错误边界改进:可以捕获在渲染期间发生的错误;
  • React.StrictMode组件:在应用程序中包裹 <React.StrictMode> 组件,React会进行严格模式检查,并在开发过程中发出一些额外的警告和错误提示,帮助开发者编写更健壮和可靠的代码;
  • 异步渲染行为警告和错误:有助于开发者更好地理解React的渲染过程,并避免一些常见的陷阱和问题;

React 18

  • fiber Reconciler:增量渲染和更好的调度机制;
  • 可中断和恢复的渲染:可中断和恢复的渲染功能,使应用程序更具响应性;
  • 自动批量更新:自动批量更新机制,减少了不必要的重渲染;
  • 改进的服务端渲染和同构应用程序支持

第二章 走进JSX

1. JSX语句

每一个DOM元素都可以用javaScript描述为一个JSON对象,而JSX的意思就是将HTML加到了javaScript代码中,再通过编译器转换到纯javaScript后,由浏览器执行。

xml 复制代码
<div>
    <div onClick={handleClick}>按钮</div>
    <div>{name}</div>
    <div>{count}</div>
    <div>{sex}</div>
</div>

2. JSX转化为真实DOM

2.1 createElement方法

接收三个参数type、config、children:

  • type:标识节点的类型
  • config:以对象形式传入,组件所有的属性都会以键值对的形式存储于config对象中
  • children:以对象形式传入,记录了组件标签之间嵌套的内容
ini 复制代码
export default createElement(type, config, children) {
    let propName; // 用于储存后面需要用到的元素属性
    let props = {}; //用于储存元素属性的键值对集合
    let key = null,ref = null,self= null,source = null;
    
    // 1. 分离属性,如ref、key、props属性等
    if(config != null){
        if (hasValidRef(config))ref = config.ref;
        if(hasValidKey(config))key = '' + config.key;
        self = config.__self === undefined ? null : config.__self;
        source = config.__source === undefined ? null : config.__source;
        for(propName in config){
            if(hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
                props[propName] = config[propName];
            }
        }
    }
    // 2. 将子元素挂载到props.children中
    var childrenLength = arguments.length - 2;
    if(childrenLength === 1){
        props.children = children;
    }else if(childrenLength > 1){
        var childArray = Array(childrenLength);
        for(var i = 0; i < childrenLength; i++){
            childArray[i] = arguments[i + 2];
        }
        props.children = childArray;
    }
    // 3. 为props属性初始化默认值
    if(type && type.defaultProps){
        var defaultProps = type.defaultProps;
        for(propName in defaultProps){
            if(props[propName] === undefined){
                props[propName] = defaultProps[propName];
            }
        }
    }
    // 禁止从props中获取key和ref,因为两个为内部使用属性
    {
      if(key || ref){
          var displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type;
          if(key){
              defineKeyPropWarningGetter(props, displayName);
          }
          if(ref){
              defineRefPropWarningGetter(props, displayName);
          }
      }
    }
    // 4. 调用ReactElement方法创建并返回Element元素
    return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}        

2.2 ReactElement方法

php 复制代码
const ReactElement = function(type, key, ref, self, source, owner, props) {
    const element = {
        $$typeof: REACT_ELEMENT_TYPE, // REACT ELEMENT TYPE是一个常量,用来标识该对象是一个ReactElement
        type: type, // 内置属性赋值
        key: key,
        ref: ref,
        props: props,
        _owner: owner, // 记录创造该元素的组件
      }
      element._store = {};
      Object.defineProperty(element._store, 'validated', {
          configurable: false,
          enumerable: false,
          writable: true,
          value: false
      });
      Object.defineProperty(element, '_self', {
          configurable: false,
          enumerable: false,
          writable: false,
          value: self
      });
      Object.defineProperty(element, '_source', {
          configurable: false,
          enumerable: false,
          writable: false,
          value: source
      });
      if(Object.freeze){
          Object.freeze(element.props);
          Object.freeze(element);
      }
      return element;
}

2.3 ReactDOM.render方法

ReactDOM.render方法可以把拿到的虚拟DOM元素渲染到页面中。

csharp 复制代码
ReactDOM.render(
    // 需要渲染的元素 ReactElement
    element,
    // 元素挂载的目标容器(一个真实的DOM)
    container,
    // 可选的回调函数,用来处理渲染结束后的逻辑
    [callback]
)

2.4 Element元素对象

ReactElement方法的返回值为一个element对象,即所谓的虚拟DOM,格式如下所示:

$$typeof:REACT_ELEMENT_TYPE 是一个Symbol类型得知,全局唯一性,也可以防止恶意代码的插入,预防XSS攻击。

javascript 复制代码
const hasSymbol = typeof Symbol === 'function' && Symbol.for;
// 如果浏览器支持Symbol则为Symbol类型的变量,否则为16进制的数字
export const REACT_ELEMENT_TYPE = hasSymbol? Symbol.for('react.element') : 0xeac7;

2.5 Element元素对象持久化

React应用更新时,会通过createElement创建一个新的Element对象,如果想要复用某个对象,就需要将其持久化。

持久化对象的方式大致可以分为实例属性或者内存缓存。

  1. class组件实例属性
scala 复制代码
class CaseTest1 extends React.Component {
  saveElement = <div>持久化</div> // 实例属性

  render(){
    return (
      <div>{this.saveElement}</div>
    )
  }
}
  1. function组件的useRef
javascript 复制代码
const CaseTest2 = () => {
  const saveElement = useRef(<div>持久化</div>); // 需要手动更新

  return (
    <div>{saveElement.current}</div>
  );
};
  1. function组件的useMemo
javascript 复制代码
const CaseTest2 = () => {
  const saveElement = useMemo(()=>{
    return <div>持久化</div>
  },[]); // 依赖项改变会重新创建Element

  return (
    <div>{saveElement}</div>
  );
};

3. children属性

xml 复制代码
<子组件>
    该部分内容会被子组件的props.children属性接收
</子组件>

3.1 React.isValidElement()

用于判断元素是否为React Element对象,底层利用了typeof关键字和元素的$$typeof属性进行判断。

typescript 复制代码
function isValidElement(object) {
    return (
        typeof object === 'object' && 
        object !== null && 
        object.$$typeof === REACT_ELEMENT_TYPE
    )
}

console.log(React.isValidElement(<p>CaseTest</p>)); // true

3.2 React.cloneElement()

csharp 复制代码
React.cloneElement(
  element, // 需要克隆的对象
  [props], // 需要合并的props
  [...children] // 新的Children元素
)

适用场景:在组件中劫持Children Element,然后通过cloneElement克隆Element对象,混入props。如React-Router的Switch组件就是基于此方式来匹配唯一的Router并加以渲染。

javascript 复制代码
function App() {
  const Clone = React.cloneElement(
    <Temp/>,
    {key: 123, name: "张三"}, // 混入props
    <div>我是新的子元素</div>, // 新增children
  )
  return <div>{Clone}</div>;
}

const Temp = (props) => {
  return (
    <div>
        <span>你好世界,{props.name}</span>
        {props.children}
    </div>
  )
};

React.cloneElement等价于以下操作:

go 复制代码
<element.type {...element.props} {...new_props}>
   {new_children}
</element.type>

React.cloneElement特点:

  • 克隆 element 对象,返回一个新的 React 元素
  • 保留 element 的 props属性,新增的 props 会通过 assign 进行浅合并
  • 保留 key 和 ref 属性,支持修改
  • 新增的 children 属性会覆盖原来的
  • props 的 this 默认指向新的 React 元素

3.3 children方法集合

  1. React.children.map

类似数组的 map 方法,返回一个新的Children数组

ini 复制代码
const CaseTest1 = (props) => {
  const newChildren = React.Children.map(props.children,(item)=>item);
  return newChildren;
};

注意⚠️:如果props.children是一个Fragment对象,它将被视作单一子节点,不会被遍历

  1. React.children.forEach

类似数组的 forEach 方法,遍历children,为每一个元素提供统一操作等

ini 复制代码
const CaseTest1 = (props) => {
  const newChildren = [];
  React.Children.forEach(props.children,(item)=>{
    // 筛选element元素
    if(React.isValidElement(item)){
      newChildren.push(item);
    }
  });
  return newChildren;
};
  1. React.children.count

类似数组的length属性,获取子元素总数量

ini 复制代码
const childrenCount = React.Children.count(props.children);
  1. React.children.toArray

返回props.children扁平化后的结果,可以深层次flat(降维)

ini 复制代码
const newChildren = React.Children.toArray(props.children);
  1. React.children.only

验证props.children是否只有一个节点(一个react元素),有就返回这个元素,否则方法抛出错误

ini 复制代码
const res = React.Children.only(props.children);

3.4 判断props.children类型

可以通过Element对象的type属性和$$typeof属性区分组件和元素

组件的type属性为对应的函数或者类本身,如TextComponent({name})。元素的type属性为对应的元素标签类型,例如div

typescript 复制代码
function mapIntoArray(children){
  const type = typeof children;
  if (type === 'undefined' || type === 'boolean')children = null;
  let invokeCallback = false;
  if (children === null) {
    invokeCallback = true;
  } else {
    switch (type) {
      case 'string':
      case 'number':
        invokeCallback = true;
        break;
      case 'object':
        switch ((children: any).$$typeof) {
          case REACT_ELEMENT_TYPE:
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }
}

第三章 React组件

1. 组件定义

在React项目中组件可以分为两类,类组件和函数组件,两者均是UI的载体,将数据传递给组件,会返回UI界面。

React早期的函数组件又称无状态组件,其只负责UI渲染,没有自身状态,没有业务逻辑代码,是一个纯函数。函数的输出只由props决定,不受其他任何因素影响。由于函数组件没有实例,没有生命周期,所以在React Hooks出现之前,函数组件和类组件最大的区别就是是否有状态

React 16.8 版本推出了React Hooks,为函数组件提供了状态,也支持在函数组件中进行数据获取、订阅事件、解绑事件等,解决了React 函数组件的状态共享以及类组件生命周期管理混乱的问题,React Hook的出现标志着 React 不再存在无状态的组件,React 将只有类组件和函数组件的概念。

2. 组件分类

2.1 函数组件

React底层对于函数组件的处理是直接执行函数,即每一次更新都会执行一次函数,获取函数返回值。

javascript 复制代码
import React, { useEffect, useState } from "react";

const CaseTest2 = () => {
  const [name,setName] = useState('');

  useEffect(()=>{
    setName('lily')
  },[]);

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

// 函数组件的静态属性
CaseTest2.Item = function(){
  return <div>lily今年25岁</div>
}

export default CaseTest2;

2.2 类组件

React中的类组件是通过继承React.Component得到的组件,对于类组件的处理在constructorClassInstance函数中,通过new的方式实例化组件,获取组件的instance(例子)。

typescript 复制代码
import React from "react";

class CaseTest1 extends React.Component {
  constructor(props){
    super(props),// 执行底层的React.Component函数 
    this.state={}// 内部状态,内部调用的是getInitialState方法
  }                               
  static number = 1;// 静态属性,内部调用的是getDefaultProps方法
  handleClick=()=>{};// 静态方法
  componentDidMount(){};// 生命周期

  render(){
    return (
      <div onClick={this.handleClick}>hello world</div>
    )
  }
}
export default CaseTest1;

2.3 两种组件对比

class组件的状态会存在this指向的实例对象上面,而Hook的出现是为了解决函数组件状态保存的,函数组件内部的状态会保存在hook上面。

对比 函数组件 React Hook 类组件
生命周期
调用 调用函数 调用函数 new实例化
是否可以获取实例this 不可以 可以 可以
是否拥有内部状态
封装灵活性

第四章 组件间数据流动

1. 基于props单向数据流

组件,从概念上类似于JavaScript函数。接受任何入参(即props)并返回用于描述页面展示内容的React元素。

单向数据流要求组件的state以props的形式流动时,只能流向组件树中比自己层级更低的组件。

1.1 父到子

父组件可以将自身的props传递给子组件,实现父子组件间的通信,props的数据类型可以是任意的,包括html结构。

  1. props传值
ini 复制代码
const Parent = () => {
    return <Child title='测试组件' click={onClick}/>
}

const Child = (props) => {
    const handleClick = props.click;
    const title = props.title;
}
  1. props.children传值
  • 传递Element元素
javascript 复制代码
function Parent() {
    return (
        <Child>
            <span>Hello, world.</span>
        </Child>
    )
}

function Child({ children }) {
    return (
        <div className="child">{children}</div>
    )
}   
  • 传递object类型
javascript 复制代码
function Parent() {
    const user = {
        firstName: 'Hello',
        lastName: 'world'
    }
    return <Child>{user}</Child>
}

function Child({ children }) {
    return (
        <div className="child">
            {children.firstName}, {children.lastName}
        </div>
    )
}   
  • 传递component
javascript 复制代码
function Child(props) {
    return <div>{props.children}</div>
}
<Child>
    <div>文本内容</div>
</Child>
  • 传递function
scala 复制代码
class Container extends Component {
  render () {
    return (
      <div>
        {this.props.children('hello world')}
      </div>
    )
  }
}

<Container>
  {(text) => <div>{text}</div>}
</Container>

1.2 子到父

父组件传递一个绑定自身上下文的函数给子组件,子组件在调用函数时可以将数据作为参数传递给父组件

javascript 复制代码
const Parent = () => {
    const onClick = (value) => {
        console.log(value,'点击了')
    }
    return(
        <div>
            <Child click={onClick}/>
        </div>
    )
}
const Child = (props) => {
    const handleClick = (value) => {
        props.click(value)
    }
    return <div onClick={()=>{handleClick(1)}}>子组件</div>
}

1.3 兄弟组件

🤔props层层传递实现组件通信不好么?

🙋:不好,不仅会增加代码量,还会破坏中间层级组件的数据结构,导致后续维护成本增加,所以不推荐使用。

1.4 父组件调用子组件的方法

父组件通过forwardRef+useImperativeHandle调用子组件内部的方法。

父组件生成ref并传递给子组件,子组件通过forwardRef转发ref,并利用useImperativeHandle向外暴露方法,父组件通过ref.current.fn调用对应的函数。

typescript 复制代码
const Parent: React.FC =(() => {
    const shareRef = useRef<any>(); // 创建ref对象
    const handleClick =()=>{
        shareRef.current.handleShare();
    }
    return (
        <div>
            <h3>父组件</h3>
            <Button onClick={handleClick}>触发子组件中的方法</Button>
            <Sun ref={shareRef}></Sun>
        </div>
    );
});

const Sun = forwardRef((props,ref) => {
    const [count, setCount] = useState(0);
    
    const handleShare = () => {
        setCount(prevCount => prevCount + 1);
        console.log('子组件的方法',count);
    }
    // 暴露方法
    useImperativeHandle(ref,()=>{
        return { handleShare }
    },[count]);

    return (
        <div>
            <h3>子组件</h3>
        </div>
    );
});

1.5 跨层级组件

javascript 复制代码
// 创建context
const ThemeContext = React.createContext('light');

// 向下传递context
<ThemeContext.Provider value='dark'>
    <ThemedButton />
</ThemeContext.Provider>

// 使用context
import ThemeContext from "./context/ThemeContext.js";

function ThemedButton (){
    const context = useContext(ThemeContext);
    render() {
       return <button>{context}</button>;
    }
}

2. "发布-订阅"模式驱动数据流

2.1 理解发布订阅模式

发布订阅模式是解决任意组件间通信的良好解决方案。常见的实现如下:

  • socket.io模式,实现跨端的发布订阅模式
  • Node.js中的EventEmitter
  • Vue.js中全局事件总线EventBus

JavaScript中的事件监听也可以理解为一个简单的发布订阅模式。

bash 复制代码
element.addEventListener(type,function,useCapture)

发布订阅模式的设计思路中,包括事件的监听和事件的触发。

  • on():负责注册事件的监听器,指定事件触发时的回调函数
  • emit():负责触发事件,通过传递参数实现在触发时携带数据
  • off():负责移除事件监听器

2.2 实现发布订阅模式

typescript 复制代码
constructor(){
    this.eventMap = {}; // 存储事件和监听函数间的关系
}
// 订阅
on(type,handler){
    if(!(handler instanceof Function)){
        throw new Error('必须传入一个函数')
    }
    if(!this.eventMap[type]){
        this.eventMap[type] = [];
    }
    this.eventMap[type].push(handler);
}
// 发布
emit(type,params){
    if(this.eventMap[type]){
        this.eventMap[type].forEach(f =>{
            f(params);
        })
    }
}
// 解绑
off(type,handler){
    if(this.eventMap[type]){
        this.eventMap[type].splice(
            this.eventMap[type].indexOf(handler) >>> 0,
            1
        )
    }
}

2.3 发布订阅模式的数据流动

2.4 事件总线eventBus

eventBus是一个事件发布/订阅的轻量级工具库,通过on向eventBus中注册事件callback,通过emit触发事件callback,通过off解绑事件callback。

该方法有一个弱点就是需要手动绑定和解绑。

csharp 复制代码
// 手动实现一个eventBus
class eventBus{
  event={};
  // 绑定注册事件
  on(eventName,cb){
    if(this.event[eventName]){
      this.event[eventName].push(cb);
    }else{
      this.event[eventName]=[cb];
    }
  }
  // 触发事件
  emit(eventName,...params){
    if(this.event[eventName]){
      this.event[eventName].forEach(cb=>cb(...params))
    }
  }
  // 解绑事件
  emit(eventName){
    if(this.event[eventName]){
      delete this.event[eventName]
    }
  }
}
 export default new eventBus();

3. Context API维护全局状态

Context API是React官方提供一种组件树全局通信的方式,其包含三个重要成员:Context对象、Provider、Consumer。

Context可以理解为从上层组件向下层组件传递状态。

React16.3.0之前版本的Context中使用propsTypes声明Context类型,同时需要使用getChildContext返回需要提供的Context,并且用静态属性childContextTypes声明需要提供的Context数据类型。

scala 复制代码
class WrapComponent extends React.Component {
  getChildContext() {
    const number = 2023;
    return number;
  }

  render(){
    return(
      <div>
        <DateComponent/>
      </div>
    )
  }
}

WrapComponent.childContextTypes = {
  number: propsTypes.object
}

------------------------
class DateComponent extends React.Component{
  render(){
    const {number} = this.context;
    return(
      <div>{number}</div>
    )
  }
}

DateComponent.contextType = {
  number: propsTypes.object;
}

export default DateComponent;

新版本的Context需要使用createContext创建Context对象,返回值包含用于传递Context对象的Provider和接收参数变化订阅的Consumer,如果Consumer上级一直找不到Provider会使用设置的默认值

ini 复制代码
const MyContext = React.createContext(2023);
const MyProvider = MyContext.Provider;
const MyConsumer = MyContext.Consumer;
const MyName = MyContext.displayName;

Context更新时会导致消费Context的Consumer重新更新。为了性能优化,可以结合useMemo和memo使用。Context可以多级嵌套,会逐层传递Context,消费者会优先获取距离自己最近一层的Provider提供的Context。

3.1 React.createContext

ini 复制代码
const context = React.createContext(defaultValue);
const { Provider, Consumer} = context;
  • Provider:数据提供者,使用时包裹需要消费数据的组件
  • Consumer:数据消费者,使用时需要通过函数返回需要渲染的React元素
xml 复制代码
<Provider title='数据'>
    <Component />
</Provider>

<Consumer>
    {value => <div>{value.tile}</div>}
</Consumer>

注意⚠️:即使shouldComponentUpdate返回false,context仍然可以穿透组件,继续向后代组件传播,进而确保数据提供者和数据消费者之间数据的一致性。

3.2 Redux数据流框架

Redux是JavaScript的状态容器,提供可预测的状态管理。

  • store:一个只读的单一数据源
  • action:对变化的描述
  • reducer:负责对变化进行分发和处理

⚠️:Redux在整个工作流程中,数据是严格单向的!

go 复制代码
// 使用createStore创建store对象
const store = createStore(
    reducer,
    initial_state,
    applyMiddleWare(middleWare1,middleWare2,...)
)
// 通过reducer创建一个新的state返回给store
const reducer = (state,action)=>{
    return newState
}
// 通过action通知reducer"让改变发生"
const action = {
    type:"ADD_ITEM",
    payload:"<li>{text}</li>"
}
// 通过dispatch派发action
store.dispatch(action);

第五章 React设计模式

1. 组合模式

组合模式适用于一些容器化组件场景,通过外层组件包裹内层组件,这种方式在Vue中被称为slot插槽。外层组件可以轻松获取内层组件的props状态,还可以控制内层组件的渲染。

在这种模式下,子组件并不是直接在父组件中标签化,而是在父组件标签内部再次标签化形成Element,绑定在父组件的props.children上。

xml 复制代码
<Groups>
    <Item>hello</Item>
    <Item>world</Item>
</Groups>

优点:

  • 通过React.cloneElement的方式向Children隐式注入props,使得子组件可以使用父组件的props,多层组件嵌套,实现props的强化;

  • 父组件向子组件传递callback,子组件调用callback实现与父组件的通信;

  • 通过父组件的props.children可以获取子组件的渲染结果,进行过滤或者配合cloneElement增加Element;

2. render props模式

render props模式中的容器组件负责传递状态,执行Children函数。

javascript 复制代码
<Groups>
   {(cProps)=> <Item {...cProps,props}>hello</Item>}
</Groups>

当子组件不是一个React Element形式时,需要验证Children的数据类型

javascript 复制代码
function Groups(props:any){
  return React.isValidElement(props.children) ?
  props.children
  :typeof props.children === 'function' ?
  props.children() : null
}

render props还可以结合子组件的静态属性来确定其身份

ini 复制代码
function Item(){};
Item.displayName='Item';

function Groups(props:any){
  const newChildren:any = [];
  React.Children.forEach(props.children,(item)=>{
    if(React.isValidElement(props.children) && item.type.displayName === 'Item'){
      newChildren.push(item);
    }
  })
  return newChildren;
}

优点:

  • 将容器组件的状态和当前组件状态结合,派生新状态;

3. 高阶组件

高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧,它基于React的组合特性形成的一种设计模式。高阶组件接受一个组件作为参数,并返回一个新的组件,一般用于强化组件、赋能组件、复用一些逻辑等。

实现高阶组件的方式有属性代理和反向继承两种方式。

3.1 属性代理

用代理组件包裹一层原始组件,在代理组件上,可以做一些对原始组件的强化操作。代理组件会返回一个新组件,被包裹的组件会在代理组件内被挂载。

scala 复制代码
import React from "react";

const HOC = (Element:any) => {
  return class Advance extends React.Component {
    state = {
      name:'强化属性'
    }
    render(){
      return <Element {...this.props} name={this.state.name}/>
    }
  }
};

export default HOC;

优点:

  • 属性代理和业务组件低耦合或者零耦合
  • 同时适用于类组件和函数组件
  • 多个HOC可以嵌套使用

缺点:

  • 无法直接获取原始组件状态,需要通过ref获取组件实例
  • 无法直接继承静态属性,需要手动处理
  • 本质上产生了一个新的组件,需要forwardRef转发ref

3.2 反向继承

包装后的组件集成了原始组件本身,无须挂载业务组件。

typescript 复制代码
const ReverHOC = (WrapComponent: any) => {
  return class extends WrapComponent {
    constructor(props: any) {
      super(props);
      this.state = { count: 1000 };
    }
    componentDidMount() {
      console.log("高阶组件 did mount", this.props, this.state);
      this.clickComponent();
    }
    render() {
      return (
        <div>
          <h1 onClick={this.clickComponent}>高阶组件</h1>
          <div>{super.render()}</div>
        </div>
      );
    }
  };
};

class ChildComponent extends React.Component {
  constructor(props: any) {
    super(props);
    this.state = {};
  }
  componentDidMount() {
    console.log("演示反向继承子组件mount");
  }
  // 点击事件
  clickComponent() {
    console.log("组件被点了");
  }
  render() {
    return <div>子组件</div>;
  }
}

const HOCView = ReverHOC(ChildComponent)

const Case = () => {
  return <HOCView />
};

// 高阶组件 did mount {} {count: 1000}
// 组件被点了

优点:

  • 方便获取组件内部状态
  • 实现对静态属性和方法的继承

3.3 适用场景

高阶组件可以用于封装错误捕获组件,实现渲染劫持及组件赋能(状态监控、埋点监控等)

  1. 错误捕获

利用class类组件的componentDidCatch、getDerivedStateFromError生命周期捕获渲染阶段的错误

javascript 复制代码
function ErrorCatch(Component){
  return class WrapComponent extends React.Component{
    constructor(props){
      super(props);
      this.state={
        isError:false
      }
    }

    static getDerivedStateFromError(){
      return{isError:true} // 无法获取this,但是返回值会合并到state中,作为新的state值向下传递
    }

    componentDidCatch(error, errorInfo){
      this.setState({
        isError:true
      })
    }

    render(){
      return this.state.isError ? 
      <div>出现错误</div>:
      <Component {...this.props}/>
    }
  }
}

错误边界无法捕获以下场景中产生的错误:

  • 函数事件处理器中的错误
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
  • 服务端渲染错误
  • 错误边界组件自身抛出的错误(并非它的子组件)
  1. 渲染劫持

反向继承时可以获取被继承组件的props属性,从而进行劫持。

scala 复制代码
function ErrorCatch(Component){
  return class WrapComponent extends Component{
    constructor(props) {
      super(props);
      this.state = {};
    }

    render(){
      if(this.props.isVisible){
        return super.render();
      }else{
        <div>暂无数据</div>
      }
    }
  }
}
  1. 修改渲染的子组件

反向继承运行super.render时,可以获取渲染结果,从而对渲染结果进行过滤或修改。

scala 复制代码
function ErrorCatch(Component){
  return class WrapComponent extends Component{
    constructor(props) {
      super(props);
      this.state = {};
    }

    render(){
      const elements = super.render();
      const newChildren= [];
      React.Children.forEach(elements.props.children,item=>{
        if(React.isValidElement(item)){
          newChildren.push(item);
        }
      })
      return React.cloneElement(elements,elements.props,newChildren);
    }
  }
}
  1. 组件赋能

不处理任何内部组件逻辑,只在外部监听内部组件的点击事件。

javascript 复制代码
function ErrorCatch(Component:any){
  return function WrapComponent(props) {
    const dom = useRef(null);

    useEffect(()=>{
      const handleClick = ()=>{
        console.log('发生了点击事件');
      }
      dom.current?.addEventListener('click',handleClick);

      return()=>{
        dom.current?.removeEventListener('click',handleClick);
      }
    },[]);

    return (
      <div ref={dom}>
        <Component {...props}/>
      </div>
    )
  }
}
  1. useErrorBoundary

React 函数组件中使用useErrorBoundary hook来定义一个错误边界,会返回一个包含两个元素的数组:错误状态和错误信息,当发生错误时,错误状态会被设置为true,错误信息会被更新为错误对象。

javascript 复制代码
function MyComponent() {   
  const [count, setCount] = useState(0);     
  
  function handleClick() {     
    setCount(count + 1);     
    if (count === 2) {       
      throw new Error('Something went wrong');     
    }   
  }  
   
  function handleError(error) {     
    console.error(error);   
  }  
   
  const [hasError, resetErrorBoundary] = useErrorBoundary(handleError); 
    
  return (     
    <div>       
      {hasError ? (         
         <div>           
            <p>Something went wrong.</p>           
            <button onClick={resetErrorBoundary}>Reset</button>         
        </div>       
      ) : (         
        <div>           
           <p>Count: {count}</p>           
           <button onClick={handleClick}>Increment</button>         
        </div>       
      )}     
    </div>   
  ); 
} 

在这个示例中,当count的值等于2时,会抛出一个错误异常。useErrorBoundary会捕获这个异常,并调用handleError函数进行处理。如果hasError的值为true,则说明组件处于错误状态,可以通过resetErrorBoundary函数重置组件并清除错误状态。

第六章 React Hooks

1. React Hooks设计动机

React-Hooks是React团队在16.8版本中推出的,其中包含了对类组件和函数组件的理解和侧重。

React-Hooks是一套能够使函数组件更强大更灵活的钩子,增加了类似于类组件的生命周期等API。Hooks就像是一个工具箱,任由开发者按需引入和使用,从而壮大其函数组件能力。

  1. 类组件
scala 复制代码
class App extends React.Component{
    constructor(){
        super(props);
    }
}

类组件是面向对象编程的一种思想表现。

  • "封装":将一类属性和方法聚拢到一个Class中
  • "继承":新的Class通过继承其他Class,实现对某一类属性和方法的复用
  1. 函数组件
javascript 复制代码
const App = function(){
    return(
        <div>{Text}</div>
    )
}

函数组件较类组件而言,更加轻量和灵活,利于代码拆分和逻辑复用。但是更为重要的一点区别是"函数组件会捕获render内部的状态"!

函数组件与类组件在底层的区别其实是"函数式编程"和"面向对象编程"思想的区别,而通过React框架公式也能体现出函数组件更符合React框架的设计思想
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> U I = r e n d e r ( d a t a ) UI = render(data) </math>UI=render(data)

React组件本身的定位就是函数,给定输入就会返回输出。React底层会把声明式的代码转化为命令式的DOM操作,把数据层面的描述映射到UI中,实现页面的更新。

3)两者对比

  • 类组件需要继承React.Component,函数组件不需要
  • 类组件可以访问生命周期方法,函数组件不可以
  • 类组件可以获取实例化后的this,并基于this做各种操作,函数组件不可以
  • 类组件可以定义和维护state状态,函数组件不可以

4)函数组件会捕获render内部的状态解读

在类组件中虽然props本身是不可变的,但是this却是可变的,其绑定的数据可以被修改。如果通过setTimeout将渲染动作推迟,则this.props捕获到的数据和渲染的数据不一致,从而导致页面更新错误。

函数组件会捕获render内部的状态其实是指函数组件可以将props和渲染绑定在一起,从而实现数据和页面的一致性。

所以React官方也觉得函数组件是一个更加匹配其设计理念,更加有利于逻辑拆分和重用的组件表达形式。

2. React Hooks分类

2.1 useState

scss 复制代码
const [state,setState] = useState(initialState);

作用:为函数组件引入状态,类似于类组件的this.state。

useState调用流程:

  • 初始化阶段
  • 更新阶段
  • mountState方法:构建链表并渲染
  • updateState方法:依次遍历链表并渲染

Hooks并不是以数组的形式存在于底层,而是以链表的形式。

2.2 useRef

通过createref或者useref创建ref对象,通常用于获取DOM节点或者组件实例

sql 复制代码
{current:null}

forwardref的初衷就是解决ref不能垮层级捕获的传递的问题,forwardref接受父级元素标记的ref信息,并把它转发下去,使得子组件可以通过props来接收上一层级或者更高层级的ref

javascript 复制代码
const NewFather = React.forwardref((props,ref)=> (
    <Father grandref={ref} {...props} />
))

<NewFather ref={(node) => this.node = node} />

ref对象特点:

  • ref会保持render的最新状态
  • ref在底层是同步更新执行
  • ref更新不会触发组件重新渲染

ref对象使用场景:

  1. 保存一些不依赖于视图更新的数据
javascript 复制代码
// 计时器指针
function App() {
  const timer = useRef(null);

  const start = () => {
    timer.current = setInterval(() => {
      // 设置新的状态值时记得这样写~
      setNum(num => num + 1)}, 1000)
  };
} 
  1. 获取dom对象
javascript 复制代码
const domRef = useRef()

return <div ref={domRef}>按钮</div>
  1. 用于父子组件组件通讯

父组件获取子组件的dom对象:

javascript 复制代码
import React, { forwardRef, useRef } from 'react';

// 需要使用 forwardRef 包裹子组件
const MyInput = forwardRef((props, ref) => {
  //绑定ref属性
  return <input {...props} ref={ref} />;
});

export default function Form() {
  // 声明ref变量
  const inputRef = useRef(null);

  // 通过current访问DOM:inputRef.current.focus()

  return (
    <>
     {/* 传递ref属性 */}
      <MyInput ref={inputRef} />
    </>
  );
}

父组件获取子组件内的数据或方法:子组件需要使用useImperativeHandle暴露出对应的数据与方法;

typescript 复制代码
interface RefType {num: number,add: () => void}

function Parent() {
  const childRef = useRef<RefType>({num: 1,add: () => {}});
  const RefChild = forwardRef(Child);

  const handler = () => {
    console.log(childRef.current) // { num: 1, add: Function }
  };

  return (
    <div>
      <button onClick={handler}>获取子组件数据</button>
      <RefChild ref={childRef} />
    </div>
  )
}

const Child: React.ForwardRefRenderFunction<RefType, any> = (props, ref) => {
  const [num, setNum] = useState(1);
  const add = () => {
    setNum(num + 1)
  };

  // 对外暴露出数据和方法
  useImperativeHandle(ref, () => {
    return {num,add}
  });

  return <div onClick={add}>{num}</div>
}

2.3 useEffect

scss 复制代码
useEffect(()=>{
    ...
},[]);

作用:允许函数组件执行副作用,类似于类组件的componentDidMount,componentDidUpdate,componentWillUnmount三个生命周期钩子。

执行时机:

  • 每一次渲染都执行的副作用
scss 复制代码
useEffect(callback);
  • 仅在挂载阶段执行一次的副作用
ini 复制代码
useEffect(callback,[]);
  • 仅在挂载阶段和卸载阶段执行的副作用
scss 复制代码
useEffect(()=>{
    // 挂载阶段执行
    ... ...
    
    // 卸载阶段执行
    return ()=>{
       ... ... 
    }
},[]);
  • 每一次渲染都执行,且卸载阶段也会执行的副作用
javascript 复制代码
useEffect(()=>{
    // 挂载、更新阶段执行
    ... ...
    
    // 卸载阶段执行
    return ()=>{
       ... ... 
    }
});
  • 依赖项改变才会触发的副作用
scss 复制代码
useEffect(()=>{
    // 依赖项改变时执行
    ... ...
},[state1,state2,...]);

3. React Hooks优点

  1. 告别难以理解的Class

难点一:this

this在获取和使用时都需要格外注意,如果直接使用函数时,不会获取到this,需要使用箭头函数或者bind改变this指向问题。

难点二:生命周期

对于开发者而言,生命周期的学习成本过高,而且容易形成滥用,导致性能降低。

  1. 利于业务逻辑拆分和复用

Hooks出现之前逻辑复用采用的是HOC(高阶组件)和 Render Props两个组件设计模式,但是在实现组件复用逻辑的同时,由于嵌套过深也破坏了组件的结构。

Hooks可以在不破坏组件结构的前提下,实现逻辑复用。

  1. 函数式编程更符合React框架思想

4. React Hooks局限

  • 没有完全补全类组件的能力
  • 不能很好的消化"复杂",在过于复杂和过度拆分中不好把握
  • 在使用层面有严格的规则约束

5. React Hooks设计原理

React-Hooks在使用时需要遵守两大原则:

  • 只能在函数组件中使用Hook
  • 不能在循环、条件和嵌套函数中调用Hook

React底层需要确保Hook的调用顺序保持不变,否则会报错

第七章 React更新驱动

1. React驱动源

state:state是组件自身的状态,改变state是让React应用自发更新的主要方式。

props:子组件消费父组件的状态叫做props,props的改变导致子组件更新,重新渲染。

2. state

2.1 分类

  1. setState
typescript 复制代码
constructor(props:any){
    super(props);
    this.state={
      isError:false
    }
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
    this.setState({
      isError:true
    })
}
  1. useState

useState返回值包含内部状态state和改变state的dispatch,对于dispatch,有两种类型:函数和非函数。

第一种是非函数情况:直接将值作为新的值赋值给state,根据React性能优化特性,传入的对象会浅层合并到新的 state 中,在同一周期内会对多个 setState 进行批处理。

scss 复制代码
const handleclick = ()=>{
    setNum(num + 1);
    setNum(num + 1);
    setNum(num + 1);
    setNum(num + 1);
    setNum(num + 1);
}

第二种是函数情况:函数的参数是上一次返回的最新的state,函数返回值作为新的state值。dispatch更新时在本次函数上下文获取不到最新的state值,只能在下一次函数组件执行时state才会被更新。

ini 复制代码
const handleclick = ()=>{
    setNum(prevState => prevState + 1);
    setNum(prevState => prevState + 1);
    setNum(prevState => prevState + 1);
    setNum(prevState => prevState + 1);
    setNum(prevState => prevState + 1);
}

2.2 异步更新

浏览器的事件循环中包括宏任务和微任务,其中宏任务并不会阻塞浏览器的渲染和响应,而微任务由于是在当前JS执行完成后立即执行,会阻塞浏览器的渲染和响应。一次宏任务完毕后,会清空微任务队列

Vue的$nextTick在底层是依靠Promise.resolve()创建的微任务,从而实现在本次事件执行阶段获取state值

React利用队列机制实现setState的异步更新,避免频繁更新state,实现性能优化。

当执行setState时,会将需要更新的state合并后放入状态队列,而不会立即更新state。React更新机制会检测目前是否为批量更新模式,如果是的话则放入更新队列,不是的话就立即执行更新任务。

直接修改的state值不会被放入队列中,后续合并队列状态时会忽略直接修改的值

state状态更新后,将新的state合并到队列中

ini 复制代码
var nextState = this._processPendingState(nextProps,nextContext);

根据更新后的队列和shouldComponentUpdate状态决定是否更新组件

ini 复制代码
var shouldUpdate = this._pendingForceUpdate || 
    !inst.shouldComponentUpdate || 
    inst.shouldComponentUpdate(nextProps,nextContext);

useState的dispatch在处理前后state时,采用的是浅比较。如果两次state指向相同的内存地址,默认其是相等,不会开启更新调度,为了解决这个问题,可以将state浅复制,重新申请一个内存空间。

scss 复制代码
const [obj,setObj] = useState({name:'lily'});

useEffect(()=>{
  // 默认相等
  obj.name = 'nancy';
  setObj(obj);
  // 改变内存地址
  setObj(...obj,{name:nancy});
},[]);

如果在shouldComponentUpdate和componentWillUpdate中调用了setState,会形成setState的循环调用情况,导致浏览器内存占满崩溃。

2.3 批量更新

触发setState后,会同时引发组件生命周期的一些钩子函数,如下:

由于render对于性能消耗特别大,所以React底层的setState在正常情况下是异步批量更新的。

⏰批量更新的逻辑:每触发一个setState就把对应的任务 放入队列 中,等到时机成熟,就把积攒的所有setState结果做合并 ,最后只针对最新的state值一次更新流程。

2.4 state工作流

  1. setState入口函数
kotlin 复制代码
ReactComponent.prototype.setState = function(partialState,callback){
    this.updater.enqueuesetState(this,partialState);
    if(callback){
        this.updater.enqueuesetCallback(this,callback,'setState');
    }
}
  1. enqueuesetState关键函数
ini 复制代码
enqueueSetState = function(publiclnstance, partialState){
    // 根据this拿到对应的组件实例
    var internallnstance = getlnternallnstanceReadyForUpdate(publiclnstance,'setState');
    
    // 这个queue对应的就是一个组件实例的state数组
    var queue = internallnstance._pendingStateQueue || (internallnstance._pendingStateQueue = [];
    queue.push(partialState);
    
    // enqueueUpdate用来处理当前的组件实例
    enqueueUpdate(internallnstance);
}

主要工作如下:

  • 将新的state任务放入组件的状态队列中
  • 使用enqueueUpdate处理将要更新的组件实例对象
  1. enqueueUpdate更新组件的函数
scss 复制代码
function enqueueUpdate(component) {
    ensurelnjected();

    // isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
    // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
    if(!batchingStrategy.isBatchingUpdates){
        batchingStrategy.batchedUpdates(enqueueUpdate,component);
        return;
    }
    // 否则,先把组件塞入 dirtyComponents 队列里,让它"再等等'
    dirtyComponents.push(component);
    if (component._updateBatchNumber === null){
        component._updateBatchNumber = updateBatchNumber + 1
    }
}
  1. batchingStrategy控制批量更新的关键锁
javascript 复制代码
var ReactDefaultBatchingStrategy = {
    // 全局唯一的锁标识
    isBatchingUpdates:false,
    
    // 发起更新动作的方法
    batchedUpdates:function(callback, a, b, c, d, e){
        // 缓存初始锁变量
        var alreadyBatchingStrategy = ReactDefaultBatchingStrategy.isBatchingUpdates
        // 把锁"锁上"
        ReactDefaultBatchingStrategy.isBatchingUpdates = true
        
        if(alreadyBatchingStrategy){
            callback(a, b, c, d,e);
        }else{
        // 启动事务,将callback放进事务里执行
        transaction.perform(callback, null, a, b, c, d, e);
       }
   }
}

ReactDefaultBatchingStrategy.isBatchingUpdates状态值区分:

  • true:当前处于批量更新状态,会将state任务放入组件事件队列中,等待更新
  • false:当前没有处于批量更新状态,会立即执行state任务

2.5 setState调用场景

  1. 正常调用------异步更新
kotlin 复制代码
increment = ()=> {
    // 进来先锁上
    isBatchingUpdates = true;
    
    console.log('increment setState前的count',this.state.count);
    this.setState({
        count: this.state.count + 1
    });
    
    console.log('increment setState后的count',this.state.count);
    
    // 执行完函数再放开
    isBatchingUpdates = false;
}
  1. setTimeout包裹时------同步更新
javascript 复制代码
increment = ()=> {
    // 进来先锁上
    isBatchingUpdates = true;
    
    // isBatchingUpdates的开关属于主线程任务,所以无法约束setTimeout内部代码的执行
    setTimeout(()=>{
        console.log('increment setState前的count',this.state.count);
        this.setState({
            count: this.state.count + 1
        });
        console.log('increment setState后的count',this.state.count);
    },0)
    
    // 执行完函数再放开
    isBatchingUpdates = false;
}

第八章 class组件生命周期

1. 生命周期流程

如果把render方法比喻为React整个生命周期的"灵魂",那么其他钩子则为"躯干"。

scala 复制代码
class LifeCircle extends React.Component {
    render(){
        console.log("render方法执行")
        return(
            <div>...</div>
        )
    }
}

2. React15生命周期

  1. Mounting阶段

组件的初始化渲染阶段(挂载)

  • constructor
typescript 复制代码
constructor(props:any){
    super(props);
    this.state={
      isError:false
    }
}

首先执行constructorClassInstance函数,用来实例化组件。

作用:初始化state,绑定this,生命周期劫持,渲染劫持,反向继承HOC

mount Component 阶段本质上是通过递归渲染内容,由于递归特性,父组件的componentWillMount在其子组件的componentWillMount之前调用,父组件的componentDidMount在其子组件的componentDidMount之后调用。

  1. Updatng阶段

组件更新,包括子组件内部状态改变导致的更新和父组件更新引发的子组件更新

  • componentWillReceiveProps方法
scss 复制代码
componentWillReceiveProps(nextProps)

componentWillReceiveProps方法不是由props的变化触发的,而是由父组件的更新触发的。

  • shouldComponentUpdate方法
scss 复制代码
shouldComponentUpdate(nextProps,nextState)

React组件根据shouldComponentUpdate方法的返回值决定是否执行之后的生命周期钩子函数,进而决定是否执行组件的re-render(重渲染)。该方法的默认值是true,表示会重新渲染组件。可以通过自动设置内部的实现逻辑,来控制组件的re-render,或者引入pureComponent方法等,进一步实现性能优化。

update Component阶段本质上是通过递归渲染内容,由于递归特性,父组件的componentWillUpdate在其子组件的componentWillUpdate之前调用,父组件的componentDidUpdate在其子组件的componentDidUpdate之后调用。

  1. Unmounting阶段

组件卸载阶段,该阶段可以执行一些收尾工作。

组件卸载场景:子组件被移除、组件包含key值且key发生改变、切换路由等

3. React16生命周期

  1. Mounting阶段

组件的初始化渲染阶段(挂载)

  1. Updatng阶段

组件更新,包括子组件内部状态改变导致的更新和父组件更新引发的子组件更新

  1. Unmounting阶段

组件卸载阶段,该阶段可以执行一些收尾工作。

4. 生命周期优化

  1. 新增了getDerivedStateFromProps方法

⚠️与此同时,废弃了componentWillMount方法

getDerivedStateFromProps不是componentWillMount的替代品,其出现是为了代替componentWillReceiveProps,其用途有且仅有一个,"使用props来派生/更新state"。

getDerivedStateFromProps方法在componentWillReceiveProps的基础上做了减法,该方法并不能完全代替componentWillReceiveProps,但是其安全性较componentWillReceiveProps得到了很大的提升。

scss 复制代码
static getDerivedStateFromProps(nextProps,prevState);
  • nextProps为父组件新传入的props
  • prevState为组件在此次渲染前待更新的state

特点如下:

  • 静态方法,内部访问不到this
  • 接收两个参数,props和state。props为父组件传递的外部props,state是组件内部初始化的state变量
  • 必须返回一个对象格式的值,React会利用返回值派生/更新组件内部的state,如果没有返回值,可以用null代替

getDerivedStateFromProps对state的更新不是覆盖式的,而是定向更新某个属性,新属性和state原有属性会组合成为state的新值

  1. 新增了getSnapshotBeforeUpdate方法

⚠️与此同时,废弃了componentWillUpdate方法

javascript 复制代码
getSnapshotBeforeUpdate(prevProps,prevState){}

该方法类似于getDerivedStateFromProps方法,区别是getSnapshotBeforeUpdate方法的返回值回作为参数传递给componentDidUpdate,其执行时机在render方法之后,真实DOM更新之前。在此阶段可以获取到更新前的真实DOM和更新前后的state&props信息。

💡一般在使用时要配合componentDidUpdate方法。

5. 生命周期优化本质

Fiber是React16对React核心算法的一次重写,其原本同步的渲染过程变成异步的。

React15及之前的版本中,diff算法会对新旧虚拟DOM树进行递归遍历,查找发生变化的部分,从而实现组件的更新。整个递归过程是同步的,且不可中断,对于浏览器性能要求很高,在此过程中,浏览器无法处理用户的任何操作,从而造成用户交互行为无响应的现象即页面卡顿等性能问题。

React16的Fiber通过将一个大的任务拆解为许多小的任务,从而避免了任务过长导致的页面卡顿问题。Fiber架构的重要特征就是可以被打断的异步渲染的重要模式,根据能否被打断这一标准,React16的生命周期被划分为render阶段和commit阶段。其中commit阶段又被划分为pre-commit和commit两部分。

  • render阶段:执行过程中允许暂停、终止和重启,其生命周期会被重复执行
  • commit阶段:同步执行,不允许中断

🤔1.为什么废弃了componentWillMount、componentWillUpdate、componentWillReceiveProps三个生命周期?

🙋:首先是因为Fiber模式导致的render中的生命周期会重复执行,而这三个钩子的滥用极其影响性能。其次是这三个钩子中的操作可以完全转移到其他钩子中,常用的是componentDidMount和componentDidUpdate两个。并且在新增加的两个钩子中,获取不到this,所以可以避免一些误操作导致的bug。

6. 类组件不同阶段执行流程

  1. First Render 执行流程:

getDefaultProps => gteInitialState => componentWillMount => render => componentDidMount

  1. Unmount 执行流程:

componentWillUnmount => second render => gteInitialState => componentWillMount => render => componentDidMount

  1. Props Change 执行流程:

componentWillReceiveProps => shouldComponentUpdate => componentWillUpdate => render =>componentDidUpdate

  1. State Change 执行流程:

shouldComponentUpdate => componentWillUpdate => render =>componentDidUpdate

第九章 Function生命周期

1. useEffect

React采用异步调用useEffect,React会像setTimeout回调函数一样,放入任务队列中,等待主线任务完成,DOM更新结束,JS执行完成后才执行,所以其副作用的执行不会阻塞浏览器绘制视图。

useEffect可以理解为"有副作用的因变量"。

scss 复制代码
componentDidMount:useEffect(()=>{},[]);

componentDidUpdate: useEffect(()=>{});

componentWillUnMount: useEffect(()=>{ return()=>{} },[]);

componentWillReceiveProps: useEffect(()=>{},[props]);

useEffect接收的第二个参数为依赖项,依赖项改变会触发重新渲染,重新渲染后会再次执行useEffect的副作用,useEffect是异步执行。useEffect执行的是副作用,在浏览器渲染后执行的。

  • 依赖项为空:组件挂载前、组件卸载前触发useEffect
  • 依赖项不为空:组件挂载前、依赖项发生改变会触发useEffect
  • 省略依赖项:组件挂载前、组件重新渲染、组件卸载前会触发useEffect

适用场景:

  • 注册监听并在组件销毁时回收监听
  • 组件初始渲染时请求接口获取数据
  • 监听状态执行副作用

注意事项:

  • 依赖项为对象时,改变对象属性的同时改变对象引用
  • 依赖项为函数时,每次渲染都会执行useEffect的副作用,可以将函数添加到useEffect内部,只监听函数依赖项或者将函数使用useCallback包裹起来

以下面hook组件为例,模拟执行时机:

javascript 复制代码
import React, { useEffect, useState } from "react";

const Item = (props,ref) => {
    const [count, setCount] = useState(0);
    const handleClick = ()=>{
        setCount(count+1);
    }
    useEffect(()=>{
        console.log('省略依赖项的副作用执行时机');

        return(()=>{
            console.log('省略依赖项的回调执行时机');
        })
    });
    {console.log('子组件render')}

    return (
        <div className="item"
            <h3>子组件</h3>
            <Button onClick={handleClick}>+1</Button>
        </div>
    );
};
  • 省略依赖项:
javascript 复制代码
useEffect(()=>{
    console.log('省略依赖项的副作用执行时机');

    return(()=>{
        console.log('省略依赖项的回调执行时机');
    })
});
  • 依赖项为空:
javascript 复制代码
useEffect(()=>{
    console.log('省略依赖项的副作用执行时机');

    return(()=>{
        console.log('省略依赖项的回调执行时机');
    })
},[]);

依赖项不为空,且改变依赖项使组件re-render:

javascript 复制代码
useEffect(()=>{
    console.log('省略依赖项的副作用执行时机');

    return(()=>{
        console.log('省略依赖项的回调执行时机');
    })
},[count]);

如果依赖项不为空,如果依赖项不更新也不会触发useEffect的副作用和回调

2. useLayoutEffect

useLayoutEffect的执行时机是在DOM更新之后,浏览器绘制之前,在这个阶段可以获取DOM信息,从而使浏览器只绘制一次即可,但同时会阻塞浏览器绘制。

scss 复制代码
useLayoutEffect(()=>{},[]);

修改DOM,改变布局时使用useLayoutEffect,其他情况均使用useEffect

3. useInsertionEffect

React 18新增加的一个Hooks,主要用于解决CSS-in-JS在渲染中注入样式的性能问题,useInsertionEffect执行时机是在DOM更新前,会对浏览器造成阻塞。

scss 复制代码
useInsertionEffect(()=>{},[]);
相关推荐
进取星辰13 小时前
21、魔法传送阵——React 19 文件上传优化
前端·react.js·前端框架
恋猫de小郭17 小时前
React Native 前瞻式重大更新 Skia & WebGPU & ThreeJS,未来可期
android·javascript·flutter·react native·react.js·ios
蓉妹妹1 天前
React+Taro选择日期组件封装
前端·react.js·前端框架
学渣y1 天前
React文档-State数据扁平化
前端·javascript·react.js
t2007181 天前
5.8 react状态管理
javascript·react.js·ecmascript
进取星辰1 天前
23、Next.js:时空传送门——React 19 全栈框架
开发语言·javascript·react.js
zwjapple1 天前
“ES7+ React/Redux/React-Native snippets“常用快捷前缀
javascript·react native·react.js
ZHOU_WUYI2 天前
用react实现一个简单的三页应用
前端·javascript·react.js
whatever who cares2 天前
React 中 useMemo 和 useEffect 的区别(计算与监听方面)
前端·javascript·react.js
老兵发新帖2 天前
前端知识-hook
前端·react.js·前端框架