1. createElemnt && render(草稿)

429 阅读3分钟

#react

1. createElemnt && render

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

通过babel把JSX转换成JS

image-20210108194815038

删除冗余信息得到

const element = React.createElement(
  "div",
  {
    id: "foo",
  },
  React.createElement("a", null, "bar"),
  React.createElement("b", null)
);
const container = document.getElementById("root");
ReactDOM.render(element, container);

(React)Node 和(React)Element

抛去JSX及虚拟DOM的概念不谈,我们先来回顾下HTML中真实的DOM是怎么划分的呢?

image-20210113125953608

具体的到 Node properties: type, tag and contents看,这个网站一级棒!

那我们现在如果需要做个虚拟DOM,即使用对象来映射上面的DOM关系,你会怎么思考呢?

image-20210113132307449

看图知道个大概意思就好了,图的细节不重要,细节在代码中呈现,如下代码:

type ReactText = string | number;

type ReactChild = ReactElement | ReactText;

interface ReactNodeArray extends Array<ReactNode> {}

type ReactNode = ReactChild | ReactNodeArray | boolean | null | undefined;

interface Attributes extends Record<string, any> {
  children?: ReactNode;
}
interface ReactElement<P extends Attributes = any, T = string> {
  type: T;
  props: P;
}

createElemnt

现在,我们开始实现React.createElement

function createTextElement(text: ReactText): ReactElement {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: []
    }
  };
}

function createElement(
  type: string,
  props: Attributes | null,
  ...children: ReactChild[]
) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child === "object" ? child : createTextElement(child)
      )
    }
  };
}

console.log(`==============>createElement("div", null, a)`);
console.log(createElement("div", null, "a"));
// { type: 'div', props: { children: [ 'a' ] } }
console.log(`==============>createElement("div", null, a, b)`);
console.log(createElement("div", null, "a", "b"));
// { type: 'div', props: { children: [ 'a', 'b' ] } }

函数的作用就是返回一个ReactElement类型,比较困惑的可能就是以下这段函数

children.map((child) =>
  typeof child === "object" ? child : createTextElement(child)
);

根据TS类型判断我们很容易知道child的类型是ReactChild

type ReactText = string | number;

type ReactChild = ReactElement | ReactText;

interface ReactElement<P extends Attributes = any, T = string> {
  type: T;
  props: P;
}

所以当child !== "object"的时候,child的类型就是ReactText,即string或者number

为了后面的代码简单和统一,我们使用createTextElement进行包裹,具体缘由在后面说。

render

创建好虚拟DOM后,我们就需要把这些DOM映射到真实的环境中,这里我们仅实现ReactDOM,即web环境。

哈哈!别的环境我也不会,又没有文章可以抄,唉!希望有大佬写个react native借我抄

function render(
  element: ReturnType<typeof createElement>,
  container: HTMLElement | null
): void {
  if (!container) {
    console.error("获取不到容器,请确保根节点是否存在");
    return;
  }
  // 把虚拟DOM,即ReactElement 转换成真实的dom
  const dom = document.createElement(element.type);
  // 递归创建真实dom
  element.props.children.forEach((child) => render(child, dom));
  // 把dom插入到页面对应的容器中
  container.appendChild(dom);
}

比较难理解的就两个

  1. 递归: element.props.children.forEach((child) => render(child, dom))
  2. 类型: element: ReturnType<typeof createElement>

根据以下两种写法,我们来回答刚刚前面问的为什么在child !== 'object'时对ReactNode进行包装?

image-20210112212931544

image-20210112213008513

因为这样可以确保返回的类型childrenReactElement[],而获取该类型是为了方便以下函数的递归。

  element.props.children.forEach((child) => render(child, dom));

image-20210112213921298

在理解递归后,我们需要对上面的代码进行分类处理:

const dom =
      element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);

还有把属性添加到dom上

 const dom =
    element.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);

  const isProperty = (key: Key) => key !== "children";

  Object.keys(element.props)
    .filter(isProperty)
    .forEach((name) => {
      // @ts-ignore
      dom[name] = element.props[name];
    });

主呀!感谢万能的@ts-ignore

然后代码就可以跑了!

image-20210113132924469

全部代码

codesandbox

/* 类型-------------------------------------------------------------------------- */

type Key = string | number;
type ReactText = string | number;

type ReactChild = ReactElement | ReactText;

interface ReactNodeArray extends Array<ReactNode> {}

type ReactNode = ReactChild | ReactNodeArray | boolean | null | undefined;

interface Attributes extends Record<string, any> {
  children?: ReactNode;
}
interface ReactElement<P extends Attributes = any, T = string> {
  type: T;
  props: P;
}

/* 源码实现-------------------------------------------------------------------------- */
function createTextElement(text: ReactText): ReactElement {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: []
    }
  };
}

function createElement(
  type: string,
  props: Attributes | null,
  ...children: ReactChild[]
) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child === "object" ? child : createTextElement(child)
      )
    }
  };
}

function render(
  element: ReturnType<typeof createElement>,
  container: Node | null
): void {
  if (!container) {
    console.error("获取不到容器,请确保根节点是否存在");
    return;
  }
  // 把虚拟DOM,即ReactElement 转换成真实的dom
  const dom =
    element.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);

  const isProperty = (key: Key) => key !== "children";

  Object.keys(element.props)
    .filter(isProperty)
    .forEach((name) => {
      // @ts-ignore
      dom[name] = element.props[name];
    });
  // 递归创建真实dom
  element.props.children.forEach((child) => render(child, dom));

  // 把dom插入到页面对应的容器中
  container.appendChild(dom);
}

const React = {
  createElement
};

const ReactDom = {
  render
};

/*用例 -------------------------------------------------------------------------- */

const element = (
  <div style="background: salmon">
    <h1>Hello World</h1>
    <h2 style="text-align:right">——忘尘</h2>
  </div>
);

const rootElement = document.getElementById("root");
ReactDom.render(element, rootElement);