看源碼一個痛處是會陷進理不順主干的困局中,本系列文章在實現(xiàn)一個 (x)react 的同時理順 React 框架的主干內(nèi)容(JSX/虛擬DOM/組件/生命周期/diff算法/setState/ref/...)
- 從 0 到 1 實現(xiàn) React 系列 —— JSX 和 Virtual DOM
- 從 0 到 1 實現(xiàn) React 系列 —— 組件和 state|props
- 從 0 到 1 實現(xiàn) React 系列 —— 生命周期和 diff 算法
- 從 0 到 1 實現(xiàn) React 系列 —— 優(yōu)化 setState 和 ref 的實現(xiàn)
前置準(zhǔn)備
看源碼一個痛處是會陷進理不順主干的困局中非竿,本系列文章在實現(xiàn)一個 (x)react 的同時理順 React 框架的主干內(nèi)容(JSX/虛擬DOM/...)
環(huán)境準(zhǔn)備
項目打包工具選擇了 parcel,使用其可以快速地進入項目開發(fā)的狀態(tài)现拒。快速開始
此外需要安裝以下 babel 插件:
"babel-core": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-plugin-transform-react-jsx": "^6.24.1"
同時 .babelrc
配置如下:
{
"presets": ["env"],
"plugins": [
// 插件如其名:轉(zhuǎn)化 JSX 語法為定義的形式
["transform-react-jsx", {
"pragma": "React.createElement"
}]
]
}
JSX 和 虛擬 DOM
const element = (
<div className="title">
hello<span className="content">world!</span>
</div>
)
JSX 是一種語法糖,經(jīng)過 babel 轉(zhuǎn)換結(jié)果如下,可以發(fā)現(xiàn)實際上轉(zhuǎn)化成 React.createElement()
的形式:
var element = React.createElement(
"div",
{ className: "title" },
"hello",
React.createElement(
"span",
{ className: "content" },
"world!"
)
);
打印 element, 結(jié)果如下:
{
attributes: {className: "title"}
children: ["hello", t] // t 和外層對象相同
key: undefined
nodeName: "div"
}
因此,我們得出結(jié)論:JSX 語法糖經(jīng)過 Babel 編譯后轉(zhuǎn)換成一種對象痢掠,該對象即所謂的虛擬 DOM
,使用虛擬 DOM 能讓頁面進行更為高效的渲染嘲恍。
我們按照這種思路進行函數(shù)的構(gòu)造:
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) // 打印結(jié)果符合預(yù)期
// {
// attributes: {className: "title"}
// children: ["hello", t] // t 和外層對象相同
// key: undefined
// nodeName: "div"
// }
虛擬 DOM 轉(zhuǎn)化為真實 DOM
上個小節(jié)介紹了 JSX 轉(zhuǎn)化為虛擬 DOM 的過程足画,這個小節(jié)接著來實現(xiàn)將虛擬 DOM 轉(zhuǎn)化為真實 DOM (頁面上渲染的是真實 DOM)。
我們知道在 React 中佃牛,將虛擬 DOM 轉(zhuǎn)化為真實 DOM 是使用 ReactDOM.render
實現(xiàn)的淹辞,使用如下:
ReactDOM.render(
element, // 上文的 element,即虛擬 dom
document.getElementById('root')
)
接著來實現(xiàn) ReactDOM.render
的邏輯:
const ReactDOM = {
render
}
/**
* 將虛擬 DOM 轉(zhuǎn)化為真實 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)
}
/**
* 給節(jié)點設(shè)置屬性
* @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) // 處理駝峰樣式為標(biāo)準(zhǔn)樣式
styleStr += `${standardCss}: ${value[klass]};`
}
dom.setAttribute(attr, styleStr)
} else { // 其它屬性
dom.setAttribute(attr, value)
}
}
至此俘侠,我們成功將虛擬 DOM 復(fù)原為真實 DOM象缀,展示如下:
另外配合熱更新蔬将,在熱更新的時候清空之前的 dom 元素,改動如下:
const ReactDOM = {
render(vdom, container) {
container.innerHTML = null
render(vdom, container)
}
}
總結(jié)
JSX
經(jīng)過 babel 編譯為 React.createElement() 的形式攻冷,其返回結(jié)果就是 Virtual DOM
娃胆,最后通過 ReactDOM.render() 將 Virtual DOM 轉(zhuǎn)化為真實的 DOM 展現(xiàn)在界面上。流程圖如下:
思考題
如下是一個 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) // 組件的掛載
項目說明
該系列文章會盡可能的分析項目細節(jié),具體的還是以項目實際代碼為準(zhǔn)禁谦。