React

192 阅读26分钟

React 事件机制

React并不是将click事件绑定到了div的真实DOM上,而是在document处监听了所有的事件,当事件发生并且冒泡到document处的时候,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂载销毁时统一订阅和移除事件。

除此之外,冒泡到document上的事件也不是原生的浏览器事件,而是由react自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用event.preventDefault()方法,而不是调用event.stopProppagation()方法。

JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。

image.png

  • 兼容所有浏览器,更好的跨平台;
  • 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。
  • 方便 react 统一管理和事务机制。

在React底层,主要对合成事件做了两件事:

  • 事件委派: React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
  • 自动绑定: React组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件。

React 高阶组件、Render props、hooks 有什么区别

这三者是目前react解决代码复用的主要方式:

  • 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
  • render props是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术,更具体的说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。

(1)HOC 官方解释∶

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

简言之,HOC是一种组件的设计模式,HOC接受一个组件和额外的参数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。

javascript
 代码解读
复制代码
// hoc的定义
function withSubscription(WrappedComponent, selectData) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        data: selectData(DataSource, props)
      };
    }
    // 一些通用的逻辑处理
    render() {
      // ... 并使用新数据渲染被包装的组件!
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };

// 使用
const BlogPostWithSubscription = withSubscription(BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id));

HOC的优缺点∶

  • 优点∶ 逻辑服用、不影响被包裹组件的内部逻辑。
  • 缺点∶ hoc传递给被包裹组件的props容易和被包裹后的组件重名,进而被覆盖

(2)Render props 官方解释∶

"render prop"是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

具有render prop 的组件接受一个返回React元素的函数,将render的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其他有效的标识符。

javascript
 代码解读
复制代码
// DataProvider组件内部的渲染逻辑如下
class DataProvider extends React.Components {
     state = {
    name: 'Tom'
  }

    render() {
    return (
        <div>
          <p>共享数据组件自己内部的渲染逻辑</p>
          { this.props.render(this.state) }
      </div>
    );
  }
}

// 调用方式
<DataProvider render={data => (
  <h1>Hello {data.name}</h1>
)}/>

由此可以看到,render props的优缺点也很明显∶

  • 优点:数据共享、代码复用,将组件内的state作为props传递给调用者,将渲染逻辑交给调用者。
  • 缺点:无法在 return 语句外访问数据、嵌套写法不够优雅

(3)Hooks 官方解释∶

Hook是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。通过自定义hook,可以复用代码逻辑。

javascript
 代码解读
复制代码
// 自定义一个获取订阅数据的hook
function useSubscription() {
  const data = DataSource.getComments();
  return [data];
}
// 
function CommentList(props) {
  const {data} = props;
  const [subData] = useSubscription();
    ...
}
// 使用
<CommentList data='hello' />

以上可以看出,hook解决了hoc的prop覆盖的问题,同时使用的方式解决了render props的嵌套地狱的问题。hook的优点如下∶

  • 使用直观;
  • 解决hoc的prop 重名问题;
  • 解决render props 因共享数据 而出现嵌套地狱的问题;
  • 能在return之外使用数据的问题。

需要注意的是:hook只能在组件顶层使用,不可在分支语句中使用。

总结∶ Hoc、render props和hook都是为了解决代码复用的问题,但是hoc和render props都有特定的使用场景和明显的缺点。hook是react16.8更新的新的API,让组件逻辑复用更简洁明了,同时也解决了hoc和render props的一些缺点。

React-Fiber

React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿

在React16推出Fiber之前,react会采用深度优先遍历(DFS) 对比虚拟DOM树,找出变更的节点。然后同步更新他们,这个过程被称为协调(reconciliation) 。当Dom的节点非常深的时候,在reconciliation期间,react会一直占用浏览器资源,占用的时间超过了16.6ms也就是一帧的时间,就会造成页面的卡顿。

因此,react希望能解决长时间占用主线程的问题,引入了Fiber。把渲染、更新过程拆分为一个个小块的任务,让reconciliation过程变的可中断,通过合理的调度机制调控时间,指定任务执行的时机,从而降低页面卡顿的概率。

