React.createElement
jsx 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖,
所有的jsx最终都会通过babel被转换成React.createElement的函数进行调用
<h2 classname="foo">Hello Wolrd</h2>
<!-- 上述JSX代码编译后的结果如下 -->
<!--
参数1 - 当前ReactElement的类型 - type
- 如果是标签元素,那么就使用字符串表示 如“div”
- 如果是组件元素,那么就直接使用组件的名称(也就是组件构造函数) 如 Cpn
参数2 - 配置对象 - config
- 所有jsx中的属性都在config中以对象的属性和值的形式存储
- 其实就是属性组成的对象 - 如果没有属性 可以传递null
参数3 - 子元素 - children
- 存放在标签中的内容,以children数组的方式进行存储
-->
<!--
JSX是React.createElement的语法糖
react解析引擎不可以直接解析JSX,需要先将其转换为React.createElement
所以如果直接使用React.createElement进行编写的时候,可以不使用babel进行编译
-->
React.createElement('h2', {
classname: 'foo'
}, 'Hello World')
React.createElement的源码
// 存储一些特殊的react属性值
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
}
// createElement的部分源码
export function createElement(type, config, children) {
// ...
// props是实际存在内容的对象
const props = {};
// 属性不为null的时候,将属性依次挂载到props上
if (config != null) {
for (propName in config) {
if (
// 如果config自身有propName属性,且propName属性不是特殊的react属性的时候
// 将该属性挂载到props上
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// 从第三个参数开始就是子元素,使用arguments接收所有的实参
// 并将所有的子元素挂载到props.children上
// 也就是说子元素在传递的时候使用 React.createElement(type, config, child1, child2, child3, ...)
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
// const childArray = Array(childrenLength); === const childArray = new Array(childrenLength);
// 在使用Array构造器创建数组的时候,new关键字可以省略
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// 调用ReactElement方法返回一个ReactElement对象,也就是vNode对象
return ReactElement(
type,
props,
// ...
);
}
hasOwnProperty
--- hasOwnProperty()
方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性
const obj = {name: 'Klaus'};
obj.__proto__.age = 23
// hasOwnProperty的参数为对应的属性名
console.log(obj.hasOwnProperty('name')) // => true
console.log(obj.hasOwnProperty('age')) // => false
// 注意: 如果hasOwnProperty方法的参数是一个没有定义的对象,那么脚本并不会报错,而是直接返回false
console.log(obj.hasOwnProperty(name)) // => false
ReactElement
通过 React.createElement方法 最终创建出来一个 ReactElement对象
而ReactElement对象的props.children属性中记录这当前元素的所有子元素
因此ReactElement本质就是vNode,其实也是vDom
React利用ReactElement对象组成了一个JavaScript的对象树,而这个JS对象树就是大名鼎鼎的虚拟DOM(Virtual DOM)
jsx
<div className="container">
<div className="header">
<h1>Hello Wolrd</h1>
</div>
<div className="content">
<p>Lorem ipsum dolor sit amet.</p>
</div>
<div className="footer">
<button>lorem</button>
</div>
</div>
经过编译后,会形成如下vDom对象
React会将jsx编译为在内存中运行的vDOM,随后在通过ReactDOM.render
将vDOM编译成对应的真实DOM
vdom的作用
1. 操作真实DOM性能较低
在JavaScript中,通过React.createElement
创建出来的js对象是一个非常庞大的对象
并且DOM操作会引起浏览器的回流和重绘,所以频繁的DOM操作必然会导致性能的损失
而传统的开发模式不可避免的会需要进行频繁的DOM操作
而vDom本质就是一个js对象,这个对象是对真实DOM在内存中的抽象和模拟
vDom在结构上和真实DOM是一一映射的
而vDOM是存在于内存中的,且vDOM比真实DOM更为的轻量级,操作起来性能也更高
因为vDOM是在内存中运行的,我们可以将所有的更新在内存中统一进行修改后,一次性映射到真实DOM中
这样就避免了频繁的操作DOM,引起的重绘和回流
2.很难跟踪状态发生的改变
传统的开发中,跟踪界面中状态的改变是十分麻烦的,只能通过debugger和console
但是有了vDOM后,React DevTool就可以通过新旧vDOM的对比,进行页面状态的改变的跟踪
3.vDOM便于跨平台
UI在真正渲染之前会先被编译为vdom,
此时就可以根据不同的平台渲染成不同的内容
例如:
在web端,使用web端的ReactDOM.render,将VDOM渲染为真实DOM
在移动端,使用移动端的ReactDOM.render, 将VDOM渲染为原生控件( 如IOS的UIButton,Android的Button)
4 声明式编程
使用了vDOM后,UI以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的JavaScript对象
我们可以通过ReactDOM.render让 虚拟DOM 和 真实DOM同步起来,这个过程中叫做协调(Reconciliation)
因为大部分的DOM操作都交给React来帮助我们进行管理
所以这就意味着我们只需要维护和定义对应的状态即可,React会确保DOM和这些状态是匹配的
我们不需要直接进行DOM操作,可以从手动更改DOM、属性操作、事件处理中解放出来
阶段案例
- 以表格形式显示书籍数据
- 底部显示书籍总价
- 点击+或-可以更改数据数量,如果数量为1的时候,静止减少
- 点击移除按钮可以移除对应数据,如果全部移除,显示购物车为空
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<style>
table {
border-collapse: collapse;
}
th, td {
border: 1px solid #333;
padding: 10px 15px;
}
.count {
padding: 0 10px;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
books: [
{
id: 1,
name: '《算法导论》',
date: '2006-9',
price: 85.00,
count: 1
},
{
id: 2,
name: '《UNIX编程艺术》',
date: '2006-2',
price: 59.00,
count: 1
},
{
id: 3,
name: '《编程珠玑》',
date: '2008-10',
price: 39.00,
count: 1
},
{
id: 4,
name: '《代码大全》',
date: '2006-3',
price: 128.00,
count: 1
},
]
}
}
// 和渲染相关的函数写在render函数的前边
// 渲染相关的函数推荐以render开头
renderTable() {
return (
<table>
<thead>
<tr>
<th></th>
<th>书籍名称</th>
<th>出版日期</th>
<th>价格</th>
<th>购买数量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{ this.state.books.map((book, index) => (
<tr key={book.id}>
<td>{ book.id }</td>
<td>{ book.name }</td>
<td>{ book.date }</td>
<td>{ `¥${ book.price.toFixed(2) }` }</td>
<td>
<button
onClick={() => this.changeCount(-1, index)}
disabled={ book.count <= 1 }
>
-
</button>
<span className="count"> { book.count }</span>
<button onClick={() => this.changeCount(1, index)}>+</button>
</td>
<td>
<button onClick={() => this.deleteBook(index)}>移除</button>
</td>
</tr>
)) }
</tbody>
</table>
)
}
render() {
return <div>
{ this.state.books.length ? this.renderTable() : <h2>购物车为空</h2> }
<p>总价格: { this.calcTotalPrice() }</p>
</div>
}
// 和逻辑相关的函数,推荐写在render函数的后边
changeCount(step, index) {
// React中有一个非常重要的设计原则
// 就是state中的数据的不可变性
// 也就是永远不要主动去修改state中的状态
// 如果需要修改state中的状态,一定要通过setState方法
const books = [...this.state.books]
books[index].count += step
this.setState({
books
})
}
deleteBook(index) {
const books = [...this.state.books]
books.splice(index, 1)
this.setState({
books
})
}
calcTotalPrice() {
return '¥' + this.state.books.reduce((totalPrice, book)
=> totalPrice + book.price * book.count, 0).toFixed(2)
}
}
ReactDOM.render(<App />, document.getElementById('app'))
</script>
</body>
</html>