【RC01】一口气看完React及核心原理

449 阅读46分钟

image.png

前言

我理解的React:
React中,可以将组件理解成一个状态机,一开始有个初始状态,然后用户互动,导致状态变化,从而触发重新渲染。

1.React基础

1.1 ReactDom.render(xxx, xxx)

ReactDom包里只要封装了Dom操作相关的包。 其中 render() 将模版转为HTML语言,并插入指定的DOM节点。

ReactDom.render(<h1>Hello</h1>, document.getElementById('root'))

1.2 ReactDom.createPortals(xxx, xxx)

提供了一种将子节点渲染到存在于父组件意外的DOM节点的优秀方案。

尽管portal可以被放置在dom树的任何地方,但在任何其他方面,其行为和普通的React子节点行为一致。仍存在于一棵React DOM树中。

1.3 挂载原理

1.3.1 元素渲染

元素是构成react应用的最小块,与浏览器的DOM元素不同,React元素是开销极小的普通对象。ReactDom会来更新DOM与React元素保持一致。

1.2 JSX语法与模版

JSX 语法的 本质 是用React.createElement(),所以他既不是字符串,也不是html,是js的扩展语法。

遇到HTML标签("<"开头),就用HTML规则解析,遇到代码块("{"开头)就用javascript解析

image.png

1.3 组件

  • 组件的第一个字母必须大写,否则会报错。
    • React在解析所有的标签时,是按照标签字母进行区分的,首字母小写,普通html,首字母大写,组件。
  • 组件只能包含一个顶层标签
  • class属性写为className属性
  • style属性时对象,style={{xxx: xxx,}}
  • 没给属性传值,默认传true

1.3.0 组件化:

image.png

1.3.1 生成一个组件类[CC]

# 定义 方式1
const HelloCom = createReactClass({
    render: function() {
        return <h1>this.props.name</h1>
    }
})

# 定义 方式2
class Hello extends React.Component {
    constructor(props) {
        super(props)
        this.state = { 
        // 只能在constructor中定义使用this.state来初始化state
            xxx: yyy,
            num: props.num
        }
    }
    render() {
        xxxx
    }
}

# 调用
React.render(<HelloCom name="John" />, document.getElementById('root'))

所有的组件类都必须有自己的render(),用于输出组件。

1.3.1.1 类组件属性限制 React.PropTypes & Com.propTypes

类组件的propTypes属性,用来验证类组件的实例是否符合要求,不能通过则报错。

React.createClass({
    propTypes: {
        title: React.PropTypes.string.isRequired,
    },
    render() {
        return <h1>{this.props.title}</h1>
    }
   
})
static propTypes: {
  xxx: React.PropTypes.number
}

后面一段时间,PropTypes从React中移除了,但有个第三方组件prop-types支持引入。

再后来TS火爆,这部分就不需要再做类型限制了

1.3.1.2 类组件属性默认值 getDefaultProps() \ static defaultProps

React.createClass({
    getDefaultProps() {
        return {
            title: "default string"
        }
    },
    render() {
        return <h1>{this.props.title}</h1>
    }
   
})

组件创建之前,会初始化默认的props和state

static defaultProps: {
  xxx: yyy
}

1.3.2 Function Component[FC]

1.3.2.1 props

在组件中,如果想要使用外部传递过来的数据,
必须显式的在构造函数参数中,定义 props 属性来接收;
通过 props 得到的任何数据都是只读的,不能重新赋值 
function Hello(props) { 

    return <div> <h1>这是在 Hello 组件中定义的元素 --- {props.name}</h1> </div>
1.3.2.2 每个组件内必须引入React对象

每个组件必须有 react 引入,否则会报错

import React from 'react'

1.4 组件状态

React中,可以将组件理解成一个状态机,一开始有个初始状态,然后用户互动,导致状态变化,从而触发重新渲染。

1.4.1 this.state[CC]

1.4.1.1 getInitialState()

定义初始状态,也就是一个对象,对象可以通过this.state获取

React.createClass({
    getInitialState() {
        return {
            xxx: yyy
        }
    }
})

1.4.1.2 this.state初始化

只能在constructor中定义使用this.state来初始化state

class Hello extends React.Component {
    constructor(props) {
        super(props)
        this.state = { 
        // 只能在constructor中定义使用this.state来初始化state
            xxx: yyy 
        }
    }
    render() {
        xxxx
    }
}

另外,后续操作中,用 this.state.msg = '123' 为 state 上的数据重新赋值,可以修改 state 中的数据值,但是,页面不会被更新,所以这种方式,React 不推荐。

1.4.1.3 this.setState({xxx: yyy})

this.setState()修改状态,并且每次修改后,自动调用this.render方法,再次渲染组件

React.createClass({
    getInitialState() {
        return {
            xxx: yyy
        }
    },
    handleClick(e) {
        this.state.xxx
        this.setState({xxx: yyy})
    }   
})

this.setState 方法,传参一个对象,也支持传递一个 function,如果传递的是 function,则在 function 内部, 必须 return 一个 对象;

// 在 function 的参数中,支持传递两个参数, 
// 第一个参数是 prevState,表示为修改之前的 老的 state 数据 
// 第二个参数,是 外界传递给当前组件的 props 数据 

this.setState( function (prevState, props) { 
    // console.log(props) 
    return { msg: '123' } 
  }, function () { 
    // 由于 this.setState 是异步执行的,
    // 所以,如果想要立即拿到最新的修改结果,最保险的方式, 
    // 在回调函 数中去操作最新的数据 
    console.log(this.state.msg) 
  }
)

经过测试发现, this.setState 在调用的时候,内部是异步执行的。

1.4.1.4 setState 什么时候是同步的,什么时候是异步的?

只要你进入了 react 的调度流程,那就是异步的。只要你没有进入 react 的调度流程,那就是同步的。什么东西不会进入 react 的调度流程? setTimeout setInterval ,直接在 DOM 上绑定原生事件等。这些都不会走 React 的调度流程,你在这种情况下调用 setState ,那这次 setState 就是同步的。 否则就是异步的。

而 setState 同步执行的情况下, DOM 也会被同步更新,也就意味着如果你多次 setState ,会导致多次更新,这是毫无意义并且浪费性能的。

executionContext 代表了目前 react 所处的阶段,而 NoContext 你可以理解为是 react 已经没活干了的状态。而 flushSyncCallbackQueue 里面就会去同步调用我们的 this.setState ,也就是说会同步更新我们的 state 。所以,我们知道了,当 executionContext 为 NoContext 的时候,我们的 setState 就是同步的。

当 react 进入它自己的调度步骤时,会给这个 executionContext 赋予不同的值,表示不同的操作以及当前所处的状态,而 executionContext 的初始值就是 NoContext ,所以只要你不进入 react 的调度流程,这个值就是 NoContext ,那你的 setState 就是同步的。

React通过状态队列机制实现了 setState 的异步更新避免重复的更新 state

setState 通过一个队列机制来实现 state 更新,当执行 setState() 时,会将需要更新的 state 浅合并(assign)后放入 状态队列,而不会立即更新 state队列机制可以高效的批量更新 state

将 nextState 浅合并到当前 state。这是在事件处理函数和服务器请求回调函数中触发 UI 更新的主要方法。不保证 setState 调用会同步执行,考虑到性能问题,可能会对多次调用作批处理

举个例子:
假设 state.count === 0

this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});

// state.count === 1, 而不是 3
分析:
本质上等同于:
// 假设 state.count === 0
Object.assign(state,
              {count: state.count + 1},
              {count: state.count + 1},
              {count: state.count + 1}
             )
// {count: 1}

如何解决这个问题呢:

可以传递一个回调函数 function(state, props) => newState 的函数作为参数。 这会将一个原子性的更新操作加入更新队列,在设置任何值之前,此操作会查询前一刻的 state 和 props。

setState() 并不会立即改变 this.state ,而是会创建一个待执行的变动。调用setState后访问 this.state 有可能会得到当前已存在的 state(指 state 尚未来得及改变)。

在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有React生命周期直接处理函数。

原因:
在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是(false)直接更新this.state还是放到队列中回头再说(true)
isBatchingUpdates默认是false,也就表示setState会同步更新this.state,
但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state。

React setState 笔试题,下面的代码输出什么?
class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  //生命周期和事件处理函数中
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};

# 分析:

- 1、第一次和第二次都是在 react 自身生命周期内,触发时 `isBatchingUpdates` 为 true,
所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0。

- 2、两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,
在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。

- 3、setTimeout 中的代码,触发时 isBatchingUpdates 为 false,所以能够直接进行更新,
所以连着输出 23。

输出: 0 0 2 3

在 componentDidMount 中调用 setState 时,batchingStrategyisBatchingUpdates 已经被设为 true,所以两次 setState 的结果并没有立即生效,而是被放进了 dirtyComponents 中。这也解释了两次打印this.state.val 都是 0 的原因,新的 state 还没有被应用到组件中。

再反观 setTimeout 中的两次 setState,如上述所示,第一个setState和第二个setState在批量更新条件之内执行,所以打印不会是最新的值, 但是如果是发生在setTimeout中,由于eventLoop 放在了下一次事件循环中执行(执行上下文变了),此时 batchedEventUpdates 中已经执行完isBatchingEventUpdates = false,所以批量更新被打破,我们就可以直接访问到最新变化的值了。也就导致了新的 state 马上生效,没有走到 dirtyComponents 分支。也就是,setTimeout 中第一次 setState 时,this.state.val 为 1,而 setState 完成后打印时 this.state.val 变成了 2。第二次 setState 同理。 (因为componentDidMount执行完毕后,就已经退出了 react 的调度) isBatchingUpdates 默认值为 false,当 react 自身的事件处理函数或 react 生命周期触发时,isBatchingUpdates 会被赋值为 true,当更新完成时又会被复原为 false

1.4.1.5 useState的setState

自从 react 出了 hooks 之后,函数组件也能有自己的状态,那么如果我们调用它的 setState 也是和 this.setState 一样的效果吗?

对,因为 useState 的 set 函数最终也会走到 scheduleUpdateOnFiber ,所以在这一点上和 this.setState 是没有区别的。

但是值得注意的是,我们调用 this.setState 的时候,它会自动帮我们做一个 state 的合并,而 hook 则不会,所以我们在使用的时候要着重注意这一点。

另外,hook还有闭包陷阱问题。

1.4.2 useState & useReducer [FC]

1.4.2.1 useState()

一个类组件只有一个 setState,但是一个函数组件却可以有很多 useState,相当于拆解setState为多个useState,粒度细化,只关注每个变量,状态分开管理,逻辑清晰,方便维护。

function App(){
    const [count,setCount] = useState(0);
    setCount(1)
}

useState()返回一个数组,两项分别解构为count和setCount
setCount不改变count值(基础数据类型)时组件不会重新渲染。

问题【1】: useState怎么知道返回的是当前组件的count而不是其他组件的count?
因为 js是单线程的,因此当useState被调用时只可能在唯一一个组件的上下文中;

