React.js从渲染内部抛出一个承诺

时间:2019-06-06 08:39:59

标签: reactjs

我试图理解以下代码为什么导致无限循环,从而使页面完全无响应。

function lazy(importTaskFactory) {
  const f = function (props) {
      console.log("enter");
      debugger;
      const [value, setValue] = useState(null);
      const memoizedImportTask = useMemo(importTaskFactory, [importTaskFactory]);
      useEffect(() => {
          console.log("useEffect");
          debugger;
          memoizedImportTask
              .then(m => {
              console.log("settled");
              debugger;
              setValue(m);
          })
              .catch(err => {
              console.error(f.displayName, err);
          });
      }, [memoizedImportTask]);
      if (value) {
          if (!value.default) {
              throw new Error(`${importTaskFactory.toString()} - default export is required`);
          }
          return React.createElement(value.default, props);
      }
      console.log("throw");
      debugger;
      throw memoizedImportTask; // Suspense API
      console.log("throw memoizedImportTask!", memoizedImportTask);
      return null;
  };
  f.displayName = `lazy(${importTaskFactory.toString()})`;
  return f;
}

此代码完成的唯一操作是记录enterthrowenterthrowenterthrow等。 useEffect挂钩永远不会被调用,因此永远不会设置该值。尽管承诺已经解决(解决),但这仍然是可行的。为什么在这种特殊情况下反应以某种同步方式运行?

1 个答案:

答案 0 :(得分:2)

我想出了问题的答案。尽管我喜欢React.js钩子编写组件的样式,但它们确实表现出与类组件有些不同的行为。

如果我们跳过useEffect钩子并将所有内容移到useMemo钩子中(这样我们就可以确定在诺言履行时我们确实设置了值)。它实际上什么也没做。这是因为当组件抛出异常时,它的挂钩被重置,这使得useState挂钩无用!另外,在渲染时抛出错误的组件不会调用其useEffect钩子,可能是出于相同的原因(该组件从未提交)。

因此,即使该方法能够解决承诺,在设置值和兑现承诺之间也会失去价值。但是,我们可以通过自己实现状态和记忆,而不是一开始就承诺(稍后会详细介绍),来解决这些限制。

export interface LazyComponent<P = {}> {
  getComponent(): React.ComponentType<P> | undefined
  importComponent(): Promise<React.ComponentType<P>>
}

type LazilyInstantiatedComponentModule<P> = {
  default: React.ComponentType<P>
}

export function lazy<P = {}>(
  importTaskFactory: () => Promise<LazilyInstantiatedComponentModule<P>>,
  fallback?: React.ReactElement
): React.FunctionComponent<P> & LazyComponent<P> {
  if (importTaskFactory === undefined) {
    throw new TypeError("importTaskFactory is undefined")
  }

  let cached: LazilyInstantiatedComponentModule<P> | undefined

  let importTask: Promise<LazilyInstantiatedComponentModule<P>> | undefined

  function importComponent() {
    if (!importTask) {
      importTask = importTaskFactory().then(m => {
        if (!(typeof m === "object" && typeof m.default === "function")) {
          throw new Error(
            `${importTaskFactory.toString()} - missing default export`
          )
        }
        return (cached = m)
      })
    }
    return importTask
  }

  function lazy(props: P) {
    const [value, setValue] = useState(cached)

    useEffect(() => {
      if (!value) {
        importComponent().then(setValue)
      }
      return undefined
    })

    if (value) {
      return React.createElement(value.default, props)
    }

    return fallback || null
  }

  lazy.getComponent = () => (cached ? cached.default : undefined)

  lazy.importComponent = () => importComponent().then(m => m.default)

  lazy.displayName = `lazy(${importTaskFactory.toString()})`

  return lazy
}

之所以可行,是因为我们的lazy加载程序是一个高阶组件。您可能想知道为什么甚至在有lazy的情况下完全实现React.lazy的原因,但这与React.lazy需要一个Suspense组件的事实有关。 ReactDOMServer不支持。

此处给出的答案反映了我自React 15以来如何解决此限制。公共API getComponentimportComponent可用于确保在您设置cached值之前达到渲染。从而避免任何闪烁(即呈现后备或null)。