react的渲染优化(fiber, react 18)

435 阅读6分钟

一、 react 16 fiber

vue中是使用Object.defineProperty / Proxy 对数据的设置 (setter)和获取 (getter)做了数据劫持、和对数据依赖的派发,vue能准确的知道数据改变后、哪些视图需要重新渲染。相比较于react颗粒度更细。

react中,setState 不会立即修改组件中引用的值,会有一个合并,数据更新之后 自顶向下重新渲染组件(从setData的组件以及它的子组件会全部重新渲染)颗粒度没有vue那么细,在react fiber 之前,react也提供了PureComponent、shouldComponentUpdate、useMemo,useCallback等方法给我们,来声明哪些是不需要连带更新子组件的
当然:上面说的渲染并不是直接渲染更新dom,大概流程是这样的:

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

举个栗子:
react中修改父组件中的值(子组件并不依赖修改的值)

image-react.png 体验地址:codesandbox.io/s/react-par…

同样在vue中

image-vue.png 体验地址:codesandbox.io/s/vue-paren…

2、fiber是什么?

因为react会生成一棵更大的树,而且每次都要递归diff完才能找到真正变化的部分,这需要花费较长的时间,会占用主线程,导致到了浏览器下一帧的时候,不能把主线程归还给浏览器去做其他工作(scroll、input、onClick)。为了使一棵很大的虚拟dom树diff的时候不会造成浏览器卡顿。所以 fiber 诞生了;

fiber是一种新的数据结构 (链表)

react fiber使得diff阶段有了被保存工作进度的能力 ( 当diff过程执行在主线程中时、到了下一帧的时候,diff过程暂停,并标记执行的地方,到了下一帧主线程有剩余时间的时候,接着diff );
react16 以后虚拟dom不再是树状结构、是链表形式的、可以指向兄弟元素、子元素、父元素

fiber大致结构:

function createFiberTree() {
    let rootFiber =  {
      type: 'div',
      sibling: null,
      return: null,
      child: {
        type: 'ul',
        return: null,
        sibling: {
          type: 'button',
          return: null,
          sibling: null,
          child: null 
        },
        child: {
          type: 'li',
          return: null,
          child: null,
          sibling: {
            type: 'li',
            return: null,
            child: null 
          }
        }
      }
    }
    rootFiber.return = null;
    rootFiber.child.return = rootFiber
    rootFiber.child.sibling.return  = rootFiber;
    let ul = rootFiber.child;
    rootFiber.child.child.return = ul;
    rootFiber.child.child.sibling.return = ul;
    return rootFiber;
  }

requesetIdleCallback

requesetIDleCalback API 受屏幕的刷新率去控制,回掉函数中可以拿到当前帧的主线程剩余时间,如果当前帧有剩余时间就接着执行diff过程, 如果没有剩余时间就 申请下一次 requesetIDleCalback

image.png

image.png 多运行几遍可以看到 requestIdleCallback 的执行并不会按照每秒60帧去执行,只有每秒20帧,这样的话给用户视觉上的体验还是会卡顿、并且requestIdleCallback浏览器的兼容性不好, 所以React中 Fiber使用是React团队自己写的类似于requestIdleCallback的方法。达到每秒60帧的效果。 ( requestAnimationFrame 和 postMessage)

requestIdleCallback 和 requestAnimationFrame

requestAnimationFrame 告诉浏览器——你希望执行一个动画,并且要求浏览器在 下次重绘之前 调用指定的回调函数更新动画, 优先级比较高,requestAnimationFrame会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成

二、 react 18 自动批量更新

1、在 react17 中 如果在 setTimeout 多次设置了 state,则 react 会多次更新

  this.setState({ count:this.state.count+1})
    console.log("count1",this.state.count)

    setTimeout(()=>{
        this.setState({ count:this.state.count+1})
        console.log("count2",this.state.count)
        this.setState({ count:this.state.count+1})
        console.log("count3",this.state.count)
    })