问题【2】:如果一个组件中有多个state,useState怎么知道那次调用返回哪个state呢?
useState按照其 第一次运行的次序返回 对应的state的。会有个队列

        let id = 0;
        function App (){
            let name,setName;
            let count ,setCount;
            id += 1;
            if(id & 1){
                //正序
                [count,setCount] = useState(0);//位置1
                [name,setName] = useState('Mike');//位置2
            }else{
                //反序
                [name,setName] = useState('Mike');
                [count,setCount] = useState(0);
            }
            return(
                <button
                    type="button"
                    onClick={() => {setCount(count + 1)    
                    // 对当前解构出count的useState对应的state值进行加1           
                    }}
                >
                Click:{count}----name:{name}
                </button>
            );
        }
        几个状态值: id             count      name
                    1               0          Mike
                    2//换顺序       Mike        1
                    3//再换         1           Mike1
                                    Mike1       2
            初始 按正序 从useState(0)中提取出 count 为 0useState('Mike')中 name 为mike;
            点击第一次, 按反序解构,useState()是按照第一次执行的顺序来的;
            !!!!注意:要记住,每个state状态 对应在  第一次执行时所在位置的useState中,
            无论变量名变成什么。
            对应的state值一直保存在其中。
            所以此时进行结构,赋值号 右边 相当于 没有调换,只是左边进行了用新变量解构。
            !!!!组件重新渲染后,状态接收新值newState。
            !!!!setState 操作是在dom操作前(渲染前)完成的
    
            setCount与count同时解构出来,故,只对应count进行操作,具体操作为其传入的参数
            加码分析:
                let id = 0;
                function App (){
                    let name,setName;
                    let count ,setCount;
                    id += 1;
                    if(id & 1){
                        [count,setCount] = useState(0);
                        [name,setName] = useState('Mike');

                                 //state状态只与useState执行的顺序有关。
                                 //因此,此处的setCount对第一个state操作
                                 //setName 对第二个State进行操作
 
                                 ## **待渲染完毕后,会重新进行解构**
                    }else{
                        [name,setName] = useState('Mike');
                        [count,setCount] = useState(0);

                                 //此处的setCount对第二个state操作
                                 //setname 对第一个State进行操作
                    }
                    return(
                        <button
                            type="button"
                            onClick={() => {setCount(count + 1)
                                            setName(name+2)}}
                        >
                        Click:{count}----name:{name}
                        </button>
                    );
                }
                //当时,setCount以及setName从哪个useState产生的state中解构出来,
                就是对那个state进行操作。
                该 加码样例 分析:
                首先,初始的count为0,name为mike,state01的值为0,state02的值为mike;
                1次点击,state01 的值进行 setCount操作+1,待渲染完毕后重新赋值给 name :1;
                        state02的值进行setName 操作+2,待渲染完毕后,重新赋值给count :Mike2;
                2次点击,此时重新解构过, state01 解构为[name,setName],因此会按照setName进行操作+2,
                                        state02 解构为[count,setCount],因此会按照setCount进行操作+1
    

问题【3】: useState调用的次数不能多也不能少,必须在组件的顶层调用,不能在条件语句里面,固定数量

相信你已经发现问题所在了,React 不允许 hook 处于条件语句是因为 React 把每次 render 中 useState 的顺序值 0、1、2、3 当成了 key,方便后续 render 用 key 查找对应的 state。这样的目的是使 useState 更简洁。(不止 useState,其他 hook 也不允许处于条件语句中)

当然,React 这么做的目的也可能是为了追求函数式或者并发,但显然大部分前端开发者并不在乎这些。react为了追求纯函数的理念,在纯函数中添加副作用,这些use副作用本质上都是声明语句,为了声明与调用的一致性,不可避免地要强调顺序。而条件和循环会破坏这种顺序一致性

注意,并不是因为 hooks 内部使用链表来实现,所以我们必须保证 hooks 的调用顺序。这种观点显然倒置了因果关系,正确的说法是:因为我们保证了 hooks 的调用顺序(不保证就会报错),所以 hooks 内部可以使用链表来实现。

确保 Hook 在每一次渲染中都按照同样的顺序被调用。以 Preact 的 Hook 的实现为例,用数组和下标来实现 Hook 的查找(React 使用链表,但是原理类似),每次 Hook 的调用都对应一个全局的 index 索引,通过这个索引去当前运行组件 currentComponent 上的 _hooks 数组中查找保存的值,也就是 Hook 返回的 [state, useState]

问题【4】:useState默认值 只有第一次传入的有效。

function App(){
      const defaultCount = props.defaultCount || 0;《1》
      const [count,setCount] = useState(defaultCount);《2》
      由《2》知,《1》处defaultCount 值只有在第一次渲染中才会用到,
      useState传入默认值只有第一次有效。每次App渲染都会重新执行《1》处逻辑,因此冗余。
      
      改进优化:
      const [count,setCount] = useState(()=>{ return  props.defaultCount || 0;});
 }
            		
若想之后传进来的  变量值 依旧可以 存到state中,
可以利用useRef暂存每次传进来的值进行比较,
当两次 传进来的 值 不同时, setState()进行更新

问题【5】:setCount不改变count值(基础数据类型)时组件不会重新渲染。 复杂数据类型的某个属性变化 或者 重新赋值一个相同结构的对象,都会重新渲染。

1.4.2.2 useReducer()

useReducer 对于单组件来说太重了。
useReducer 更适合拿来做简单场景下的数据流。useReducer 是阉割版的 redux,只缺了一个状态共享能力,用 hooks 的 useContext 刚刚好,实现项目深层传递。

官方文档中推荐的使用场景是“deep update”,即==局部数据流==。

这可能是 hooks 比起类组件另一个最亮眼的地方,而且配合 hooks 的自由组合能力,==天然把 redux 那坨令人费解的中间件能力给支持了==,说实话我看到 redux 的中间件就头秃。

const reducer = (state, action) => {
    switch (action.type) {
      case 'updateWeakLoading':
        return {
          ...state,
          submitWeakLoading: action.data || false,
        };
      default:
        return state;
    }
  };
  const _initState = {
    submitWeakLoading: false,
  };
  const [_state, _dispatch] = useReducer(reducer, _initState);
  // _dispatch({
  //   type: 'updateWeakLoading',
  //   data: false,
  // });

1.4.2.3 二者的关系

在 React 内部,useState 就是用 useReducer 实现的,useState 返回的函数内部封装了一个 dispatch。类似自定义reducer Hook。

1.4.2.4 总结

项目开发到后期一块很大的维护成本就是大组件,大组件一般都是逻辑复杂、逻辑和 UI 没解耦。

React 推荐的开发方式和逻辑和 UI 分离,类组件时期实现的方式是通过 container 管理数据,UI 组件只负责展示,
但在紧张的项目迭代中要时刻做到这一点是很麻烦的,原因是类组件承载业务逻辑的最小单元是组件,这个粒度太大了。

这里就要说到 useState 为什么要细粒度的使用,其实从它的 API 也可以看出来,
为什么 useState 返回的是一个数组而不是一个对象?
因为可以重命名。
为什么要重命名?
因为需要有明确的语义。
什么时候需要明确的语义?
当这个值的含义很具体的时候。
可以看出 React 希望开发者使用 `useState 管理的状态都是原子级的`,这样有什么好处呢?为了逻辑解耦啊。
否则把一大坨逻辑抽到另一个函数,和放在组件里并没有什么区别。

useState 算是`组件内`状态管理一个非常优雅的解决方案,但是`组件间状态管理的问题还没有解决`。

Redux 是比较符合 React 理念的数据流管理工具(单向数据流、易追踪、严格 immutable),
而且它和 useContext 结合起来简直就是天衣无缝,React 吸收了 redux 的思想,也算是补齐了在数据流上的能力。
可惜 Redux 适合管理大型应用的复杂数据流,简单场景用着总有点头重脚轻的感觉,useReducer 当然也有同样的问题,
他虽然比 redux 轻量,可以做组件内状态管理,但是大多数情况用着会头重脚轻的,除非万不得已,
否则我并不想看到 reducer 那坨模板代码。但是拿来做简易的组件间数据流管理就很合适,
Hooks 在这点上的能力比类组件要强大很多。

1.5 获取真实元素

DOM的本质:由浏览器提供的固定API操作DOM,用JS表示UI元素。

虚拟DOM的本质:使用JS来模拟DOM树。

虚拟DOM的目的:实现DOM节点的高效更新。

1.5.1 什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?

我对 Virtual DOM 的理解是,

首先对我们将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,
比如一个元素对象,包含 TagName、props 和 Children 这些属性。
然后我们将这个 js 对象树给保存下来,最后再将 ** DOM 片段** 插入到文档中。

当页面的状态发生改变,我们需要对页面的 DOM 的结构进行调整的时候,
我们首先  根据变更的  状态,重新构建起一棵对象树,
然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。

最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。

我认为 Virtual DOM 这种方法对于我们需要有大量的 DOM 操作的时候,
能够很好的提高我们的操作效率,
通过在操作前确定需要做的最小修改,尽可能的减少 DOM 操作带来的重流和重绘的影响。
其实 Virtual DOM 并不一定比我们真实的操作 DOM 要快,
这种方法的目的是为了提高我们开发时的可维护性,
在任意的情况下,都能保证一个尽量小的性能消耗去进行操作。

1.5.2 Ref

1.5.2.1 this.refs.[refName] [CC]

在render中打上标识ref='xxx', 再this.refs.xxx获取 样例: image.png

1.5.2.2 React.createRef() [CC]

  • 当 ref 属性被用于一个普通的 HTML 元素时,React.createRef() 将接收底层 DOM 元素作为它的 current 属性以创 建 ref
  • 当 ref 属性被用于一个自定义类组件时,ref 对象将接收该组件已挂载的实例作为它的 current
  • 不能在函数式组件上使用 ref 属性,因为它们没有实例

image.png

可以在函数式组件内部使用 ref,只要它指向一个 DOM 元素或者 class 组件。

1.5.2.3 ref={(cur) => { textInput = cur; }} [FC]

