react和react-dom是如何将react元素渲染成真实DOM

1,083 阅读6分钟

前言

我们试图了解某样东西的原理时,要先会使用它,再去弄懂它每一步是如何操作的及其操作返回的结果。

分析 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元素是一个对象,我们重点看一下对象中propstype这两个属性

    • 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'))

总结

  1. 分析reac(createElement)t和react-dom(render)核心函数
  2. createElement返回值是一个对象 {type,props}
  3. react-dom的render函数就是将react元素({type,props})解析为dom元素