一、JSX的转换
<div id="app"></div>
<div id="app1"></div>
<script type="text/babel">
const msg1 = <h2>Hello React</h2>
{/*
上面的是JSX语法 其是React.createElement的语法糖
JSX 语法是没有办法被浏览器所直接识别的
其需要使用Babel将JSX转换为React.createElement的形式进行再交由浏览器来进行解析和调用
*/}
const msg2 = React.createElement('h2', null, 'Hello React')
ReactDOM.render(msg1, document.getElementById('app'))
ReactDOM.render(msg2, document.getElementById('app1'))
</script>
/*
@param1: type: 当前ReactElement的类型
如果是标签元素,那么就使用字符串表示 “div”
如果是组件元素,那么就直接使用组件的名称 App
@param2: config
所有jsx中的属性都在config中以对象的属性和值的形式存储
@param3: children
存放在标签中的内容(子组件),以children数组的方式进行存储;
*/
React.createElement(type, config, children)
使用Babel官网进行转换
转换前 --- JSX
<main>
<header className="header">我是头部</header>
<section className="section">
<div>我是content</div>
<button>我是按钮</button>
</section>
<footer className="footer">我是footer</footer>
</main>
转换后 --- React.createElement
"use strict";
/*#__PURE__*/
React.createElement("main", null, /*#__PURE__*/React.createElement("header", {
className: "header"
}, "\u6211\u662F\u5934\u90E8"), /*#__PURE__*/React.createElement("section", {
className: "section"
}, /*#__PURE__*/React.createElement("div", null, "\u6211\u662Fcontent"), /*#__PURE__*/React.createElement("button", null, "\u6211\u662F\u6309\u94AE")), /*#__PURE__*/React.createElement("footer", {
className: "footer"
}, "\u6211\u662Ffooter"));
从上述的转换可以看出以下几点:
在使用
Babel对JSX代码进行转换的时候,其会开启严格模式上述的代码最终被转换为了类似于
React.createElement(type, config, child1, child2, child3 ....)的形式每一个
child最后都会被继续遵循React.createElement的形式进行转换
- 在每一个
React.createElement的代码进行转换的过程中,每一个函数之前都有一个/*__PURE__*/;来表示是一个转换后的组件的开始
但是
Babel转换后的形式是React.createElement(type, config, child1, child2, child3 ....)但是
React中createElement定义的方式是React.createElement(type, config, children)其并没有使用
扩展参数, 那么其是怎么获取到所有的子组件的哪?其内部其实使用了
arguments参数,React中定义和使用的方式如下
1.1 ReactElement
在我们调用
React.createElement方法来创建对象的时候,其会返回一个ReactElement类型的对象其上存在当前的组件的类型
type,属性,子组件等相关信息所以
ReactElement类型的对象也就是我们常说的虚拟DOM即
React利用ReactElement对象组成了一个JavaScript的对象树所有的
DOM操作都会在这个vdom树上来进行相应的操作,执行完毕以后在将这棵VDOM树渲染为真实DOM
1.2 React中dom的渲染过程
使用
React.createElement方法来创建ReactElement对象(vdom对象)将
VDOM映射为真实DOM
VDOM只是存在于内存中的一颗DOM树,所以其上的每一个节点,都是和真实DOM一一映射(对应)的

1.3 为什么使用VDOM
在没有VDOM之前
-
需要手动操作dom,还要考虑浏览器兼容性等问题,于是出现了
Jquery -
JQuery等库,简化了dom操作,我们也不需要考虑浏览器兼容性等问题,
-
随着前端项目的复杂,dom操作也变得复杂,我们既要考虑操作数据,也要考虑操作dom,于是出现了
模板引擎但是
模板引擎没有解决跟踪状态变化的问题,就是当数据发生变化后,无法获取上一次的状态,只好把界面上的元素删除,然后在重新创建,jquery写的一个列表,当新增删除排序时,添加一个过渡效果,操作时列表会先被删除,然后重建,耗费性能。模板引擎没有解决跟踪状态变化的问题,所以有了基于虚拟dom的MVVM框架 -
基于
VDOM的mvvm框架帮我们解决可视图和状态的同步问题。也就是当数据发生变化自动更新视图,当视图发生变化,自动更新数据。 例如在
react中,我们只需要使用JSX告诉React,界面需要长成什么样,数据有哪些即可,react会自动帮助我们进行渲染,我们不需要进行任何的DOM操作如果数据发生了改变,我们只需要通过
setState的方式来告诉react界面需要进行修改,其会自动进行界面的修改,不需要我们进行任何的DOM操作
使用VDOM的原因:
- 很难跟踪状态发生的改变:
原有的开发模式,我们很难跟踪到状态发生的改变,不方便针对我们应用程序进行调试; - 操作真实DOM性能较低:传统的开发模式会进行频繁的DOM操作,而这一的做法性能非常的低;
document.createElement本身创建出来的就是一个非常复杂的对象- 频繁的DOM操作会导致页面需要经常的进行页面的
重绘和回流操作 - VDOM是在
内存中进行批量操作的,运行时间和效率比较高 - VDOM中的更新使用了
diff算法,以最为高效的方式去进行DOM的更新 - 虚拟DOM帮助我们从命令式编程转到了声明式编程的模式
举例:
比如我们有一组数组需要渲染:[1, 2, 3, 4,5],界面上显示的格式为

