親手擼一個React(一):JSX與虛擬DOM

前言

react的火熱程度已經達到了94.5k個start,本系列文章主要用簡單的代碼來實現一個react,來了解JSX、虛擬DOM、diff算法以及state和setState的設計。javascript

提到react,固然少不了vue,vue的api設計十分簡單 上手也很是容易,但黑魔法不少,使用起來有點虛, 而react沒有過多的api,它的深度體如今設計思想上,使用react開發則讓人比較踏實、能拿捏的住,這也是我喜歡react的緣由之一。css

JSX

react怎麼少的了JSXJSX是什麼,讓我來看個例子
如今有下面這段代碼:html

const el = <h3 className="title">Hello Javascript</h3>

這樣的js代碼若是不通過處理會報錯,jsx是語法糖,它讓這段代碼合法化,經過babel轉化後是這樣的:vue

const el = React.createElement(
    'h3',
    { className: 'title' },
    'Hello Javascript'
)

這種例子官網首頁也有demojava

準備開始

開始編碼以前,先介紹兩個東西:parcelbabel-plugin-transform-jsx,等會咱們用parcel搭建一個開發工程,babel-plugin-transform-jsxbabel的一個插件,它能夠將jsx語法轉成React.createElement(...)node

下面咱們開始react

簡單的搭建

parcel這裏就不介紹了,一句話概況就是爲你生成一個零配置的開發環境。git

  1. yarn global add parcel-bundlernpm install -g parcel-bundler
  2. 新建項目文件夾,這裏取名爲simple-react
  3. simple-react中執行 yarn init -ynpm init -y 生成package.json
  4. 建立一個index.html
  5. 建立src文件夾 再在src下建立index.js 而後再index.html中引入index.js

若是你先麻煩,能夠直接下載源碼修改。github

以上步驟完可能不完整,最好參考parcel裏的內容。以上工做完成後,咱們須要安裝babel-plugin-transform-jsx算法

npm insatll babel-plugin-transform-jsx --save-dev
或者
yarn add babel-plugin-transform-jsx --dev

而後添加.babelrc文件,並在該文件中加入下面這段代碼:

{
  "presets": ["env"],
  "plugins": [["transform-jsx", { "function": "React.createElement" }]]
}

上面代碼的意思是 使用transform-jsx插件,並配置爲經過React.createElement方法來解析JSX,固然你也能夠不用React.createElement和自定義方法,好比preact使用的h方法。

React.createElement()

如今咱們在index.js裏開始編碼。
首先寫入代碼:

const el = <h3 className="title">Hello Javascript</h3>;
console.log(el);

咱們在什麼都不寫的狀況下,打印看看el是什麼。
圖片描述

打印報錯:React沒有定義。 這是由於在.babelrc文件中,咱們使用的這段代碼起了做用:

["transform-jsx", { "function": "React.createElement" }]

上面說過,它會經過React.createElement方法來轉譯JSX,那麼咱們就給出這個方法:
咱們把剛纔那段代碼改變一下:

const React = {
  createElement: function(...args) {
    return args[0];
  }
};

const el = <h3 className="title">Hello Javascript</h3>;

console.log(el);

上面代碼添加了一個React對象,並在其中添加一個createElement方法,如今再執行一下看看打印出什麼:
圖片描述

由打印結果能夠看出,jsx在使用React.createElement方法轉譯時,createElement方法應該是這樣的:

createElement({ elementName, attributes, children });
  • elementName: dom對象的標籤名 好比div、span等等
  • attributes: 當前dom對象的屬性集合 好比class、id等等
  • children: 全部子節點

如今咱們改寫一下createElement方法,讓key的名稱簡單一點:

const React = {
  createElement: function({ elementName, attributes, children }) {
    return {
      tag: elementName,
      attrs: attributes,
      children
    };
  }
};

如今能夠看到打印結果是:
圖片描述

咱們再打印個複雜點的DOM結構:

const el = (
  <div style="color:red;">
    Hello <span className="title">JavaScript</span>
  </div>
);

console.log(el);

圖片描述

和咱們想要的結構同樣。
其實上面打印出來的就是虛擬DOM,如今咱們要作的就是如何把虛擬DOM轉成真正的DOM對象並顯示在瀏覽器上。

ReactDOM.render()

要想將虛擬dom轉成真實dom並渲染到頁面上,就須要調用ReactDOM.render,好比:

ReactDOM.render(<h1>Hello World</h1>, document.getElementById('root'));

這段代碼轉換後的樣子:

ReactDOM.render(
  React.createElement('h1', null, 'Hello World'),
  document.getElementById('root')
);

這時,react會將<h1>Hello World</h1>掛載到id爲root的dom下,從而在頁面上顯示出來。

如今咱們實現render方法:

function render(vnode, container) {
  const dom = createDom(vnode); //將vnode轉成真實DOM
  container.appendChild(dom);
}

上面代碼中先調用createDom將虛擬dom轉成真實DOM而後掛載到container下。

咱們來實現createDom方法:

function createDom(vnode) {
  if (vnode === undefined || vnode === null || typeof vnode === 'boolean') {
    vnode = '';
  }

  if (typeof vnode === 'string' || typeof vnode === 'number') {
    return document.createTextNode(String(vnode));
  }

  const dom = document.createElement(vnode.tag);

    //設置屬性
  if (vnode.attrs) {
    for (let key in vnode.attrs) {
      const value = vnode.attrs[key];
      setAttribute(dom, key, value);
    }
  }
    //遞歸render子節點
  vnode.children.forEach(child => render(child, dom));
  return dom;
}

因爲屬性的種類比較多,咱們抽出一個setAttribute方法來設置屬性:

function setAttribute(dom, key, value) {
  //className
  if (key === 'className') {
    dom.setAttribute('class', value);

    //事件
  } else if (/on\w+/.test(key)) {
    key = key.toLowerCase();
    dom[key] = value || '';
    //style
  } else if (key === 'style') {
    if (typeof value === 'string') {
      dom.style.cssText = value || '';
    } else if (typeof value === 'object') {
      // {width:'',height:20}
      for (let name in value) {
      //若是是數字能夠忽略px
        dom.style[name] =
          typeof value[name] === 'number' ? value[name] + 'px' : value[name];
      }
    }

    //其餘
  } else {
    dom.setAttribute(key, value);
  }
}

如今render方法已經完整的實現了,咱們將建立ReactDOM對象,將render方法掛上去:

const ReactDOM = {
  render: function(vnode, container) {
    container.innerHTML = '';
    render(vnode, container);
  }
};

這裏在調用render以前加了一句container.innerHTML = '',就不解釋了,相信你們都明白。

那麼萬事具有,咱們來測試一下,直接上一個比較複雜的dom結構並加上屬性:

const element = (
  <div
    className="Hello"
    onClick={() => alert(1)}
    style={{ color: 'red', fontSize: 30 }}
  >
    Hello <span style={{ color: 'blue' }}>javascript!</span>
  </div>
);

ReactDOM.render(element, document.getElementById('root'));

打開頁面,是咱們想要的結果:

圖片描述

再看看控制檯的dom:

圖片描述

很完美,這是咱們想要的東西

後語

該demo代碼在這裏喲~~

本文敘述了JSX和虛擬DOM,以及將虛擬DOM轉成真實DOM的過程,後面的文章會繼續敘述react中的組件、生命週期、diff算法和異步setState,敬請期待~

相關文章
相關標籤/搜索