美文网首页
听说你想写个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