前言
在 React 中,我們都知道可以寫 jsx 代碼會被編譯成真正的 DOM 插入到要顯示的頁面上。這具體是怎么實現的,今天我們就自己動手做一下。
實現 createElement 方法
這個方法平時開發我們并不會用到,因為它是經 babel 編譯后的代碼,我們新建一個 React 項目,index.js 最簡單的代碼結構如下:
import?React?from?'react'
import?ReactDOM?from?'react-dom'
ReactDOM.render(<h1?className='title'>Hello?Reacth1>,?document.getElementById('root'))
這里就 jsx 會變編譯成真正的 DOM ,把 html 代碼拿到 babel 官網編譯

于是我們就看到了 React.createElement() 方法,但這只是調用這個方法,它具體做了什么返回什么我們還不知道,我們可以打印這個函數運行的結果:
console.log(
??React.createElement(
????'h1',
????{
??????className:?'title',
????},
????'Hello?React'
??)
)
返回的這個對象就是虛擬 DOM 了。
我們來分析它返回的對象參數,首先第一個是
$$typeof: REACT_ELEMENT_TYPE
這個是 React 元素對象的標識屬性
REACT_ELEMENT_TYPE 的值是一個 Symbol 類型,代表了一個獨一無二的值。如果瀏覽器不支持 Symbol 類型,值就是一個二進制值。
?為什么是 Symbol?主要防止 XSS 攻擊偽造一個假的 React 組件。因為 JSON 中是不會存在 Symbol 類型的。
?
- key:這個比如循環中會用到這個 key 值
- props:傳入的屬性值,比如 id, className, style, children 等
- ref:DOM 的引用
- 剩下的是私有屬性(本篇不展開討論)
在本篇我們會用自己簡單的方式實現這兩個方法,而不是根據源碼,所以實現上的方法只要能實現它的基本功能即可;有個基本概念在,以后再循序漸進學習源碼。
而 createElement 中有三個參數,更確切說是 n 個參數:
- 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)
}
?歡迎關注我掘金賬號和Github技術博客:
?
- 掘金:https://juejin.im/user/1257497033714477
- Github:https://github.com/Jacky-Summer
- 覺得對你有幫助或有啟發的話歡迎 star,你的鼓勵是我持續創作的動力~
- 如需在微信公眾號平臺轉載請聯系作者授權同意,其它途徑轉載請在文章開頭注明作者和文章出處。