深入源码,react中是如何进行渲染控制的?

hey🖐! 我是小黄瓜😊😊。不定期更新,期待关注➕ 点赞,共同成长~

当我们在使用框架进行单页应用开发的时候,比如vue,react,abgular等。我们通常只需要关注于业务逻辑的开发即可,对于视图层是如何渲染的,框架的底层已经帮我们做好了,无需再对视图层的渲染进行额外的操作。

但是对于一些大型的复杂项目来说,业务逻辑非常繁重的时候,我们不可避免的需要进行项目优化。而对于浏览器来说消耗性能最为严重的部分就是渲染和绘制。对于react来说,以一个组件的范围而言,首先就是要着眼于stateprops的减少执行次数。

不过在此之前,我们还需要了解一下react整个大致的执行流程,不过本文的重点还是讲解关于渲染控制api的讲解,整个react的执行流程只会关注一个一致的流程走向。

一. 绕不开的Fiber

react整个创建和更新流程中起到关键作用的就是Fiber架构,那么到底什么是Fiber呢? 它在react中作为最小的执行单元,作为一个节点存在,在内部包含了一系列创建和更新所需要的信息。在HTML中的一切都可以找到所代表的fiber节点,比如标签,普通文本等等。许多个fiber节点互相关联、嵌套,就组成了代表整个应用的fiber树。

js 复制代码
function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Fiber元素的静态属性相关
  this.tag = tag;
  this.key = key; // fiber的key
  this.elementType = null;
  this.type = null; // 对应的DOM元素的标签类型,div、p...
  this.stateNode = null; // 实例,类组件场景下,是组件的类,HostComponent场景,是dom元素

  // Fiber 链表相关
  this.return = null; // 指向父级fiber
  this.child = null; // 指向子fiber
  this.sibling = null; // 同级兄弟fiber
  this.index = 0;

  this.ref = null; // ref相关

  // Fiber更新相关
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null; // 存储update的链表
  this.memoizedState = null; // 类组件存储fiber的状态,函数组件存储hooks链表
  this.dependencies = null;

  this.mode = mode;

  // Effects
  // flags原为effectTag,表示当前这个fiber节点变化的类型:增、删、改
  this.flags = NoFlags;
  this.nextEffect = null;

  // effect链相关,也就是那些需要更新的fiber节点
  this.firstEffect = null;
  this.lastEffect = null;

  this.lanes = NoLanes; // 该fiber中的优先级,它可以判断当前节点是否需要更新
  this.childLanes = NoLanes;// 子树中的优先级,它可以判断当前节点的子树是否需要更新

  /*
  * 可以看成是workInProgress(或current)树中的和它一样的节点,
  * */
  this.alternate = null;

}

其实整个fiber树是以链表的形式进行首尾连接的,比如说我们现在有如下jsx代码:

jsx 复制代码
function App() {
  return (
    <div>
      hello
      <p>瓜瓜</p>
    </div>
  )
}

它最终会会形成如下fiber链表:

其中每一个 fiber 是通过 returnchildsibling(同级) 三个属性建立起联系的。return: 指向父级 Fiber 节点。 child: 指向子 Fiber 节点。 sibling(同级):指向兄弟 fiber 节点。

整个fiber架构中还存在双缓存树的概念,在每个fiber结构中的alternate保存着与之对应的workInProgress缓存树。 当进行视图更新的时候,会同时存在两棵fiber树,一个current树,是当前显示在页面上内容对应的fiber树。另一个是workInProgress树,它是依据current树深度优先遍历构建出来的新的fiber树,所有的更新最终都会体现在workInProgress树上。当更新未完成的时候,页面上始终展示current树对应的内容,当更新结束时(commit阶段的最后),页面内容对应的fiber树会由current树切换到workInProgress树,此时workInProgress树即成为新的current树。

如下图所示:

还有一个点需要注意一下几个概念,elementfiberDOM、其实这是在不同的处理过程的不同形态。

  • elementReact 视图层在代码层面的表现,也就是的 jsx 语法,元素结构,都会被创建成 element 对象的形式。上面保存了 propschildren 等信息。
  • DOM 是元素在浏览器上真正的dom元素,也就是用于在浏览器中绘制的html。
  • fiber 可以说是是 element 和真实 DOM 之间的交流桥梁,每一个类型 element 都会有一个与之对应的fiber 类型,element 变化引起更新流程都是通过 fiber 做一次调和改变,然后形成新的 DOM 做视图渲染。

调和指的是:新旧dom树进行对比的过程。

