听说你想写个React - dom

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

今天将会开启一个新的系列,如何打造自己的 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: "//www.greatytc.com/bar" } },
      { type: "span", props: {} }
    ]
  }
};

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

<div id="container">
  <input value="foo" type="text">
  <a href="//www.greatytc.com/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 的实现,敬请期待。

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容