聽說你想寫個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: "/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 的實現,敬請期待。

參考資料

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容