function CustomTextInput(props) { 
    // 这里必须声明 textInput,这样 ref 回调才可以引用它 
    let textInput = null; 
    function handleClick() { 
        textInput.focus(); 
    }
    return ( 
        <div> 
            <input type="text" 
                ref={(cur) => { textInput = cur; }} 
            /> 
            <input type="button" value="Focus the text input" 
                onClick={handleClick} /> 
        </div> );

1.5.2.4 React.useRef() [FC]

类组件中使用是通过在constructor中加入this.ref=React.createRef()实现,在函数式组件中使用useRef实现;

作用:

  • 【1】获取子组件或者Dom节点的句柄;
  • 【2】不同渲染周期之间共享数据的存储;(state赋值会触发重新渲染,ref不会)如果要使用上一次渲染时的一些数据(包括state),只要置于ref中即可,在下次渲染时就可以获得。
#【1】
        function App(props){
            const counterRef = useRef();
            ...
            const onClick = useCallback(()=>{
               console.log(counterRef.current)
             },[counterRef])
            return <div>
                <Counter ref = {counterRef}  onClick={onClick}/>
                //若Counter 组件是函数组件会报错。函数组件没有实例,并不可以直接给它refs属性
                // 上述代码 若Counter是类声明有实例,点击时可打印Counter组件对象(counterRef.current)。
                // 可获取到组件实例对象
                // 证明:函数形式组件暂时不能完全替代类组件
                // 如果counter组件定义时有一些函数成员,之后要调用则可以通过 couterRef.current.[函数成员]
            </div>
        }
#【2】
        function App (){
            let it;
            useEffect(()=>{
                it = setInterval(()=>{
                    setCount(count=>{return count + 1})
                },1000)
            },[]);
            useEffect(()=>{
                if(count>10){
                    clearInterval(it)
                }
            })
        }
        该例子,计时到10并不会停止计时器;
        原因:
            App组件每次重新渲染(刷新)都会重新执行let it;
            而 启动定时器并进行赋值给it的useEffect只执行一次;
            故,但第二个useEffect 进行clearInterval 时,
            it已经不为定时器id了
            
        修改:利用useRef第二个特点,实现不同生命周期间共享数据
        function App (){
            let it = useRef();
            useEffect(()=>{
                it.current = setInterval(()=>{
                    setCount(count=>{return count + 1})
                 },1000)
            },[]);
            useEffect(()=>{
                if(count>10){
                    clearInterval(it.current)
                }
            })
        }

1.6 插槽

1.6.1 利用组件的children属性

  • this.props.children [CC]
  • React.Children [CC]
  • props.children [FC]

返回结果

# this.props.children

    - 如果没有子节点,是undefined
    - 有一个子节点,是对象
    - 多个子节点,是list
    
# React.Children
    - 是list

实例

const NodeList = React.createClass({
    render() {
        return (<ol>
            {
                this.props.children.map(item => {
                    return <li>{item}</li>
                })
            }
        </ol>)
    }
})

ReactDom.render(<NodeList>
    <span>Hello</span>
    <span>World</span>
</NodeList>, document.body)

1.7 Fragments

Fragments 可以让你聚合一个子元素列表,并且不在 DOM 中增加额外节点。

# 方式1
<>
...
</>

# 方式2
<React.Fragment>
...
<.React.Fragment>

1.8 PureComponent & React.memo(FC)

1.8.1 PureComponent [CC]

在 shouldComponentUpdate 函数中我们可以通过返回布尔值来决定当前组件是否需要更新。这层代码逻辑可以是简单地浅比较一下当前 state 和之前的 state 是否相同,也可以是判断某个值更新了才触发组件更新。如果只是单纯的浅比较一下,可以直接使用 PureComponent,底层就是实现了浅比较 state。不必写你自己的shouldComponentUpdate,它只做一个浅比较 ,PureComponent 将会在this.props.[property] 的新旧值之间做一个简单的比较。

一般来说不推荐完整地对比当前 state 和之前的 state 是否相同,因为组件更新触发可能会很频繁,这样的完整对比性能开销会有点大,可能会造成得不偿失的情况。

当然如果真的想完整对比当前 state 和之前的 state 是否相同,并且不影响性能也是行得通的,可以通过 immutable 或者 immer 这些库来生成不可变对象。

class CounterButton extends React.PureComponent {

1.8.2 memo(FC)[FC]

memo 直译过来是“备忘录”的意思,本质上它就是把一个对象的引用记录下来防止函数组件弄丢而已,实现原理也很简单,用 useRef 就可以实现了

对于无状态子组件,采用function的形式。便可以使用memo方法。进行子组件更新渲染的判断。原理就是上述purecomponent效果一致.

const Test = React.memo(() => (<div>PureComponent</div>))

如果props 引用不会变化,子组件不会重新渲染,但组件中的计算依然会重新执行。 如果 Counter 计算量很大,那瓶颈就不是重渲染而是重执行的这个过程了。

实例

function Counter({ count }) {
  console.log('Counter 重新执行了!', count);
  
  
  // 重新执行计算,但没重新执行渲染!!!!
  
  
  // ...进行了一堆很复杂的计算!
  return <span>{count}</span>;
}

function App() {
  const [count, setCount] = React.useState(0);
  const [stateAutoChange, setStateAutoChange] = React.useState(0);

  React.useEffect(() => {
    setInterval(() => {
      setStateAutoChange(s => s + 1);
    }, 500);
  }, []);

  return (
    <div>
      <div>{stateAutoChange}</div>
      <div>
        {/* count 是不会变化的 */}
        <Counter count={count} />
      </div>
    </div>
  );
}

解决: 世界清静了

方案一:

const Counter = React.memo(({ count }) => {
  console.log('Counter 重新执行了!', count);

  // ...进行了一堆很复杂的计算!
  
  return <span>{count}</span>;
});

方案二:

const memoCounter = React.useMemo(() => <Counter count={count} />, [count]);

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

什么时候应该用 memo 和 useMemo?
我们可能只有在复杂应用中才会关注到性能问题(某些简单的应用可能永远都不会出现性能问题- -),我的看法是:

  1. 对于组件开发者来说:开销比较大的组件都要用 memo,开销小的随便。比如上面的例子中,如果 Counter 只是单纯展示 count,重执行重渲染很明显不会造成性能问题,那爱用不用,可能 memo 做浅比较和存储上一个 props 值的开销比 Counter 重执行渲染的开销还要大。但是如果 Counter 涉及到大量计算,就要用 memo,一定可以减少性能开销。

  2. 对于平台/容器层开发者来说:因为你甚至可能不知道接进来的组件是谁开发的,也无法保证组件的质量,那么可以用 useMemo 统一包裹接进来的组件,降低组件带来的性能影响。

  3. 用 useMemo 和 useCallback 来控制子组件 props 的引用,和 memo 一起使用效果是最佳的,原因上面的例子也呈现了,子组件会有重新执行的开销,没有配套 memo 的话还可能出现反效果,这几个 API 各司其职,性能优化是一个整体的过程,不是单独在某一个组件里面做一些操作就可以得到改善的。推荐看一下官方文档 关于性能的 FAQ。

  4. 虽然我说如果组件开销很小那用不用 memo 无所谓,但如果是一个大团队 + 一个非常大型的应用,协同的同学可能非常多,比如我们团队,人数本来就众多,因为业务膨胀,新同学源源不断加进来,还有众多外包同学,这时候代码的质量就需要做一个拉齐,不然维护成本会越来越大,那么可以制定规范(比如组件开发者就必须用 memo,比如传一个回调 prop 就必须用 useCallback),通过 eslint 来约束代码质量,让代码的风格尽量统一。

总之性能问题不是一个单点问题,一旦一个应用出现性能问题一般都要整条链路一起优化,对于复杂应用日常开发中就需要随时关注性能问题了,具体策略也是根据具体情况来,

只要记住几个 API 的功能就可以了:

  • useMemo 避免频繁的昂贵计算,
  • useCallback 让 shouldComponentUpdate 可以正常发挥作用,
  • memo 就是 shouldComponentUpdate。
  • context 导致的频繁更新就另说了,redux 已经提供了解决方案: useSelector。

1.9 HOC

高阶组件和高阶函数就是同一个东西。我们实现一个函数,传入一个组件,然后在函数内部再实现一个函数去扩展传入的组件,最 后返回一个新的组件,这就是高阶组件的概念,作用就是为了更好的复用代码。

image.png

其实 HOC 和 Vue 中的 mixins 作用是一致的,并且在早期 React 也是使用 mixins 的方式。 但是在使用 class 继承的方式 (es6)创建组件以后,mixins 的方式就不能使用了,
并且其实 mixins 也是存在一些问题的,比如:

  • 1 隐含了一些依赖,比如我在组件中写了某个 state 并且在 mixin 中使用了,就这存在了一个依赖关系。万一下次别人要移除它,就得去 mixin 中查找依赖
  • 2 多个 mixin 中可能存在相同命名的函数,同时代码组件中也不能出现相同命名的函数,否则就是重写了,其实我一直觉得命名 真的是一件麻烦事。。
  • 3 雪球效应,虽然我一个组件还是使用着同一个 mixin,但是一个 mixin 会被多个组件使用,可能会存在需求使得 mixin 修改原本的函数或者新增更多的函数,这样可能就会产生一个维护成本。

HOC 解决了这些问题,并且它们达成的效果也是一致的,同时也更加的政治正确(毕竟更加函数式了)

1.9.1 跨层级捕获ref

高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。我们可以通过forwardRef来解决这个问题。

/**
 * 
 * @param {*} Component 原始组件
 * @param {*} isRef  是否开启ref模式
 */
function HOC(Component,isRef){
  class Wrap extends React.Component{
     render(){
        const { forwardedRef ,...otherprops  } = this.props
        return <Component ref={forwardedRef}  {...otherprops}  />
     }
  }
    if(isRef){
      return  React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> )
    }
    return Wrap
}

class Index extends React.Component{
  componentDidMount(){
      console.log(666)
  }
  render(){
    return <div>hello,world</div>
  }
}

const HocIndex =  HOC(Index,true)

export default ()=>{
  const node = useRef(null)
  useEffect(()=>{
     /* 就可以跨层级,捕获到 Index 组件的实例了 */ 
    console.log(node.current.componentDidMount)
  },[])
  return <div><HocIndex ref={node}  /></div>
}
复制代码

打印结果:

forwardRef.jpg

如上就解决了,HOC跨层级捕获ref的问题。

1.10 dangerouslySetInnerHTML

react强制将 字符串 转为 html 标签。

<div dangerouslySetInnerHTML={{ __html: this.props.captchaData}} ></div>

2. 事件绑定

2.1(主要是:类中this指向的问题)[CC]

  • 在事件callback中绑定this

直接绑定是bind(this)来绑定,但是这样带来的问题是每一次渲染是都会重新绑定一次bind;

class Home extends React.Component {

 constructor(props) {
  super(props);
  this.state = {
  };
 }

 del(){
  console.log('del')
 }

 render() {
  return (
   <div className="home">
    <span onClick={this.del.bind(this)}></span>
   </div>
  );
 }
}
  • 在构造函数中绑定this

好处是仅需要绑定一次,避免每次渲染时都要重新绑定,函数在别处复用时也无需再次绑定

class Home extends React.Component {

 constructor(props) {
  super(props);
  this.state = {

  };
  this.del=this.del.bind(this)
 }

 del(){
  console.log('del')
 }

 render() {
  return (
   <div className="home">
    <span onClick={this.del}></span>
   </div>
  );
 }
}
  • 用箭头函数绑定this

箭头函数不仅是函数的'语法糖',它还自动绑定了定义此函数作用域的this,因为我们不需要再对它们进行bind方法

class Home extends React.Component {
 
 constructor(props) {
  super(props);
  this.state = {
 
  };
 
 }
 
 del=()=>{
  console.log('del')
 }
 
 render() {
  return (
   <div className="home">
    <span onClick={this.del}></span>
   </div>
  );
 }
}

2.2 阻止默认行为

React中不能用return false来阻止默认行为。 必须明确使用preventDefault来阻止默认行为。

2.3 React事件机制

2.3.0 写在JSX的事件最终变成了什么?

class Index extends React.Component{
    handerClick= (value) => console.log(value) 
    render(){
        return <div>
            <button onClick={ this.handerClick } > 按钮点击 </button>
        </div>
    }
}

经过babel转换成React.createElement形式,如下:

babel.jpg

最终转成fiber对象形式如下:

fiber.jpg

fiber对象上的memoizedPropspendingProps保存了我们的事件。

2.3.1 React合成事件绑定

2.3.1.1 点击事件是否绑定在了每一个标签上?

事实当然不是,JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault

我们看一下当前 2.3.0 案例中这个元素<button>上有没有绑定这个事件监听器呢?

button_event.jpg

我们可以看到 ,button上绑定了两个事件,一个是document上的事件监听器,另外一个是button,但是事件处理函数handle,并不是我们的handerClick事件,而是noop

noop是什么呢?我们接着来看。

noop就指向一个空函数。
真实的dom上的click事件被单独处理,已经被react底层替换成空函数。

noop.jpg

然后我们看document绑定的事件

document.jpg

可以看到click事件被绑定在document上了。

react并不是一开始,把所有的事件都绑定在document上,而是采取了一种按需绑定,比如发现了onClick事件,再去绑定document click事件。

2.3.1.2 实现合成事件的目的是什么呢?

  • 1 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力
  • 2 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象
    • 这样的方式不仅减少了内存消耗
    • 还能在组件挂载销毁时统一订阅和移除事件
当用户在为onClick添加函数时,React并没有将Click时间绑定在DOM上面。

而是在document处监听所有支持的事件,当事件发生并冒泡至document处时,
React将事件内容封装交给中间层SyntheticEvent(负责所有事件合成)

所以当事件触发的时候,对使用统一的分发函数dispatchEvent将指定函数执行。

image.png

2.3.2 React事件系统详解

分三部分:

  • 事件注册
  • 事件绑定
  • 事件触发

image.png

2.3.2.1 事件注册(初始化)

初始化必要的映射关系

咱们面向过程,通过几个关键函数来解析注册了哪些映射关系

legacy-event/EventPluginRegistry.js

  1. injectEventPluginsByName 注册事件
injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin,
});

