react render的原理及触发时机说明

react render的原理及触发时机理解react的render函数,要从这三点来认识。原理、执行时机、总结。 原理 在类组件和函数组件中,render函数

react render的原理及触发时机

理解react的render函数,要从这三点来认识。原理、执行时机、总结。

原理

在类组件和函数组件中,render函数的形式是不同的。

在类组件中render函数指的就是render方法;而在函数组件中,指的就是整个函数组件。

class Foo extends React.Component {
    render() { //类组件中

        return <h1> Foo </h1>;

    }

}
function Foo() {  //函数组件中

    return <h1> Foo </h1>;

}

在render函数中的jsx语句会被编译成我们熟悉的js代码

render过程中,React 将新调用的 render函数返回的树与旧版本的树进行比较,这一步是决定如何更新 DOM 的必要步骤,然后进行 diff 比较,更新 DOM树 

触发时机

render的执行时机主要分成了两部分:

类组件调用 setState 修改状态:

class Foo extends React.Component {
  state = { count: 0 };
 
  increment = () => {
    const { count } = this.state;
 
    const newCount = count < 10 ? count + 1 : count;
 
    this.setState({ count: newCount });
  };
 
  render() {
    const { count } = this.state;
    console.log("Foo render");
 
    return (
      <div>
        <h1> {count} </h1>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }

}

函数组件通过useState hook修改状态:

function Foo() {
  const [count, setCount] = useState(0);
 
  function increment() {
    const newCount = count < 10 ? count + 1 : count;
    setCount(newCount);
  }
 
  console.log("Foo render");
  
  return (
    <div>
      <h1> {count} </h1>
      <button onClick={increment}>Increment</button>
    </div>
  ); 

}

函数组件通过useState这种形式更新数据,当数组的值不发生改变了,就不会触发render

小结一下:

render函数里面可以编写JSX,转化成createElement这种形式,用于生成虚拟DOM,最终转化成真实DOM

在React 中,类组件只要执行了 setState 方法,就一定会触发 render 函数执行,函数组件使用useState更改状态不一定导致重新render

组件的props 改变了,不一定触发 render 函数的执行,但是如果 props 的值来自于父组件或者祖先组件的 state

在这种情况下,父组件或者祖先组件的 state 发生了改变,就会导致子组件的重新渲染

所以,一旦执行了setState就会执行render方法,useState 会判断当前值有无发生改变确定是否执行render方法,一旦父组件发生渲染,子组件也会渲染

react Scheduler 原理

学习react也有一段时间了,最近零零碎碎看了些东西,总觉得改写点东西沉淀下,联系到react快速响应的理念,我觉得时间切片的使用是再出色不过了,时间切片的使用离不开scheduler,那就谈谈scheduler吧

scheduler是什么?

react16开始整个架构分成了三层,scheduler,Reconciler,renderer,因为为了实现将一个同步任务变成异步的可中断的任务,react提出了fiber,因为最开始用的是stack,任务是无法中断的,js执行时间太长时会影响页面的渲染造成卡顿,fiber中任务是可以终端,但是中断的任务怎么连上,什么时间执行,哪个先执行,这都属于是新的问题,因此scheduler出生了,以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时,scheduler会通知我们,同时scheduler会进行一系列的任务优先级判断,保证任务时间合理分配。

总结下scheduler的两个功能:

  • 时间切片
  • 优先级调度

时间切片

JS脚本执行和浏览器布局、绘制不能同时执行。在每16.6ms时间内,需要完成 JS脚本执行 ----- 样式布局 ----- 样式绘制,当JS执行时间过长,超出了16.6ms,这次刷新就没有时间执行样式布局和样式绘制了。

页面掉帧,造成卡顿。时间切片是在浏览器每一帧的时间中,预留一些时间给JS线程,React利用这部分时间更新组件,预留的初始时间是5ms。

超过5ms,React将中断js,等下一帧时间到来继续执行js。其实浏览器本身已经实现了时间切片的功能,这个API叫requestIdleCallback,requestIdleCallback 是 window 属性上的方法,它的作用是在浏览器一帧的剩余空闲时间内执行优先度相对较低的任务。

但是由于requestIdleCallback 的这两个缺陷,react决定自己模拟时间切片