二. render

render阶段实际上是在内存中构建一棵新的fiber树(称为workInProgress树),构建过程是依照现有fiber树(current树)从root开始深度优先遍历再回溯到root的过程,这个过程中每个fiber节点都会经历两个阶段:beginWorkcompleteWork

beginWork是向下调和的过程。就是由 fiberRoot 按照 child 指针逐层向下调和,而completeWork是向上归并的过程,如果有兄弟节点,会返回 sibling(同级)兄弟,没有返回 return 父级,一直返回到 FiebrRoot。 组件的状态计算、diff的操作以及render函数的执行,发生在beginWork阶段,effect链表的收集、被跳过的优先级的收集,发生在completeWork阶段。构建workInProgress树的过程中会有一个workInProgress的指针记录下当前构建到哪个fiber节点,这是React更新任务可恢复的重要原因之一。

期间还会有Scheduler进行任务调度,以便高优先级的任务会被优先处理。 但是本文重点不在此,所以不会赘述,以后会写别的文章。

三. commit

render阶段结束后,会进入commit阶段,该阶段不可中断,主要是去依据workInProgress树中有变化的那些节点(render阶段的completeWork过程收集到的effect链表),去完成DOM操作,将更新应用到页面上,除此之外,还会异步调度useEffect以及同步执行useLayoutEffect

commit 细分可以分为三个阶段:

  • Before mutation 阶段:执行 DOM 操作前

没修改真实的 DOM ,是获取 DOM 快照的最佳时期,如果是类组件有 getSnapshotBeforeUpdate,会在这里执行。

  • mutation 阶段:执行 DOM 操作

对新增元素,更新元素,删除元素。进行真实的 DOM 操作。

  • layout 阶段:执行 DOM 操作后

DOM 已经更新完毕。

React最粗略的执行流程就说完了,而对于渲染控制,就是在各个流程的执行中,控制 render 的方式,通常有两种方式:

  • 在父组件直接阻断子组件的渲染,比如 memo
  • 从组件自身来控制 render ,比如:PureComponentshouldComponentUpdate

四. PureComponent

当类组件选择了继承 PureComponent,就会会对propsstate 进行浅比较,从而跳过不必要的更新(减少 render 的次数),提高组件性能。

jsx 复制代码
import { PureComponent } from "react";

class Index extends PureComponent<any, any> {

	constructor(props: any) {
		super(props);
		this.state = {
		data: {
			num: 0,
		},
	};
}

  
render() {
	const { data } = this.state;
	return (
		<>
		<div>hello</div>
			<div> 数字: {data.num}</div>
			<button
				onClick={() => {
					const { data } = this.state;
					data.number++;
					this.setState({ data });
				}}>	
				数字长大
			</button>
		</>
	);
}

export default Index;

可以看到数字并没有增加,因为PureComponent 会比较两次的 data 对象,引用地址并没有被改变,所以数字并不会增加,视图也不会更改·。 浅比较只会比较基础数据类型,对于引用类型,因为浅比较两次 data 还是指向同一个内存空间,所以并不会进行更新。

如果想要视图更新也很简单,只需要改变一下赋值方式:

js 复制代码
const { data } = this.state;
data.number++;
this.setState({ data: {...data} })

那么 PureComponent 内部是如何工作的呢?

当我们的类组件选择继承 PureComponent 组件的时候,原型链上挂载一个 isPureReactComponent 属性:

js 复制代码
pureComponentPrototype.isPureReactComponent = true;

isPureReactComponent 这个属性在更新组件 updateClassInstance 方法中会被使用:

js 复制代码
function checkShouldComponentUpdate(
  workInProgress: Fiber, 
  ctor: any,
  oldProps: any,
  newProps: any,
  oldState: any,
  newState: any,
  nextContext: any
  ){ 
	// 省略其他代码...
	if (ctor.prototype && ctor.prototype.isPureReactComponent) { 
		return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) 
	} 
}

可以看到,react在内部会判断组件的原型链中是否存在isPureReactComponent属性,然后进行浅比较propsstate

shallowEqual函数是如何进行浅比较的呢?

js 复制代码
function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (Object.is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      !Object.is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }

  return true;
}
  1. 首先比较新旧 propsstate 是否相等,如果相等,则返回 true,不更新组件;
  2. 接下来判断新旧 propsstate 是否为对象,如果不是对象或为 null 的情况,则返回 false,更新组件;
  3. 通过 Object.keys 转化为数组,判断长度是否相等,不相等则需要更新
  4. 然后将新旧 propsstate ,利用key的长度进行遍历,深层次的进行比对,有新增或减少,返回 false,更新组件;