所以 React 通过Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:

  • 分批延时对DOM进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;
  • 给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。

核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。

一般情况下,浏览器1S绘制60帧,1帧16.6毫秒,一帧会做以下7件事

image.png

GUI渲染线程与JS引擎线程,如果在某个阶段执行任务特别长,时间已经明显超过了16ms,那么就会阻塞页面的渲染,从而出现卡顿现象

Fiber可以理解为一个执行单元,也可以理解为一种数据结构。

执行单元 执行单元就是react把一个大的任务拆分成一个个的小块任务,每一个小块任务是在一次执行必须完成的,每一个小块的任务执行完成后会向浏览器询问是否还有多余的时间,如果没有多余的时间就把控制权交回给浏览器,如果还有多余的时间就继续执行下一个任务块。

image.png

数据结构 Fiber 还可以理解为是一种数据结构,React Fiber 就是采用链表实现的。每个 Virtual DOM 都可以表示为一个 fiber,如下图所示,每个节点都是一个 fiber。一个 fiber包括了 child(第一个子节点)、sibling(兄弟节点)、return(父节点)等属性,React Fiber 机制的实现,就是依赖于以下的数据结构

image.png

Fiber结构是使用链表实现的,Fiber tree实际上是个单链表树结构

image.png

每个node都有一个payload和nextUpdate

class Update {
  constructor(payload, nextUpdate) {
    this.payload = payload // payload 数据
    this.nextUpdate = nextUpdate // 指向下一个节点的指针
  }
}

工作流程 Reconciliation(调和) 此阶段会找出所有节点的变更,如节点新增、删除、属性变更等,这些变更 react 统称为副作用(effect),此阶段会构建一棵Fiber tree,以虚拟dom节点为维度对任务进行拆分,即一个虚拟dom节点对应一个任务,最后产出的结果是effect list,从中可以知道哪些节点更新、哪些节点增加、哪些节点删除了。

1、创建与标记更新节点

2、收集副作用列表

Commit

commit 阶段需要将上阶段计算出来的需要处理的副作用一次性执行,此阶段不能暂停,否则会出现UI更新不连续的现象。此阶段需要根据effect list,将所有更新都 commit 到DOM树上。

RequestIdleCallback和RequestAnimationFrame

requestIdleCallback(myNonEssentialWork, { timeout: 2000 });

function myNonEssentialWork (deadline) {
  // 当回调函数是由于超时才得以执行的话,deadline.didTimeout为true
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
         tasks.length > 0) {
       doWorkIfNeeded();
    }
  if (tasks.length > 0) {
    requestIdleCallback(myNonEssentialWork);
  }
}


// 循环字体
  const loopText = () => {
    if (textLen.current <= content.length) {
      rafId.current = window.requestAnimationFrame(loopText);
      setShowContent((pre) => pre + content.charAt(textLen.current));
      textLen.current += 1;
      scrollToBottom('#chatList');
    } else {
      cancelAnimationFrame(rafId.current);
      setMsg(index);
    }
  };

image.png requestAnimationFrame的回调会在每一帧确定执行,属于高优先级任务,而requestIdleCallback的回调则不一定,属于低优先级任务。 我们所看到的网页,都是浏览器一帧一帧绘制出来的,通常认为FPS为60的时候是比较流畅的

图中一帧包含了用户的交互、js的执行、以及requestAnimationFrame的调用,布局计算以及页面的重绘等工作。 假如某一帧里面要执行的任务不多,在不到16ms(1000/60)的时间内就完成了上述任务的话,那么这一帧就会有一定的空闲时间,这段时间就恰好可以用来执行requestIdleCallback的回调

由于requestIdleCallback利用的是帧的空闲时间,所以就有可能出现浏览器一直处于繁忙状态,导致回调一直无法执行,这其实也并不是我们期望的结果(如上报丢失),那么这种情况我们就需要在调用requestIdleCallback的时候传入第二个配置参数timeout了

React 高阶组件

官方解释∶

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

