我们打算实现一下jsx语法的转换过程。但是在此之前要说一下react17之后的一个变化。
在v17之前,我们即使没有直接使用React,也需要引入React。这是因为babel在转译之后会触发React.createElement,所以不引入会报错。
import React from 'react';
function App() {
return <h1>Hello World</h1>;
}
=============编译为=======================
import React from 'react';
function App() {
return React.createElement('h1', null, 'Hello world');
}
使用了全新的转换,所以可以单独引入jsx(禁止自己引入),而无需引入React。
function App() {
return <h1>Hello World</h1>;
}
==================================
// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
因此,如果我们自己想要实现 React.createElement 就需要修改package.json的运行配置。
cross-env是运行跨平台设置和使用环境变量的脚本。这里我就不详细说了,如果想了解可以看这篇文章
npm install --save-dev cross-env
// cross-env 需要自己安装
scripts": {
"start": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts start",
"build": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts build",
"test": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts test",
"eject": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts eject"
},
这里再提一下react18版本的改变。ReactDOM中废弃了render()
,用createRoot进行了替代render
这里是createRoot的使用,创建了一个root后,再用render()去渲染。更详细的介绍请看这篇文章
// v18
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
// Create a root.
const root = ReactDOM.createRoot(container);
// Initial render: Render an element to the root.
root.render(<App tab="home" />);
// During an update, there's no need to pass the container again.
root.render(<App tab="profile" />);
import React from 'react';
import ReactDOM from 'react-dom';
let element1 = (
<div className='title' style={{ color: 'red' }}>
<span>hello</span> world
</div>
)
console.log(JSON.stringify(element1, null, 2))
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(element1)
我们要实现react.js
和 react-dom.js
的源码。 在src文件夹下新建 react.js
和 react-dom.js
我们需要做的:
先创建一个函数,然后把函数放到 React对象中,最后导出。
function createElemet() {
}
const React = { createElemet }
export default React
复制代码
经过babel处理,将Es6的内容转换为了Es5,因为这样;浏览器才识别。
let element1 = (
<div className='title' style={{ color: 'red' }}>
<span>hello</span> world
</div>
)
如下的元素,是上面的element1元素 处理后得到的。
// 转换为Es5
let element2 = React.createElement('div', {
"className": "title",
"style": { "color": "red" }
},
// 子节点 所以又使用 React.createElement 创建节点
React.createElement("span", null, "hello"),
"world"
)
根据Es5的内容完善react.js
/**
*
* @param {*} type 元素类型
* @param {*} config 配置对象
* @param {*} children
*/
// 需要实现 虚拟DOM的创建方法 和 render渲染的方法
function createElemet(type,config,children) {
// 如果config存在 删除以下两个属性(官方react使用的我们不需要就删掉了)
if(config){
delete config.__source
delete config.__selef
}
let props = {...config};
// arguments是函数内置的存储实参的容器 这里>3说明children不止一个,还有其他
if(arguments.length>3){
children = Array.prototype.slice.call(arguments,2)
}
props.children = children
// 在这里得到虚拟DOM元素
return {
type,
props
}
}
const React = { createElemet }
export default React
最后将虚拟DOM暴露出去。然后就需要对虚拟DOM转为真实DOM的处理
document.createTextNode
将其添加到节点上。<div>
')和props(属性对象),通过 document.createElement
将其添加到节点。/**
* @param {*} vdom 要渲染的虚拟DOM
* @param {*} container 要把虚拟DOM转换为真实DOM并插入到xx容器内
*/
function render(vdom, container) {
const dom = createDOM(vdom)
container.appendChild(dom)
}
/**
* 把虚拟DOM变成真实DOM
* @param {*} vdom
*/
function createDOM(vdom) {
// 处理vdom是数字或者字符串 就好比我们刚才element中的字符串 返回一个文本节点
if (typeof vdom === 'string' || typeof vdom === 'number') {
return document.createTextNode(vdom)
}
//否则 他是一个虚拟DOM对象了,也就是React元素
// type 是一个字符串 如:'<div>' '<span>' props是一个属性对象
let {type,props} = vdom;
// 创建一个真实DOM
let dom = document.createElement(type)
return dom
}
const ReactDOM = { render }
export default ReactDOM
复制代码
在我们的index.js中 引入我们写好的 react.js
和 react-dom.js
import React from './react';
import ReactDOM from './react-dom';
let element1 = (
<div className='title' style={{ color: 'red' }}>
<span>hello</span> world
</div>
)
ReactDOM.render(element1,document.getElementById('root'))
复制代码
可以看到虚拟DOM已经打印了,但是页面上并没有内容
看一下元素 发现只有div其属性和子元素都没有添加上。
在我们刚才写好的方法中去调用 updateProps方法
/**
* 把虚拟DOM变成真实DOM
* @param {*} vdom
*/
function createDOM(vdom) {
// 处理vdom是数字或者字符串 就好比我们刚才element中的字符串 返回一个文本节点
if (typeof vdom === 'string' || typeof vdom === 'number') {
return document.createTextNode(vdom)
}
//否则 他是一个虚拟DOM对象了,也就是React元素
// type 是一个字符串 如:'<div>' '<span>' props是一个属性对象
let { type, props } = vdom;
// 创建一个真实DOM
let dom = document.createElement(type)
// 使用虚拟DOM的属性更新刚创建出来的真实DOM的属性
updateProps(dom, props)
// 在这里处理props.children属性
// 如果只有一个儿子,并且这个儿子是一个文本
if (typeof props.children == 'string' || typeof props.children == 'number') {
dom.textContent = props.children
// 如果只有一个儿子,并且这个儿子是一个虚拟DOM元素
} else if (typeof props.children === 'object' && props.children.type) {
// 递归 把自己的儿子变成真实DOM插到自己身上
render(props.children, dom)
// 如果儿子是一个数组,说明,儿子有多个
} else if (Array.isArray(props.children)) {
console.log(44)
reconcileChildren(props.children, dom)
} else {
document.textContent = props.children ? props.children.toString() : ''
}
// 把真实DOM作为一个DOM属性放在虚拟DOM,为以后更新做准备
// vdom.dom = dom
return dom
}
/**
*
* @param {*} childrenVdom 儿子们的虚拟DOM
* @param {*} parentDOM 父亲的真实DOM
*/
function reconcileChildren(childrenVdom, parentDOM) {
for (let i = 0; i < childrenVdom.length; i++) {
let childVdom = childrenVdom[i]
// 将儿子挂载到父亲身上
render(childVdom, parentDOM)
}
}
/**
*
* @param {*} dom 真实DOM
* @param {*} newProps 新属性对象
*/
function updateProps(dom, newProps) {
for (let key in newProps) {
if (key === 'children') continue; //不在此处处理
// 处理样式
if (key === 'style') {
let styleObj = newProps.style;
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr]
}
} else {
dom[key] = newProps[key]
}
// className不需要处理成class吗? 不需要 className是真实DOM的写法
}
}