填充 namesToPlugins
事件模块插件名 -> 事件模块插件的映射

const namesToPlugins = {
    SimpleEventPlugin,
    EnterLeaveEventPlugin,
    ChangeEventPlugin,
    SelectEventPlugin,
    BeforeInputEventPlugin,
}

并执行下一步 recomputePluginOrdering()

  1. recomputePluginOrdering

遍历namesToPlugins

  • 填充plugins,上面注册的所有插件模块(pluginModule)列表,初始化为空。
const  plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];
  • 遍历当前事件插件模块 pluginModule中的事件类型(eventTypes),并对其中的每个事件(原生)类型执行 publishEventForPlugin(pluginModule.eventTypes[eventName],pluginModule,eventName)
# 当前事件插件模块 pluginModule 

const SimpleEventPlugin = {
    eventTypes:{ 
        'click':{ /* 处理点击事件  */
            phasedRegistrationNames:{
                bubbled: 'onClick',       // 对应的事件冒泡 - onClick 
                captured:'onClickCapture' //对应事件捕获阶段 - onClickCapture
            },
            dependencies: ['click'], //事件依赖
            ...
        },
        'blur':{ /* 处理失去焦点事件 */ },
        ...
    }
    extractEvents:function(topLevelType,targetInst,){ 
    /* eventTypes 里面的事件对应的统一事件处理函数,接下来会重点讲到 */ }
}
dispatchConfig -> 原生事件对应配置项 { phasedRegistrationNames :{ 冒泡 捕获 } } 
pluginModule -> 事件插件 比如SimpleEventPlugin 
eventName -> 原生事件名称。 
  1. publishEventForPlugin 绑定合成事件和
  • 从当前原生事件的配置中,获取 phasedRegistrationNames 所有的合成事件。
  • 遍历phasedRegistrationNames,
    • 获取合成事件名称phasedRegistrationName,如上述'onClick','onClickCapture'。
    • 填充形成 registrationNameModules React 合成事件 -> React 处理事件插件映射关系
        {
            onBlur: SimpleEventPlugin,
            onClick: SimpleEventPlugin,
            onClickCapture: SimpleEventPlugin,
            onChange: ChangeEventPlugin,
            onChangeCapture: ChangeEventPlugin,
            onMouseEnter: EnterLeaveEventPlugin,
            onMouseLeave: EnterLeaveEventPlugin,
            ...
        }   
    
    • 充形成 registrationNameDependencies React 合成事件 -> 原生事件 映射关系
        {
            onBlur: ['blur'],
            onClick: ['click'],
            onClickCapture: ['click'],
            onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
            onMouseEnter: ['mouseout', 'mouseover'],
            onMouseLeave: ['mouseout', 'mouseover'],
            ...
        }
    

2.3.2.2 事件绑定

2.3.2.2.1 diffProperties处理合成事件

第一步,首先我们绑定给hostComponent种类的fiber(如上的button元素),会 button 对应的fiber上,以memoizedProps 和 pendingProps形成保存。 58E6A4AF-1902-42BC-9D11-B47234037E01.jpg

第二步,React在调合子节点后,进入diff阶段,如果判断是HostComponent(dom元素)类型的fiber,会用diff props函数diffProperties单独处理。diffProperties函数在 diff props 如果发现是合成事件(eg.onClick, registrationNameModules中判断) 就会调用legacyListenToEvent函数,注册事件监听器。

2.3.2.2.2 legacyListenToEvent 注册事件监听器

根据React合成事件类型,在registrationNameDependencies中找到对应的原生事件的类型,然后调用判断原生事件类型,大部分事件都按照冒泡逻辑处理,少数事件会按照捕获逻辑处理(比如scroll事件)

调用 legacyTrapBubbledEvent, 执行将绑定真正的dom事件的函数 ,会执行addTrappedEventListener(element,topLevelType,PLUGIN_EVENT_SYSTEM,false)

2.3.2.2.3 addTrappedEventListener 绑定 dispatchEvent,进行事件监听

绑定真正的dom事件的函数,绑定在document上,dispatchEvent 为统一的事件处理函数

addTrappedEventListener(targetContainer,topLevelType,eventSystemFlags,capture)

首先绑定我们的事件统一处理函数 dispatchEvent,绑定几个默认参数,

const listener = dispatchEvent.bind(null,topLevelType,eventSystemFlags,targetContainer)

事件类型 topLevelType 元素上的click ,还有绑定的容器doucment然后真正的事件绑定,添加事件监听器addEventListener 事件绑定阶段完毕。

2.3.2.3 事件触发

一次点击事件,在react底层系统会发生什么?

2.3.2.3.1 执行dispatchEvent
  • 用户交互,触发事件回调,也就是dispatchEvent 事件绑定阶段React事件注册时候,统一的监听器dispatchEvent
    也就是当我们点击按钮之后,首先执行的是dispatchEvent函数,
    因为dispatchEvent前三个参数已经被bind了进去,所以真正的事件源对象event,被默认绑定成第四个参数(上述null)。

  • dispatchEvent中调用 attemptToDispatchEvent 尝试调度事件

attemptToDispatchEvent(topLevelType,eventSystemFlags, targetContainer, nativeEvent);

主要处理流程:

① getEventTarget: 首先获取原生事件 e.target 真实的 dom 元素nativeEventTarget。
② getClosestInstanceFromNode: 根据dom元素,找到与它对应的 fiber 对象targetInst,我们 demo中 button 按钮对应的 fiber。

React 在初始化真实 dom 的时候,
用一个随机的 key internalInstanceKey 指针指向了当前dom对应的fiber对象,
fiber对象用stateNode指向了当前的`dom`元素

fiber_dom.jpg

③ dispatchEventForLegacyPluginEventSystem: 正式进去legacy模式的事件处理系统,也就是我们目前用的React模式都是legacy模式下的,在这个模式下,批量更新。

2.3.2.3.2 legacy模式事件处理系统与批量更新
预备:
    getTopLevelCallbackBookKeeping:从React 事件池中取出一个 bookKeeping,将 topLevelType(事件),targetInst 等属性赋予给事件;
    引入执行批量更新 handleTopLevel ,事件处理的主要函数;
执行:
    对于v16事件池,我们接下来会讲到,首先 batchedEventUpdates为批量更新的主要函数
        通过开关isBatchingEventUpdates来控制是否启用批量更新,置true
        handleTopLevel(bookKeeping),最后的处理逻辑就是执行我们说的事件处理插件(SimpleEventPlugin)中的处理函数`extractEvents`
        isBatchingEventUpdates置为false
释放事件池
    releaseTopLevelCallbackBookKeeping(bookKeeping)

所以如果我们在handerClick里面触发setState,那么就能读取到 isBatchingEventUpdates = true这就是React的合成事件为什么具有批量更新的功能了,就能批量更新了。

2.3.2.3.3 extractEvents形成事件对象event 和事件处理函数队列

extractEvents 可以作为整个事件系统核心函数。首先如果点击按钮,最终走的就是extractEvents函数。 执行事件对应的处理插件中的extractEvents,合成事件源对象,每次React会从事件源开始,从上遍历类型为 hostComponent即 dom类型的fiber,判断props中是否有当前事件比如onClick,最终形成一个事件执行队列,React就是用这个队列,来模拟事件捕获->事件源->事件冒泡这一过程。

事件插件系统的核心extractEvents主要做的事是:

  • ① 首先形成React事件独有的合成事件源对象,这个对象,保存了整个事件的信息。将作为参数传递给真正的事件处理函数(handerClick)。
    # 合成事件源对象
    function SyntheticEvent( dispatchConfig,targetInst,nativeEvent,nativeEventTarget){
      this.dispatchConfig = dispatchConfig;
      this._targetInst = targetInst;
      this.nativeEvent = nativeEvent;
      this._dispatchListeners = null;
      this._dispatchInstances = null;
      this._dispatchCurrentTargets = null;
      this.isPropagationStopped = () => false; /* 初始化,返回为false  */
    
    }
    SyntheticEvent.prototype={
        stopPropagation(){ this.isPropagationStopped = () => true;  }, /* React单独处理,阻止事件冒泡函数 */
        preventDefault(){ },  /* React单独处理,阻止事件捕获函数  */
        ...
    }
    
  • ② 然后声明事件执行队列 ,按照冒泡捕获逻辑,从事件源开始逐渐向上,查找dom元素类型HostComponent对应的fiber ,收集上面的 React 合成事件,例如 onClick / onClickCapture ,对于冒泡阶段的事件(onClick),将 push 到执行队列后面 , 对于捕获阶段的事件(onClickCapture),将 unShift到执行队列的前面。
  • ③ 最后将事件执行队列,保存到React事件源对象上。等待执行。
