likes
comments
collection
share

面试官:React的memo第二个参数是什么?请实现一个memo

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

前言

最近网上冲浪时看到了这道面试题,虽然不是自己面试,但是每当看到别人发面试题时,自己都会心里默想自己能不能回答上来。这次也是,第一个提问挺好回答,第二个我就一时想不出来了,心里不经颤抖,所以赶紧总结起来。

React的memo第二个参数

React的新文档其实挺详细的,memo的用法:memo(Component, arePropsEqual?)

第二个参数arePropsEqual是用来判断新旧的props是否相等,相等则返回true,代表组件不用更新,否则相反。一般第二个参数可不传,React默认会对props进行浅比较。

那啥时候用第二个参数,这里举个简单的例子

const App = () => {
  const [name, setName] = useState('')
  const [age, setAge] = useState('')
  return (
    <>
      <label>
        名称:
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        年龄:
        <input value={age} onChange={e => setAge(e.target.value)} />
      </label>
      <Profile profile={{ name, age }} />
    </>
  )
}

const Profile = memo(({ profile }) => {
  const { name, age } = profile
  return (
    <ul>
      <li>名称:{name}</li>
      <li>年龄:{age}</li>
    </ul>
  )
})

在上面的例子中,Profile组件记忆化(memoized)根本无意义,因为传入的 props 中 profile 属性永远是新对象。

那么我们可以手动比较 props。

function arePropsEqual({ profile: oldProfile }, { profile: newProfile }) {  
  return oldProfile.name === newProfile.name && oldProfile.age === newProfile.age
}

const Profile = memo(({ profile }) => {
  const { name, age } = profile
  return (
    <ul>
      <li>名称:{name}</li>
      <li>年龄:{age}</li>
    </ul>
  )
}, arePropsEqual)

上面只是举例了 memo 第二个参数的使用,当然这其实并不是一个好的做法。 我们也许在 Profile 组件外部就应该把 profile 属性记忆化(memoized),例如

const App = () => {
  const [name, setName] = useState('')
  const [age, setAge] = useState('')
  
  const profile = useMemo(() => ({ name, age }), [name, age])
  
  return (
    <>
      <label>
        名称:
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        年龄:
        <input value={age} onChange={e => setAge(e.target.value)} />
      </label>
      <Profile profile={profile} />
    </>
  )
}

这种做法也不一定是最好的,react文档 更推荐最小化 props 的变化。如下,其实就是把属性分开传进组件。

const App = () => {
  const [name, setName] = useState('')
  const [age, setAge] = useState('')
  
  return (
    <>
      <label>
        名称:
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        年龄:
        <input value={age} onChange={e => setAge(e.target.value)} />
      </label>
      <Profile name={name} age={age} />
    </>
  )
}

const Profile = memo(({ name, age }) => {
  return (
    <ul>
      <li>名称:{name}</li>
      <li>年龄:{age}</li>
    </ul>
  )
})

那这样子,我们其实还是没必要手动使用 memo 的第二个参数,也确实,一般开发过程中很少去用。我再举个可能用到的场景。例如,在项目中封装了一个业务组件,需求比较明确,场景比较专一。

const MyCard = memo(({ onClick, style }) => {
  return (
    <div className="card" onClick={onClick} style={style}>
      {/* 省略很多要显示的内容... */}
    </div>
  )
// 不需要跟着父组件更新渲染
}, () => true)

const App = () => {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(c => c + 1)
    alert('hello')
  }
  
  return (
    <button onClick={() => setCount(c => c + 1)}>{count}</button>
    {/* ... */}
    <MyCard onClick={handleClick} style={{ marginTop: 20 }} />
  )
}

如上面的例子,MyCard是一个需要展示挺多内容的组件,由于需求比较明确传入的属性不会变化,那么直接把 memo 的第二个参数传入函数始终返回 true。因此,外部的 onClick 就可以懒得用 useCallback 包裹了(当然要注意闭包问题),style 也是。

memo的实现

memo函数

在react源码中memo函数其实很简单 react/src/ReactMemo.js

