Saber2pr's Blog

createElement

jsxFactory 函数

即 React.createElement 函数,用于生成 VNode 节点并链接成 VNode 树

函数声明

function createElement<K extends keyof HTMLElementTagNameMap>(
  tag: K,
  props: Partial<HTMLElementTagNameMap[K]>,
  ...childNodes: JSX.Element[]
): JSX.Element

K 泛型参数约束为 keyof HTMLElementTagNameMap,例如"div"、"a"、"button"等,可以看看 TS 标准库中对 HTMLElementTagNameMap 的定义:

interface HTMLElementTagNameMap {
  a: HTMLAnchorElement;
  abbr: HTMLElement;
  address: HTMLElement;
  applet: HTMLAppletElement;
  area: HTMLAreaElement;
  article: HTMLElement;
  aside: HTMLElement;
  audio: HTMLAudioElement;
  b: HTMLElement;
  base: HTMLBaseElement;
  basefont: HTMLBaseFontElement;
  bdo: HTMLElement;
  blockquote: HTMLQuoteElement;
  body: HTMLBodyElement;
  br: HTMLBRElement;
  button: HTMLButtonElement;
  ...
}

所以 HTMLElementTagNameMap[K]就是 K 对应 HTMLElement 的属性类型。

childNodes 为子节点,举个例子

const List = React.createElement(
  "ul",
  null,
  React.createElement("li", null),
  React.createElement("li", null)
)

这个 List 是个 JSX.Element 实例,其 childNodes 为[{tag:"li", props:null}, {tag:"li", props:null}],渲染到真实 DOM 就是

<ul>
  <li></li>
  <li></li>
</ul>

函数实现

export function createElement<K extends keyof HTMLElementTagNameMap>(
  tag: K,
  props: Partial<HTMLElementTagNameMap[K]>,
  ...childNodes: JSX.Element[]
): JSX.Element {
  // 用于map的映射函数
  // 判断childNode类型,如果它是string或者number类型,则生成一个为tag为text的VNode
  // 将childNode(就是文本节点内容)作为props中nodeValue的值
  const mapper = (c: any): any =>
    typeof c === "string" || typeof c === "number"
      ? createElement("text" as "span", { nodeValue: c as string })
      : c

  // 对childNodes中每个子节点执行上面的映射函数
  const children = [].concat(...childNodes).map(mapper)
  // 将处理好的children保存在props中然后返回一个VNode节点
  return <any>{ tag, props: { ...props, children } }
}

这里有个非常有趣的操作,看似无用

;[].concat(...childNodes)

[].concat(…array) 这个表达式常用来对 array 数组降维,例如

;[].concat(...[1, 2, [3, 4]]) // [1, 2, 3, 4]

那么 childNodes 数组什么时候可能会变的不“平坦”呢?

举个场景例子,在 React 组件中常有一种操作

比如想通过数组['a', 'b']得到一个 a, b, c 的列表

<ul>
  <li>a</li>
  <li>b</li>
  <li>c</li>
</ul>

在 React 中 JSX 标签可以看作是值,那么可以使用数组 map 来高效生成:

<ul>
  {["a", "b"].map(ch => (
    <li key={ch}>{ch}</li>
  ))}
  <li>c</li>
</ul>

编译之后

React.createElement(
  "ul",
  null,
  ["a", "b"].map(ch =>
    React.createElement(
      "li",
      { key: ch },
      React.createElement("text", { nodeValue: ch })
    )
  ),
  React.createElement(
    "li",
    null,
    React.createElement("text", { nodeValue: "c" })
  )
)

分析一下它生成的 VNode 树

{
  tag: "ul", props: null,
  [
    {tag:"li", null,
      {tag:"text", {nodeValue: 'a'}}
    },
    {tag:"li", null,
      {tag:"text", {nodeValue: 'b'}}
    }
  ],
  {tag:"li", null,
      {tag:"text", {nodeValue: 'c'}}
  }
}

简化一下就是变成了[['a', 'b'], 'c']的结构,变不“平坦”了!但是三个 li 标签在结构上应该是['a', 'b', 'c']才对,所以需要数组降维。