likes
comments
collection
share

mini-react系列大结局!实现 useMemo & useCallback 🎉🎉

作者站长头像
站长
· 阅读数 3

本系列会实现一个简单的react,包含最基础的首次渲染,更新,hooklane模型等等,本文是本系列的第一篇。这对于我也是一个很大的挑战,不过这也是一个学习和进步的过程,希望能坚持下去,一起加油!期待多多点赞!😘😘

本文致力于实现一个最简单的优化策略执行过程,代码均已上传至github,期待star!✨:

本文是系列文章,阅读的联系性非常重要!!

期待点赞!😁😁

食用前指南!本文涉及到react的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!

上一篇文章使用bailout​策略和eagerState​策略实现了更新时减少多余的渲染次数。在react中同样提供了很多可以使开发者手动控制的优化手段。

React.memo

import { memo } from 'react';

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

memo​用来缓存组件的渲染,避免不必要的更新。

在一般情况下,只要父组件的某一个状态改变了,无论是否对子组件进行更新,所有子组件都会重新再次进行渲染,因此,这时候就需要使用React.memo​对当前组件进行缓存。 ​memo​可以检测从父组件接收的props​,并且在父组件改变state​的时候对比这个state​是否是本组件在使用,如果不是,不会重新触发渲染。

import React from 'react';
// 使用memo包裹
const MyComponent = React.memo(({ name }) => {
  return <div>{name}</div>;
});

// 在父组件中使用
function ParentComponent() {
  const [name, setName] = React.useState('mimi');

  const handleClick = () => {
    setName('Gu');
  };

  return (
    <div>
      <MyComponent name={name} />
      <button onClick={handleClick}>change</button>
    </div>
  );
}

memo​实现的本质:在子组件与父组件之间增加一个MemoComponent​,MemoComponent​通过 「props的浅比较」 命中bailout​策略。

mini-react系列大结局!实现 useMemo & useCallback 🎉🎉

由于我们本质上是用memo​包裹需要性能优化的组件,所以memo​作为一个高阶组件使用。在react创建fiber​树时也会作为一个节点存在,所以新增一个$$typeof​类型REACT_MEMO_TYPE​用来标记memo​的element​节点:

函数组件作为type​属性传入,由于函数组件的函数体也是保存在函数组件element​对象的type​属性,所以相当于形成了对象type​属性的嵌套,所以在memo​节点上获取函数体需要使用memo.type.type​来获取。

export function memo(
	type,
	compare
) {
	const fiberType = {
		$$typeof: REACT_MEMO_TYPE,
		type,
		compare: compare === undefined ? null : compare
	};
	// memo fiber.type.type
	return fiberType;
}

在创建fiber​节点时,创建MemoComponent​类型的节点,代表memo​组件。

export const MemoComponent = 15;

export function createFiberFromElement(element) {
	const { type, key, props, ref } = element;
	let fiberTag: WorkTag = FunctionComponent;

	if (typeof type === 'string') {
		// <div/> type: 'div'
		fiberTag = HostComponent;
	} else if (typeof type === 'object') {
		// memo组件节点
		switch (type.$$typeof) {
			case REACT_MEMO_TYPE:
				fiberTag = MemoComponent;
				break;
			default:
				console.warn('未定义的type类型', element);
				break;
		}
	}
	// ...
	 else if (typeof type !== 'function' && __DEV__) {
		console.warn('为定义的type类型', element);
	}
	// 创建fiber对象
	const fiber = new FiberNode(fiberTag, props, key);
	fiber.type = type;
	fiber.ref = ref;
	return fiber;
}

进入render​阶段后,根据不同的节点类型进入不同的处理逻辑。如果处理到memo​组件类型的节点,调用updateMemoComponent​函数处理,主要是判断props​是否发生变化。

export const beginWork = (wip, renderLane) => {
	// bailout策略
	didReceiveUpdate = false;
	const current = wip.alternate;

	// ...

	// 比较,返回子fiberNode
	switch (wip.tag) {
		case HostRoot:
			return updateHostRoot(wip, renderLane);
		case HostComponent:
			return updateHostComponent(wip);
		case HostText:
			return null;
		case FunctionComponent:
			return updateFunctionComponent(wip, wip.type, renderLane);
		case Fragment:
			return updateFragment(wip);

		// ...
		// memo组件类型
		case MemoComponent:
			return updateMemoComponent(wip, renderLane);
	
		// ...
	}
	return null;
};

取出current​树的props​,与本次更新的props​进行浅比较。如果props​属性没有变化并且该fiber​节点上没有待执行的更新任务,命中性能优化策略,对子fiber​树进行复用。如果没有命中,正常进行FunctionComponent​类型节点的处理逻辑。详细过程见:

