美文网首页
听说你想写个React - dom

听说你想写个React - dom

作者: 微微笑的蜗牛 | 来源:发表于2021-05-22 20:18 被阅读0次

大家好,我是微微笑的蜗牛,🐌。

今天将会开启一个新的系列,如何打造自己的 React 框架。包括如下几部分:

  • dom 节点描述与创建
  • jsx
  • virtual dom
  • component

这一篇文章主要讲 dom 节点描述与创建。

dom api

我们首先来看一下如何使用 dom api 来创建节点。节点分为两种类型:元素和文字。

对于元素来说,使用 createElement 创建,参数传入类型。如下所示,创建了一个 input 节点,document 是一个全局对象。

const domInput = document.createElement("input");

可通过元素 id 获取元素:

const domRoot = document.getElementById("root");

可设置元素属性:

domInput["type"] = "text";
domInput["value"] = "Hi world";
domInput["className"] = "my-class";

可监听事件:

domInput.addEventListener("change", e => alert(e.target.value));

对于文字来说,使用 createTextNode 创建,文字内容用属性 nodeValue 填充。如下所示:

// Create a text node
const domText = document.createTextNode("");

// Set text node content
domText["nodeValue"] = "Foo";

// Append an element
domRoot.appendChild(domInput);

在了解这些 api 后,我们接下来就可以着手设计自己框架中的节点描述格式了。我将这个框架称之为 SLReact,当然你也可以用你喜欢的名字。

节点描述

我们将使用 js 对象来描述一个节点信息。节点信息包括类型 type 和属性 props。

  • type 用来描述节点类型,是个字符串,比如 div/span
  • props 是节点属性信息。如果它有子节点的话,则会包含 children 字段。children 是一个数组,同样包含描述信息。

举个栗子:

const element = {
  type: "div",
  props: {
    id: "container",
    children: [
      { type: "input", props: { value: "foo", type: "text" } },
      { type: "a", props: { href: "/bar" } },
      { type: "span", props: {} }
    ]
  }
};

上面这段信息,描述了如下的 dom 结构。div 节点中包含了 3 个子节点,input、a、span

<div id="container">
  <input value="foo" type="text">
  <a href="/bar"></a>
  <span></span>
</div>

render

在有了节点描述信息之后,下一步就是如何将其转换为真正的 dom 节点,并添加到 dom 树上。这里我们将会实现自己的 render 方法。

元素节点

有了节点类型,很容易通过 createElement 这个 api 来创建 dom。

const { type, props } = element;
const dom = document.createElement(type);

若有子节点的话,递归调用 render 方法即可。如下所示:

const childElements = props.children || [];
childElements.forEach(childElement => render(childElement, dom));

但是需要注意的是,还有属性需要处理。props 中包含属性,同时也会包含事件信息。比如:

const element = {
  type: "div",
  props: {
    onChange: () => {},
      children: [],
        other: 'xx'
  }
};
  • on 开头的是事件监听,它的值是个方法。比如 onChange,表明我们想监听 change 事件。
  • children 是子节点信息。
  • 其余就是普通属性。

事件监听

将以 on 开头的属性过滤出来,以全小写的方式取出事件名称,最后使用 addEventListener 来监听事件。

如下所示:

const isListener = name => name.startsWith("on");
  Object.keys(props).filter(isListener).forEach(name => {
    const eventType = name.toLowerCase().substring(2);
    dom.addEventListener(eventType, props[name]);
  });

添加属性

在上一步,我们处理了事件监听的属性。这里再来处理普通属性,过滤掉以 on 开头的属性和 children 即可,然后将属性添加到 dom 中。

如下所示:

const isAttribute = name => !isListener(name) && name != "children";
  Object.keys(props).filter(isAttribute).forEach(name => {
    dom[name] = props[name];
  });

文本节点

先来看下 createTextNode 函数的说明。其中参数 data 为文本内容,它指定了节点属性 nodeValue 的值。下面会用到这个知识点。

/**
 * Creates a text string from the specified value.
 * @param data String that specifies the nodeValue property of the text node.
 */
