React에서 최적화를 위한 캐싱 장치들을 공부해보겠다.
대표적으로 memo, useMemo, useCallback가 있는데 이들의 목적은 불필요한 연산 반복을 방지하기 위해 메모이제이션을 통해 리렌더링을 막아주는 역할을 한다.
컴포넌트 내에서 메모이제이션을 하는 훅이다.
마운트할 때 deps의 여부와 상관없이 바로 nextCreate
를 실행해 초기값으로 잡아준다.
function useMemo(create, deps) {
var dispatcher = resolveDispatcher();
return dispatcher.useMemo(create, deps);
}
.
.
{
useMemo: function (create, deps) {
currentHookNameInDev = 'useMemo';
mountHookTypesDev();
checkDepsAreArrayDev(deps);
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountMemo(create, deps);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
}
function mountMemo(nextCreate, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
업데이트의 경우 마운트와 달리 deps내부 변수들의 변화여부를 확인하고 변경되었을 때만 nextCreate
함수를 실행시켜준다.
그리고 이전 deps값과 현재값을 비교하고 업데이트를 시켜준다.
useMemo: function (create, deps) {
currentHookNameInDev = 'useMemo';
updateHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateMemo(create, deps);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
function updateMemo(nextCreate, deps) {
var hook = updateWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
var prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
var nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
React.memo는 컴포넌트 자체를 메모이제이션 해 부모 컴포넌트가 리렌더링하더라도 자식은 하지않도록 막아준다.
function memo(type, compare) {
{
if (!isValidElementType(type)) {
error('memo: The first argument must be a component. Instead ' + 'received: %s', type === null ? 'null' : typeof type);
}
}
var elementType = {
$$typeof: REACT_MEMO_TYPE,
type: type,
compare: compare === undefined ? null : compare
};
{
var ownName;
Object.defineProperty(elementType, 'displayName', {
enumerable: false,
configurable: true,
get: function () {
return ownName;
},
set: function (name) {
ownName = name;
if (!type.name && !type.displayName) {
type.displayName = name;
}
}
});
}
return elementType;
}
memo함수에 두번째 인자로 compare를 받는데 이는 props를 비교하는 함수를 넣어주는 곳이다. 인자를 따로 넣지않으면 단순비교로 넘어간다.
이전 props와 현재 props를 비교하고 context가 바뀌었는지 확인하고 모두 동일하면 bailoutOnAlreadyFinishedWork
함수를 실행한다. 하나라도 다르면 updateFunctionComponent
함수를 실행한다.
이때 props 비교함수로 shallowEqual
를 사용해 다른 캐싱 hook과는 다르게 얕은 비교를 해주고있다.
function updateMemoComponent(current, workInProgress, Component, nextProps, renderLanes) {
if (current === null) {
var type = Component.type;
if (isSimpleFunctionComponent(type) && Component.compare === null &&
Component.defaultProps === undefined) {
var resolvedType = type;
{
resolvedType = resolveFunctionForHotReloading(type);
}
workInProgress.tag = SimpleMemoComponent;
workInProgress.type = resolvedType;
return updateSimpleMemoComponent(current, workInProgress, resolvedType, nextProps, renderLanes);
}
var child = createFiberFromTypeAndProps(Component.type, null, nextProps, workInProgress, workInProgress.mode, renderLanes);
child.ref = workInProgress.ref;
child.return = workInProgress;
workInProgress.child = child;
return child;
}
{
var _type = Component.type;
var _innerPropTypes = _type.propTypes;
if (_innerPropTypes) {
checkPropTypes(_innerPropTypes, nextProps, //
'prop', getComponentNameFromType(_type));
}
}
var currentChild = current.child;
var hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
if (!hasScheduledUpdateOrContext) {
var prevProps = currentChild.memoizedProps;
var compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
workInProgress.flags |= PerformedWork;
var newChild = createWorkInProgress(currentChild, nextProps);
newChild.ref = workInProgress.ref;
newChild.return = workInProgress;
workInProgress.child = newChild;
return newChild;
}
function updateSimpleMemoComponent(current, workInProgress, Component, nextProps, renderLanes) {
if (current !== null) {
var prevProps = current.memoizedProps;
if (shallowEqual(prevProps, nextProps) && current.ref === workInProgress.ref && (
workInProgress.type === current.type )) {
didReceiveUpdate = false;
workInProgress.pendingProps = nextProps = prevProps;
if (!checkScheduledUpdateOrContext(current, renderLanes)) {
workInProgress.lanes = current.lanes;
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
didReceiveUpdate = true;
}
}
}
return updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes);
}
useCallback은 함수를 캐싱하기 위한 훅이다.
useCallback의 두번째 인자인 deps가 이전과 다르다면 새롭게 함수를 정의해주고 같다면 캐싱된 함수를 그대로 사용한다.
보통 리렌더링 방지를 위해 컴포넌트에 memo를 사용하는 경우 함수를 인자로 넘겨주기위해 useCallback을 함께 사용해 함수를 재정의하는 것을 방지시켜준다.
mountCallback
에서 할당된 memoizedState
는 updateCallback
에서 areHookInputsEqual
함수로 이전 deps와 현재 deps를 비교해준다. 이 둘이 같다면 이전에 할당됐었던 prevState[0]
을 그대로 반환시켜주고 다르면 새로운 callback
을 재할당해준다.
function mountCallback(callback, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
function updateCallback(callback, deps) {
var hook = updateWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
var prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}