一、 react 16 fiber
vue中是使用Object.defineProperty / Proxy 对数据的设置 (setter)和获取 (getter)做了数据劫持、和对数据依赖的派发,vue能准确的知道数据改变后、哪些视图需要重新渲染。相比较于react颗粒度更细。
react中,setState 不会立即修改组件中引用的值,会有一个合并,数据更新之后 自顶向下重新渲染组件(从setData的组件以及它的子组件会全部重新渲染)颗粒度没有vue那么细,在react fiber 之前,react也提供了PureComponent、shouldComponentUpdate、useMemo,useCallback等方法给我们,来声明哪些是不需要连带更新子组件的
当然:上面说的渲染并不是直接渲染更新dom,大概流程是这样的:
- 组件渲染生成一棵新的虚拟dom树;
- 新旧虚拟dom树对比,找出变动的部分;(也就是常说的diff算法)
- 为真正改变的部分创建真实dom,把他们挂载到文档,实现页面重渲染;
举个栗子:
react中修改父组件中的值(子组件并不依赖修改的值)
体验地址:codesandbox.io/s/react-par…
同样在vue中
体验地址: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
多运行几遍可以看到 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)
})
执行结果:
早期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)
})
})
执行结果:
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)
})
运行结果:
如果在新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)
});
})
结果:
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>)
}
执行结果:
结论: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>
)
}