执行结果:

image.png

早期react提供了手动批量处理 Api React-dom.unstable_batchedUpdates 可以手动实现批量处理

 this.setState({ count:this.state.count+1})
    console.log("count1",this.state.count)

    setTimeout(()=>{
        unstable_batchedUpdates(()=>{
          this.setState({ count:this.state.count+1})
          console.log("count2",this.state.count)
          this.setState({ count:this.state.count+1})
          console.log("count3",this.state.count)
        })
    })

执行结果:

image.png

react 18 中 可以使用 使用全新的Api: ReactDOM.createRoot( root ) .render( ) 来支持自动批量更新:

根目录下index.js中:

// ReactDOM.render(
//   <React.StrictMode>
//     <App />
//   </React.StrictMode>,
//   document.getElementById('root')
// );

ReactDOM.createRoot(document.getElementById("root")).render(<App/>)

组件方法:

  this.setState({ count:this.state.count+1 })
    console.log("count1",this.state.count)

    setTimeout(()=>{
      this.setState({ count:this.state.count+1 })
      console.log("count2",this.state.count)
      this.setState({ count:this.state.count+1 })
      console.log("count3",this.state.count)
    })

运行结果:

image.png

如果在新API中不需要批量处理,就是要单独处理渲染,React 18 又提供了补救方案:flushSync

 import { flushSync } from 'react-dom'; 
  
  this.setState({ count:this.state.count+1 })
        console.log("count1",this.state.count)

        // 如果不想自动批量更新
        setTimeout(()=>{
            flushSync(() => {
                this.setState({ count:this.state.count+1 })
                console.log("count2",this.state.count)
            });
            flushSync(() => {
                this.setState({ count:this.state.count+1 })
                console.log("count3",this.state.count)
            });
        })

结果:

image.png

2、Suspense

作用类似于骨架屏…… ……

const initialResource = fetchProfileData()
const [resource, setResource] = useState(initialResource);
  
return <>
     <div> 
       <h2>  同步加载内容……………… </h2> 
    </div>
    <Suspense  fallback={
        <>
          <h2>This is Page Loading ...</h2>
        </>}>
        <Sibling name="one" />
        <ProfileDetails resource={resource} />
    </Suspense>
    </>
}
function Sibling({ name }) {
    return <h2> 空闲 时间 异步加载内容</h2>;
  }

function ProfileDetails({ resource }) {
const user = resource.user.read();
return <h2> 异步请求内容: {user.name}</h2>;
}

3、startTransition

startTransition告诉浏览器、其他紧急的任务优先处理,这里是一些不紧急的任务,可以稍后处理
类似于 promise、setTimeout , 我们可以把它们三个做一个比较:

import React,{ useState, useEffect, startTransition, useMemo } from 'react';
import "./index.css"
export default function Transitions(){
  const [value,setValue] = useState(0)
  const [time,setTime] = useState(0)
  const [pro,setPro] = useState(0) 
  const [text,setText] = useState('')
  const [list,setList] = useState(new Array(9220).fill(1))
  
  const sleep = delay => {
    for (let start = Date.now(); Date.now() - start <= delay;) {console.log('循环中 ******') }
  }

  const onChange = ()=>{
    setTimeout(()=>{
      setTime(1)
    })
    startTransition(()=>{
        setValue(1)
    })

    new Promise(res=>res('ok')).then((res=>{ 
      setPro(1)
     }))
    sleep(100)
    console.log("循环完毕")
  }

  useEffect(()=>{
    console.log("startTransition 设置value完毕")
  },[value])

  useEffect(()=>{
    console.log("setTimeout 设置time完毕")
  },[time])

  useEffect(()=>{
    console.log("promise 设置time完毕")
  },[pro])

  const handleChange = (e)=>{
    // setText(e.target.value);
    startTransition(()=>{
      setText(e.target.value);
    })

  }

  const showList = useMemo(()=>{
  return  <div>
    {
        list.map(v=>(
          <h4> 输入内容:{text} </h4>
        ))
    }
    </div>
  },[list, text])

  return (
    <div>
      <h2>   - - - - - - - - - -  startTransition API   - - - - - - - - - - </h2>
            
      <button onClick={onChange}>  修改value </button>
      <div>
        <input  onChange={handleChange}/>
      </div>

      {showList}
     
    </div>)
}