function updateMemoComponent(wip, renderLane) {
	// props浅比较
	const current = wip.alternate;
	const nextProps = wip.pendingProps;
	const Component = wip.type.type;

	if (current !== null) {
		// 获取上次更新的props
		const prevProps = current.memoizedProps;

		// 浅比较props
		if (shallowEqual(prevProps, nextProps) && current.ref === wip.ref) {
			// 命中性能优化策略
			didReceiveUpdate = false;
			wip.pendingProps = prevProps;

			// 没有更新任务
			if (!checkScheduledUpdateOrContext(current, renderLane)) {
				wip.lanes = current.lanes;
				// 不再重新创建子fiber树,复用子树
				return bailouOnAlreadyFinishedWork(wip, renderLane);
			}
		}
	}
	// 没有命中,正常执行函数组件的处理逻辑
	return updateFunctionComponent(wip, Component, renderLane);
}

浅比较两个对象:

  • 基本类型使用Object.is​判断是否相同
  • 对两个对象取键组成的数组
  • 比较长度
  • 遍历取值,挨个进行对比
export function shallowEqual(a, b) {
	// 基本类型使用Object.is
	if (Object.is(a, b)) {
		return true;
	}

	if (
		typeof a !== 'object' ||
		a === null ||
		typeof b !== 'object' ||
		b === null
	) {
		return false;
	}
	// 取对象的键组成的数组
	const keysA = Object.keys(a);
	const keysB = Object.keys(b);
	// 判断长度是否一致
	if (keysA.length !== keysB.length) {
		return false;
	}
	// 遍历每个属性
	for (let i = 0; i < keysA.length; i++) {
		const key = keysA[i];
		// b没有key、 key不相等
		if (!{}.hasOwnProperty.call(b, key) || !Object.is(a[key], b[key])) {
			return false;
		}
	}
	return true;
}

Object.is()​ 方法用来判断两个值是否相等,接收两个参数,分别是需要比较的两个值。

不会进行类型转换,返回一个 Boolean​ 值这两个值是否相等。

Object.is(123, 123);  // true
Object.is(123, '123');  // false
Object.is([], []);  // false
Object.is(NaN, NaN); // true

同样是比较两个值是否相同, ==​ 比较两个值是否相等,如果两边的值不是同一个类型的话,会将他们转为同一个类型后再进行比较。

123 == '123';   // true
'' == false;    // true
false == 0;     // true
NaN == NaN;     // false

===​ 不会对类型进行转换,两边的值必须相等且类型相同才会等到 true​ 。对于 0 和 NaN 的比较。无论 0 的正负,他们都是相等的,而 NaN 是与任何值都不相等的,包括他本身。

+0 === -0;  // true
0 === -0;  // true
+0 === 0;  // true
NaN === NaN;  // false

Object.is()​ 会将 NaN 与 NaN 视为相等,无符号的 0 归为整数。

Object.is(0, +0);    // true
Object.is(-0, 0);    // false
Object.is(-0, +0);    // false
Object.is(NaN, NaN);    // true

‍ 在实现useMemo​与useCallback​两个hook之前,先来了解一下react中的hook架构。

数据共享层

hook​架构在实现时,脱离了react部分的逻辑,在内部实现了一个数据共享层,类似于提供一个接口。任何满足了规范的函数都可以通过数据共享层接入处理hook​的逻辑。这样就可以与宿主环境解耦,灵活性更高。

mini-react系列大结局!实现 useMemo & useCallback 🎉🎉

// 内部数据共享层
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
	currentDispatcher
};

const currentDispatcher = {
	current: null
};

currentDispatcher​为我们本次实现的hook​。

所以对应到我们的render​流程以及hook​的应用,他们之间的调用关系是这样的:

mini-react系列大结局!实现 useMemo & useCallback 🎉🎉hook​怎么知道当前是mount​还是update​?

我们在使用hook​时,react在内部通过currentDispatcher.current赋予不同的函数来处理不同阶段的调用,判断hooks 是否在函数组件内部调用。

hook架构

hook​可以看做是函数组件和与其对应的fiber​节点进行沟通和的操作的纽带。在react​中处于不同阶段的fiber​节点会被赋予不同的处理函数执行hooks​:

  • 初始化阶段 -----> HookDispatcherOnMount
  • 更新阶段 -----> HookDispatcherOnUpdate
const HookDispatcherOnMount = {
	useState: mountState,
	useEffect: mountEffect,
	useRef: mountRef,
	useMemo: mountMemo,
	useCallback: mountCallback
};

const HookDispatcherOnUpdate = {
	useState: updateState,
	useEffect: updateEffect,
	useRef: updateRef,
	useMemo: updateMemo,
	useCallback: updateCallback
};

但是实现之前,还有几个问题需要解决:

