一、渲染过程
我们知道react是把Virtual DOM(虚拟dom),渲染为真实的dom。
1、先看什么是虚拟dom:
具体的虚拟dom格式类似如下:
vdom: {
type: 'ul',
props: {
className: 'list'
},
children: [
{
type: 'li',
props: {
className: 'item',
style: {
background: 'blue',
color: '#cd0303'
},
onClick: function () {
alert(1);
}
},
children: 'aaaa'
},
{
type: 'li',
props: {
className: 'item',
onDoubleClick: () => {
console.log(111)
}
},
children: 'bbbb'
}
]
}
其中:type代表节点类型,props表示节点属性,children代表该节点下的子节点信息。
2、转换渲染过程
从jsx到浏览器上面展示的真实dom过程,需要经过以下阶段:
注意:下面的render是ReactDOM.render而非组件component内的render
如这段代码:
function hello() {
return <div>
<span>Hello world!</span>
<div style={{color:'red',background:'blue'}}>aaaa</div>
<div onClick={()=>alert(111)}>
11111111
</div>
<div onDoubleClick={()=>console.log(222) }>2222</div>
</div>;
}
首先Babel(点击在线查看转换)会将jsx语法转换为,React语法糖(不使用jsx的react)React.createElement(component, props, ...children)
看下Babel的转换结果(去掉注释后如下):
function hello(){
return React.createElement("div", null,
React.createElement("span", null, "Hello world!"),
React.createElement("div", {
style: {
color: "red",
background: "blue"
}
}, "aaaa"),
React.createElement("div", {onClick: () => alert(111)}, "11111111"),
React.createElement("div", {onDoubleClick: () => console.log(222)}, "2222")
);
}
接着React.createElement()会创建并且返会虚拟DOM,最后React.render()会将虚拟dom渲染并且挂在到真实dom上面。
二、简易版React渲染实现
我们已经知道jsx是由Babel编译转换为react语法,所以要实现react的渲染我们只需要拿转换过后的代码格式来实现即可。 以上面的转换代码为例,我们需要实现必要有两个主要方法:
-
React.createElement(component, props, ...children) -
React.render(vdom,document.getElementById('root'))
1、对React.createElement方法的实现
/**
* createElement方法,用来创建虚拟dom数据
* @param type
* @param props
* @param children
* @returns {{children: *[], type, props}}
*/
myCreateElement = (type, props, ...children) => {
const extendsChildren = [...children]
const newChildren = extendsChildren.map(item => {
return item
})
return {
type: type,
props: props,
children: newChildren,
}
}
type是组件的类型(dom),props是一些dom的属性,最后一个是子节点。因为createElement存在父子级的话是链式调用,比如说:
return React.createElement("div", null,
React.createElement("div", {onDoubleClick: () => console.log(222)}, "2222")
);
所以必须返回children为数组。
我们对比自己写的myCreateElement与react.createElement创建出来的虚拟dom数据:
// Babel编译后的原生react语法,创建虚拟Dom的写法
const hello = () => {
return React.createElement("div", null,
React.createElement("span", null, "Hello world!"),
React.createElement("div", {
style: {
color: "red",
background: "blue"
}
}, "aaaa"),
React.createElement("div", {onClick: () => alert(111)}, "11111111"),
React.createElement("div", {onDoubleClick: () => console.log(222)}, "2222")
);
}
// 我们自己写的
const hello2 = () => {
return this.myCreateElement("div", null,
this.myCreateElement("span", null, "Hello world!"),
this.myCreateElement("div", {
style: {
color: "red",
background: "blue"
}
}, "aaaa"),
this.myCreateElement("div", {onClick: () => alert(111)}, "11111111"),
this.myCreateElement("div", {onDoubleClick: () => console.log(222)}, "2222")
);
}
console.log('hello()',hello())
console.log('hello222()',hello2())
结构并无多大差异,只是简陋了:
2、对ReactDom.render方法的实现
这部分主要是将上面生成的虚拟dom数据转换为真实的Dom过程,核心是利用html原生的document.createTextNode()生成纯文本和document.createElement()创建真实dom这两个方法。
其中生成真实dom比较复杂一点,需要把事件和属性赋值添加到dom上面。
/**
* render方法,用来渲染虚拟dom
* @param vdom
* @param relaDom
* @returns {*|ActiveX.IXMLDOMNode}
*/
myRender = (vdom, relaDom) => {
const mount = relaDom ? el => relaDom.appendChild(el) : el => el // 挂载虚拟dom到真实dom上面
if (typeof vdom === 'string' || typeof vdom === "number") { // 如果是文本:字符串或者数字
return mount(document.createTextNode(vdom))
}
if (vdom && (typeof vdom === 'object')) {
let createTypeDom = ''
if (typeof vdom.type === 'function') {
// 如果type本身就是一个组件,这里先不做
} else if (typeof vdom.type === 'string') {
createTypeDom = document.createElement(vdom.type)
}
if (vdom.children?.length > 0) {
for (let child of vdom.children) {
this.myRender(child, mount(createTypeDom))
}
}
return mount(createTypeDom)
}
}
经过我们写的方法渲染的效果如图:
但是这还不够,还需要给dom添加属性。
3、给真实dom设置属性
例如虚拟dom上面有input的type='button'、className或者其他属性也需要绑定到真实dom上面。
- 普通属性我们可以通过原生的setAttribute来实现
element.setAttribute(attributename,attributevalue)
attributename表示要添加的属性名称,attributevalue表示属性值
- 样式使用原生的setproperty或者直接
Object.assign()把样式对象连接来实现。
object.setProperty(propertyname, value)
propertyname表示样式名称,value表示样式值。
/**
* 给真实dom设置属性
* @param relaDom
* @param key
* @param propsParams
*/
mySetAttribute = (relaDom, key, propsParams) => {
if (Object.prototype.toString.call(propsParams) === "[object Function]") { //如果是事件方法
// this.myEvent(relaDom, key, propsParams) 见下面第4点:事件处理方法
} else if (typeof propsParams === 'object' && (key === 'style')) { // 样式
for (let cssKey in propsParams) {
relaDom.style.setProperty(cssKey, propsParams[cssKey])
}
// Object.assign(relaDom.style, propsParams) // 也可以使用
} else if (typeof propsParams === 'string') { // 其他属性
for (let otherKey in propsParams) {
relaDom.setAttribute(otherKey, propsParams[otherKey])
}
}
}
在render的时候调用这个方法。
myRender = (vdom, relaDom) => {
const mount = relaDom ? el => relaDom.appendChild(el) : el => el
if (typeof vdom === 'string' || typeof vdom === "number") {
return mount(document.createTextNode(vdom))
}
if (vdom && (typeof vdom === 'object')) {
let createTypeDom = ''
if (typeof vdom.type === 'function') {
// 如果type本身就是一个组件,这里先不做
} else if (typeof vdom.type === 'string') {
createTypeDom = document.createElement(vdom.type)
}
if (vdom.children?.length > 0) {
for (let child of vdom.children) {
this.myRender(child, mount(createTypeDom))
}
}
for (let key in vdom.props) { // 使用循环,props可能有多个属性
this.mySetAttribute(createTypeDom, key, vdom.props[key]) // 这里调用我们的mySetAttribute
}
return mount(createTypeDom)
}
}
此时样式等其它属性已经生效了,看看效果:
但是props中的事件还没有给真实dom绑定上去,接下来我们就要处理事件。
4、事件处理
虚拟dom上面有一些属性是事件属性,例如onClick,在生成真实dom中我们需要给他绑定到上面并且对应事件可执行。
在React中事件有自己的一套事件机制(react的事件都是合成事件,例如onClick等等),顺序与原生的事件一致,他们是先捕获进行事件收集,再分发事件后执行。
原生事件执行如下草图:
所以我们这里实现事件用原生的addEventListener来实现,给dom添加事件监听。
element.addEventListener(event, function, useCapture) event表示事件名称,function表示事件触发时执行的函数,useCapture表示在捕获阶段执行还是在冒泡阶段执行。
这样我们就可以实现虚拟dom上面的props携带的事件方法通过addEventListener去执行了.
/**
* 事件处理
* @param relaDom
* @param eventName
* @param event
*/
myEvent = (relaDom, eventName, event) => {
const eventConfig = {
onClick: 'click',
onDoubleClick: 'dblclick',
onKeyDown: 'keydown',
onDragEnd: 'dragend',
// 其他更多事件...
}
for (let key in eventConfig) {
if (key === eventName) {
relaDom.addEventListener(eventConfig[key], event)
}
}
}
三、实现效果
最后我把最初的babel转换后的代码拿来测试下,把react的方法替换为我们的实现方法:(完整代码)
import React, {Component} from 'react';
class Index extends Component {
componentDidMount() {
const myReactVDom = () => {
// 最初由Babel转换过的代码
return this.myCreateElement("div", null,
this.myCreateElement("span", null, "Hello world!"),
this.myCreateElement("div", {
style: {
color: "red",
background: "blue"
}
}, "aaaa"),
this.myCreateElement("div", {onClick: () => alert(111)}, "11111111"),
this.myCreateElement("div", {onDoubleClick: () => console.log(222)}, "2222")
);
}
this.myRender(myReactVDom(), document.getElementById('myRoot'))
}
/**
* createElement方法,用来创建虚拟dom数据
* @param type
* @param props
* @param children
* @returns {{children: *[], type, props}}
*/
myCreateElement = (type, props, ...children) => {
const extendsChildren = [...children]
const newChildren = extendsChildren.map(item => {
return item
})
return {
type: type,
props: props,
children: newChildren,
}
}
/**
* render方法,用来渲染虚拟dom
* @param vdom
* @param relaDom
* @returns {*|ActiveX.IXMLDOMNode}
*/
myRender = (vdom, relaDom) => {
const mount = relaDom ? el => relaDom.appendChild(el) : el => el // 挂载虚拟dom到真实dom上面
if (typeof vdom === 'string' || typeof vdom === "number") {
return mount(document.createTextNode(vdom))
}
if (vdom && (typeof vdom === 'object')) {
let createTypeDom = ''
if (typeof vdom.type === 'function') {
// 如果type本身就是一个组件,这里先不做
} else if (typeof vdom.type === 'string') {
createTypeDom = document.createElement(vdom.type)
}
if (vdom.children?.length > 0) {
for (let child of vdom.children) {
this.myRender(child, mount(createTypeDom))
}
}
for (let key in vdom.props) {
this.mySetAttribute(createTypeDom, key, vdom.props[key])
}
return mount(createTypeDom)
}
}
/**
* 事件处理
* @param relaDom
* @param eventName
* @param event
*/
myEvent = (relaDom, eventName, event) => {
const eventConfig = {
onClick: 'click',
onDoubleClick: 'dblclick',
onKeyDown: 'keydown',
onDragEnd: 'dragend',
// 其他更多事件...
}
for (let key in eventConfig) {
if (key === eventName) {
relaDom.addEventListener(eventConfig[key], event)
}
}
}
/**
* 给真实dom设置属性
* @param relaDom
* @param key
* @param propsParams
*/
mySetAttribute = (relaDom, key, propsParams) => {
if (Object.prototype.toString.call(propsParams) === "[object Function]") { //如果是事件方法
this.myEvent(relaDom, key, propsParams)
} else if (typeof propsParams === 'object' && (key === 'style')) { // 样式
for (let cssKey in propsParams) {
relaDom.style.setProperty(cssKey, propsParams[cssKey])
}
// Object.assign(relaDom.style, propsParams) // 也可以使用
} else if (typeof propsParams === 'string') { // 其他属性
for (let otherKey in propsParams) {
relaDom.setAttribute(otherKey, propsParams[otherKey])
}
}
}
render() {
return (
<div id={'myRoot'}>
</div>
);
}
}
export default Index;
上面虚拟dom由代码的执行效果:
到这里我们就基本完成了非常简易版本的react渲染