2.3.2.3.4 触发事件处理函数,释放事件池

最后通过runEventsInBatch执行事件队列,如果发现阻止冒泡,那么break跳出循环,最后重置事件源,放回到事件池中,完成整个流程

函数runEventsInBatch,所有事件绑定函数,就是在这里触发.

function runEventsInBatch(){
    const dispatchListeners = event._dispatchListeners;
    const dispatchInstances = event._dispatchInstances;
    if (Array.isArray(dispatchListeners)) {
    for (let i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) { /* 判断是否已经阻止事件冒泡 */
        break;
      }
      
      dispatchListeners[i](event)
    }
  }
  /* 执行完函数,置空两字段 */
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

dispatchListeners[i](event)就是执行我们的事件处理函数比如handerClick,从这里我们知道,我们在事件处理函数中,返回 false ,并不会阻止浏览器默认行为

2.3.2.4 关于react v17版本的事件系统

React v17 整体改动不是很大,但是事件系统的改动却不小,首先上述的很多执行函数,在v17版本不复存在了。我来简单描述一下v17事件系统的改版。

1 事件统一绑定container上,ReactDOM.render(app, container);而不是document上,这样好处是有利于微前端的,微前端一个前端系统中可能有多个应用,如果继续采取全部绑定在document上,那么可能多应用下会出现问题。

image.png 2 对齐原生浏览器事件
React 17 中终于支持了原生捕获事件的支持, 对齐了浏览器原生标准。同时 onScroll 事件不再进行事件冒泡。onFocus 和 onBlur 使用原生 focusin, focusout 合成。

3 取消事件池
React 17 取消事件池复用,也就解决了上述在setTimeout打印,找不到e.target的问题。

2.3.3 React事件与原生事件:

注意:
不要将react事件与原生事件混用:
阻止react时间冒泡的行为只能用于react合成事件中,对原生事件不起作用; 阻止原生事件冒泡行为可以阻止react事件的冒泡行为;

区别:事件传播与阻止事件传播;事件类型(子集);事件绑定方式;事件对象;

2.3.4 阻止原生事件(e.nativeEvent)与最外层document上的事件间的冒泡

e.nativeEvent.stopImmediatePropagation();

2.3.5 支持的事件(取自大佬博客梳理)

image.png

image.png

image.png

3. React生命周期

3.1 生命周期 [CC]

image.png

3.1.1【组件创建阶段】:(一生只执行一次)

componentWillMount

组件将要被挂载,还没开始渲染虚拟DOM,props和state已经初始化, 但内存中还没构建虚拟DOM

render

第一次开始渲染虚拟DOM,当render执行完,内存中就有了完整的虚拟DOM

componentDidMount

完成了挂载,组件已经渲染到了页面上。
如果我们想操作 DOM 元素,最早,只能在 componentDidMount 中进行;

3.1.2【组件运行阶段】:

componentWillReceiveProps(nextProps)

组件将要接收新属性,只要这个生命周期被触发,就证明父组件向子组件传递了新属性值。

shouldComponentUpdate(nextProps, nextState)

组件是否要被更新,返回true或者false,此时组件尚未被更新,props和state是最新的。
this.state是旧的
this.props是旧的

如果返回的值是 false,则 不会继续执行后续的生命周期函数,而是直接退回到了 运行中 的状态,此时有序 后续的 render 函数并没有被调用,因此,页面不会被更新,但是,组件的state 状态,却被修改了;(即使页面不更新,但是 state 确实变了) 只做一个浅比较

componentWillUpdate(nextProps, nextState)

组件将要被更新,尚未开始更新,内存中的虚拟DOM树还是旧的。
this.state是旧的
this.props是旧的

render

根据最新的state和props重新渲染虚拟DOM树,当render执行完,内存中就有了新的虚拟DOM树,页面还没有更新。

componentDidUpdate(prevProps, prevState)

页面重新渲染,新的state和props等状态同步至页面。

this.state是新的
this.props是新的

3.1.3【组件销毁阶段】(一生只执行一次):

componentWillUnmount

组件将要被卸载

3.2 useEffect & useLayoutEffect[FC]

3.2.1 useEffect()

useEffect 一般用于处理状态更新导致的 side effects。

useEffect 可以看成 componentDidMount / componentDidUpdate / componentWillUnmount 这 3 个生命周期函数的替代。useEffect 是在浏览器渲染结束之后才执行的

第一次渲染那之后的调用相当于componentDidMount,后面渲染之后的调用都相当于 componentDidUpdate。

useEffect 第二个参数非常重要!!!!!!依赖
        为一个可选的数组,根据数组中参数  控制useEffect的执行
        只有数组的每一项都不变的情况下,useEffect才不会重新执行
        两个特例:1)不传数组,即每次渲染后都要执行
                2)传入空数组,因此useEffect只会在第一次渲染执行一次(比如绑定事件的逻辑只执行一次就可以)
    
        数组中每项 不变指的是?????
            值(基本数据类型)不变,与之前useState触发组件重新渲染类似,
            count为1,重新赋值count为1,则不会导致重新渲染

案例演示:state = 0, 监听变化将state = 2。

页面效果:先渲染展示0, 再重新赋值, 再渲染展示2。

解释:useEffect 是在浏览器重绘之后才异步执行的,所以点击按钮之后按钮上的数字会先变成 0,再变成一个随机数;

但还是推荐useEffect: 因为 useEffect 不会阻塞浏览器重绘,而且平时业务中我们遇到的绝大多数场景都是时机不敏感的,比如取数、修改 dom、事件触发/监听…… 所以首推用 useEffect 来处理 side effects,性能上的表现会更好一些。

3.2.2 useLayoutEffect()

React 还有一个官方的 hook 是完全等价于这三个生命周期函数的(componentDidMount / componentDidUpdate / componentWillUnmount),叫 useLayoutEffect.

案例演示:state = 0, 监听变化将state = 2。

页面效果:先重新赋值, 再渲染展示2。

解释:useLayoutEffect 是在浏览器重绘之前同步(阻塞)执行的,所以两次 setCount 合并到 300 毫秒后的重绘里了。等价于class component 中的 三个生命周期函数。

3.2.3 useMemo()

useMemo 是拿来保持一个对象引用不变的。
useMemo 的返回值 可直接 参与渲染。因此 是在渲染期间完成的。
useMemo 定义判断一段 函数逻辑是否重复执行。
不能保证依赖不变化,就一定不会重新执行,因为要考虑到内存优化等

(1)分析

# 组件调用
return <LineChart 
  dataconfig={{ // 取数配置
    ...dataConfig,
    datasetId: getDatasetId(queryId)
  }} 
  fetchData={fetchData} 
/>
# 组件内部

React.useEffect(() => {
    fetchData(dataConfig); //取数
}, [dataConfig, fetchData])

在 Function Component 中我们把依赖交给 React 自动管理了,虽然减少了手动做 diff 的工作量,但也带来了副作用:因为 React 做的是浅比较( Object.is() )所以当 fetchData 的引用变化了,也会导致重新取数

(2)其实React的依赖处理是合理的

但这个重取数逻辑上其实是合理的, 因为对于 React 来说,任何一个依赖项改变了都应该重新处理 hooks 中的逻辑,如果一个依赖的函数改变了,有可能是确实是函数体已经改变了。这和 React 的 callback ref 的处理方法是一致的: 如果每次传一个变化的 callback,那么 React 认为你需要重新处理这个 ref,因此他会重新初始化 ref。

(3)但,业务场景需要解决引用变化导致的性能问题

  • 想办法让 fetchData 的引用不变化。 官方提供了一个 hooks —— useCallback 来解决函数引用的问题
const fetchData = React.useCallback((newDataConfig) => {
    realFetchData(newDataConfig);
  }, [realFetchData]);
  • 只要 props 更新,组件还是每次都会重新取数,因为监听的dataConfig 也是一个每次都会引用变化的 prop,每次都生成一个新的对象。
const dataConfig = React.useMemo(() => ({
    ...dataConfig,
    datasetId: getDatasetId(queryId)
  }), [getDatasetId, queryId]); // 包括handler都监听

(4)useMemo反例,掉坑

实践的时候 useMemo 还是很容易误操作导致没效果甚至反效果的,这里分享一个我遇到过的经典例子,囊括两个最容易产生的 useMemo 误操作。

  • 1 每次 ComponentCard 重渲染时 ComponentCardHeader 都会重渲染。
 <div className="card">
        {getComponentCardHeader(color)}
        <div className="card-body">
          <Element {...elementProps} />
        </div>
</div>
 // 对 header 做一个 memo
    const getComponentCardHeader = React.useMemo(() => {
      return (themeColor) => (
        <ComponentCardHeader
          style={getStyleByColor(themeColor)}
          title={title}
        />
      );
    }, [title]);
    
    这段代码又反应了两个问题,导致该组件不断重新渲染:
        a. useMemo 返回一个函数,而它的目的是存储一段 JSX,这么写会让 useMemo 失去作用,
        因为函数每次都会重新执行生成一个全新的 JSX。
        
        b. 每次 getStyleByColor ,ComponentCardHeader 组件都会重新执行返回新的 props,
        即使第 1 点做对了也无法达到预期效果。

解决方法

        a. getStyleByColor 返回的对象用 useMemo 包裹。
        b. getComponentCardHeader 换成 memoComponentHeader,存储最终的 JSX 而不是一个函数。
        
    // 正确用法
    //     const memoStyle = React.useMemo(() => getStyleByColor(color), [color]);

    //     const memoComponentHeader = React.useMemo(() => {
    //       return <ComponentCardHeader style={memoStyle} title={title} />;
    //     }, [title, memoStyle]);
  • 2 active 变化导致重渲染,所以先找到依赖 active 的 hook:
  // 只有当 active 为 true 的时候才会传递新的 props
  const elementProps = React.useMemo(() => {
    if (active || prevElementProps.current === null) {
      const newProps = {
        a: 1,
        b: 2,
        count
      };
      prevElementProps.current = newProps;
      return newProps;
    }
    return prevElementProps.current;
  }, [active, count]);

解释:
是一个很常见的问题,我们往往记得做第一层的 memo,却忘了做第二层的 memo,也就是说 memo 的粒度是原子性的,如果两个引用对象要合并,那他们需要分开 memo。在这段代码里,每次 active 从 false 变为 true,会进入到这个逻辑分支,生成一个新的 newProps,即使实际上它并没有产生变化。

解决方法
把 newProps 单独拎出来 memo 一下。直白点,就是所有的对象都要包一层。

 const memoProps = React.useMemo(
    () => ({
      a: 1,
      b: 2,
      count,
    }),
    [count],
  );
 