所以继承PureComponent其实就相当于在内部自动帮你实现了shouldComponentUpdated

五. shouldComponentUpdated

shouldComponentUpdate 是一个生命周期的方法,直接在组件内部使用:

jsx 复制代码
 class Index extends Component{ 
	 state={ 
		 a:0, b:0 
	 }
	 shouldComponentUpdate(newProp,newState){ 
		 if(newState.a !== this.state.a){
		  return true 
		 } 
		 return false 
	  } 
	  render(){}
 }

shouldComponentUpdate其实就是手动实现了PureComponent的功能,两者也可以嵌套使用,在checkShouldComponentUpdate函数中也有相对应shouldComponentUpdate逻辑的处理。

js 复制代码
function checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext) {
  const instance = workInProgress.stateNode;
  // 判断是否存在shouldComponentUpdate?
  if (typeof instance.shouldComponentUpdate === 'function') {
    let shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );
    return shouldUpdate;
  }

  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }

  return true;
}

从代码里面也可以看到当shouldComponentUpdatePureComponent两者同时存在时,shouldComponentUpdate优先级更高。

六. memo

可以对比 props 变化,来决定是否渲染组件,首先先来看一下 memo 的基本用法。 memo 接受两个参数,第一个参数 Component 原始组件本身,第二个参数 fn 是一个函数,可以根据一次更新中 props 是否相同决定原始组件是否重新渲染。

js 复制代码
memo(Component,fn)
  • memo第二个参数 返回 true 组件不渲染 , 返回 false 组件重新渲染。
  • memo 当二个参数 fn 不存在时,会用浅比较 处理 props,相当于只比较 props 版本的 pureComponent
jsx 复制代码
import { Component, memo } from "react";

const Child = ({ num, msg = "" }) => {
  return (
    <>
      {console.log(`${msg}子组件渲染`)}
      <p>
        {msg}数字:{num}
      </p>
    </>
  );
};

const MemoChild = memo(Child, (pre, next) => {
  if (pre.num === next.num) return true;
  if (next.num > 7) return false;
  return true;
});

class Index extends Component {
  constructor(props) {
    super(props);
    this.state = {
      flag: true,
      num: 1,
    };
  }

  render() {
    const { flag, num } = this.state;
    return (
      <div>
        <Child num={num} />
        <MemoChild num={num} msg="memo" />
        <button onClick={() => this.setState({ flag: !flag })}>
          状态切换{JSON.stringify(flag)}
        </button>
        <button
          style={{ marginLeft: 8 }}
          onClick={() => this.setState({ num: num + 1 })}
        >
          数字加一:{num}
        </button>
      </div>
    );
  }
}

export default Index;

当我们变更无关变量 flag 时,没有被 memo 包裹的子组件 Child 也会进行渲染,而包裹的则不会。同时 memo 的第二个参数可以主动控制是否渲染。

memo 包裹的组件,element 会被打成 REACT_MEMO_TYPE 类型的 element 标签,在 element 变成 fiber 的时候, fiber 会被标记成 MemoComponent 的类型。

js 复制代码
function memo(type,compare){
  const elementType = {
    $$typeof: REACT_MEMO_TYPE, 
    type, 
    compare: compare === undefined ? null : compare,  //第二个参数
  };
  return elementType
}


fiberTag = MemoComponent;

而React 对 MemoComponent 类型的 fiber 有单独的函数 updateMemoComponent进行处理。

js 复制代码
function updateMemoComponent(){
    if (updateExpirationTime < renderExpirationTime) {
         let compare = Component.compare;
         compare = compare !== null ? compare : shallowEqual //如果 memo 有第二个参数,则用二个参数判定,没有则浅比较props是否相等。
        if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
        }
    }
    // 返回将要更新组件,memo包装的组件对应的fiber,继续向下调和更新。
}

七. useMemo & useCallback

useMemo

useMemo:理念与 memo 相同,都是判断是否满足当前的条件来决定是否执行callback 函数。在依赖不变的情况下,会返回相同的引用,避免子组件进行无意义的重复渲染。

返回值:更新之后的数据源,即 fn 函数的返回值,如果 deps 中的依赖值发生改变,将重新执行 fn,否则取上一次的缓存值。

js 复制代码
const cacheData = useMemo(fn, deps)
js 复制代码
import { useState, useMemo } from "react";

