前言
本文主要参考了从零开始实现一个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
的值,然后重新渲染组件。
附上本文代码