A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES. 来源
React 就是定义用户界面的 UI 库。有什么特点呢:
- 使用可管理的小型组件构造出一个强大的应用。
- 不需要花费时间寻找 DOM 节点,而是去维护应用的状态。
- 代码更偏向于 声明式。
Hello World
首先需要获取一份 React 库的源代码,或者直接使用网络 CDN,在 <head> 标签内引入。在我们的 Hello World 例子中,我们只引入 react、react-dom 和 babel。babel 用于转译 JSX 语法。第二步,在使用 JSX 的 <script> 标签中标明 type="text/babel",用以确认 babel 转码。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World</title>
</head>
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<body>
<div id="app"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Hello World</h1>,
document.getElementById('app')
);
</script>
</body>
</html>
此外,你还可以通过 React 官方提供的 create-react-app 进行项目构建,它其实就是一个脚手架工具。
JSX 语法
上面的例子中,我们使用了 JSX 语法,这是一项完全独立于 React 的技术。我们需要通过 Babel 进行转译,Babel 可以转译最新的 JavaScript 语言特性,也支持转译 JSX。
有两个网站可以参考,一个是将 JSX 转译成 JavaScript 的网站: babel;另一个将 HTML 转为 JSX 语法: HTML-to-JSX
JSX 的基本语法规则,遇到 < 就以 HTML 规则解析,遇到 花括号,就使用 JavaScript 语法解析。
Babel 将 JSX 向下编译成 React.createElement() 调用。
const element = (
<h1 className="greeting">
Hello World!
</h1>
);
和下面的是一致的。
const element = {
'h1',
{className: 'greeting'},
'Hello World!'
};
JSX 也是表达式
转译之后,JSX 表达式变成普通的 JavaScript 对象。这意味着你可以在 if 语句、for 循环中使用 JSX,可以将它作为参数传递,也可以返回。
function getGreeting(user){
if (user){
return <h1>Hello, {user.name}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
自动分号补全
如果我们将 JSX 表达式进行多行书写,最好加上 (...) 以防止 自动分号补全。
const element = (
<div>
<h1>Hello World!</h1>
</div>
);
闭合标签
从上面的例子可以看到,HTML 中有些标签不强制闭合;但 JSX 中,所有标签都强制闭合。
使用 JSX 确定属性
- 可以直接使用 字符串字面量
const element = <div className="wrap"></div> - 或直接使用花括号包裹 JavaScript 表达式
const element = <img src={user.name}></div>
有这么几点注意:
- 无法使用 class 和 for 属性,因为这两者是 JavaScript 的保留字,使用
className和htmlFor替代。 - CSS 属性名使用 驼峰命名法。
- 在花括号外部不能包含
" ",因为 JSX 会当作字符串字面量处理。 style属性值是一个对象
var styles = {
fontSize: '2em', /*驼峰命名法*/
lineHeight:'1.6'
};
const em = <em style={styles} />; /*传递一个对象*/
/*也可以使用内联模式*/
const em2 = <em style= />
JSX 注释
为了防止 右边的花括号 被注释掉,建议在花括号内使用 /*...*/ 注释代码。
展开运算符
这是个 ES6 的属性,当定义的时候,可以使用便捷方式。
var attr = {
href: 'http://yangdy.com',
target: '_blank'
}
const element = <a {...attr}>Hello</a>
自定义组件需要首字母大写
如果一个元素类型以 小写字母 开始,会被认为是 内建组件,而传递给 React.createElement。如果是类似于 <Foo /> 就会被传递到 React.createElement(Foo)。
import React from 'react';
function HelloWorld() {
return <Hello />
}
ReactDOM.render()
react-dom 包提供应用最顶层的 DOM 方法。其中 ReactDOM.render() 是最常用的。这个方法在给定的 container 将一个 React 元素渲染到 DOM,返回一个 组件的引用(如果是无状态组件,返回 null)。
ReactDOM.render(
element,
container,
[callback]
)
- 如果先前已经有 React 元素渲染到 DOM,会使用新的 React 元素更新。
- 如果提供可选的 callback 函数,会在组件渲染或者更新之后执行。
- 已经存在 container 中的 DOM 元素会第一次被 ReactDOM.render() 渲染的内容取代,之后的调用会采用 React’s DOM diffing algorithms。
- ReactDOM.render() 不会修改 container 节点,只修改它的子节点。
<div id="app">
<p>buji love yuer</p>
</div>
<script type="text/babel">
var e = ReactDOM.render(
<h1> Hello World</h1>,
document.getElementById('app')
);
console.log(e);
</script>
<div>内的内容被 ReactDOM.render() 渲染的内容取代。console.log(e)打印的内容是<h1 data-reactroot=""> Hello World</h1>。
ReactDOM.render() 是渲染 React 元素的方法。上面就是我们渲染一个 Hello World 的例子。
同时,React 元素是 不可变 的。一旦你创建了元素,你就不可以改变它们的子元素(children)和特性(attributes)。
上面这个例子,React 元素指的是,
<h1 data-reactroot=""> Hello World</h1>
<div id="app"></div>
<script type="text/babel">
function tick (){
const element = (
<div>
<h1>buji love yuer</h1>
<p>It is now {new Date().toLocaleTimeString()}.</p>
</div>
);
ReactDOM.render(
element,
document.getElementById('app')
);
}
setInterval(tick,1000);
</script>
我们用上面这个例子来解释一下刚才提到的第三点 - React’s DOM diffing algorithms。这个例子中我们设置了一个 setInterval() 定时器,每隔 1s 调用 tick() 函数。也就是说,每隔一秒我们就会去重新渲染 UI,但是除了第一次会被 ReactDOM.render() 渲染的内容取代,之后的调用只替换其中需要更新的部分 - 也就是时间。这就是 diffing algorithms。算法的实现以后会具体讲。
组件 – Components
组件是什么东西,很好理解,其实就是用来 可重用 的视图。从 React 的角度来讲,组件类似于 JavaScript 的 函数。接收 props 参数,返回一个 React 元素。
简单写法:
function Hello (props) {
return <h1>buji love {props.name}</h1>
}
ES6 语法:
class Hello extends React.Component {
render() {
return <h1>buji love {this.props.name}</h1>
}
}
渲染一个组件
下面是一个例子:
class Hello extends React.Component {
render() {
return <h1>buji love {this.props.name}</h1>
}
}
const element = <Hello name="yuer" />;
ReactDOM.render(
element,
document.getElementById('app')
)
props
组件可以接收属性,所有的属性都可以通过 this.props 对象获取。this.props 是只读的。
组件的生命周期
下面我们要介绍 state。state 是负责组件内部数据的。和 props 正好相反,props 是外部传递给组件的;但 state 完全私有,并且完全由组件控制。
组件渲染自身时用到的数据,当 state 发生变化时,会自动重建用户界面。目的在于,只需要关心数据的变化即可,而不需要关心界面变化了。
state 最好当作 只读属性。可以通过 this.state 取得 state,更新 state 时,需要使用 this.setState() 方法。
需要注意的是,this.props 和 this.state 是异步更新。
下面是一个例子:
<script type="text/babel">
class Clock extends React.Component {
constructor(props){
super(props);
this.state = {date: new Date()};
}
render(){
return (
<div>
<h1>buji love yuer</h1>
<p>It is now {this.state.date.toLocaleTimeString()}</p>
</div>
)
}
componentDidMount () {
this.timerID = setInterval(
() => this.tick(), 1000
);
}
componentWillUnmount () {
clearInterval(this.timerID);
}
tick (){
this.setState({
date: new Date()
});
}
}
ReactDOM.render (
<Clock />,
document.getElementById('app')
)
</script>
- 我们添加了一个 类构造器 给
this.state赋初值(有时候可能会通过 props)。 - 当
<Clock>第一次渲染到 DOM,我们称之为mounting,componentDidMount()方法 - 我们还增加了一个
componentWillUnmount(),这个称之为unmounting,在组件从 DOM 中移除时触发
我们来看一下,这个应用是怎么工作的。
<Clock />传递给ReactDOM.render(),React 调用 Clock 组件的构造器。因为 Clock 需要展示当前时间,所以初始化this.state。- React 然后调用 Clock 组件的
render()方法,然后更新 DOM。 - 当 Clock 插入到 DOM,React 调用
componentDidMount()生命周期钩子(hook)。在函数的内部,每秒调用tick()函数。 - 在
tick()函数内部,通过setState()来更新 UI,React 知道 state 已经改变。然后再次调用render()函数。这时候的this.state.date已经不同了,相对应地 React 更新 DOM。 - 如果 Clock 组件从 DOM 移除了,React 调用
componentWillUnmount()钩子,然后清除定时器。
除了上面提到的两个生命周期钩子,我们还有来总结一下。
componentWillUpdate()当组件再次渲染时,在render()方法前调用(在组件的 props 或 state 发生改变时也会触发该方法)。componentDidUpdate()在 render() 函数执行完毕,且更新的组件已被同步到 DOM 后立即调用。该方法不会在初始化渲染时触发。componentWillMount()在新节点插入 DOM 结构之前触发。componentDidMount()在新节点插入DOM 结构之后触发。componentWillUnmount()在组件从 DOM 中移除时立刻触发。shouldComponentUpdate(newProps, newState)这个方法在 componentWillUpdate() 之前触发,给你一个机会返回 false 以取消更新组件,也就是 render() 方法将不会被调用。
React 事件处理函数
第一,React 使用 合成事件 来消除浏览器之间的不一致情况(其实就是代码封装)。比如,事件对象 e,可以直接使用。阻止浏览器的默认行为,可以显式调用 preventDefault。
第二,React 事件命名使用 camelCase。比如,点击事件是 onClick。
this
在 JavaScript,class 内部的方法并不会默认绑定(this 的绑定丢失情况)。所以,通常会使用 bind() 进行强绑定。有三种方法可以解决:
- 在
constructor()使用bind()进行强制绑定。constructor(props){ super(props); ... this.foo = this.foo.bind(this); } foo() { //... } - 使用 ES7 的新语法,这种规避方法是因为
=>的this指向包裹它的函数。foo = ()=>{ //... } - 这种方法和 2 是一致的,只不过是在回调阶段使用
=>。reder() { return ( <button onClick={(e)=>this.foo(e)}> ) };
Todo-list
最终的效果图如下:

材料:
- 脚手架工具 访问React+Webpack+ES6+JSX 脚手架工具 或者
github主页 - Sass
- Bootstap
组件分割
为了合成一个 <TodoList /> 组件,我们将组件分割成
<TodoForm />,由<input>输入框和<submit>按钮组成<ItmesList />构成 lists
入口文件
入口文件 app.js 引入 ReactDOM.render() 进行渲染。
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './components/TodoList.jsx';
import './css/style.scss';
ReactDOM.render(
<TodoList />,
document.getElementById('app')
);
父组件 TodoList.jsx
//TodoList.jsx
import React from 'react';
import TodoForm from './TodoForm.jsx';
import ItemsList from './ItemsList.jsx';
export default class TodoList extends React.Component {
constructor(props){
super(props);
this.state = {
items:[
'yuer love buji',
'buji love yuer'
]
};
this.addItem = this.addItem.bind(this);
this.deleteItem = this.deleteItem.bind(this);
}
addItem (item) {
let newItems = this.state.items;
newItems.push(item);
this.setState({item: newItems});
}
deleteItem (idx) {
let newItems = this.state.items;
newItems.splice(idx,1);
this.setState({item: newItems});
}
render(){
return (
<div className="main">
<div className="todo-list">
<h1>To-Do <small>List</small></h1>
<TodoForm submitAction={this.addItem} />
<ItemsList items={this.state.items} clickAction={this.deleteItem} />
</div>
</div>
)
}
}
子组件 TodoForm.jsx
//TodoForm.jsx
import React from 'react';
export default class TodoForm extends React.Component {
constructor(props){
super(props);
this.state = {task:''};
this.updateText = this.updateText.bind(this);
this.submitForm = this.submitForm.bind(this);
}
updateText (e){
this.setState({task:e.target.value});
}
submitForm (e){
e.preventDefault();
let item = e.target[0].value;
if(!item) {
alert('Please enter a task');
}else {
this.props.submitAction(item);
this.setState({task: ''});
}
}
render() {
return (
<form onSubmit={this.submitForm} className="todo-form">
<input type="text" className="form-control" placeholder="enter task" onChange={this.updateText} value={this.state.task} />
<input type="submit" className="btn btn-primary self-btn" />
</form>
);
}
}
子组件 ItemsList.jsx
//ItemsList.jsx
import React from 'react';
export default class ItemsList extends React.Component {
render() {
let listItems = this.props.items.map((item,i) => {
return (
<li key={i}>
<div className="text">{item}</div>
<button onClick={this.props.clickAction.bind(this,i)} className="btn btn-danger">Del</button>
</li>
);
});
return <ul>{listItems}</ul>
}
}
敲黑板,父子组件的通信
在我们的上面的父子组件中,涉及到父子组件的通信。组件 <TodoForm /> 每添加一个 todo,就要传回 <TodoList />,再由 <ItemsList /> 显示出来。
思路是这样的:
- 父组件可以向子组件传递 props, props 带有初始化子组件的数据或者回调函数。
- 子组件的 state 发生变化时,在子组件的事件处理函数中,手动触发父函数传递进来的回调函数,同时将子组件的数据传递出去。
也就是说,父子组件的通信是通过 props 和 state 来完成的。
我们仔细看一下父组件的代码:
//TodoList.jsx
render(){
return (
<div className="main">
<div className="todo-list">
<h1>To-Do <small>List</small></h1>
<TodoForm submitAction={this.addItem} />
<ItemsList items={this.state.items} clickAction={this.deleteItem} />
</div>
</div>
)
}
对于,<TodoForm /> 我们传递了一个 submitAction 函数(submitAction 实际上是 addItem 的一个引用)。向 <ItemList /> 传递了一个 items 数组 和一个 clickAction 函数。这样,这些内容都可以被 props 捕获。我们可以用 console.log(props) 验证一下。
在 TodoForm.jsx 加入:
constructor(props){
super(props);
this.state = {task:''};
this.updateText = this.updateText.bind(this);
this.submitForm = this.submitForm.bind(this);
// add this
console.log(props); /*{submitAction: ƒ}*/
}
在 ItemsList.jsx 加入:
//add this
constructor(props){
super(props);
console.log(props);/*{items: Array(2), clickAction: ƒ}*/
}
那么,子组件怎么向父组件通信呢。这里,只存在 <TodoForm /> 向 <TodoList /> 通信,也就是当我们点击提交按钮的时候,<TodoForm /> 应该将保存在自己 state 的内容传回 <TodoList />。
//TodoForm.jsx
submitForm (e){
e.preventDefault();
let item = e.target[0].value;
if(!item) {
alert('Please enter a task');
}else {
this.props.submitAction(item); //通过回调函数传递参数给父组件
this.setState({task: ''});
}
}
敲黑板,处理列表 <li>
使用 map() 函数。
在我们的 <ItemsList /> 是一个基本的 List Component。
//ItemsList.jsx
import React from 'react';
export default class ItemsList extends React.Component {
render() {
let listItems = this.props.items.map((item,i) => {
return (
<li key={i}>
<div className="text">{item}</div>
<button onClick={this.props.clickAction.bind(this,i)} className="btn btn-danger">Del</button>
</li>
);
});
return <ul>{listItems}</ul>
}
}
根据我们前面所解释的,this.props.items 初始状态是 ['yuer love buji', 'buji love yuer']。也就是说,我们对这两个数组元素进行遍历,很简单,正常的 map() 函数是这样的:
let listItems = this.props.items.map((item) =>{//...});
也就是说,这里平白出现的 i 和 key 属性是什么呢。key 是一个特殊的属性,用来帮助 React 辨认哪些项改变了、添加了、删除了。
当然,最好的方法是父组件传递过来一个 object,使用自定义唯一的 id 标识。但是我们如果没有这个 id,你可以像我们的例子中这样使用。
key 必须是 唯一的,唯一的意思指的是同属的 <li> 之间唯一,并不要求 globally 唯一。
敲黑板,受控组件
Controlled component,在 HTML 中,表单元素比如 <input> 、<textarea> 和 <select> 元素会根据用户输入维持并更新自己的状态。但是在 React 中,只能用 setState() 更新。在 <TodoForm /> 中,我们就是这样处理的。
//TodoForm.jsx
import React from 'react';
export default class TodoForm extends React.Component {
constructor(props){
super(props);
this.state = {task:''}; //look here
this.updateText = this.updateText.bind(this);
this.submitForm = this.submitForm.bind(this);
}
updateText (e){
this.setState({task:e.target.value}); //处理改变
}
submitForm (e){
e.preventDefault();
let item = e.target[0].value;
if(!item) {
alert('Please enter a task');
}else {
this.props.submitAction(item);
this.setState({task: ''});
}
}
render() {
return (
<form onSubmit={this.submitForm} className="todo-form">
<input type="text" className="form-control" placeholder="enter task" onChange={this.updateText} value={this.state.task} />
<input type="submit" className="btn btn-primary self-btn" />
</form>
);
}
}
<input> 元素设置了 value 属性,值一直都是 this.state.task。每次按键都会触发 updateText 函数,所以展示的值也会随着用户输入而更新。
引入 Bootstrap
为了非常非常简单,我们是直接在 index.html 引入 bootstrap。
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0-beta/css/bootstrap.min.css">
<title>todo-list</title>
</head>
<body>
<div id="app"></div>
</body>
</html>