(5)总结一下:

  1. 弄清楚你的 useMemo 的目的是什么,要么就是存一个对象 prop,要么就是存一个 JSX,没搞清楚目的之前就先别用 useMemo 了,像这个例子中 useMemo 不仅没有效果还增加了 useMemo 本身的开销。

  2. useMemo 的粒度是原子性的,useMemo 中用到其他引用类型也要做 useMemo,否则在某些场景下 useMemo 可能会失效。比较复杂的业务场景建议配合 useWhatChanged 和 Profile 一起使用。

(6)对于对象dataConfig的处理

// 简单的useMemo实现
function useMemo(callback, deps) {
  const refResult = React.useRef(callback());
  const depsRef = React.useRef(deps);

  const isDepsChanged = deps.some((dep, index) => dep !== depsRef.current[index]);

  depsRef.current = deps;

  // 依赖变化才重新执行 callback
  if (isDepsChanged) {
    refResult.current = callback();
  }

  return refResult.current;
}

组件并不知道 dataConfig 的变化时机,想要减少取数的次数,只能对 dataConfig 做深比较了,其实就是把 useMemo 中的浅比较换成深比较,也就是 deep memo,简单实现 useDeepMemo

function useDeepMemo(value) {
  const refValue = React.useRef(value);

  // 深比较,有变化时更新引用
  if (!_.isEqual(refValue.current, value)) {
    refValue.current = value;
  }

  return refValue.current;
}

3.2.4 useCallback()

向子组件传递的函数方法需要useCallback 包裹一下;

如果useMemo返回的是一个函数,可以直接用useCallback替代,并省去其中顶层的函数

useMemo(()=>fn)
useCallback(fn)
等价于:
    const onClick = useCallback(()=>{
        {console.log('Click')}
    },[])
    useCallback并不能阻止 创建新的函数,但是该函数不一定被返回,因此性能有所提升。
    不能保证依赖不变化,就一定不会重新执行,因为要考虑到内存优化等

3.2.5 知识拓展

3.2.5.1 ComponentWillReciverProps怎么用hooks表示?

ComponentWillReceiveProps 是在组件接收到新 props 时执行的,和 useEffect 的执行时机完全不一致,事实上它和 useMemo 才是执行时机一致的,但是为什么却推荐用 useEffect 而不是 useMemo 来替代它呢?

看一个典型的 Class Component 可能会在 willReceiveProps 里做什么事情?

componentWillReceiveProps(nextProps) {
  
  if (nextProps.queryKey !== this.props.queryKey) {
    // 触发外部状态变更
    nextProps.setIsLoading(true);
    // 获取数据
    this.reFetch(nextProps.queryKey);
  }
  
  if (nextProps.value !== this.props.value) {
    // state 更新
    this.setState({
      checkList: this.getCheckListByValue(nextProps.value);
    })
  }
  
  if (nextProps.instanceId !== this.props.instanceId) {
    // 事件 / dom
    event.emit('instanceId_changed', nextProps.instanceId);
  }
  
}

浅比较

Function Component 中我们把依赖交给 React 自动管理了,虽然减少了手动做 diff 的工作量,但也带来了副作用:因为 React 做的是浅比较( Object.is() ),所以当 fetchData 的引用变化了,也会导致重新取数。

// Function Component
function LineChart ({ dataConfig, fetchData }) {
  
  React.useEffect(() => {
    fetchData(dataConfig);
  }, [dataConfig, fetchData])
  
}

执行时机: 在组件接收新的props时执行。

ComponentWillReceiveProps 经常被拿来:

  • a. 触发回调,造成外部状态变更
  • b. 事件监听和触发、dom 的变更
  • c. 重新获取数据
  • d. state 更新

第一时间拿到 props 和 nextProps ,方便我们做对比,而现在 React 已经接管了这个对比的工作,我们完全可以使用 useEffect 来替代,不阻塞浏览器重渲染,用户会觉得页面更加流畅

对于第 4 种情况我们需要思考一下,在组件更新期间更新状态是否是一个恰当的行为?归根到底组件需要动态根据某个 prop 来生成某个数据,如果在 Class Component 中,直接在 render 方法中生成即可,完全不需要 setState;如果是在 Function Component 中,确实是一个适合使用 useMemo 的场景,但是注意我们不是想要“更新状态”,而是因为“依赖改变了所以对象更新了”。

3.2.5.2 CC生命周期

getSnapShotBeforeUpdate、 componentDidCatch、 getDerivedStateFromErrors、 这些生命周期函数暂时没有实现 hooks

4. 组件间通信

4.1 Context扩展

4.1.1 createContext

context不能乱用,会影响组件之间的独立性,因此在一个组件中,最多只使用一个context就好,减少嵌套。

  • 跨多层组件组件通信
const StateContext = React.createContext({
    xxx:yyy //初始值
});

class Parent from React.Component {
    render() {
        return (
            <StateContext.Provider value="abc">
                <Child />
            </StateContext.Provider>
        )
    }
}

class Child from React.Component {
    render() {
        return (
            <StateContext.Consumer>
                {
                    context => (`name is ${context}`)
                }
            </StateContext.Consumer>
        )
    }
}

注意:每当 Provider 的值发生改变时, 作为 Provider 后代的所有 Consumers 都会重新渲染。从 Provider 到其后代的 Consumers 传播不受 shouldComponentUpdate 方法的约束,因此即使祖先组件退出更新时,后代 Consumer 也会被更新。

经常需要从组件树中某个深度嵌套的组件中更新 context。在这种情况下,可以通过 context 向下传递一个函数,以允许 Consumer 更新 context。

<ThemeContext.Provider value={{theme, toggleTheme}}> 
    <Content /> 
</ThemeContext.Provider>


<ThemeContext.Consumer>
    {({theme, toggleTheme}) => ( // value 是一个对象
        <button
           onClick={toggleTheme}
        >
            Toggle Theme

        </button>
    )}
</ThemeContext.Consumer>

4.1.2 useContext()


取代consumer
类似contextType的用法
    function Counter(){
       const count = useContext((CountContext));

       相当于:

        // static ContextType = CountContext;
        // const count = this.context

        return (
            <div>
                {count}
            </div>
        )
    }

4.2 Redux

存在多层数据流向时,可以用Redux。

4.3 状态提升至公共父组件

在 React 中,状态分享是通过将 state 数据提升至离需要这些数据的组件最近的父组件来完成的。这就是所谓的状态 提升。然后通过 props 传递给子组件,就可以保持同步。 事间处理函数也应从从父组件传过来。否则,子组件改变 input 中的值时无法与其他的子组件保持同步。 子向父组件传值也是讲父组件的方法的引用传过去。

5. Diff算法

image.png

5.1 如何比较两个 DOM 树的差异?

两个树的完全 diff 算法的时间复杂度为 O(n^3) ,
但是在前端中,我们很少会跨层级的移动元素,
所以我们只需要比较同一层级的元素进行比较,这样就可以将算法的时间复杂度降低为 O(n)。

(解释:
首先 DOM 是一个多叉树的结构,如果需要完整的对比两颗树的差异,那么需要的时间复杂度会是 O(n ^ 3),
这个复杂度肯定是不能接受的。于是 React 团队优化了算法,实现了 O(n) 的复杂度来对比差异。 
实现 O(n) 复杂度的关键就是只对比同层的节点,而不是跨层对比,
这也是考虑到在实际业务中很少会去跨层的移动 DOM 元素。)

算法首先会对新旧两棵树进行一个深度优先的遍历,会给每个节点添加索引,这样每个节点都会有一个序号。
在深度遍历的时候,每遍历到一个节点,我们就将这个节点 和 新的树中的节点 进行比较,
如果有差异,则将这个差异记录到一个对象中。

(解释:
在第一步算法中我们需要判断新旧节点的 tagName 是否相同,如果不相同的话就代表节点被替换了。
如果没有更改 tagName 的话,就需要判断是否有子元素,有的话就进行第二步算法。
在第二步算法中,我们需要判断原本的列表中是否有节点被移除,
在新的列表中需要判断是否有新的节点加入,还需要判断节点是否有移动。
那么在实际的算法中,我们如何去识别改动的是哪个节点呢?
这就引入了 key 这个属性,想必大家在 Vue 或者 React 的列表中都用过这个属性。
这个属性是用来给每一个节点打标志的,用于判断是否是同一个节点。
当然在判断以上差异的过程中,我们还需要判断节点的属性是否有变化等等。

当我们判断出以上的差异后,就可以把这些差异记录下来。当对比完两棵树以后,就可以通过差异去局部更新 DOM,实现性能的最优化)

在对列表元素进行对比的时候,由于 TagName 是重复的,所以我们不能使用这个来对比。
我们需要给每一个子节点加上一个 key,
列表对比的时候使用 key 来进行比较,这样我们才能够复用老的 DOM 树上的节点。

image.png

5.2 keys

Keys 可以在 DOM 中的某些元素被增加或删除的时候帮助 React 识别哪些元素发生了变化。因此你应当给数组中的每一个元素赋予一个确定的标识。一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。如果列表项目的顺序可能会变化,我们不建议使用索引来用作键值。

元素的 key 只有放在其环绕数组的上下文中才有意义。比方说,如果你提取出一个 ListItem 组件,你应该把 key 保存在数组中的这个元素上,而不是放在 ListItem组件中的< li>元素上。可以这么理解,谁做遍历,给谁绑定key。被遍历的主体的diff索引可能会导致问题。

然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的键。

key 会作为给 React 的提示,但不会传递给你的组件

6. Error Boundaries

错误边界是用于捕获其子组件树 JavaScript 异常,记录错误并展示一个回退的 UI 的 React 组件,而不是整个组件树的异常。错误边界在渲染期间、生命周期方法内、以及整个组件树构造函数内捕获错误。

注意---错误边界无法捕获如下错误:

1 事件处理
2 异步代码 (例如 setTimeout 或 requestAnimationFrame 回调函数)
3 服务端渲染
4 错误边界自身抛出来的错误 (而不是其子组件)

如果一个 class 组件中定义了 static getDerivedStateFromError()componentDidCatch() 这两个生命周期方法中的任意 一个(或两个)时,那么它就变成一个错误边界。

如果子组件有错,当抛出错误后,就会被该错误边界捕获,并执行这俩方法:

  • 使用 static getDerivedStateFromError() 渲染备用 UI ,
  • 使用 componentDidCatch() 打印错误信息。
class ErrorBoundary extends React.Component { 
    constructor(props) { 
        super(props); 
        this.state = { hasError: false }; 
    }
    static getDerivedStateFromError(error) { 
        // Update state so the next render will show the fallback UI. 
        return { hasError: true }; 
    }
    componentDidCatch(error, info) { 
        // You can also log the error to an error reporting service 
        logErrorToMyService(error, info); 
    }
    render() { 
        if (this.state.hasError) { 
            // You can render any custom fallback UI 
            return <h1>Something went wrong.</h1>; 
        }
        return this.props.children; 
    } 
}

错误边界只针对React 组件。只有 class 组件才可以成为成错误边界组件。大多数情况下, 你只需要声明一次错误边界组件, 并在整个应用中使用它。

注意错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。

7. Fiber

7.1 简介

