React hook · 幻影显形

1,031 阅读3分钟

本文从一个小点, 思考了一下, 一目千行的查阅了下 hook 源码. 然后光顾了 react 禁术, 就像 Harry Potter 的幻影显形咒一样.

你没事吧?这种感觉需要慢慢适应。
——阿不思·邓布利多

Story

1. Origin

如下组件, a, b, c 判断条件要写三个, 如何写一遍?

export const Origin: React.FC = () => {
  const a = true;
  const b = true;
  const c = true;
  return (
    <div>
      {(a || b || c) && (
        <div>
          <h5>title</h5>
          {a && <div>a</div>}
          {b && <div>b</div>}
          {c && <div>c</div>}
        </div>
      )}
    </div>
  );
};

2. 抽了一个组件, 判断 children

如下,判断ReactChildNode, 如全部为空, 则返回 null。就不需要写一遍全部条件 a || b || c 了,

三思而后行, 看起来比原始代码复杂太多

const Wrapper: React.FC = ({ children }) => {
  const children = (props as any)?.children
  console.info('children', children)
  if (
    !children ||
    (Array.isArray(children) && children.every((item) => !item))
  ) {
    return null
  }
  return (
    <div>
      <h5>title</h5>
      {children}
    </div>
  )
}

export const V1: React.FC = () => {
  const a = true
  const b = false
  const c = false

  return (
    <Wrapper>
      {a && <div>a</div>}
      {b && <div>b</div>}
      {c && <div>c</div>}
    </Wrapper>
  )
}

3. 当我遇到了一个空组件?

想依然能够判断。

const Null: React.FC = () => null

export const V2: React.FC = () => {
  return (
    <Wrapper>
        <Null />
        <Null />
    </Wrapper>
  )
}

4. 函数式 React Node 是一个函数, 我运行一下它

console.info(<Null />) 看到 {... type: fn Null(), props: {} ...}

于是,


const Wrapper: React.FC = ({ children }) => {
  const children = (props as any)?.children
  console.info('children', children)
  if (
    !children ||
    (Array.isArray(children) && children.every((item) => {
        if(!item) {
            return true
        }
        if(typeof item?.type === 'function') {
            return !item.type(item.props)
        }
        return false
    }))
  ) {
    return null
  }
  return (
    <div>
      <h5>title</h5>
      {children}
    </div>
  )
}

[此处有运行效果, <Null />节点可被忽略],

5. 虽然可以, 隐优巨大, 如过 <Null /> 里有 hook

const Null: React.FC<{ hi?: string }> = ({ hi = '_' }) => {
  useEffect(() => {
    console.info('null <--', hi)
    return () => {
      console.info('null  -->', hi)
    }
  }, [])
  return null
}

[图1. 当我运行,children 全为空时, 可以被过滤掉, 但运行了一次]

[图2. 当我运行,children 不全为空时, 运行了两次]

6. 我能拿到消除副作用的函数吗?如果可以,我提前执行它去。

先看看 useEffect 是什么? 找到了它 react/src/ReactHooks.js#L95

@flow
export function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher()
  return dispatcher.useEffect(create, deps)
}

dispatcher 是什么东西? useEffect 调用的结果会不会存在它身上? 🤔️

import ReactCurrentDispatcher from './ReactCurrentDispatcher';

function resolveDispatcher() {
  return ReactCurrentDispatcher.current;
}
--- ./ReactCurrentDispatcher.js
const ReactCurrentDispatcher = {
  current: (null: null | Dispatcher),
};

export default ReactCurrentDispatcher;

咦, 有个 export default, 可以的

[图1 , 我使用编辑器输入 React.ReactCu... 让它提示出来, 什么都没有]

[图2 , 我点到 node_modules/@types/react/index.d.ts 全文搜索🔍 ReactCurrentDispatcher, 什么都没有]

[图3 , 我打开 node_modules/react/cjs/react.development.js, 🔍搜索到了 !!]

var ReactSharedInternals = {
  ReactCurrentDispatcher: ReactCurrentDispatcher,
  ...
};

exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactSharedInternals;

用这个 API 会被炒鱿鱼🦑, 😯 ! 而且, 似乎我拿不到 return 的内容

6.1 但, 看到 .current 的我转念一想

同步方法执行过程中, 我替换掉 dispatcher.current, 完后再替换回来, 稳! 于是

export const apparate = function (exec: () => any) {
  const x = (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
  const origin = x.ReactCurrentDispatcher.current
  x.ReactCurrentDispatcher.current = new Proxy(
    {},
    {
      get: () => () => undefined,
    },
  )
  const result = exec()
  x.ReactCurrentDispatcher.current = origin
  return result
}

export const Wrapper: React.FC<T> = (props) => {
  const children = (props as any)?.children
  console.info('children', children)
  if (
    !children ||
    (Array.isArray(children) &&
      children.every((item) => {
        if (!item) {
          return true
        }
        if (typeof item?.type === 'function') {
          try {
            return !apparate(() => item.type(item.props))
          } catch (error) {
            console.warn(' execute error')
            return false
          }
        }
        return false
      }))
  ) {
    return null
  }
  return (
    <div>
      <h5>title</h5>
      {children}
    </div>
  )
}

👀看, 执行 function component function 的时候, 原地幻影移形了一下.

[图 1. 我执行代码, children 全空时, 没有执行]

[图 2. 我执行代码, children 不全空时, 执行了一次]

好, 超出预期, 比原计划的主动消除副作用好多了

Play

封装起来玩

My Style:

/** 消隐无踪(Deletrius), 如果子元素全空, 则自身消失 */
export function selfOffHoc<T>(Wrapper: React.FC<T>) {
  const SelfOffWrapper: React.FC<T> = (props) => {
    const children = (props as any)?.children
    if (
      [].concat(children).every((item: any) => {
        if (!item) {
          return true
        }
        if (typeof item?.type === 'function') {
          try {
            return !apparate(() => item.type(item.props))
          } catch (error) {
            console.warn(' execute error')
            return false
          }
        }
        return false
      })
    ) {
      return null
    }
    return <Wrapper {...props}>{children}</Wrapper>
  }
  return SelfOffWrapper
}

用法:

// const ExtraWrapper: React.FC<{ loading?: boolean }> = ... 
const ExtraWrapper = selfOffHoc<{ loading?: boolean }>(({ children }) => (
  <View style={{ marginRight: '0px' }}>
    <View>Below:</View>
    <View>{children}</View>
  </View>
))
// 以上, 对比常规方式, 似乎没有动锁进

export default function () {
  return (
    <ExtraWrapper>
      {null && <a>1</a>}
      {false && <b>2</b>}
      {0 && <code>3</code>}
      <Null />
    </ExtraWrapper>
  )
}

结:

  • 纯展示性组件, 无副作用组件, 可使用此方法减少冗余一遍的判断条件
  • 在 useEffect 改变自身状态的, 不适用
    额外建议 -- 不要在 ..._BE_FIRED 的肩膀上挑战它, 搞 async 幻影移形 大概会写出一整套 anti - react hooks, 或许会路过 @vue/reactivity.

It does not to do dwell on dreams, and forget to live. ——Dumbledore

哈利,人不能活在梦里,不要依赖梦想而忘记生活。 ——邓布利多