createTextNode(data: string): Text;

文本的描述结构同元素,但是请注意:文本内容将被当做一个子节点。这跟《听说你想写个渲染引擎》中的处理是一样的。

比如一段文本:<span>Foo</span>。在 React 中,它的描述结构如下:

const reactElement = {
  type: "span",
  props: {
    children: ["Foo"]
  }
};

其中,文本内容 Foo 被当成了子节点,但它是一个字符串。

上面我们提到,children 中的结构也是一段描述信息。为了统一处理,这里将文本内容信息修改同样的结构,使用 type + props 的描述方式。文本内容使用 nodeValue 属性来描述。

如下所示:

const textElement = {
  type: "span",
  props: {
    children: [
      {
        type: "text",
        props: { nodeValue: "Foo" }
      }
    ]
  }
};

这样,当我们遇到类型是 text 的节点时,便认为它是文本,而属性 nodeValue 的值就是文本内容。所以将属性塞给 dom 就好。

const { type, props } = element;

  // 创建节点
  const isTextElement = type === "text";
  const dom = isTextElement
    ? document.createTextNode("")
    : document.createElement(type);

完整的 render 方法如下所示:

function render(element, parentDom) {
    const { type, props } = element;

    // 创建节点
    const isTextElement = type === "text";
    const dom = isTextElement
      ? document.createTextNode("")
      : document.createElement(type);

    // 处理事件监听
    const isListener = (name) => name.startsWith("on");
    Object.keys(props)
      .filter(isListener)
      .forEach((name) => {
        const eventType = name.toLocaleLowerCase().substring(2);
        dom.addEventListener(eventType, props[name]);
      });

    // 处理属性
    const isAttribute = (name) => !isListener(name) && name != "children";
    Object.keys(props)
      .filter(isAttribute)
      .forEach((name) => {
        dom[name] = props[name];
      });

        // 处理子节点
    const childElements = props.children || [];
    childElements.forEach((child) => render(child, dom));

        // 添加父节点
    parentDom.appendChild(dom);
}

测试代码

function importFromBlow() {
    function render(element, parentDom) {
        // 省略实现
    }

    return { render };
}

const SLReact = importFromBlow();

const stories = [
  { name: "part1", url: "http://bit.ly/2pX7HNn" },
  { name: "part2", url: "http://bit.ly/2qCOejH" },
  { name: "part3", url: "http://bit.ly/2qGbw8S" },
  { name: "part4", url: "http://bit.ly/2q4A746" },
  { name: "part5", url: "http://bit.ly/2rE16nh" },
];

const appElement = {
  type: "div",
  props: {
    children: [
      {
        type: "ul",
        props: {
          children: stories.map(storyElement),
        },
      },
    ],
  },
};

// 生成 element 结构
function storyElement({ name, url }) {
  const likes = Math.ceil(Math.random() * 100);
  const buttonElement = {
    type: "button",
    props: {
      children: [
        { type: "text", props: { nodeValue: likes } },
        { type: "text", props: { nodeValue: " 🐶" } },
      ],
    },
  };

  const linkElement = {
    type: "a",
    props: {
      href: url,
      children: [{ type: "text", props: { nodeValue: name } }],
    },
  };

  return {
    type: "li",
    props: {
      children: [buttonElement, linkElement],
    },
  };
}

SLReact.render(appElement, document.getElementById("root"));

看看最后一句,是不是就很像 React 中的用法了?在 render 方法中传入节点描述信息和 dom 根节点即可。

最后的效果图如下所示(有自定义 css):

image

完整的代码在此:https://github.com/silan-liu/slreact/tree/master/part1

总结

这一篇文章主要介绍了如何定义节点描述信息,以及实现自己的 render 方法,来完成 dom 节点的创建和属性设置。感谢阅读~

下一篇文章将会讲述 jsx 的实现,敬请期待。

参考资料

相关文章

网友评论

      本文标题:听说你想写个React - dom

      本文链接:https://www.haomeiwen.com/subject/kxeojltx.html