在 V16 版本中引入了 Fiber 机制。这个机制一定程度上的影响了部分生命周期的调用,调用栈过长,再加上中间进行了复杂的操作,就可能导致长时间阻塞主线程,带来不好的用户体验。

Fiber 就是为了解决该问题而生,Fiber 本质上是一个虚拟的堆栈帧新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。

对于动画这种实时性很高的东西,也就是 16 ms 必须渲染一次保证不卡顿的情况下,React 会每 16 ms(以内) 暂停一下更新,返回来继续渲染动画。对于异步渲染,现在渲染有两个阶段:reconciliationcommit 。前者过程是可以打断的,后者不能暂停,会一直更新界面直到完成。

React 通过Fiber 架构,让自己的Reconcilation 过程变成可被中断。 '适时'地让出CPU执行权

  • 协调阶段 Reconciliation: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更React 称之为'副作用(Effect)' . 以下生命周期钩子会在协调阶段被调用:

    • constructor
    • componentWillMount 废弃
    • componentWillReceiveProps 废弃
    • static getDerivedStateFromProps
    • shouldComponentUpdate
    • componentWillUpdate 废弃
    • render
  • ⚛️ 提交阶段 Commit: 将上一个阶段计算出来的需要处理的**副作用(Effects)**一次性执行了。这个阶段必须同步执行,不能被打断. 这些生命周期钩子在提交阶段被执行:

    • getSnapshotBeforeUpdate() 严格来说,这个是在进入 commit 阶段前调用
    • componentDidMount
    • componentDidUpdate
    • componentWillUnmount

getDerivedStateFromProps 用于替换 componentWillReceiveProps ,该函数会在初始化和 update 时被调用.
getSnapshotBeforeUpdate 用于替换 componentWillUpdate ,该函数会在update 后 DOM 更新前被调用,用于读取最新的 DOM 数据。

也就是说,在协调阶段如果时间片用完,React就会选择让出控制权。因为协调阶段执行的工作不会导致任何用户可见的变更,所以在这个阶段让出控制权不会有什么问题。

需要注意的是:因为协调阶段可能被中断、恢复,甚至重做,⚠React 协调阶段的生命周期钩子可能会被调用多次! , 例如 componentWillMount 可能会被调用两次。

因此建议 协调阶段的生命周期钩子不要包含副作用. 索性 React 就废弃了这部分可能包含副作用的生命周期方法,例如componentWillMountcomponentWillUpdate

7.2 有react fiber,为什么不需要 vue fiber呢;

7.2.1 react、vue的响应式原理

react需要调用setState方法,而vue直接修改变量就行。

从底层实现来看修改数据

  • 在react中,组件的状态是不能被修改的,setState没有修改原来那块内存中的变量,而是去新开辟一块内存,所以经常能看到react相关的文章里经常会出现一个词"immutable";
  • 而vue则是直接修改保存状态的那块原始内存。

数据修改了,接下来要解决视图的更新

  • react中,调用setState方法后,会自顶向下重新渲染组件,自顶向下的含义是,该组件以及它的子组件全部需要渲染;
  • 而vue使用Object.defineProperty(vue@3迁移到了Proxy)对数据的设置(setter)和获取(getter)做了劫持,也就是说,vue能准确知道视图模版中哪一块用到了这个数据,并且在这个数据修改时,告诉这个视图,你需要重新渲染了。数据劫持。

所以,当一个数据改变,react的组件渲染是很消耗性能的——父组件的状态更新了,所有的子组件得跟着一起渲染,它不能像vue一样,精确到当前组件的粒度。

每次的视图更新流程是这样的:

  1. 组件渲染生成一棵新的虚拟dom树;
  2. 新旧虚拟dom树对比,找出变动的部分;(也就是常说的diff算法)
  3. 为真正改变的部分创建真实dom,把他们挂载到文档,实现页面重渲染;

由于react和vue的响应式实现原理不同,数据更新时,第一步中react组件会渲染出一棵更大的虚拟dom树。

实例理解是:

react时的效果,修改父组件的状态,父子组件都会重新渲染:
点击`change Father state`,不仅打印了`Father:render`,还打印了`child:render`

vue时的效果,无论是修改哪个状态,组件都只重新渲染最小颗粒:
点击`change Father state`,只打印`Father:render`,不会打印`child:render`

7.2.2 fiber是什么?

为什么需要react fiber:

  • 在数据更新时,react生成了一棵更大的虚拟dom树,给第二步的diff带来了很大压力——我们想找到真正变化的部分,这需要花费更长的时间。

react fiber没法让比较的时间缩短,但它使得diff的过程被分成一小段一小段的,因为它有了“保存工作进度”的能力。js会比较一部分虚拟dom,然后让渡主线程,给浏览器去做其他工作,然后继续比较,依次往复,等到最后比较完成,一次性更新到视图上。

在老的架构中

在老的架构中,节点以树的形式被组织起来,要找到两棵树的变化部分,
最容易想到的办法就是深度优先遍历(DFS 后续遍历),规则如下:

1. 从根节点开始,依次遍历该节点的所有子节点;
2. 当一个节点的所有子节点遍历完成,才认为该节点遍历完成;

这种遍历有一个特点,必须一次性完成。
假设遍历发生了中断,虽然可以保留当下进行中节点的索引,
下次继续时,我们的确可以继续遍历该节点下面的所有子节点,
但是没有办法找到其父节点——因为每个节点只有其子节点的指向。
断点没有办法恢复,只能从头再来一遍。

以该树为例:

在遍历到节点2时发生了中断,我们保存对节点2的索引,下次恢复时可以把它下面的3、4节点遍历到,但是却无法找回5、6、7、8节点。

在新的架构中fiber

在新的架构中,每个节点有三个指针:分别指向第一个子节点、下一个兄弟节点、父节点。
这种数据结构就是fiber,它的遍历规则如下:

1.  从根节点开始,依次遍历该节点的子节点、兄弟节点,如果两者都遍历了,则回到它的父节点;
2.  当一个节点的所有子节点遍历完成,才认为该节点遍历完成;

当遍历发生中断时,只要保留下当前节点的索引,
断点是可以恢复的——因为每个节点都保持着对其父节点的索引。

同样在遍历到节点2时中断,fiber结构使得剩下的所有节点依旧能全部被走到。

这就是react fiber的渲染可以被中断的原因。树和fiber虽然看起来很像,但本质上来说,一个是树,一个是链表实现链表结构,是为了模拟函数调用栈

另外fiber被认为是协程的一种实现形式。协程是比线程更小的调度单位:它的开启、暂停可以被程序员所控制。具体来说,react fiber是通过requestIdleCallback(react实际使用的是一个polyfill(自己实现的api))这个api去控制的组件渲染的“进度条”

`requesetIdleCallback`是一个属于宏任务的回调,就像setTimeout一样。
不同的是,setTimeout的执行时机由我们传入的回调时间去控制,
requesetIdleCallback是受屏幕的刷新率去控制。
它每隔16ms会被调用一次,
它的回调函数可以获取 本次可以执行的 时间,
每一个16ms除了`requesetIdleCallback`的回调之外,还有其他工作,
所以 能使用的时间 是不确定的,但只要时间到了,就会停下“节点的遍历”,让出线程。
    其他工作:
        浏览器在一帧内可能会做执行下列任务,而且它们的执行顺序基本是固定的:
        -   处理用户输入事件
        -   Javascript执行
        -   requestAnimation 调用
        -   布局 Layout
        -   绘制 Paint
        上面说理想的一帧时间是 `16ms` (1000ms / 60),
        如果浏览器处理完上述的任务(布局和绘制之后),还有盈余时间,
        浏览器就会调用 `requestIdleCallback` 的回调

`requestIdleCallback`的意思是:
让浏览器在'有空'的时候就执行我们的回调,
这个回调会传入一个期限,表示浏览器有多少时间供我们执行, 
为了不耽误事,我们最好在这个时间范围内执行完毕。

通过超时检查的机制来让出控制权确定一个合理的运行时长,然后在合适的检查点检测是否超时(比如每执行一个小任务),如果超时就停止执行,将控制权交换给浏览器

可以通过传入的参数deadLine.timeRemaining()检查当下还有多少时间供自己使用。

  • requestIdleCallback,受屏幕的刷新率去控制回调触发时间,
  • 执行回调
    • 是否让出线程,
    • 不让出线程,会进行遍历节点等操作,并检查剩余执行时间
    • 时间到了,让出线程,停止节点遍历。

综上,React Fiber是React 16提出的一种更新机制,使用链表取代了树,将虚拟dom连接,使得组件更新的流程可以被中断恢复;它把组件渲染的工作分片,到时会主动让出渲染主线程。

上面是使用旧的react时,获得每一帧的时间点,下面是使用fiber架构时,获得每一帧的时间点,因为组件渲染被分片,完成一帧更新的时间点反而被推后了,我们把一些时间片去处理用户响应了。

注意,不会出现“一次组件渲染没有完成,页面部分渲染更新”的情况,react会保证每次更新都是完整的。

7.2.3 react不如vue?

并不是。孰优孰劣是一个很有争议的话题,在此不做评价。因为vue实现精准更新也是有代价的,一方面是需要给每一个组件配置一个“监视器”,管理着视图的依赖收集和数据更新时的发布通知,这对性能同样是有消耗的;另一方面vue能实现依赖收集得益于它的模版语法,实现静态编译,这是使用更灵活的JSX语法的react做不到的。

在react fiber出现之前,react也提供了PureComponent、shouldComponentUpdate、useMemo,useCallback等方法给我们,来声明哪些是不需要连带更新子组件

8. 自定义Hooks [FC]

解决类组件问题:方便复用状态逻辑

**与函数组件 **

  • 只有输入输出的区别。根据其他区分不出二者。
  • hook可以返回jsx参与渲染。

作用:

  • 抽离出逻辑,公用;

8.1 useResize

    function  useSize (){
        const [size,setSize] = useState({
            width:document.documentElement.clientWidth,
            height:document.documentElement.clientHeight
        })
        const onResize = useCallback(()=>{
            setSize({
                width:document.documentElement.clientWidth,
                height:document.documentElement.clientHeight
            })
        },[])
        useEffect(()=>{
            window.addEventListener('resize',onResize,false);
            return ()=>{
                window.removeEventListener('resize',onResize,false);
            }
        },[])
        return size; //返回的值
    }

    之后就可以在各组件中直接 const size = useSize();

8.2 业务

            import { useCallback } from 'react';
            import { h0 } from './fp';
    
            export default function useNav(departDate, dispatch, prevDate, nextDate) {
                const isPrevDisabled = h0(departDate) <= h0();  
                //当前进行 选择车票的 天 ,最起码为今天
                const isNextDisabled = h0(departDate) - h0() > 20 * 86400 * 1000;  
                //买 未来20天的车票
    
                const prev = useCallback(() => {
                    if (isPrevDisabled) {
                        return;
                    }
                    dispatch(prevDate());
                }, [isPrevDisabled]);
    
                const next = useCallback(() => {
                    if (isNextDisabled) {
                        return;
                    }
                    dispatch(nextDate());
                }, [isNextDisabled]);
    
                return {
                    isPrevDisabled,
                    isNextDisabled,
                    prev,
                    next,
                };
            }
            
            # 之后在所有组件中便可以
            const { isPrevDisabled, isNextDisabled, prev, next } = useNav(
                departDate,
                dispatch,
                prevDate,
                nextDate
            );
            
            # 来复用该块逻辑。

