前言
本文主要參考了從零開始實現一個React和從 0 到 1 實現React
在上一節JSX和虛擬DOM中,我們了解了react
中的JSX
到虛擬dom
,以及如何將虛擬dom
渲染成真實的dom
。在這一節中,我們將會了解react
中組件是如何渲染的。
組件
在react
中,組件有兩種使用方法:
import React from 'react'
// 類定義的組件
class Hello extends React.Component {
render() {
return <div>hello</div>
}
}
// 無狀態組件,通過函數來定義
const World = () => {
return <h1>world!</h1>
}
ReactDom.render(<Hello />, document.getElementById('#root'))
通過類定義組件時,是需要繼承React.component
的,我們第一步就從React.Component
的實現開始。
類實現的組件有自己私有的state
,同時可以通過this.props
來獲取傳進來的參數。
function Component(props) {
this.props = props
this.state = {}
}
-
setState
我們知道在react
中,我們可以通過setState
來改變組件state
的值,而且當state
改變后,組件對應的也會重新渲染。
- 改變
state
的值:我們可以使用Object.assign
來實現。 - 重新渲染組件:我們可以在改變
state
值后,調用render
函數,重新渲染。異步的setState
在后面的章節會實現。
Component.prototype.setState = function (updateState) {
// 更新 state
this.state = Object.assign({}, this.state, updateState)
// 重新渲染組件
render(this)
}
渲染
在上一節中,我們知道ReactDom.render()
,會將其第一個參數轉成React.createElement()
形式,而組件也會被轉為React.createElement(Hello, null)
這種形式。
所以react
中組件在渲染時會被當成函數渲染的。所以我們在render
函數中需要判斷虛擬dom
的標簽屬性(此處用tag
表示的)是函數還是原生dom
。如果是函數的話,我們只需要拿到組件的jsx
轉換后對應的虛擬dom
, 然后在進行渲染。
const render = (vdom, root) => {
if (typeof vdom.tag === 'function') {
let component
if (vdom.tag.prototype.render) {
// 類定義的組件, vdom.attrs 是傳入的 props
component = new vdom.tag(vdom.attrs)
} else {
// 函數定義組件
component = vdom.tag(vdom.attrs)
}
return _render(component, root)
}
_render(vdom, root)
}
對應的_render():
const _render = (vdom, root) => {
// 類組件的話,需要從 render 函數中拿到 jsx 轉換后的虛擬 dom
const vdomNode = vdom.render ? vdom.render() : vdom
if (typeof vdomNode === "string" || typeof vdomNode === "number") {
root.innerText += vdomNode
return
}
const dom = document.createElement(vdomNode.tag)
if (vdomNode.attrs) {
for (let attr in vdomNode.attrs) {
const value = vdomNode.attrs[attr]
setAttribute(dom, attr, value)
}
}
// 遍歷子節點, 渲染子節點
vdomNode.childs && vdomNode.childs.forEach(child => render(child, dom))
// 將父節點 root 掛到 vdom 上,當再次渲染組件時,跟據 vdom.root 直接渲染 dom
if (vdom.root) {
vdom.root.innerText = ''
vdom.root.appendChild(dom)
return
}
vdom.root = root
// 將子元素掛載到其真實 DOM 的父元素上
root.appendChild(dom)
}
試一試,剛出鍋的代碼效果如何。
import React from "./react"
import ReactDom from "./reactDom"
const World = props => {
return (
<h1>
world!<p>{props.world}</p>
</h1>
)
}
class Hello extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
addCount() {
const { count } = this.state
this.setState({
count: count + 1
})
}
render() {
return (
<div ha="lou">
hello
<World world="function props" />
<span>{this.props.initProps}</span>
<div>{this.state.count}</div>
<button onClick={this.addCount.bind(this)}> + </button>
</div>
)
}
}
ReactDom.render(
<Hello initProps="this is props" />,
document.getElementById("root")
)
小結
react
在渲染組件時,組件會被babel
轉為React.createElement(fn, null)
這種形式,第一參數是函數,所以我們需要從fn
中獲取由組件的jsx
轉換后的虛擬dom
,然后在將虛擬dom
渲染成真實dom
。
setState
:在調用setState
時,先用Object.assign
更新state
的值,然后重新渲染組件。
附上本文代碼