高阶组件(HOC)就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件,它只是一种组件的设计模式,这种设计模式是由react自身的组合性质必然产生的。我们将它们称为纯组件,因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件中的任何行为。

① 复用逻辑:高阶组件更像是一个加工react组件的工厂,批量对原有组件进行加工包装处理。我们可以根据业务需求定制化专属的HOC,这样可以解决复用逻辑。

② 强化props:这个是HOC最常用的用法之一,高阶组件返回的组件,可以劫持上一层传过来的props,然后混入新的props,来增强组件的功能。代表作react-router中的withRouter

③ 赋能组件HOC有一项独特的特性,就是可以给被HOC包裹的业务组件,提供一些拓展功能,比如说额外的生命周期,额外的事件,但是这种HOC,可能需要和业务组件紧密结合。典型案例react-keepalive-router中的 keepaliveLifeCycle就是通过HOC方式,给业务组件增加了额外的生命周期。

④ 控制渲染:劫持渲染是hoc一个特性,在wrapComponent包装组件中,可以对原来的组件,进行条件渲染节流渲染懒加载等功能,后面会详细讲解,典型代表做react-reduxconnectdvadynamic 组件懒加载。

使用:装饰器模式和函数包裹模式

@withStyles(styles)
@withRouter
@keepaliveLifeCycle
class Index extends React.Componen{
    /* ... */
}

注意一下包装顺序,越靠近Index组件的,就是越内层的HOC,离组件Index也就越近。

function Index(){
    /* .... */
}
export default withStyles(styles)(withRouter( keepaliveLifeCycle(Index) )) 

嵌套HOC

对于不需要传递参数的HOC,我们编写模型我们只需要嵌套一层就可以,比如withRouter,

function withRouter(){
    return class wrapComponent extends React.Component{
        /* 编写逻辑 */
    }
}

对于需要参数的HOC,我们需要一层代理,如下:

function connect (mapStateToProps){
    /* 接受第一个参数 */
    return function connectAdvance(wrapCompoent){
        /* 接受组件 */
        return class WrapComponent extends React.Component{  }
    }
}

HOC的两种方式

正向属性代理

function HOC(WrapComponent){
    return class Advance extends React.Component{
       state={
           name:'alien'
       }
       render(){
           return <WrapComponent  { ...this.props } { ...this.state }  />
       }
    }
}

反向继承

class Index extends React.Component{
  render(){
    return <div> hello,world  </div>
  }
}
function HOC(Component){
    return class wrapComponent extends Component{ /* 直接继承需要包装的组件 */

    }
}
export default HOC(Index) 

常见的HOC

混入props

function classHOC(WrapComponent){
    return class  Idex extends React.Component{
        state={
            name:'alien'
        }
        componentDidMount(){
           console.log('HOC')
        }
        render(){
            return <WrapComponent { ...this.props }  { ...this.state }   />
        }
    }
}
function Index(props){
  const { name } = props
  useEffect(()=>{
     console.log( 'index' )
  },[])
  return <div>
    hello,world , my name is { name }
  </div>
}

export default classHOC(Index)

抽离state控制更新

function classHOC(WrapComponent){
  return class  Idex extends React.Component{
      constructor(){
        super()
        this.state={
          name:'alien'
        }
      }
      changeName(name){
        this.setState({ name })
      }
      render(){
          return <WrapComponent { ...this.props }  { ...this.state } changeName={this.changeName.bind(this)  }  />
      }
  }
}
function Index(props){
  const [ value ,setValue ] = useState(null)
  const { name ,changeName } = props
  return <div>
    <div>   hello,world , my name is { name }</div>
    改变name <input onChange={ (e)=> setValue(e.target.value)  }  />
    <button onClick={ ()=>  changeName(value) }  >确定</button>
  </div>
}

export default classHOC(Index)

动态渲染