9. 性能优化

9.1 Object.is (浅比较)

  • Hook 内部使用 Object.is 来比较新/旧 state 是否相等

  • 与 class 组件中的 setState 方法不同,如果你修改状态的时候,传的状态值没有变化,则不重新渲染

  • 与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果

9.2 降低重渲染次数(依赖store)

  1. useMemo / useEffect 细粒度依赖
  • 尽量只依赖最小所需部分,比如以来const {} = a
  1. useCallback 改成 useRef
  • useCallback 依赖 store 的话(const store = useContext(StoreContext);),可能会导致当前组件频繁更新,这是一个很抓狂的问题,这时候唯一的解决方法就是把 useCallback 换成 useRef

3.pureComponent / React.memo()

9.3 降低重渲染耗时

  1. key 到底应该用 index 还是 id
  • 操作频繁的长列表,如果以 index 为 key,删除了第 1 个元素,剩下的 dom 都得更新,因为 key 变化了。这种情况下应该用 id 作为 key,由于 key 不变,其他不受影响的 item dom 都不会更新。
  • 操作Dom渲染问题
  1. 拆分 useMemo / 拆分组件
  • 用 useMemo 把组件返回的 dom 包起来出发点是好的,但是依赖了 store 的话,useMemo 可能会名存实亡。(原子化、精细化的依赖)

9.4 按需重渲染

  • Mobx

mobx 是一个非常适合中型应用的数据流管理方案,对于非常复杂的应用有可能会失控,后续的可维护性是一个需要重点考虑的地方;

可维护性的问题,mobx 最受人诟病的一点就是它太自由了,在多人协同的场景下很容易变得群魔乱舞

虽然这个问题无法完全解决,但是可以缓解,mobx 的自由度可以通过一定的规范来约束

● 优点:上手成本小,精准按需重渲染不必担心性能问题。 ● 缺点:对代码有一定的入侵性,数据流的可维护性是个问题,比较适合中小型应用。

  1. store 中数据的修改全部通过内部的方法来操作,不要直接操作 store 的属性;

  2. 增加业务自定义的打点来降低未来的维护成本;

import { useObserver, useLocalStore } from 'mobx-react' // 6.x or mobx-react-lite@1.4.0

function Person() {
  const person = useLocalStore(() => ({ name: 'John' }))
  return useObserver(() => (
    <div>
      {person.name}
      <button onClick={() => (person.name = 'Mike')}>No! I am Mike</button>
    </div>
  ))
}
  • Redux

Redux 自备按需重渲染能力和强大的数据追溯能力(核心竞争力),特别适合强依赖数据流的大规模应用。

● 优点:按需重渲染,数据流可维护性优秀,调试体验无敌,适合大型应用。 ● 缺点: useSelector 用起来挺烦的,我经常要搭配 ref 来保持数据的引用。

10. Hooks妙用 [FC]

10.1 Reducer数据流模型

// reducer
const reducer = (action: Action, state: State) => {
  switch (action.type) {
    case 'increment':
      return {
        ...state,
        count: state.count + 1,
      };
    default:
      return state;
  }
};


// 初始化
const StoreContext = React.createContext(null);

const Parent = () => {
  const [store, dispatch] = useReducer(reducer, { count: 0, dataList: [] });
  
  return (

      <StoreContext.Provider value={store, dispatch}>
        <Child />
      </StoreContext.Provider>
  );
};

// /Child.tsx
const {store, dispatch} = useContext(StoreContext)

10.2 useCustomState ---- 自定义hook useReducer,

// 复杂状态管理 - demo 【架构分析及设计】
// 精准追溯数据流标识,time traveler
import React, { useReducer } from 'react';

export enum ActionType {
  updateWeakLoading = 'updateWeakLoading',
}
export interface ActionItem {
  type: ActionType;
  data: any;
}
export interface InitialState {
  submitWeakLoading: boolean;
}

export function useCustomState(initialState: InitialState) {
  // state更新
  const reducer = (state, action: ActionItem) => {
    switch (action.type) {
      case ActionType.updateWeakLoading: // 系统级别架构设计,可以只保留更新,保持一致性提高可维护性
        // immutable
        return {
          ...state,
          submitWeakLoading: action.data || false,
        };
      default:
        return state;
    }
  };

  const [_state, _dispatch] = useReducer(reducer, initialState);

  return [_state, _dispatch];
}


调用:
const _initState = {
    submitWeakLoading: false,
};

const [_submit_state, setSubmitState] = useCustomState(_initState);
  
  setSubmitState({
      type: ActionType.updateWeakLoading,
      data: false,
  });

改进,衍生前端系统技术架构-数据流设计

# 文件1: 初始化state,以及state声明
# states/xxxState

export interface InitialState {
  submitWeakLoading: boolean;
}

const _initState = {
  submitWeakLoading: false,
};

# 文件2: types/xxxActionType

export enum ActionType {
  updateWeakLoading = 'updateWeakLoading',
}

export interface ActionItem {
  type: ActionType;
  data: any;
}
# 文件3: reducers/xxxReducer

export function useCustomState(initialState: InitialState) {
  // state更新
  const reducer = (state, action: ActionItem) => {
    switch (action.type) {
      case ActionType.updateWeakLoading: // 系统级别架构设计,可以只保留更新,保持一致性从而提高可维护性
        // immutable
        return {
          ...state,
          submitWeakLoading: action.data || false,
        };
      default:
        return state;
    }
  };

  const [_state, _dispatch] = useReducer(reducer, initialState);

  return [_state, _dispatch];
}

# 文件4: actions/xxxActions
const [_submit_state, setSubmitState] = useCustomState(_initState);

setSubmitState({
    type: ActionType.updateWeakLoading,
    data: true,
});

10.3 useResize事件处理封装

    function  useSize (){
        const [size,setSize] = useState({
            width:document.documentElement.clientWidth,
            height:document.documentElement.clientHeight
        })
        const onResize = useCallback(()=>{
            setSize({
                width:document.documentElement.clientWidth,
                height:document.documentElement.clientHeight
            })
        },[])
        useEffect(()=>{
            window.addEventListener('resize',onResize,false);
            return ()=>{
                window.removeEventListener('resize',onResize,false);
            }
        },[])
        return size; //返回的值
    }

    之后就可以在各组件中直接 const size = useSize();

10.4 useClose事件处理封装

// 关闭事件,自定义hook,单独提取封装
function useClose(classID, onClose = () => null) {
  // 处理关闭事件
  function handleClick(e: any) {
    if (!e.target.closest(classID) && onClose) {
      //只要不是点击这个元素
      onClose();
    }
  }

  function handleEsc(e: KeyboardEvent) {
    if (e.keyCode === 27 && onClose) {
      onClose();
    }
  }

  React.useEffect(() => {
    document.addEventListener('click', handleClick);
    document.addEventListener('keydown', handleEsc);
    return () => {
      document.removeEventListener('click', handleClick);
      document.removeEventListener('keydown', handleEsc);
    };
  }, []);
}
使用: 
const closeEmoji = () => {
    _setShowEmoji(false);
  };

  useClose('.emoji-button-toggle', closeEmoji as any);
  
  
 <div className="rich-text-emoji">
    <Button icon="pope-Hexago" className="emoji-button-toggle" onClick={() => _setShowEmoji(!_showEmoji)}></Button>
        {_showEmoji && (
            <div>
                <Picker
                  onSelect={d => {
                    onChange && onChange(`${value}${d?.native}`);
                  }}
                  // className="emoji-button-toggle"
                  set="google"
                  title="Pick your emoji…"
                  emoji="point_up"
                  style={{ position: 'absolute', bottom: '40px', right: '0px', zIndex: 1000000000000, boxShadow: '0 2px 16px 0 rgba(51,51,51,0.20)' }}
                />
            </div>
        )}
    </Button>
</div>

11、React妙用

11.1 dymanic import

import('./components/Bar').then(Bar => {
      // 处理Bar
});

11.2 lazy() & < Suspence>

import { lazy } from 'react' 
//  lazy()的返回就是一个 react 组件。       
# 定义
      const About = lazy(() => import(/* webpackChunkName: "about" */ './Abount.jsx'));
        !!!!!!此时直接 在render( )中使用懒加载的About组件会报错。
        !!!!!!使用 lazy()会存在一个渲染空档,需要Suspense组件配合。
# 调用
       return(
            <div>
                <Suspense fallback = {<Loading />}> 
                    <About></About>
                </Suspense>
            </div>
        )

11.3 函数组件没有forceUpdate 如何强制刷新渲染组件?

在 对应的组件中,setState
    const [updater,setUpdater] = useState(0);
    function forceUpdate(){
        setUpdater(updater=> updater+1)
    }
    执行 forceUpdate 间接强制刷新渲染了该组件

12. 其他Hooks总结

12.1 Hooks 解决的问题

(1) 类组件的不足

  • 状态逻辑难复用: 在组件之间复用状态逻辑很难,可能要用到 render props渲染属性)或者 HOC高阶组件),但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余

  • 趋向复杂难以维护:

    • 在生命周期函数中混杂不相干的逻辑(如:在 componentDidMount 中注册事件以及其他的逻辑,在 componentWillUnmount 中卸载事件,这样分散不集中的写法,很容易写出 bug )
    • 类组件中到处都是对状态的访问和处理,导致组件难以拆分成更小的组件
  • this 指向问题:父组件给子组件传递函数时,必须绑定 this

    • react 中的组件四种绑定 this 方法的区别

(2)Hooks 优势

  • 能优化类组件的三大问题
    • 能在无需修改组件结构的情况下复用状态逻辑(自定义 Hooks )
      • 能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
      • 副作用的关注点分离副作用指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。而 useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行。

12.2 自定义 Hook 必须以 use 开头吗?

必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则

12.3 闭包陷阱

图片.png

简单来说, 就是react hooks在渲染的时候维护了一个链表, 来记录useState和useEffect的位置和值, (这也是state不能使用if else的原因, 因为可能会导致链表中state useEffect的顺序错乱, 从而不能获取到正确的数值)

在每次state更新时, 链表从头开始重新渲染, 但是由于上面示例中useEffect没有依赖任何state, 所以只有在第一次渲染的时候才会触发, setCount渲染更新时, useEffect里面的回调函数并没有触发 因此里面的setInterval里面的count还是初始化时的值, 并没有获取到最新的. 这就是闭包陷阱

解决:useRef 每次拿到的都是这个对象本身, 是同一个内存空间的数据, 所以可以获取到最新的值