【React 原理(一)】實現 createElement 和 render 方法

前言

在 React 中,咱們都知道能夠寫 jsx 代碼會被編譯成真正的 DOM 插入到要顯示的頁面上。這具體是怎麼實現的,今天咱們就本身動手作一下。javascript

實現 createElement 方法

這個方法平時開發咱們並不會用到,由於它是經 babel 編譯後的代碼,咱們新建一個 React 項目,index.js 最簡單的代碼結構以下:html

import React from 'react'
import ReactDOM from 'react-dom'
ReactDOM.render(<h1 className='title'>Hello React</h1>, document.getElementById('root'))
複製代碼

這裏就 jsx 會變編譯成真正的 DOM ,把 html 代碼拿到 babel 官網編譯java

因而咱們就看到了 React.createElement() 方法,但這只是調用這個方法,它具體作了什麼返回什麼咱們還不知道,咱們能夠打印這個函數運行的結果:react

console.log(
  React.createElement(
    'h1',
    {
      className: 'title',
    },
    'Hello React'
  )
)
複製代碼

返回的這個對象就是虛擬 DOM 了。

咱們來分析它返回的對象參數,首先第一個是git

  • $$typeof: REACT_ELEMENT_TYPE

這個是 React 元素對象的標識屬性github

REACT_ELEMENT_TYPE 的值是一個 Symbol 類型,表明了一個獨一無二的值。若是瀏覽器不支持 Symbol類型,值就是一個二進制值。數組

爲何是 Symbol?主要防止 XSS 攻擊僞造一個假的 React 組件。由於 JSON 中是不會存在 Symbol 類型的。瀏覽器

  • key:這個好比循環中會用到這個key值
  • props:傳入的屬性值,好比 id, className, style, children 等
  • ref: DOM 的引用
  • 剩下的是私有屬性(本篇不展開討論)

在本篇咱們會用本身簡單的方式實現這兩個方法,而不是根據源碼,因此實現上的方法只要能實現它的基本功能便可;有個基本概念在,之後再按部就班學習源碼。babel

而 createElement 中有三個參數,更確切說是 n 個參數:app

  • type:表示要渲染的元素類型。這裏能夠傳入一個元素 Tag 名稱,也能夠傳入一個組件(如div span 等,也能夠是是函數組件和類組件)
  • props:建立React元素所須要的props。
  • childrens(可選參數):要渲染元素的子元素,這裏能夠向後傳入n個參數。能夠爲文本字符串,也能夠爲數組

初步 createElement 方法:

// 建立 JSX 對象
function createElement(type, props, ...childrens) {
    return {
        type,
        props: {
          ...props,
          children: childrens.length <= 1 ? childrens[0] || '' : childrens,
        },
}
複製代碼

參數中 props 和 childrens 是並列關係,而後返回的 props 對象,裏面包含了 children,因此咱們須要再 props 裏面添加 children 參數,而後根據 children 參數爲一個或多個的可能在進行取值處理。

調用該方法:

console.log(
  createElement(
    'h1',
    {
      className: 'title',
    },
    'Hello React'
  )
)
複製代碼

除去其它本篇咱們不討論的屬性,目前算是實現了一半;咱們觀察原來 React 自身方法輸出的結果有 key, ref, 同輸出的 props 也是並列關係,因而咱們進一步做出處理

function createElement(type, props, ...childrens) {
  let ref, key
  if ('ref' in props) {
    ref = props['ref']
    props['ref'] = undefined
  }
  if ('key' in props) {
    key = props['key']
    props['key'] = undefined
  }
  return {
    type,
    props: {
      ...props,
      children: childrens.length <= 1 ? childrens[0] || '' : childrens,
    },
    ref,
    key,
  }
}
複製代碼

一樣的方式調用結果以下:

若是添加多一些屬性,咱們來看看結果

console.log(
  createElement(
    'div',
    { id: 'box', className: 'box', style: { color: 'red' }, key: '20' },
    'this is text',
    createElement('h2', { className: 'title' }, 'hello'),
    createElement('div', { className: 'content' }, 'Hi')
  )
)
複製代碼

用了這種比較粗魯的方式添加,設置爲 undefined 在實現 render 方法的時候咱們會根據這個忽略props內部的 key 和 props 屬性,這裏就實現了最基本的 createElement 方法了。

實現 render 方法

render 方法的第一個參數接收的是 createElement 返回的對象,也就是虛擬DOM; 第二個參數則是掛載的目標DOM。一樣的作法,咱們用 babel 編譯來看:

執行後,就被掛在到頁面了

實現代碼以下:

/* * 功能:把建立的對象生成對應的DOM元素,最後插入到頁面中 * objJSX: createElement 返回的 JSX 對象 * container:掛載的容器,如 document.getElementById('root') */
function render(objJSX, container) {
  let { type, props } = objJSX
  let newElement = document.createElement(type)
  for (let attr in props) {                 // 遍歷傳入的 props 屬性
    if (!props.hasOwnProperty(attr)) break  // 不是私有的直接結束遍歷
    let value = props[attr]                 // >若是當前屬性沒有值,直接不處理便可
    if (value == undefined) continue        // NULL OR UNDEFINED
    
    // 對幾個特殊屬性單獨設置
    switch (attr.toUpperCase()) {
        case 'ID':
            newElement.setAttribute('id', value)
            break
      case 'CLASSNAME':
            newElement.setAttribute('class', value)
            break
      case 'STYLE': // 傳入的行內樣式 style 是個對象,故需遍歷賦值
        for (let styleAttr in value) {
          if (value.hasOwnProperty(styleAttr)) {
            newElement['style'][styleAttr] = value[styleAttr]
          }
        }
        break
      case 'CHILDREN':
        /* * 多是一個值:多是字符串也多是一個JSX對象 * 多是一個數組:數組中的每一項多是字符串也多是JSX對象 */
        // 首先把一個值也變爲數組,這樣後期統一操做數組便可
        !(value instanceof Array) ? (value = [value]) : null
        value.forEach((item, index) => {
          // 驗證ITEM是什麼類型的:若是是字符串就是建立文本節點,若是是對象,咱們須要再次執行RENDER方法,把建立的元素放到最開始建立的大盒子中
          if (typeof item === 'string') {
            let text = document.createTextNode(item)
            newElement.appendChild(text)
          } else {
            render(item, newElement)
          }
        })
        break
      default:
        newElement.setAttribute(attr, value)
    }
  }
  container.appendChild(newElement)
}
複製代碼

相關文章
相關標籤/搜索