本文使用版本 React v16.2.0
React是一个声明式的,高效的,并且灵活的用于构建用户界面的 JavaScript 库
一个最简单的React例子
ReactDom.render(
Hello World
,
document.getElementById('root')
)
ReactDom.render
接受两个参数,第一个是要被插入的内容,第二个是插入到DOM或者说index.html
的位置
如下是一个 React 组件
class ShoppingList extends React.Componnet {
render() {
return (
Shoping List for {this.props.name}
- Instagram
- WhatApp
- Oculus
)
}
}
// Example usage:
在这里,ShoppingList是一个 React组件类,或 React组件类型。组件接受参数,称为属性 props
, 并通过 render
方法返回一个现实的视图层次结构。
render
方法返回您要渲染的内容描述,然后React接受该描述并将其渲染到屏幕上,特别是,render
返回一个React 元素,这是一个渲染内容的轻量级的描述。大多数
React 开发人员使用 JSX
语法,也是上述案例写到的语法。
JSX
语法的转换规则为: 语法在构建是被转换为
React.createElement('div')
。因此,上面的例子等价于:
return React.createElement('div', {className: 'shopping-list'},
React.createElement('h1', /* h1 children ... */),
React.createElement('ul', /* ul children ... */)
);
JSX
它是 Javascript
的一种拓展语法,能够让你的 Javascript
中和正常描述 HTML一样编写 HTML。
你可以用 花括号
将任意 Javascript
表达式嵌入到 JSX
中。例如:表达式 1 + 2
, 变量 user.firstName
, 和函数 formatName(User)
等都可以嵌入使用
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'harper',
lastName: 'Perez'
}
const element = {
Hello, {formatName(user)}!
}
ReactDOM.render (
element,
document.getElementById('root')
)
请注意,为了方便阅读开发者们常将 JSX
分割成多行包裹起来,因为这可以避免分号自动插入的陷阱,如
{ 1
2 } 3
// is transformed to
{ 1
;2 ;} 3;
JSX 也是一个表达式
编译之后, JSX
表达式也就成了一个常规的 javascript
对象
也正因为如此,我们可以在 if
语句或这是 for
循环语句中使用 JSX
,用它给变量赋值,当做参数接受,或者作为函数的返回值
function getGreeting(user) {
if (user) {
return Hello. {formatName(User}
;
}
return Hello, Stranger
}
用 JSX 指定属性值
你可以用花括号嵌入一个 JavaScript 表达式作为属性值
// 用引号形式
const element = ;
// 用表达式,并且表达式用花括号包裹
const element = ;
用 JSX 指定子元素
如果是空标签,可以直接用 />
闭合
const element =
如果包含子标签:
Hello!
Good to see you here.
比起 HTML
,JSX
更接近于Javascript
,所以React DOM
规范使用驼峰(camelCase)属性命名约定,而不是HTML属性名称,当然,html的部分属性名称也作为保留字,不可使用,例如 class
;
因此,class
在 JSX 中 变为 className
, tableindex
变为 tableIndex
。
用 JSX 防止注入攻击
在JSX 中嵌入用户输入是安全的:
const title = response.potentiallyMaliciousInput;
// 这样是安全的
const element = {title}
默认情况下, 在渲染之前, React DOM 会格式化(escapes) JSX中的所有值. 从而保证用户无法注入任何应用之外的代码. 在被渲染之前,所有的数据都被转义成为了字符串处理。 以避免 XSS(跨站脚本) 攻击。
正常情况下,你的 index.html
文件下会有这么一个div
这个root
DOM 节点挂在所有React DOM的位置。正常情况下,对于一个React单页面应用构建,只需要一个单独的根DOM节点即可。但如果要把React整合到现有的APP中,则可能会使用到多个DOM节点。
React利用render
方法将React元素渲染到DOM上,一旦元素被渲染到页面了之后,就不能在修改器子元素或任何元素的属性,就像电影里的一帧,在某以特定的时间点的UI效果,那元素的更新呢?没错,就是重新 render
function tick() {
cosnt element = {
Hello, world
It is {new Date().toLocaleTimeString()}.
};
ReactDom.render (
element,
document.getElementById('root')
)
}
setInterval(tick, 1000);
实际上,大多数 React 应用只会调用一次ReactDom.render(),而实现组件更新的办法就是将代码封装在有状态的组件中。
React 只更新必须更新的部分
这正是 React 的强大之处。React DOM 会将元素及其子元素与之前版本逐一对比,并只对有必要更新的 DOM 进行更新, 以达到 DOM 所需的状态。
开发过程中,更应该每个时间点UI的表现, 而不是关注随着时间不断更新UI的状态, 可以减少很多奇怪的 bug
组件 components
和属性 props
,其中,属性是单词 property
的代码简写。
定义组件的两种办法
定义组件有两种方式
Javascript
函数function Welcome(props) {
return Hello, props.name
}
这就是一个有效的组件,它接首了一个 props
参数,并返回了一个React元素,这是一个函数式组件,表面上看,他就是一个 Javascript
函数。
类组件的定义则依赖ES6 的 class
来定义,下面这种定义方法和上方是等效的;
class Welcome extends React.Component {
render() {
return Hello, {this.props.name}
;
}
}
渲染一个组件
// DOM标签作为组件
const element = ;
// React 元素作为组件
const element = ;
当React 遇到一个代表用户定义组件的元素时,它将 JSX
属性以一个单独对象即props对象
的形式传递给相应的组件,例如
function Welcome(props) {
return Hello, {props.mname]
;
}
const element = ;
ReactDOM.render(
element,
document.getElementById('root')
)
【理解】
ReactDOM.render()
方法并向其传入了
元素Welcome
组件,并向其传入了 {name: ‘Sara’}
作为 props对象
Welcome
组件返回 Hello, Sara
Hello, Sara
组件名称总是以大写字母开始, 如本例子中
, 而不是
构成组件
既然组件是单独的一个React元素,那他能单独工作,因此我们能在一个React 元素中多次引用到相同的组件, 举个例子:
function Welcome(props) {
return Hello, {props.name}
}
function App() {
return (
)
}
ReactDOM.render(
,
document.getElementBuId('root')
)
通常情况下, React apps 都有一个单独的顶层的 App
组件。如果是在已有的应用中整合React,也需要由下至上的从小的组件开始逐步整合到视图顶层的组件中。
组件必须返回一个单独的根元素,这就是为什么我们要添加一个
来包裹所有的元素的原因
提取组件
对于一个React 元素,如果其中含有可复用或可能会重复使用的内容,不要害怕把它单拿出来多个更小的组件。
提取组件可能看起来是一个繁琐的工作,但是在大型的
App
中可以回报给我们的是大量的可复用组件。一个好的经验准则是如果你 UI 的一部分需要用多次 (Button
,Panel
,Avatar
),或者本身足够复杂(App
,FeedStory
,Comment
),最好的做法是使其成为可复用组件。Props 是只读的
无论你用函数或类的方法来声明组件,
虽然 React 很灵活,但是它有一条严格的规则:**所有 React 组件都必须是纯函数,并禁止修改其自身 props **。所谓的纯函数就是:传入函数参数不会在函数执行过程中发生改变,比如自增操作
a++
。如果
props
是只读的,那传递给子元素(子组件)的参数岂不是不能修改了?那子元素如何与父元素做交互呢?React还给我们提供了状态属性state
供我们在子组件内部修改值状态和生命周期
状态
state
, 生命周期liftcircle
.
之前说过,一旦元素被渲染了之后就不可改变了,但我们可以通过重新渲染的方法使页面得以刷新,同样我们提到过最常用的方法是编写一个可复用的具有状态的组件,这里的状态,就是我们将要说的state
我们对上述提过的计时器
tick
中的计时功能封装成一个函数式组件如下:function Clock(props) { return (
) }Hello, world!
It is {props.date.toLocaleTimeString()}.
然后把他当做一个元素放入
tick
中进行渲染function tick() { ReactDOM.render(
, document.getElementById('root') ) } setInterval(tick, 1000); 在这个例子中,我们将计时功能代码封装成了一个独立的可复用的组件,并通过属性date的方式将参数传入,但还不能到达我们想要的结果,那就是不能再组件内部修改参数值,组件中显示的数据依旧受控于父组件中
date
属性传递过来的值,那如果我们把这个date
属性也添加到Clock
内部呢?来看下ReactDOM.render(
, document.getElementById('root') ) 这时父组件中只保留了对计时组件
Clock
的一个单纯的引用。剩下的事情全部依托以组件Clock
自己去实现。要怎么实现这个需求?这里React提出了另一个数据对象,即state
,它用来保存组件内部的数据,与props
类似,不同的是state
是组件私有的,并且由组件本身完全控制。它能实现数据在组件内部的修改和更新。怎么使用这个state
?继续往下讲之前我们先拓展一个知识我们知道组件有两种定义方式,即函数式组件和类组件,虽然函数式组件更加简洁更加接近原生
javascript
,但类组件却拥有一些额外的属性,这个类组件专有特性,就是状态和生命周期钩子,到这里也能清楚知道状态的关键作用,然而函数式组件没有这两个特性,因此,在需要使用到状态state
情况下,我们需要将函数式组件转换成类组件函数式组件转化成类组件
尝试把一个函数式组件转化成类组件,官网给出了以下步骤,以
Clock
组件为例
- 创建一个继承自
React.Component
类的ES6 class
同名类- 添加一个名为
render()
的空方法- 把原函数中的所有内容移至
render()
中- 在
render()
方法中使用this.props
替代props
- 删除保留的空函数声明
class Clock extents React.Component { render() { return (
) } }Hello, world
It is {this.props.date.toLocaleTimeString()}.
到此,
Clock
组件已经成功被我们修改成了一个类组件,我们便可以在其中添加本地状态state
和生命周期钩子class Clock extends React.Component { // 用类构造函数constructor初始化 this.state constructor(props) { // 使用super()将props传递给基础构造函数 super(props); this.state = {date: new Date()}; } render() { return (
) } }Hello, world
It is {this.state.date.toLocaleTimeString()}.
这样,我们的类组件
Clock
就拥有了自己的属性this.state.date
,也就不需要引用组件向其传递值了,因此,我么可以把组件引用中的date
属性删掉,最终,我们将其渲染到DOM上,只使用组件引用,其他都交给组件Clock
自己去实现ReactDOM.render(
, document.getElementById('root') ) 到这里就结束了?细心的你会发现,组件
Clock
只是实现了当前时间的显示,而我们要改装的功能是一个计时器,计时功能去哪里了?没实现啊?我们需要在组件Clock
中找到一个合适的时机去实现这个功能,为此,React团队引入了 声明周期方法,也叫生命周期钩子在类组件中添加生命周期方法
在一个具有许多组件的应用程序中,在组件被销毁时释放所占用的资源是非常重要的。就像浏览器的垃圾回收机制,近期内不需要再用的资源,应该及时清除。
当
Clock
第一次渲染到DOM时,我们要设置一个定时器 。 这在 React 中称为 “挂载(mounting)” 。它有一个生命钩子componentDidMount()
当
Clock
产生的 DOM 被销毁时,我们也想清除该计时器。 这在 React 中称为 “卸载(unmounting)” 。它的生命钩子是componentWillUnmount()
我们的计时器是在页面加载之后,页面生成初始化状态,然后由计时器去触发状态的刷新,因此,在挂载完成是去设置计时器是个非常不错的选择
componentDidMount() { this.timerID = setInterval( () => this.tick(), 1000 ) }
这样我们就实现了组件计时功能,或许你注意到了,在该例中,我们把
timerID
存放在this
中而不是this.state
中。其实,
this.props
和this.state
也是数据对象与普通对象一样用来存放数据,只是他们被React团队赋予了新的职能,this.props
由React本身设定,用来存放在组件引用时的属性键值对对象集,不允许Coder们自己去修改;而this.state
也具有特殊的含义,即存放组件本身的、用于视觉输出的数据,但也不是说在编写React程序的时候就必须用用这两个,我们依然可以自己定义普通的数据结构。既然
state
是用于存放组件视觉输出的数据,那在render()
方法中没有被引用的,就不应该出现在state
中了。养成良好的编码习惯,编写好计时器时,及时的编写卸载事件。卸载时我们清除的数据也是从
this
中拿的。componentWillUnmount() { clearInterval(this.timerID); }
挂载时我们声明了一个
tick()
方法,接下来我们就要实现这个方法,是用来触发UI更新。嗯哼?UI更新?我们的页面状态state
不是已经更新了吗?为啥还要UI更新?这里有一个非常重要的方法:
setState()
。我们先把代码补充完整再说明componentDidMount() { // ... } tick() { this.setState({ date: new Date() }) } componentWillUnmount() { // ... }
setState()
是React触发页面更新的第二个办法,第一个办法开篇即说过,即render()
方法。setState
作用就是通知React检查带状态的组件中是否含有脏值。此时react会生成一个虚拟DOM与之前的版本进行对比,只有有必要更新时才会更新。关于 state 与 setState过程 在我的另一篇文章中有详细说明,有兴趣的可以翻过去看看。为什么不把
tick()
方法写到componentDidMount()
中?因为tick()
只是一个普通方法,他不需要在生命周期中触发,也不用自动触发。只要谁调用了触发即可。因此不需要也不能放在生命周期钩子函数中。现在这个时钟每秒都会走了。整理一下,我们整个计时器代码如下:
class Clock extends React.Component { constructor(props) { super(props); this.state = {date: new Date()}; } componentDidMount() { this.timerID = setInterval( () => this.tick(), 1000 ); } componentWillUnmount() { clearInterval(this.timerID); } tick() { this.setState({ date: new Date() }); } render() { return (
); } } ReactDOM.render(Hello, world!
It is {this.state.date.toLocaleTimeString()}.
, document.getElementById('root') ); 整个流程的执行过程是这样的:
- 当
被传入
ReactDOM.render()
时, React 会调用Clock
组件的构造函数。 因为Clock
要显示的是当前时间,所以它将使用包含当前时间的对象来初始化this.state
。我们稍后会更新此状态。- 然后 React 调用了
Clock
组件的render()
方法。 React 从该方法返回内容中得到要显示在屏幕上的内容。然后,React 然后更新 DOM 以匹配Clock
的渲染输出。- 当
Clock
输出被插入到 DOM 中时,React 调用componentDidMount()
生命周期钩子。在该方法中,Clock
组件请求浏览器设置一个定时器来一次调用tick()
。- 浏览器会每隔一秒调用一次
tick()
方法。在该方法中,Clock
组件通过setState()
方法并传递一个包含当前时间的对象来安排一个 UI 的更新。通过setState()
, React 得知了组件state
(状态)的变化, 随即再次调用render()
方法,获取了当前应该显示的内容。 这次,render()
方法中的this.state.date
的值已经发生了改变, 从而,其输出的内容也随之改变。React 于是据此对 DOM 进行更新。- 如果通过其他操作将
Clock
组件从 DOM 中移除了, React 会调用componentWillUnmount()
生命周期钩子, 所以计时器也会被停止。正确的使用State(状态)
对于
setState()
有三件事情是我们应该要知道的(1)不要直接修改state
真正触发React对比不同版本的虚拟DOM是setState()
方法,直接修改state
页面不会刷新,这一点与原生javascript
区别较大,需要理解。// 这么做不会触发React更新页面 this.state.comment = 'hello'; // 使用 setState() 代替 this.setState({ comment: 'hello' });
【注意】在组件中,唯一可以初始化分配
this.state
的地方就是构造函数constructor(){}
(2)state(状态)更新可能是异步的
React为了优化性能,有可能会将多个setState()
调用合并为一次更新。这就导致this.props
和this.state
可能是异步更新的,你不能依赖他们的值计算下一个state(状态)
// counter 计数更新会失败 this.setState({ counter: this.state.counter this.props.increment })
如果我们有这种需求,可以使用以下
setState()
办法:// ES6 箭头函数法 this.setState((prevState, props) => ({ counter: prevState.counter + props.increment })); // 常规函数法 this.setState(function(prevState, props) { return { counter: prevState.counter + props.increment }; })
(3)state(状态)更新会被合并
当你调用setState()
, React将合并你提供的对象到当前状态中。例如,你的状态可能包含几个独立的变量,然后你用几个独立的setState方法去调用更新,如下constructor(props) { super(props); this.state = { posts: [], comments: [] }; } componentDidMount() { fetchPosts().then(response => { this.setState({ posts: response.posts }); }); fetchComments().then(response => { this.setState({ comments: response.comments }); }); }
合并是浅合并,所以,
this.setState({comments})
在合并过程中不会改变this.state.posts
的值,但是会完全替换this.state.comments
的值数据向下流动
无论是作为父组件还是子组件,它都无法或者一个组件是否有状体,同时也不需要关心另一个组件是定义为函数组件还是类组件。这就是为什么
state
经常被称为 本地状态 或 封装状态 的原因, 他不能被拥有并设置它的组件以外的任何组件访问。那如果需要访问怎么处理?
(1)作为其子组件的props(属性)// 在组件中使用
It is {this.state.date.toLocaleTimeString()}
// 传递给子组件作为props虽然
FormattedDate
组件通过props
接收了date
的值,但它仍然不能获知该值是来自于Clock
的state
, 还是Clock
的props
, 或者一个手动创建的变量.这种数据关系,一般称为”从上到下”或”单向”的数据流。任何
state(状态)
始终由某个特定组件所有,并且从该state
导出的任何数据 或 UI 只能影响树”下方”的组件如果把组件树想像为
props(属性)
的瀑布,所有组件的state(状态)
就如同一个额外的水源汇入主流,且只能随着主流的方向向下流动。各组件完全独立
借用上文的
Clock
组件,我们创建一个App
组件,并在其中渲染三个Clock
:
function App() { return ( // 之前说过组件只能返回一个根节点,所以用
包起来) } } reactDOM.render(); } ReactDOM.render(, document.getElementById('root') ); 每个
Clock
都设立它自己的计时器并独立更新,如果App
中有一个数据变量,也能被三个Clock
相互独立修改。至于何时使用有状态组件,何时使用无状态组件,被认为是组件的一个实现细节,取决于你当时的需求,你可以在有状态组件中使用无状态组件,也可以在无状态组件中使用有状态组件
事件处理
通过 React 元素处理事件跟在 DOM 元素上处理事件非常相似。但是有一些语法上的区别:
- React 事件使用驼峰命名,而不是全部小写
- 通过 JSX , 传递一个函数作为事件处理程序,而不是一个字符串
// html usage // React usage
在React中不能通过返回
false
来阻止默认行为。必须明确的调用preventDefault
// html usage Click me
// React usage class ActionLink extends React.Component { function handleClick(e) { e.preventDefault(); console.log('clicked.'); } return ( Click me ) }
在这里,React团队帮Coder们实现了e事件的跨浏览器兼容问题。当使用React时,我们也不需要调用
addEventListener
在DOM 元素被创建后添加事件监听器。相反,只要当元素被初始渲染的时候提供一个监听器就可以了。当使用ES6类定义一个组件时,通常的一个事件处理程序就是类上的一个方法,看个例子,
Toggle
组件渲染一个按钮,让用户在 “ON” 和 “OFF” 状态之间切换:class Toggle extends React.Component { constructor(props) { super(props); this.state = {isToggleOn: true}; // 这个绑定是必要的,使`this`在回调中起作用 this.handleClick = this.handleClick.bind(this); } handleClick() { this.setState(prevState => ({ isToggleOn: !prevState.isToggleOn })); } render() { return ( ); } } ReactDOM.render(
, document.getElementById('root') ); 绑定类方法
在JSX回调中你必须注意
this
的指向。 在 JavaScript 中,类方法默认没有 绑定 的。如果你忘记绑定this.handleClick
并将其传递给onClick
,那么在直接调用该函数时,this
会是undefined
。这不是 React 特有的行为;这是
JavaScript
中的函数如何工作的一部分,可以使用属性初始值设置来正确地 绑定(bind
) 回调,但这是实验性做法,不建议使用,以后有可能会废弃,如果你没有使用属性初始化语法
(1)可以在回调中使用一个arrow functions
class LoginButton extends React.Component { handleClick() { console.log('this is: ', this) } render() { // 这个语法确保 `this` 被绑定在 handleClick 中 return ( ); } }
(2)使用
Function.prototype.bind
方法,相对简洁方便递参数给事件处理程序
在循环内部,通常需要将一个额外的参数传递给事件处理程序,常用的有一下两种方案;
上面两个例子中,参数
e
作为 React 事件对象将会被作为第二个参数进行传递。通过箭头函数的方式,事件对象必须显式的进行传递,但是通过bind
的方式,事件对象以及更多的参数将会被隐式的进行传递。条件渲染
在 React 中,你可以创建不同的组件封装你所需要的行为。然后,只渲染它们之中的一些,取决于你的应用的状态。
整个组件的条件渲染
React 中的条件渲染就可在JS中的条件语句一样,使用JS操作符如
if
或者条件控制符来创建渲染当前的元素,并且让React更新匹配的UI。比如我们有一个需求,需要判断用户是否登录,来显示不同组件function UserGreeting(props) { return
Welcome back!
} function GustGrreeting(props) { returnPlease sign up.
}function Greeting(props) { const isLoggedIn = props.isLoggedIn; if (isLoggedIn) { return
} return } ReactDOM.render( , document.getElementById('root') ); 使用元素变量条件渲染部分内容
你可以用变量来存储元素。这可以帮助您有条件地渲染组件的一部分,而输出的其余部分不会更改。下方两个组件用于显示登出和登入按钮
function LoginButton() { return( ) } function LogoutButton(props) { return ( ) }
登入登出按钮已做好,接下来需要实现有切换功能的一个有状态的组件,为了更系统化学习,我们把前面的
Greeting
组件一起加进来class LoginControl extends React.Component { constructor(props) { super(props); this.state = { isLoginedIn: false } } handleLoginClick() { this.setState({ isLoggedIn: true }); } handleLogoutClick() { this.setState({ isLoggedIn: false }); } render() { const isLoggedIn = this.state.isLoggedIn; let button = null; if (isLoggedIn) { button =
} else { button = } return ( {button} , document.getElementById('root') ) 使用
if
是很常见的一种做法,当然也有一些更简短的语。JSX
中有几种内联条件的方法,(1)使用逻辑与&&操作符的内联if用法
我们可以在JSX
中嵌入任何表达式,方法是将其包裹在花括号中,同样适用于JS的逻辑与&&运算符function Mailbox(props) { const unreadMessages = props.unreadMessages; return (
) } cosnt message = ['React', 'Re: React', 'Re:Re: React']; ReactDOM.render(Hello!
{ unreadMeaasges.length > 0 &&You have {unreadMessages.length} unread messages. }
, document.getElementById('root') ); 该案例是可以正常运行的,因为在
JavaScript
中,true && expression
总是会评估为expression
,而false && expression
总是执行为false
。并且我们可以在表达式中嵌入表达式(2)使用条件操作符的内联If-Else
条件操作符 即三目表达式:condition ? trueExpression : falseExpression
// 条件渲染字符串
The user is {isLoggedIn ? 'currently' : 'not'} logged in.// 条件渲染组件{isLoggedIn ? () : ( )} 总之,遵循一个原则,哪种方式易于阅读,就选择哪种写法。并且,但条件变得越来越复杂时,可能是提取组件的好时机。
阻止组件渲染
在极少数情况下,您可能希望组件隐藏自身,即使它是由另一个组件渲染的。为此,返回
null
而不是其渲染输出。注意这里是不渲染,不是不显示。在下面的例子中,根据名为
warn
的props
值,呈现。如果
props
值为false
,则该组件不渲染:function WarningBanner(props) { if (props.warn) { return null; } return (
Warning) } class Page extends React.Component { constructor(props) { super(props); this.state = { showWarning: true } } handleToggleClick() { this.setState(prevState => ({ showWarning: !prevState.showWarning })); } render() { return () } } ReactDOM.render(, document.getElementById('root') ) 从组件的
render
方法返回null
不会影响组件生命周期方法的触发。 例如,componentWillUpdate
和componentDidUpdate
仍将被调用。因此需要组件刚载入时就要判断执行返回null