從 0 到 1 實現 React 系列 —— 1.JSX 和 Virtual DOM

看源碼一個痛處是會陷進理不順主幹的困局中,本系列文章在實現一個 (x)react 的同時理順 React 框架的主幹內容(JSX/虛擬DOM/組件/生命週期/diff算法/setState/ref/...)html

環境準備

項目打包工具選擇了 parcel,使用其能夠快速地進入項目開發的狀態。快速開始node

此外須要安裝如下 babel 插件:react

"babel-core": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-plugin-transform-react-jsx": "^6.24.1"

同時 .babelrc 配置以下:git

{
    "presets": ["env"],
    "plugins": [
        // 插件如其名:轉化 JSX 語法爲定義的形式
        ["transform-react-jsx", {
            "pragma": "React.createElement"
        }]
    ]
}

JSX 和 虛擬 DOM

const element = (
  <div className="title">
    hello<span className="content">world!</span>
  </div>
)

JSX 是一種語法糖,通過 babel 轉換結果以下,能夠發現實際上轉化成 React.createElement() 的形式:github

var element = React.createElement(
  "div",
  { className: "title" },
  "hello",
  React.createElement(
    "span",
    { className: "content" },
    "world!"
  )
);

打印 element, 結果以下:算法

{
  attributes: {className: "title"}
  children: ["hello", t] // t 和外層對象相同
  key: undefined
  nodeName: "div"
}

所以,咱們得出結論:JSX 語法糖通過 Babel 編譯後轉換成一種對象,該對象即所謂的虛擬 DOM,使用虛擬 DOM 能讓頁面進行更爲高效的渲染。babel

咱們按照這種思路進行函數的構造:app

const React = {
  createElement
}

function createElement(tag, attr, ...child) {
  return {
    attributes: attr,
    children: child,
    key: undefined,
    nodeName: tag,
  }
}

// 測試
const element = (
  <div className="title">
    hello<span className="content">world!</span>
  </div>
)

console.log(element) // 打印結果符合預期
// {
//   attributes: {className: "title"}
//   children: ["hello", t] // t 和外層對象相同
//   key: undefined
//   nodeName: "div"
// }

虛擬 DOM 轉化爲真實 DOM

上個小節介紹了 JSX 轉化爲虛擬 DOM 的過程,這個小節接着來實現將虛擬 DOM 轉化爲真實 DOM (頁面上渲染的是真實 DOM)。框架

咱們知道在 React 中,將虛擬 DOM 轉化爲真實 DOM 是使用 ReactDOM.render 實現的,使用以下:dom

ReactDOM.render(
  element, // 上文的 element,即虛擬 dom
  document.getElementById('root')
)

接着來實現 ReactDOM.render 的邏輯:

const ReactDOM = {
  render
}

/**
 * 將虛擬 DOM 轉化爲真實 DOM
 * @param {*} vdom      虛擬 DOM
 * @param {*} container 須要插入的位置
 */
function render(vdom, container) {
  if (typeof(vdom) === 'string') {
    container.innerText = vdom
    return
  }
  const dom = document.createElement(vdom.nodeName)
  for (let attr in vdom.attributes) {
    setAttribute(dom, attr, vdom.attributes[attr])
  }
  vdom.children.forEach(vdomChild => render(vdomChild, dom))
  container.appendChild(dom)
}

/**
 * 給節點設置屬性
 * @param {*} dom   操做元素
 * @param {*} attr  操做元素屬性
 * @param {*} value 操做元素值
 */
function setAttribute(dom, attr, value) {
  if (attr === 'className') {
    attr = 'class'
  }
  if (attr.match('/on\w+/')) {   // 處理事件的屬性:
    const eventName = attr.toLowerCase().splice(1)
    dom.addEventListener(eventName, value)
  } else if (attr === 'style') { // 處理樣式的屬性:
    let styleStr = ''
    let standardCss
    for (let klass in value) {
      standardCss = humpToStandard(klass) // 處理駝峯樣式爲標準樣式
      styleStr += `${standardCss}: ${value[klass]};`
    }
    dom.setAttribute(attr, styleStr)
  } else {                       // 其它屬性
    dom.setAttribute(attr, value)
  }
}

至此,咱們成功將虛擬 DOM 復原爲真實 DOM,展現以下:

另外配合熱更新,在熱更新的時候清空以前的 dom 元素,改動以下:

const ReactDOM = {
  render(vdom, container) {
    container.innerHTML = null
    render(vdom, container)
  }
}

總結

JSX 通過 babel 編譯爲 React.createElement() 的形式,其返回結果就是 Virtual DOM,最後經過 ReactDOM.render() 將 Virtual DOM 轉化爲真實的 DOM 展示在界面上。流程圖以下:

思考題

以下是一個 react/preact 的經常使用組件的寫法,那麼爲何要 import 一個 React 或者 h 呢?

import React, { Component } from 'react' // react
// import { h, Component } from 'preact' // preact

class A extends Component {
  render() {
    return <div>I'm componentA</div>
  }
}

render(<A />, document.body) // 組件的掛載

項目說明

該系列文章會盡量的分析項目細節,具體的仍是以項目實際代碼爲準。

項目地址

相關文章
相關標籤/搜索