实现createElement函数
上一章中有讲到,js文件可以识别jsx语法,是利用插件调用 react内部的createElement方法,传递相应的参数,生成虚拟的dom对象。在自己搭建react-source项目中,我们就先建一个文件夹react,然后创建一个index.js文件
index.js文件暴露一个React对象,这个对象上面一定有一个方法,createElement方法。我们就先这么写:
const React = {
createElement
}
// 生成虚拟的dom对象
function createElement(tag, attrs, ...children) {
return {
tag,
attrs,
children
}
}
export default React;
再建一个react-dom文件夹。
src下面的index.js引入新建好的文件:
import React from './react'
import ReactDOM from './react-dom'
const ele = (
<div className='active'>
hello <h1>react</h1>
</div>
)
console.log(ele);
ReactDOM.render(ele, document.getElementById('app'));
然后npm run dev,打开页面,看到控制台打印出来这样的一个对象,这就我们所说的虚拟dom对象,createElement方法是在es6转es5的过程中,插件去调用的,并且解析相应的参数传递给这个方法。
可以看到,createElement这个函数很简单,只需要把传递进来的参数返回就好,这样jsx转换成了虚拟的DOM对象。
但是,看下页面,并没有任何展示,这是因为react-dom中的render函数没有写任何的功能,也就是没有把虚拟的DOM转化为html。
实现render函数
调用render的时候传递的第一个参数是ele,也就是一个虚拟的dom对象,这个值有几种情况:
1,参数没有传递
创建一个空的文本节点document.createTextNode('');
2,传递的是字符串,是一个文本节点
document.createTextNode(v),可以直接创建文本节点
3,传递的是虚拟的dom对象
获取虚拟对象中的属性
const { tag = '', attrs = '', children = [] } = v;
分析一下tag情况
a:tag是dom标签
直接创建一个dom元素,document.createElement(tag),标签上可能有属性,标签内部还会有子节点,实现一个setArribute函数处理dom上的属性。setArribute调用的时候,传递创建好的dom标签,属性和对应的属性值。
属性分4种情况:
- class
- style
- 事件
- 自定义属性。
class
// class
if(key === 'className') {
key = 'class';
}
事件
这种情况需要把onClick大写的事件转化为原生的小写,添加到dom上。
// 事件
if(/on\w+/.test(key)) {
key = key.toLowerCase()
dom[key] = value || ''
}
样式 style
- style的值是字符串
- style的值是对象
字符串;
<body>
<div id='app' style="color: red"></div>
</body>
<script>
var app = document.getElementById('app');
console.log(app.style.cssText);
</script>
所以可以直接这么处理:
if(!value || typeof value === 'string') {
dom.style.cssText = value || ''
}
如果是对象,需要枚举属性,判断属性值,如果是number类型的,需要拼接'px'。
自定义属性
原来dom上存在相同的自定义属性,可以直接替换
原来dom上没有,并且传递了value值,则需要设置属性dom.setAttribute(key,value)
咩有传递value,是需要移除这个自定义属性, dom.removeAttribute(key)
完成的代码如下:
function setArribute(dom, key, value) {
// class
if(key === 'className') {
key = 'class';
}
// 事件
if(/on\w+/.test(key)) {
key = key.toLowerCase()
dom[key] = value || ''
} else if(key === 'style') { // 样式
if(!value || typeof value === 'string') {
dom.style.cssText = value || ''
} else if(value && typeof value === 'object') {
for(let k in value) {
if(typeof value[k] === 'number') {
dom.style[k] = value[k] + 'px'
} else {
dom.style[k] = value[k]
}
}
}
} else {
if(key in dom) {
dom[key] = value;
}
if(value) {
dom.setAttribute(key,value)
} else {
dom.removeAttribute(key)
}
}
}
如果说传递的是一个函数组件,看下tag会是什么?
import React from './react'
import ReactDOM from './react-dom'
function Home() {
return (
<div className='home'>
hello <h1>react</h1>
</div>
)
}
console.log(<Home title='home'/>)
ReactDOM.render(<Home title='home'/>, document.getElementById('app'));
可以看到tag是一个函数。
如果说是一个class 类呢?
import React from './react'
import ReactDOM from './react-dom'
class Home extends React.Component {
render() {
return (
<div className='active'>
'hello'
<h1 >react</h1>
</div>
)
}
}
console.log(<Home title='home' />)
ReactDOM.render(<Home title='home' />, document.getElementById('app'));
注意:也可以在react脚手架创建的项目种看下打印结果。
tag也是一个函数,所以还有一种情况是tag是函数。
b, tag是函数
函数组件也转化成了虚拟的DOM,只不过这个虚拟对象的tag是一个函数。
- 创建一个组件
- 设置组件的属性
把tag和attrs传递给createComponent函数。这个函数接收到参数以后,需要先判断一下这个tag是class类生成的,还是函数生成的。
区分函数组件和class组件:函数组件没有render函数,而class类组件原型上有render函数。
类组件:直接new comp(props)创建一个实例,属性传递过去。
函数组件:定义一个空的class类,创建一个实例,修改实例的constructor,给实例增加一个render方法,内部调用函数组件,传递props。
function createComponent(comp, props) {
let inst;
// 如果是类定义的组件
if(comp.prototype && comp.prototype.render) {
return inst = new comp(props);
}
// 如果是函数,这里我们需要转化为类,方便后面统一管理
inst = new Component(props);
inst.constructor = comp;
inst.render = function() {
return this.constructor(props);
}
return inst;
}
src/react/component.js
class Component {
constructor(props = {}){
this.props = props;
this.state={}
}
}
export default Component;
组件创建好以后,还需要渲染组件,把组件变成真是的dom。先给组件设置一个props属性,再来渲染组件。
怎么把组件html节点变成虚拟的dom的对象呢?组件内部都有render函数,返回一串jsx代码,可以调用这个函数拿到jsx代码快,也就生成虚拟的dom对象了。
给组件实例增加一个属性props。
function setComeponentProps(comp, props) {
// 设置组件的props
comp.props = props;
renderComponent(comp)
}
然后渲染组件
function renderComponent(comp) {
// v虚拟的dom对象
const v = comp.render();
// 生成真实的dom
comp.base = _render(v);
}
4,children有值
如果children是有值的,说明还有字节点需要渲染,这时只需要循环递归调用_ender就可以了。
初步代码如下:
function _render(v) {
if(v === undefined || v === null || typeof v === 'boolean') return document.createTextNode('');
// 如果是数值
if(typeof v === 'number') v = String(v)
// 如果是字符串
if(typeof v === 'string') {
return document.createTextNode(v);
}
// 如果tag是函数,则渲染组件
const { tag = '', attrs = '', children = [] } = v;
if(typeof tag === 'function') {
// 创建组件
const comp = createComponent(tag, attrs)
// 设置组件属性
setComeponentProps(comp, attrs)
// 组件渲染的节点返回
return comp.base;
}
// 是一个虚拟dom对象
// const { tag = '', attrs = '', children = [] } = v;
const dom = document.createElement(tag);
if(attrs) {
Object.keys(attrs).forEach((key) => {
const value = attrs[key];
setArribute(dom, key, value);
})
}
isArray(children) && children.forEach((child)=>render(child, dom))
return dom
}
function isType(type) {
return function(obj) {
return {}.toString.call(obj) == "[object " + type + "]"
}
}
var isObject = isType("Object")
var isString = isType("String")
var isArray = Array.isArray || isType("Array")
var isFunction = isType("Function")
var isUndefined = isType("Undefined")
看,我们的页面又能正常展示了。此时的src/index.js