前言
我们试图了解某样东西的原理时,要先会使用它,再去弄懂它每一步是如何操作的及其操作返回的结果。
分析 createElement
- 在使用react的时候,我们并不直接使用createElement去创建react元素,而是直接写成html结构,事实上是babeljs会将其转换为createElement
- 这种在js中写html的语法,称之为JSX,它是语法糖
import React from 'react'
import ReactDOM from 'react-dom'
/*
此处<h1>hello react</h1>,babel会将其转换为
React.createElement('h1',{},'hello react')
*/
ReactDOM.render(<h1>hello react</h1>, document.getElementById('root'))
react元素
- createElement函数的返回结果即为react元素,我们看一下react元素的结构
let dom1 = React.createElement(
'h1',
{
className: 'title',
style: {
color: 'red'
}
},
'hello'
)
console.log(dom1)
let dom2 = React.createElement(
'h1',
{
className: 'title',
style: {
color: 'red'
}
},
'hello',
dom1
)
console.log(dom2)
-
上述代码控制台打印结果
-
dom1

-
dom2

-
-
分析react元素
由上图可以看出,createElement返回的react元素是一个对象,我们重点看一下对象中props和type这两个属性
- type的值是当前需要渲染的dom的html标签名
- props的值包含两个部分:children 和其他属性,children的值是子节点,其他的属性都会作为行内属性挂载到dom上
- children
我们可以从上述两个图可以看出,children属性的值是不一样的 可以是字符串(当前dom元素的文本节点) 可以是对象(当前元素的一个子节点)(这个可以自行去验证) 可以是数组(当前元素的多个子节点)
-
总结:
1. createElement 返回的是一个对象{type,props},针对于渲染成真实dom,我们只关注props和type这两个属性,
2. type的值是html标签
3. props包含两种属性: children 和其它属性 (以后会挂载到dom元素上的行内属性)
4. childen的值有三种情况:1.字符串,2.react元素对象,3.数组
实现createElement
/*
1. 创建一个函数 createElement ,有三个参数
type: html标签名
config: 属性对象
children: 第三个及其以后的参数都包含在children里面
2. 返回值是一个对象{type,props}
3. 处理props
*/
const ReactElement = (type,props)=>{
let element ={type, props}
return element
}
function createElement(type, config = {}, children) {
let props = {},
childrenLength = arguments.length - 2 // children的长度
// 将属性对象一一映射到props上
for (let propName in config) {
props[propName] = config[propName]
}
// 处理children
if (childrenLength === 1) {
// children只有一个,直接赋值给props的children属性
props.children = children
} else {
props.children = Array.from(arguments).slice(2)
}
return ReactElement(type, props )
}
export default { createElement }
分析 render
- react-dom的核心即为render方法,render将react元素渲染成真实dom
- render方法有两个参数,一个react元素(也可能是字符串或者number),一个当前需要挂载dom元素的容器,也就是说render方法的内部将react元素经过一系列的处理成为一个dom元素
1. react元素即为一个对象{type,props}
2. 根据type我们可以创建一个dom元素,document.createElement(type)
3. 我们处理props,props分两种情况其他属性(行内属性)和children
4. 针对行内属性的特殊处理情况有以下三种
a. className 和 htmlFor ,我们不可以使用setAttribute直接去挂载到dom上,dom[属性]=属性值
b. style的属性部分可能是驼峰命名,例如:fontSize,要改成"font-size"这种dom上能够识别的style属性
c. 其它的可以直接使用setAttribute(属性,属性值)即可挂载到dom是哪个
5.children属性,也是react元素(字符串或者number),所以要使用递归
6.如果要处理的元素为string或者number,直接创建一个文本节点返回document.createTextNode
实现 render
/*
1. 创建一个render函数,参数:1.react元素element 2.容器父元素 parentNode
2. 判断react元素是否为基本类型string和number,如果是直接创建文本节点挂载到parentNode 上
3. 解构element => {type, props}
4. 根据type创建dom元素
5. 遍历props处理属性propName
- 如果为className和htmlFor => dom[propName] = props[propName]
- 如果是style,我们需要对style对象的属性值进行处理,驼峰命名改为由"-"连接
- 如果是children,判断children是否为数组,如果不是数组包装为数组,并且递归调用render,因为children也是react元素
- 如果是其它属性,则直接挂载到dom元素上
6. 将创建的dom元素挂载到parentNode下
*/
function render(element, parentNode) {
//判断react元素是否为基本类型string和number,如果是直接创建文本节点挂载到parentNode 上
if (typeof element === 'string' || typeof element === 'number') {
return parentNode.appendChild(document.createTextNode(element))
}
let { type, props } = element
let dom = document.createElement(type)
for (let propName in props) {
if (propName === 'className' || propName === 'htmlFor') {
dom[propName] = props[propName]
} else if (propName === 'style') {
let prop = props[propName]
// 例如fontSize改为 font-size
prop = Object.keys(prop)
.map((item) => {
return (
item.replace(/[A-Z]/g, function (ele) {
return '-' + ele.toLowerCase()
}) +
':' +
prop[item]
)
})
.join(';')
dom.style.cssText = prop
} else if (propName === 'children') {
let children = props[propName]
children = Array.isArray(children) ? children : [children]
console.log(children)
children.forEach((child) => {
render(child, dom)
})
} else {
dom.setAttribute(propName, props[propName])
}
}
parentNode.appendChild(dom)
}
export default { render }
其他情况
函数组件
- 函数组件的返回的是一个react元素(一般就是jsx语法,我们在这直接认为是react元素),执行函数获取到的返回值即为
- 代码实现
function render(element, parentNode) {
//判断react元素是否为基本类型string和number,如果是直接创建文本节点挂载到parentNode 上
if (typeof element === 'string' || typeof element === 'number') {
return parentNode.appendChild(document.createTextNode(element))
}
let type, props
type = element.type
props = element.props
/* 针对 函数组件加的处理 start */
if (typeof type === 'function') {
let ReturnedElement = type()
type = ReturnedElement.type
props = ReturnedElement.props
}
/* 针对 函数组件加的处理 end */
let dom = document.createElement(type)
for (let propName in props) {
if (propName === 'className' || propName === 'htmlFor') {
dom[propName] = props[propName]
} else if (propName === 'style') {
let prop = props[propName]
// 例如fontSize改为 font-size
prop = Object.keys(prop)
.map((item) => {
return (
item.replace(/[A-Z]/g, function (ele) {
return '-' + ele.toLowerCase()
}) +
':' +
prop[item]
)
})
.join(';')
dom.style.cssText = prop
} else if (propName === 'children') {
let children = props[propName]
children = Array.isArray(children) ? children : [children]
console.log(children)
children.forEach((child) => {
render(child, dom)
})
} else {
dom.setAttribute(propName, props[propName])
}
}
parentNode.appendChild(dom)
}
export default { render }
- 例子
import React from './react'
import ReactDOM from './react-dom'
function WelcomeFunc(props) {
return React.createElement(
'h1',
{ style: { color: 'red' } },
props.name,
props.age
)
}
let element = React.createElement(WelcomeFunc, { name: 'juejin', age: 1 })
ReactDOM.render(element, document.getElementById('root'))
类组件
- 由于类组件也是属于函数,所以在createElement里我们需要创建一个Component类,内置一个static属性未isReactComponent = true以区分函数组件和类组件
class Component {
static isReactComponent = true
constructor(props) {
this.props = props
}
}
const ReactElement = (type, props) => {
let element = { type, props }
return element
}
function createElement(type, config = {}, children) {
let props = {},
childrenLength = arguments.length - 2
for (let propName in config) {
props[propName] = config[propName]
}
if (childrenLength === 1) {
props.children = children
} else {
props.children = Array.from(arguments).slice(2)
}
return ReactElement(type, props)
}
export default { createElement, Component }
- render的处理,类组件实例上的render方法返回值为react元素(简单处理同函数一致),new _Class().render()
function render(element, parentNode) {
if (typeof element === 'string' || typeof element === 'number') {
return parentNode.appendChild(document.createTextNode(element))
}
let type, props
type = element.type
props = element.props
/* 针对 类组件加的处理 start */
if (type.isReactComponent) {
let ReturnedElement = new type(props).render()
type = ReturnedElement.type
props = ReturnedElement.props
} /* 针对 类组件加的处理 end */ else if (typeof type === 'function') {
let ReturnedElement = type(props)
type = ReturnedElement.type
props = ReturnedElement.props
}
let dom = document.createElement(type)
for (let propName in props) {
if (propName === 'className' || propName === 'htmlFor') {
dom[propName] = props[propName]
} else if (propName === 'style') {
let prop = props[propName]
prop = Object.keys(prop)
.map((item) => {
return (
item.replace(/[A-Z]/g, function (ele) {
return '-' + ele.toLowerCase()
}) +
':' +
prop[item]
)
})
.join(';')
dom.style.cssText = prop
} else if (propName === 'children') {
let children = props[propName]
children = Array.isArray(children) ? children : [children]
console.log(children)
children.forEach((child) => {
render(child, dom)
})
} else {
dom.setAttribute(propName, props[propName])
}
}
parentNode.appendChild(dom)
}
export default { render }
- 例子
import React from './react'
import ReactDOM from './react-dom'
class WelcomeClass extends React.Component {
render() {
return React.createElement(
'h1',
{ style: { color: 'red' } },
this.props.name,
this.props.age
)
}
}
let element = React.createElement(WelcomeClass, {
name: 'juejin class',
age: 1
})
ReactDOM.render(element, document.getElementById('root'))
总结
- 分析reac(createElement)t和react-dom(render)核心函数
- createElement返回值是一个对象 {type,props}
- react-dom的render函数就是将react元素({type,props})解析为dom元素