面试官:React的memo第二个参数是什么?请实现一个memo

__DEV__部分是用来校验参数或开发调试用的,可忽略,那其实代码就简洁如下

export function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {
  const elementType = {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
  return elementType;
}

创建fiber

在创建 fiber 的过程中,其中的 createFiberFromTypeAndProps 函数,会将 memo 创建的对象,把其对应的 fiber 的 tag 标记为 MemoComponent

面试官:React的memo第二个参数是什么?请实现一个memo

updateMemoComponent和updateSimpleMemoComponent函数

紧接着,针对 MemoComponent 的 fiber 使用 updateMemoComponent 函数进行更新。

面试官:React的memo第二个参数是什么?请实现一个memo

如上,current 为 null 表示第一次更新(这时还没有正在用使用的 fiber)。可以看到,满足简单函数组件的条件时,又把MemoComponent改成了SimpleMemoComponent,然后进入updateSimpleMemoComponent的逻辑。啥是 isSimpleFunctionComponent ? 我们可以继续看这个函数

function shouldConstruct(Component: Function) {
  const prototype = Component.prototype;
  return !!(prototype && prototype.isReactComponent);
}

export function isSimpleFunctionComponent(type: any): boolean {
  return (
    typeof type === 'function' &&
    !shouldConstruct(type) &&
    type.defaultProps === undefined
  );
}

其实就是把 class 组件排除,那么满足是函数组件、另外没有defaultProps、没有传memo的第二个参数compare,那这个 fiber 就是 SimpleMemoComponent

然后在updateSimpleMemoComponent函数中,如下,如果 current 不为 null 时(即有新旧 fiber 对比了)。会进行组件的新旧props浅比较,然后ref是否是相等的,再来判断是否有待处理的更新和 Context 变化,如果没有即可使用 bailoutOnAlreadyFinishedWork 函数尝试跳过更新。

面试官:React的memo第二个参数是什么?请实现一个memo

大概讲完了updateSimpleMemoComponent,我们重新回到 updateMemoComponent 函数,往下看逻辑。同样的,判断是否有待处理的更新和 Context 变化,然后使用memo的第二个参数compare函数进行新旧props对比,再判断ref是否相等,最终决定是否跳过更新。

面试官:React的memo第二个参数是什么?请实现一个memo

总结

上述原理基本理清楚了,我们至少要总结一段话来回答面试官:

memo会把传入的参数返回一个对象,这个对象标记为 REACT_MEMO_TYPE。然后处理 fiber 时把它区分成MemoComponent 和 SingleMemoComponent。SingleMemoComponent的判断条件是 非 class 组件、没有 defaultProps、没有自定义比较函数。SingleMemoComponent 检查更新的时候,会默认对新旧props进行浅比较,组件的新旧ref是否相等,当前是否有待处理的更新,是否有Context变化。综合来决定是否尝试跳过更新。MemoComponent 判断是否要更新也基本同理,就是换成自定义的比较函数来对比新旧props。

注意上面说的是MemoComponent 和 SingleMemoComponent 的判断是否需要更新的逻辑基本同理,但是它们处理 fiber 的逻辑是不同的,SingleMemoComponent 可以直接把对应的 fiber 当成函数组件来处理,而 MemoComponent 每次还需要考虑合并defaultProps,并且考虑是函数组件或类组件等分开处理。

再最后,如果非要自己实现个 memo 函数,也可以使用高阶函数这样做。当然这是简单例子,更多还需要考虑ref、lazy等问题。

function memo(WrappedComponent, compare) {
  let prevProps = null;
  let memoizedComponent = null;
  compare = typeof compare === "function" ? compare : shallowEqual;

  function MemoizedComponent(props) {
    if (!shallowEqual(props, prevProps)) {
      prevProps = props;
      memoizedComponent = <WrappedComponent {...props} />;
    }

    return memoizedComponent;
  }

  return MemoizedComponent;
}

最后

看到了最后不妨给个赞吧💓,如有不对的地方请轻喷