如何确定fiber对应的hook上下文?

还记得我们在处理函数组件类型的fiber​节点时,调用renderWithHooks​函数进行处理,在我们在执行hook​相关的逻辑时,将当前fiber​节点信息保存在一个全局变量中:

// 当前正在render的fiber
let currentlyRenderingFiber = null;
export function renderWithHooks(wip) {
	// 赋值操作
	currentlyRenderingFiber = wip;
	// 重置
	wip.memoizedState = null;
	const current = wip.alternate;

	if (current !== null) {
		// update
		// hooks更新阶段
	} else {
		// mount
		// hooks初始化阶段
	}

	const Component = wip.type;
	const props = wip.pendingProps;
	const children = Component(props);

	// 重置操作
	// 处理完当前fiber节点后清空currentlyRenderingFiber
	currentlyRenderingFiber = null;

	return children;
}

将当前正在处理的fiber​节点保存在全局变量currentlyRenderingFiber​ 中,我们在处理hook​ 的初始化及更新逻辑中就可以获取到当前的fiber​节点信息。

hook是如何存在的?保存在什么地方?

注意hook​函数只存在于函数组件中,但是一个函数组件的fiber​节点时如何保存hook​信息呢?

答案是:memoizedState​。

fiber​节点中保存着非常多的属性,有作为构造fiber​链表,用于保存位置信息的属性,有作为保存更新队列的属性等等。

而对于函数组件类型的fiber​节点,memoizedState​属性保存hook​信息。hook​在初始化时,会创建一个对象,保存此hook​所产生的计算值,更新队列,hook​链表。

const hook = {
	// hooks计算产生的值 (初始化/更新)
	memoizedState: "";
	// 对此hook的更新行为
	updateQueue: "";
	// hooks链表指针
	next: null;
}

多个hook如何处理?

例如有以下代码:

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  const [age, setAge] = useState(10);

  function handleClick() {
    setCount(count + 1);
  }

  function handleAgeClick() {
    setCount(age + 18);
  }

  return (
    <button onClick={handleClick}>
      add
    </button>
	<button onClick={handleAgeClick}>
      age
    </button>
  );
}

在某个函数组件中存在多个hook​,此时每个hook​的信息该如何保存呢?这就是上文中hook​对象中next​ 属性的作用,它是一个链表指针。在hook​对象中,next​ 属性指向下一个hook​。

mini-react系列大结局!实现 useMemo & useCallback 🎉🎉

换句话说,如果在一个函数组件中存在多个hook​,那么在该fiber​节点的memoizedState​属性中保存该节点的hooks​链表。

函数组件对应 fiber​ 用 memoizedState​ 保存 hook​ 信息,每一个 hook​ 执行都会产生一个 hook​ 对象,hook​ 对象中,保存着当前 hook​的信息,不同 hook​保存的形式不同。每一个 hook​ 通过 next​ 链表建立起关系。

useMemo & useCallback

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

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

const cacheData = useMemo(fn, deps)
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;

mini-react系列大结局!实现 useMemo & useCallback 🎉🎉

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

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

由于在更新函数组件时,归根结底是执行函数。所以每次执行FunctionComponent​都是对函数组件内部定义的函数重新执行。这时候react提供useCallback​这个hook对某些不想每次更新都执行的函数进行缓存。

const res = useCallback(fn, deps)

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

用法:

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>
    </>
  );
};

useMemo​和useCallback​内部的实现非常相似:

初始化:

首先在mountWorkInProgressHook​内部创建hook对象,然后加入该fiber​节点的hook链表。useMemo​根据用户传入的函数计算结果缓存,而useCallback​直接保存函数。

// mountMemo
function mountMemo<T>(
  nextCreate: () => T, 
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;、
  // 执行传入的nextCreate函数,保存结果
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
// mountCallback
function mountCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 直接返回用户传入的callback,保存执行函数
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

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

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

更新:

// 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​保存;如果没发生改变,则直接返回缓存的值。

// 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;
}

// 对比
function areHookInputsEqual(nextDeps, prevDeps) {
	if (prevDeps === null || nextDeps === null) {
		return false;
	}
	for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
		if (Object.is(prevDeps[i], nextDeps[i])) {
			continue;
		}
		return false;
	}
	return true;
}

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

‍‍ ‍写在最后 ⛳

经过差不多前后三个月的时间,终于把react系列的核心功能实现并且输出文章了,刚开始的时候并没想到能写这么多篇。真的是耗费了很大的心血。但是也受益良多。经过输出十几篇文章,对整个react的运行机制有了更深刻的理解。

无论做什么事情,开始行动就已经胜利了一半。一直空想只会让你停滞不前。

未来可能继续输出antd源码解析系列文章,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳 ‍ ‍ ‍ ‍