function renderHOC(WrapComponent){
  return class Index  extends React.Component{
      constructor(props){
        super(props)
        this.state={ visible:true }  
      }
      setVisible(){
         this.setState({ visible:!this.state.visible })
      }
      render(){
         const {  visible } = this.state 
         return <div className="box"  >
           <button onClick={ this.setVisible.bind(this) } > 挂载组件 </button>
           { visible ? <WrapComponent { ...this.props } setVisible={ this.setVisible.bind(this) }   />  : <div className="icon" ><SyncOutlined spin  className="theicon"  /></div> }
         </div>
      }
  }
}

class Index extends React.Component{
  render(){
    const { setVisible } = this.props
    return <div className="box" >
        <p>hello,my name is alien</p>
        <img  src='https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=294206908,2427609994&fm=26&gp=0.jpg'   /> 
        <button onClick={() => setVisible()}  > 卸载当前组件 </button>
    </div>
  }
}
export default renderHOC(Index)

反向继承 : 渲染劫持


const HOC = (WrapComponent) =>
  class Index  extends WrapComponent {
    render() {
      if (this.props.visible) {
        return super.render()
      } else {
        return <div>暂无数据</div>
      }
    }
  }

HOC源码分析

function withRouter(Component) {
  const displayName = `withRouter(${Component.displayName || Component.name})`;
  const C = props => {
      /*  获取 */
    const { wrappedComponentRef, ...remainingProps } = props;
    return (
      <RouterContext.Consumer>
        {context => {
          return (
            <Component
              {...remainingProps}
              {...context}
              ref={wrappedComponentRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  };

  C.displayName = displayName;
  C.WrappedComponent = Component;
  /* 继承静态属性 */
  return hoistStatics(C, Component);
}

export default withRouter

React Hooks

react-hooks是react16.8以后,react新增的钩子API,目的是增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。笔者认为,react-hooks思想和初衷,也是把组件,颗粒化,单元化,形成独立的渲染环境,减少渲染次数,优化性能。

1 react-hooks可以让我们的代码的逻辑性更强,可以抽离公共的方法,公共组件。

2 react-hooks思想更趋近于函数式编程。用函数声明方式代替class声明方式,虽说class也是es6构造函数语法糖,但是react-hooks写起来更有函数即组件,无疑也提高代码的开发效率(无需像class声明组件那样写声明周期,写生命周期render函数等)

3 react-hooks可能把庞大的class组件,化整为零成很多小组件,useMemo等方法让组件或者变量制定一个适合自己的独立的渲染空间,一定程度上可以提高性能,减少渲染次数

function组件的调用流程

组件初始化

renderWithHooks(
    null,                // current Fiber
    workInProgress,      // workInProgress Fiber
    Component,           // 函数组件本身
    props,               // props
    context,             // 上下文
    renderExpirationTime,// 渲染 ExpirationTime
);

组件更新

renderWithHooks(
    current,
    workInProgress,
    render,
    nextProps,
    context,
    renderExpirationTime,
);

current fiber树: 当完成一次渲染之后,会产生一个current树,current会在commit阶段替换成真实的Dom树。

workInProgress fiber树: 即将调和渲染的 fiber 树。再一次新的组件更新过程中,会从current复制一份作为workInProgress,更新完毕后,将当前的workInProgress树赋值给current树。

workInProgress.memoizedState: 在class组件中,memoizedState存放state信息,在function组件中,memoizedState在一次调和渲染过程中,以链表的形式存放hooks信息。

currentHook : 可以理解 current树上的指向的当前调度的 hooks节点。

workInProgressHook : 可以理解 workInProgress树上指向的当前调度的 hooks节点。

image.png

执行以下代码

function App() {
  const [name, setName] = useState("guang");
  useState('dong');

  const handler = useCallback((evt) => {
      setName('dong');
  },[1]);

  useEffect(() => {
    console.log(1);
  });
  
  useRef(1);

  useMemo(() => {
    return 'guang and dong';
  })

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p onClick={handler}>
          {name}
        </p>
      </header>
    </div>
  );
}

image.png 这就是hook存储数据的地方,存储在memorizedState上,以链表的形式

调用useXXX的执行过程

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

以上mountXXX方法里面都调用了mountWorkInProgressHook,一起来看看这个方法里面做了什么

function mountWorkInProgressHook() {
  const hook: Hook = {
    memoizedState: null,  // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
  if (workInProgressHook === null) { // 例子中的第一个`hooks`-> useState(0) 走的就是这样。
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

hook是以链表的形式保存在workInProgress.memoizedState上的,useState中保存state信息,useMemo和useCallback保存缓存的值和deps,useRef保存ref

hook的数据结构

memoizedState: null,  // useState中 保存 state信息  useEffect  保存着 effect 对象  useMemo  保存的是缓存的值和deps  useRef中保存的是ref 对象
baseState: null,
baseQueue: null,
queue: null,
next: null,

函数组件执行完后,workInProgress的关系图

image.png

为什么hook不能写在条件语句中

 let curRef  = null
 if(isFisrt){
  curRef = useRef(null)
 }

image.png

组件第一次渲染后,形成一颗workInProgress树,它的memoizedState已链表的形式保存了hook的信息,那如果有条件语句,组件更新后,workInProgress树中的链表结构将会被破坏,和current树中的链表结构将会不一致,

虚拟DOM和Diff算法

JS DOM操作非常消耗性能,而React把真实原生JS DOM转换成了JavaScript对象。这就是虚拟Dom(Virtual Dom)

image.png

虚拟DOM的结构

// 真实DOM
<div className='Index'>
  <div>我是小杜杜</div>
  <ul>
    <li>React</li>
    <li>Vue</li>
  </ul>
</div>

虚拟DOM
{
    type: 'div',
    props: { class: 'Index' },
    children: [
        {
            type: 'div',
            children: '我是小杜杜'
        },
        {
            type: 'ul',
            children: [
                {
                    type: 'li',
                    children: 'React'
                },
                {
                    type: 'li',
                    children: 'Vue'
                },
            ]
        }
    ]
}
  • type:实际的标签
  • props:标签内部的属性(除keyref,会形成单独的key名)
  • children: 为节点内容,依次循环

React中,组件为何要大写?

我们写的代码最终是要呈现在浏览器上,浏览器会识别你的代码是React吗?很显然,浏览器并不知道你的代码是React,更不会识别JSX了,实际上浏览器对ES6的一些语法都识别不了,要想让浏览器识别,就需要借助Babel

要通过Babel去对JSX进行转化为对应的JS对象,才能让浏览器识别,此时就会有个依据去判断是原生DOM标签,还是React组件,而这个依据就是标签的首字母

如果标签的首字母是小写,就会被认定为原生标签,反之就是React组件

虚拟DOM有什么优势

提高效率

使用原生JS的时候,我们需要的关注点在操作DOM上,而React会通过虚拟DOM来确保DOM的匹配,也就是说,我们关注的点不在时如何操作DOM,怎样更新DOMReact会将这一切处理好

此时,我们更加关注于业务逻辑,从而提高开发效率

性能提升

经过之前的讲解,我们发现虚拟DOM优势明显强于真实的DOM,我们来看看虚拟DOM如何工作的?

实际上,React会将整个DOM保存为虚拟DOM,如果有更新,都会维护两个虚拟DOM,以此来比较之前的状态当前的状态,并会确定哪些状态被修改,然后将这些变化更新到实际DOM上,一旦真正的DOM发生改变,也会更新UI

要牢记一句话:浏览器在处理DOM的时候会很慢,处理JavaScript会很快

所以在虚拟DOM感受到变化的时候,只会更新局部,而非整体。同时,虚拟DOM会减少了非常多的DOM操作 ,所以性能会提升很多

虚拟DOM一定会提高性能吗?

通过上面的理解,很多人认为虚拟DOM一定会提高性能,一定会更快,其实这个说法有点片面,因为虚拟DOM虽然会减少DOM操作,但也无法避免DOM操作

它的优势是在于diff算法批量处理策略,将所有的DOM操作搜集起来,一次性去改变真实的DOM,但在首次渲染上,虚拟DOM会多了一层计算,消耗一些性能,所以有可能会比html渲染的要慢

注意,虚拟DOM实际上是给我们找了一条最短,最近的路径,并不是说比DOM操作的更快,而是路径最简单

就好比条条大路通罗马,虽然走的方向不同,但最终到达的目的地都是相通的,不同的路径对应的时间不同,虚拟DOM就是规划出最短的路径,但最终还是需要人(真实DOM)去走的(有不对的地方,欢迎评论区讨论~)

超强的兼容性

React具有超强的兼容性,可分为:浏览器的兼容跨平台兼容

  • React基于虚拟DOM实现了一套自己的事件机制,并且模拟了事件冒泡和捕获的过程,采取事件代理批量更新等方法,从而磨平了各个浏览器的事件兼容性问题
  • 对于跨平台,ReactReact Native都是根据虚拟DOM画出相应平台的UI层,只不过不同的平台画法不同而已

Diff算法

React需要同时维护两棵虚拟DOM树:一棵表示当前的DOM结构,另一棵在React状态变更将要重新渲染时生成。React通过比较这两棵树的差异,决定是否需要修改DOM结构,以及如何修改。这种算法称作Diff算法。

传统的diff算法通过循环递归对节点进行依此对比,其算法复杂度达到了O(n^ 3) ,也就是说,如果展示 一千个节点,就要计算十亿次

React中的diff算法,算法复杂度为O(n),如果展示一千个节点,就要计算一千次

Diff算法的三大策略

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  3. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

分别对应:tree diffcomponent diffelement diff

diff算法对新旧两棵树做的是深度优先遍历,避免对两棵树做完全比较,算法复杂度可以达到O(n)。

tree diff

由于跨层级的DOM移动操作较少,所以React diff算法的tree diff没有针对此种操作进行深入比较,只是简单进行了删除和创建操作

在这里插入图片描述 如图所示,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单地考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。

当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,diff 的执行情况:create A → create B → create C → delete A

由此可以发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的整个树被重新创建。这是一种影响 React 性能的操作,因此官方建议不要进行 DOM 节点跨层级的操作。

基于上述原因,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真正地移除或添加 DOM 节点

component diff component diff是专门针对更新前后的同一层级间的React组件比较的diff 算法:

  • 如果是同一类型的组件,按照原策略继续比较 Virtual DOM 树(例如继续比较组件props和组件里的子节点及其属性)即可。
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点,即销毁原组件,创建新组件。
  • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切知道这点,那么就可以节省大量的 diff 运算时间。因此,React 允许用户通过 shouldComponentUpdate()来判断该组件是否需要进行 diff 算法分析

在这里插入图片描述

如图 所示,当组件 D 变为组件 G 时,即使这两个组件结构相似,一旦 React 判断 D 和G 是不同类型的组件,就不会比较二者的结构,而是直接删除组件 D,重新创建组件 G 及其子节点。

虽然当两个组件是不同类型但结构相似时,diff 会影响性能,但正如 React 官方博客所言:不同类型的组件很少存在相似 DOM树的情况,因此这种极端因素很难在实际开发过程中造成重大的影响

element diff element diff是专门针对同一层级的所有节点(包括元素节点和组件节点)的diff算法。当节点处于同一层级时,diff 提供了 3 种节点操作,分别为 INSERT_MARKUP(插入)MOVE_EXISTING(移动)REMOVE_NODE(删除)

我们将虚拟dom树中欲比较的某同一层级的所有节点的集合分别称为新集合和旧集合,则有以下策略:

  • INSERT_MARKUP:新集合的某个类型组件或元素节点不存在旧集合里,即全新的节点,需要对新节点执行插入操作。
  • MOVE_EXISTING:新集合的某个类型组件或元素节点存在旧集合里,且 element 是可更新的类型,generateComponent-Children 已调用receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
  • REMOVE_NODE:旧集合的某个组件或节点类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者旧组件或节点不在新集合里的,也需要执行删除操作。

在这里插入图片描述 如图 所示,旧集合中包含节点A、B、C 和 D,更新后的新集合中包含节点 B、A、D 和C(只是发生了位置变化,各自节点以及内部数据没有变化),此时新旧集合按顺序进行逐一的diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除旧集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。

React 发现这类操作烦琐冗余,因为这些都是相同的节点,但由于位置顺序发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。

针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分

Key的作用

同一层级的节点位置发生了变化后。react diff算法通过新旧节点比较后,如果发现了key值相同的新旧节点,就会执行移动操作(然后依然按原策略深入节点内部的差异对比更新),而不会执行原策略的删除旧节点,创建新节点的操作。这无疑大大提高了React性能和渲染效率

不用index做key是因为index会变,react会做删除新增操作,key是唯一的话,就只会做移动操作。

React.lazy与Suspense

在现代Web应用中,特别是在使用React构建的大型应用中,代码分割是一种常见的优化技术,用于按需加载组件,从而减少初始加载时间,提高用户体验。React 16.6版本引入了React.lazy()Suspense两个API,为代码分割提供了更高级的支持。

代码分割允许你将应用分割成多个小块,只有当用户需要某个特定功能时才加载对应的代码。这有助于减少首次加载时间,提高应用性能,尤其是在移动设备或低带宽网络环境下。

React.lazy()是一个工厂函数,它接受一个函数作为参数,这个函数必须调用import()返回一个Promise,该Promise解析为一个模块对象。这个模块对象应该有一个默认导出,这个默认导出就是你想要懒加载的组件。

import React, { lazy, Suspense } from 'react';

const PostDetail = lazy(() => import('./PostDetail'));

function App() {
  return (
    <div className="App">
      <PostsList />
      <Suspense fallback={<div>Loading...</div>}>
        <PostDetail />
      </Suspense>
    </div>
  );
}

export default App;

Suspense是一个React组件,它允许你在等待异步数据(如动态导入的组件)加载时展示一个“加载中”状态。它通常与React.lazy()一起使用,提供一个优雅的加载体验。

分析React.lazy()的工作原理

当你使用React.lazy()时,React会在需要渲染组件时异步加载组件。这个过程发生在浏览器的后台,不会阻塞UI线程,因此用户界面仍然响应迅速。

分析Suspense的工作原理

当Suspense的子组件(如PostDetail)还没有加载完成时,Suspense会挂起整个渲染树,直到所有的异步数据加载完成。在此期间,Suspense会展示fallback属性指定的内容。

错误捕获

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

React 生命周期

老的生命周期

挂载

  • constructor
  • componentWillMount
  • render
  • componentDidMount

更新

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

卸载

  • componentWillUnmount

新的生命周期

  • 挂载

    • constructor
    • getDerivedStateFromProps
    • render
    • componentDidMount
  • 更新

    • getDerivedStateFromProps
    • shouldComponentUpdate
    • render
    • getSnapshotBeforeUpdate
    • componentDidUpdate
  • 卸载

    • componentWillUnmount

从以上生命周期的对比,我们不难看出,React从v16.3开始废弃 componentWillMount componentWillReceiveProps componentWillUpdate 三个钩子函数

getDerivedStateFromProps

getDerivedStateFromProps 首先它是 静态 方法, 方法参数分别下一个 props、上一个 state, 这个生命周期函数是为了替代 componentWillReceiveProps 而存在的, 主要作用就是监听 props 然后修改当前组件的 state

// 监听 props 如果返回非空值, 则将返回值作为新的 state 否则不进行任何处理
static getDerivedStateFromProps(nextProps, prevState) {
  const { type } = nextProps;

  // 返回 nuyll: 对于 state 不进行任何操作
  if (type === prevState.type) {
    return null;
  }

  // 返回具体指则更新 state
  return { type }
}

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate 生命周期将在 render 之后 DOM 变更之前被调用, 此生命周期的返回值将作为 componentDidUpdate 的第三个参数进行传递, 当然通常不需要此生命周期, 但在重新渲染期间需要手动保留 DOM 信息时就特别有用

getSnapshotBeforeUpdate(prevProps, prevState){
  console.log(5);
  return 999;
}

componentDidUpdate(prevProps, prevState, snapshot) {
  console.log(6, snapshot);
}