现在要修改为 [1, 2, 3, 4, 5, 6, 7, 8, 9,10]
方式一:重新遍历整个数组(不推荐)
方式二:在ul后面追加另外5个li

上面这段代码的性能怎么样呢?非常低效
-
因为我们通过
document.createElement创建元素,再通 过ul.appendChild(li)渲染到DOM上,进行了多次DOM操作; -
对于批量操作的,最好的办法不是一次次修改DOM,而是对
批量的操作进行合并;(比如可以通过DocumentFragment进行合 并);
而我们正式可以通过 Virtual DOM来帮助我们解决上面的问题;
1.4 VDOM帮助我们从命令式编程转到了声明式编程
React官方的说法:Virtual DOM 是一种编程理念。
- 在这个理念中,UI以一种理想化或者说虚拟化的方式保存在
内存中,并且它是一个相对简单的JavaScript对象 - 我们可以通过
ReactDOM.render让虚拟DOM和真实DOM同步起来,这个过程中叫做协调(Reconciliation); - 这种编程的方式赋予了
React声明式的API:- 你只需要告诉React希望让UI是什么状态;
- React来确保DOM和这些状态是匹配的; 你不需要直接进行DOM操作,可以从手动更改DOM、属性操作、事件处理中解放出来;
二 、React 阶段案例

需求:
- 在界面上以表格的形式渲染一些数据
- 在底部显示表格的总价
- 点击+ 或者 - 号的时候,可以秀爱购买数量,并实时修改总价
- 如果购买数量为0的时候,其就无法在进行减去,-号按钮的交互变得不可变
- 点击移除按钮的时候,对应的行会被移除,总价也会发生相应的改变
- 如果列表数据全部移除后,在界面中显示 No Data
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
table {
border: 1px solid #333;
/*
这是数组特有的样式,作用是将表格的各边框给合并折叠起来
*/
border-collapse: collapse;
width: 80%;
}
th {
background-color: #eee;
}
th,
td {
border: 1px solid #333;
text-align: center;
}
.count {
margin: 0 10px;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="./dist/react.development.js"></script>
<script src="./dist/react-dom.development.js"></script>
<script src="./dist/babel.min.js"></script>
<script type="text/babel">
class App extends React.Component{
constructor() {
super()
this.state = {
books: [
{
id: 1,
title: '《算法导论》',
date: '2006-09',
price: 85,
count: 1
},
{
id: 2,
title: '《UNIX编程艺术》',
date: '2006-02',
price: 59,
count: 1
},
{
id: 3,
title: '《编程珠玑》',
date: '2008-10',
price: 39,
count: 1
},
{
id: 4,
title: '《代码大全》',
date: '2006-03',
price: 128,
count: 1
}
]
}
}
// 渲染函数写在render函数的前面
/*
有数据的时候,渲染table
*/
renderTable() {
const { books } = this.state
return (
<div>
<table>
<thead>
<tr>
<th> </th>
<th>书籍名称</th>
<th>出版日期</th>
<th>价格</th>
<th>购买数量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{
books.map((item, index) => (
<tr>
<td>{ item.id }</td>
<td>{ item.title }</td>
<td>{ item.date }</td>
<td>{ this.formatPrice(item.price) }</td>
<td>
<button disabled={item.count <= 0} onClick={ () => this.changeCount(index, -1) }> - </button>
<span className="count">{ item.count }</span>
{/* 事件中的callback需要使用箭头函数来修正this指向 */}
<button onClick={() => this.changeCount(index, 1) }> + </button>
</td>
<td>
<button onClick={ () => this.remove(item.id) }>移除</button>
</td>
</tr>
))
}
</tbody>
</table>
<p>TotalPrice: <strong> { this.formatPrice(this.getTotalPrice()) } </strong></p>
</div>
)
}
render() {
return (
<div>
{/* 条件判断 */}
{ this.state.books.length > 0 ? this.renderTable() : <h4>Not Data</h4> }
</div>
)
}
// 功能性函数写在render函数后边
remove(id) {
// 注意: 在React中有一个最为重要的概念,即不要去直接修改state中的数据
// 所以在这里没有使用splice对books中的数据进行修改,而是使用filter函数,返回了一个新的数组
this.setState({
books: this.state.books.filter(item => item.id != id)
})
}
changeCount(index, v) {
// 基于State中的数据不可变的特性
// 在这里浅拷贝了一份新的数组,对新的数组上的操作,就不会直接修改原本state上的数组
const newBooks = [... this.state.books]
newBooks[index].count += v
// 使用新的数组地址覆盖旧的数组地址
this.setState({
books: newBooks
})
}
formatPrice(price) {
// parseFloat 是将可以转换为数字的字符串转换为数字(因为只有数字类型的值才有toFixed方法)
// 使用 A || 0 的作用是赋予一个默认值
// 也就是如果price不可以转换为数字(例如 'aaa')那么其parseFloat函数调用后的返回值是NaN
// 为了避免这种情况的发生,我们需要赋予其一个默认值,即为0
return '$' + (parseFloat(price) || 0).toFixed(2)
}
getTotalPrice() {
return this.state.books.reduce((preV, item) => preV + item.price * item.count, 0)
}
}
ReactDOM.render(<App />, document.getElementById('app'))
</script>
</body>
</html>