const usePow2 = (list) => {
  return list.map((item) => {
    console.log("我是usePow2");
    return item * 2;
  });
};

// 被useMemo包裹
const usePow = (list) => {
  return useMemo(
    () =>
      list.map((item) => {
        console.log(1);
        return Math.pow(item, 2);
      }),
    [],
  );
};

const Index = () => {
  let [flag, setFlag] = useState(true);

  const data = usePow([1, 2, 3]);
  const data2 = usePow2([1, 2, 3]);

  return (
    <>
      <div>数字集合:{JSON.stringify(data)}</div>
      <div>数字集合2:{JSON.stringify(data2)}</div>
      <button onClick={() => setFlag((v) => !v)}>
        状态切换{JSON.stringify(flag)}
      </button>
    </>
  );
};

export default Index;

可以看到,即使传入的参数没有改变,未被useMemo包裹的函数依然会执行。

useCallback

useMemo 用法一致,唯一不同的点在于,useMemo 返回的是值,而 useCallback 返回的是函数。

js 复制代码
const res = useCallback(fn, deps)

返回值:即 fn 函数,如果 deps 中的依赖值发生改变,将重新执行 fn,否则取上一次的函数。

用法:

js 复制代码
const Index = () => {
  let [count, setCount] = useState(0);
  let [flag, setFlag] = useState(true);

  const add = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <>
      <button onClick={() => setCount((v) => v + 1)}>普通点击</button>
      <button onClick={add}>useCallback点击</button>
      <div>数字:{count}</div>
      <button onClick={() => setFlag((v) => !v)}>
        切换{JSON.stringify(flag)}
      </button>
    </>
  );
};

useMemouseCallback内部的实现非常相似:

初始化:

js 复制代码
// mountMemo
function mountMemo<T>(
  nextCreate: () => T, 
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
js 复制代码
// mountCallback
function mountCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

在初始化中,useMemo 首先创建一个 hook,然后判断 deps 的类型,执行 nextCreate,这个参数是需要缓存的值,然后将值与 deps 保存到 memoizedState 上

useCallback 直接将 callback和 deps 存入到 memoizedState 里。

更新:

js 复制代码
// updateMemo
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  // 判断新值
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      //之前保存的值
      const prevDeps: Array<mixed> | null = prevState[1];
      // 与useEffect判断deps一致
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

useMemo通过判断两次的 deps 是否发生改变,如果发生改变,则重新执行 nextCreate(),将得到的新值重新复制给 memoizedState保存;如果没发生改变,则直接返回缓存的值。

js 复制代码
// updateCallback
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
        //之前保存的值
      const prevDeps: Array<mixed> | null = prevState[1];
      // 与useEffect判断deps一致
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

可以看到useMemo只是直接将依赖函数执行之后进行保存,而updateCallback直接将依赖函数保存。

值得注意的是:虽然react提供了很多可以进行手动来控制渲染的方法,但是我们在大多数情况下不需要去特别的追求完美的控制每一次多余的更新和渲染。而且如果使用不慎,甚至会适得其反。如果在业务中封装业务组件或者需要一次性展示大量的数据等等情况下可以考虑手动进行优化。

至于后续react是如何根据生成的fiber信息进行更新的,且听下回分解。

写在最后

未来可能会更新react和工程化相关的系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳

本文参考: juejin.cn/book/723062...

juejin.cn/book/694599...

相关推荐
m0_548514779 分钟前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
AndrewPerfect9 分钟前
xss csrf怎么预防?
前端·xss·csrf
Calm55013 分钟前
Vue3:uv-upload图片上传
前端·vue.js
浮游本尊17 分钟前
Nginx配置:如何在一个域名下运行两个网站
前端·javascript
m0_7482398317 分钟前
前端bug调试
前端·bug
m0_7482329220 分钟前
[项目][boost搜索引擎#4] cpp-httplib使用 log.hpp 前端 测试及总结
前端·搜索引擎
新中地GIS开发老师25 分钟前
《Vue进阶教程》(12)ref的实现详细教程
前端·javascript·vue.js·arcgis·前端框架·地理信息科学·地信
m0_7482495427 分钟前
前端:base64的作用
前端
html组态33 分钟前
web组态可视化编辑器
前端·物联网·编辑器·web组态·组态·组态软件
~央千澈~40 分钟前
如果你的网站是h5网站,如何将h5网站变成小程序-除开完整重做方法如何快速h5转小程序-h5网站转小程序的办法-优雅草央千澈
前端·apache