  • 1.浏览器兼容不好的问题
  • 2.requestIdleCallback 的 FPS 只有 20,也就是 50ms 刷新一次,远远低于页面流畅度的要求

回顾一个知识浏览器在16.6ms里面要做哪些事情

宏任务-- 微任务 -- requestAnimationFrame -- 浏览器重排/重绘 -- requestIdleCallback

我们可以一起来看下时间切片应该放在哪里,首先排除requestIdleCallback,缺点上文已经提到了,实际上时间切片是放在宏任务里面的,可以先说下为什么不放在其他地方的原因:

1.为什么不是微任务里面

微任务将在页面更新前全部执行完,所以达不到「将主线程还给浏览器」的目的。

2.为什么不使用requestAnimationFrame

如果第一次任务调度不是由 rAF() 触发的,例如直接执行 scheduler.scheduleTask(),那么在本次页面更新前会执行一次 rAF() 回调,该回调就是第二次任务调度。所以使用 rAF() 实现会导致在本次页面更新前执行了两次任务。

为什么是两次,而不是三次、四次?因为在 rAF() 的回调中再次调用 rAF(),会将第二次 rAF() 的回调放到下一帧前执行,而不是在当前帧前执行。

另一个原因是 rAF() 的触发间隔时间不确定,如果浏览器间隔了 10ms 才更新页面,那么这 10ms 就浪费了。(现有 WEB 技术中并没有规定浏览器应该什么何时更新页面,所以通常认为是在一次宏任务完成之后,浏览器自行判断当前是否应该更新页面。如果需要更新页面,则执行 rAF() 的回调并更新页面。否则,就执行下一个宏任务。)

3.既然是宏任务,那么是settimeout吗?

递归执行 setTimeout(fn, 0) 时,最后间隔时间会变成 4 毫秒,而不是最初的 1 毫秒,因为settimeout的执行时机是和js执行有关的,递归是会不准,最终使用 MessageChannel 产生宏任务,但是由于兼容,如果当前宿主环境不支持MessageChannel,则使用setTimeout。

在React的render阶段,开启Concurrent Mode时,每次遍历前,都会通过Scheduler提供的shouldYield方法判断是否需要中断遍历,使浏览器有时间渲染:

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

是否中断的依据,最重要的一点便是每个任务的剩余时间是否用完。

在Schdeduler中,为任务分配的初始剩余时间为5ms。如果shouldYield为true,任务就会中断,中断之后再次执行就要用到调度了

任务调度

Scheduler对外暴露了一个方法unstable_runWithPriority,这个方法可以用来获取优先级

unction unstable_runWithPriority(priorityLevel, eventHandler) {
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

//。。。省略
}

可以看到有5种优先级,比如,我们知道commit阶段是同步执行的。可以看到,commit阶段的起点commitRoot方法的优先级为ImmediateSchedulerPriority。

ImmediateSchedulerPriority即ImmediatePriority的别名,为最高优先级,会立即执行。可是优先级只是一个名称,react如何判断优先级的高低呢,这里我觉得和操作系统里面的一些概念还是挺相似的
给不同任务给上过期时间,谁快过期了就先执行谁

var timeout;
switch (priorityLevel) {
  case ImmediatePriority:
    timeout = IMMEDIATE_PRIORITY_TIMEOUT;
    break;
  case UserBlockingPriority:
    timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
    break;
  case IdlePriority:
    timeout = IDLE_PRIORITY_TIMEOUT;
    break;
  case LowPriority:
    timeout = LOW_PRIORITY_TIMEOUT;
    break;
  case NormalPriority:
  default:
    timeout = NORMAL_PRIORITY_TIMEOUT;
    break;
}

var expirationTime = startTime + timeout;
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;

可以看到 IMMEDIATE_PRIORITY_TIMEOUT =-1,说明比当前时间还早,已经过期,必须快执行,初此之外,react新增了两个队列:已就绪任务 ,未就绪任务

所以,Scheduler存在两个队列:timerQueue:保存未就绪任务,taskQueue:保存已就绪任务

每当有新的未就绪的任务被注册,我们将其插入timerQueue并根据开始时间重新排列timerQueue中任务的顺序。当timerQueue中有任务就绪,即startTime <= currentTime,我们将其取出并加入taskQueue。

取出taskQueue中最早过期的任务并执行他。

简单介绍下scheduler的原理,其实要更多了解scheduler,还要再看看lane模型,这块之后再说吧,还有fiber啥的,有时间再写。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持好代码网。