执行结果:

image.png

结论:startTransition 、promise、setTimeout 的优先顺序 promise > startTransition > setTimeout

它与setTimeout有什么不同?

  • SetTimeout被安排在稍后,而startTransition立即执行。传递给startTransition的函数是同步运行的,但是其中的任何更新都被标记为“transitions”。React将在稍后处理更新时使用这些信息来决定如何呈现更新。这意味着我们开始渲染更新的时间要比包装在timeout中的更新要早。
  • 另一个重要的区别是setTimeout内部的大屏幕更新仍然会锁定页面,就在超时之后。但是标记为startTransition的状态更新是可中断的,所以它们不会锁定页面。它们允许浏览器在呈现不同组件之间的小间隙中处理事件。如果用户输入发生了变化,React就会继续呈现有关最新变化的内容。
  • 最后,由于编写异步代码,通常很容易显示setTimeout的加载指示器;但是使用转换api, React可以跟踪挂起状态,根据转换的当前状态更新它,并让你能够在用户等待时显示加载反馈。 一句话: setTimeout的延迟是在处理逻辑上的延迟,startTransition 的延迟是在元素渲染更新上的延迟,并且更新可以中断,优先级给更高的 input之类的用户操作

4、useDeferredValue

过度单个状态值,让状态滞后变化,避开紧急任务的渲染,让出优先级

useDeferredValue允许我们选择的UI的特定部分并特意推出它们,每次有数据重新赋值,让其他UI优先更新,再更新延迟更新部分、控制渲染顺序; 把优先级让给 input 的 onChange 之类的与用户的交互操作 体验代码:

import React,{ useState, useDeferredValue, useMemo } from 'react';
import "./index.css"
const arr2 = new Array(1).fill({name:'李四',age:"20"})
const arrLisi = new Array(9999).fill({name:'李四',age:"20"})

export default function UseDeferred(){
    const [lisi,setLisi] = useState(arr2)
    const deferredValue = useDeferredValue(lisi, { timeoutMs: 100 });
    const [text,setText] = useState('')
    const setMoreAndMore = ()=>{
        setLisi(arrLisi)
    }
    const handleChange = (e)=>{
        setText(e.target.value);
    }
    const lisiList = useMemo(()=>{
     return  <div>
        {
          deferredValue.length &&  deferredValue.map((v,i)=>(
                <div className='lisi' key={i}>  
                    <Lisi  data={{...v,i}}/>
                </div>

            ))
        }
    </div>
    },[deferredValue])

    // const lisiList2 = useMemo(()=>{
    //     return  <div>
    //        {
    //          lisi.length &&  lisi.map((v,i)=>(
    //                <div className='lisi' key={i}>  
    //                    <Lisi  data={{...v,i}}/>
    //                </div>
   
    //            ))
    //        }
    //    </div>
    // },[lisi])

    return(
      <>
      <h2>   - - - - - - - - - -  useDeferredValue API   - - - - - - - - - - </h2>
            <button onClick={setMoreAndMore}>
                设置更多值
            </button>
            <h4> 输入内容:{text} </h4>
            <input value={text} onChange={handleChange}/>
            <div className='my-useDeferred'>
                {lisiList}
                {/* {lisiList2} */}
            </div>
      </>
    )
}

function Lisi ({data}){
    console.log("李四",data.i)
    return(
        <label>
            {data.name} : {data.i}
        </label>
    )
}