官方介绍
- React:React 是一个用于构建用户界面的 JavaScript 库。
- Vue: Vue是一个渐进式JavaScript 框架
Hello World
React
ReactDOM.render(
<h1>Hello, World</h1>,
document.getElementById('root')
)
Vue
new Vue({
el: '#root',
render(h) {
return h('h1', {}, 'Hello, World')
}
})
JSX 与 Template
Jsx与Template分别是React与Vue处理模板语法的方式。
1. JSX
jsx是一个JavaScript的语法扩展。
在JSX中嵌入表达式
const name = 'Josh Perez'
const element = <h1>Hello, {name}</h1>
RenderDOM.render(
element,
document.getElementById('root')
)
对比Template:
const element = `
<h1>Hello, {{name}}</h1>
`
new Vue({
el: '#root',
template: element,
data() {
return {
name: 'Josh Perrez'
}
}
})
在JSX语法中,你可以在大括号内放置任何有效的JavaScript表达式。例如, 2+ 2,user.firstName或者formatName(user)都是有效的JavaScript表达式。
在Vue中使用双花括号({{}})来放置任何有效的JavaScript表达式。例如, 2+ 2,user.firstName或者formatName(user)都是有效的JavaScript表达式。
在下面的示例中,我们将调用JavaScript函数formatName(user)的结果,并将结果嵌入到<h1>元素中
function formatName(user) {
return user.firstName + ' ' + user.lastName
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
}
const element = (
<h1>
Hello, {formatName(user)}
</h1>
)
ReactDOM.render(
element,
document.getElementById('root')
)
对比template:
const element = `
<h1>
Hello, {{formatName(user)}}
</h1>
`
new Vue({
el: '#root',
template: element,
data() {
return {
user: {
firstName: 'Harper',
lastName: 'Perez'
}
}
},
methods: {
formatName(user) {
return user.firstName + ' ' + user.lastName
}
}
})
JSX也是一个表达式
在编译之后,JSX表达式会被转为普通JavaScript函数调用,并且对其取值后得到JavaScript对象。
也就是说,你可以在if语句和for循环的代码块中使用JSX,将JSX赋值给变量,把JSX当作参数传入,以及从函数中返回JSX。
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>
}
return <h1>Hello, Stranger.</h1>
}
在Vue中更多是通过指令来实现类似的功能,如v-if, v-show
JSX特定属性
你可以通过引号,来将属性值指定为字符串变量
const element = <div tabIndex="0"></div>
也可以使用大括号,来在属性值插入一个JavaScript表达式:
const element = <img src={user.avatarUrl}></img>
在属性中嵌入JavaScript表达式时,不要在大括号外面加上引号。你应该仅使用引号或者大括号中的一个,对于同一属性不能同时使用两种符号。
在Vue中的Template语法中,绑定动态属性是通过v-bind指令来实现:
v-bind:src 也可以简写成:src
const element = `
<img :src="user.avatarUrl"></img>
`
JSX表示对象
babel会把JSX转译成一个名为React.createElement()函数调用。
以下两种代码示例完全等效:
const element = (
<h1 className="greeting">
Hello, World
</h1>
)
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, World'
)
React.createElement()会预先执行一些检查,以帮助你编写无错误的代码,但实际上它创建了一个这样的对象:
// 注意:这是简化过的结构
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, World'
}
}
这些对象被称为“React元素”。
元素渲染
元素是构成React应用的最小砖块。
Vue当中没有元素这个概念,但类似的是以组件来代替,其目的都是构建应用的单元。
元素描述了你在屏幕上想看到的内容。
const element = <h1>Hello, World</h1>
与浏览器的DOM元素不同,React元素是创建开销极小的普通对象。ReactDOM会负责更新DOM来与React元素保持一致。
将一个元素渲染为DOM
假设你的HTML文件某处有一个<div>:
<div id="root"></div>
我们将其称为根DOM节点,因为该节点内的所有内容都将同ReactDOM管理。
仅使用React构建的应用通常只有单一的根DOM节点。如果你在将React集成进一个已有应用,那么你可以在应用中包含任意多的独立要DOM节点。
想要将一个React元素渲染到根DOM节点中,只需要把它们一起传入ReactDOM.render():
const element = <h1>Hello,world</h1>
ReactDOM.render(element, document.getElementById('root'))
更新已渲染的元素
React元素是不可变对象,一旦被创建,你就无法更改它的子元素或者属性。一个元素就像电影的单帧:它代表了某个特定时刻的UI。
根据我们已有的知识,更新UI唯一的方式是创建一个全新的元素,并将其传入ReactDOM.render()
考虑一个计时器的例子:
function tick() {
const now = new Date().toLocalTimeString()
const element = <h1>现在时间是:{now}</h1>
ReactDOM.render(element, document.getElementById('root'))
}
setInterval(tick, 1000)
而使用Vue来实现,这可能是这样的:
const template = `
<h1>现在时间是:{{time}}</h1>
`
new Vue({
el: '#root',
template,
data() {
return {
time: null
}
},
created() {
this.now()
},
methods: {
now() {
setInterval(() => {
this.time = new Date().toLocaleTimeString()
}, 1000)
}
}
})
这个例子会在setInterval()回调函数,每秒都调用ReactDOM.render()。
React只更新它需要更新的部分
React DOM会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使DOM达到预期的状态。
在这里React只更新它需要更新的部分,而Vue是更新它需要更新的标签。
React只更新部分文字内容如下图所示:
Vue更新是包含文字内容所在的整个标签如下图所示:
组件 & Props
组件允许你将UI拆分为独立可复用的代码片段,并对每个片段进行独立构思。
组件,从概念上类似于JavaScript函数。它接受任意的入参(即props),并返回用于描述页面展示内容的React元素。
函数组件与class组件
定义组件最简单的方式就是编写JavaScript函数:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>
}
该函数是一个有效的React组件,因为它接收唯一带有数据的“props”对象与并返回一个React元素。这类组件被称为函数组件,因为它本质上就是JavaScript函数。
你同时还可以使用ES6的class来定义组件:
class Welcome extends React.Component{
render() {
return <h1>Hello, {this.props.name}</h1>
}
}
上述两个组件在React里是等效的。在Vue当中也有函数组件跟类组件。
函数组件有一个functional的关键字,来表明这是一个函数组件,同时函数组件,没有生命周期,所有传值,都是通过props来传递
渲染组件
之前,我们遇到的React元素都只是DOM标签:
const element = <div />
不过,React元素也可以是用户自定义的组件:
const element = <Welcome name ="Sara">
当React元素为用户自定义组件时,它会将JSX所接收的属性(attributes)转换为单个对象传递给组件,这个对象被称之为props
function Welcome(props) {
return <h1>Hello, {props.name}</h1>
}
const element = <Welcome name= "Sara">
ReactDOM.render(element, document.getElementById('root'))
Vue在渲染组件时,与React比较大的不同是Vue组件需要先声明后使用,而React则可以直接使用,React把组件也被看作是一个React元素,可以直接通过render函数渲染。
用Vue实现上面的例子:
const Welcome = {
props: {
name: {
type: String,
default: null
}
},
template: `
<h1>Hello, {{name}}
`
}
const template = `<Welcome name="Sara" />`
new Vue({
el: '#root',
template,
components: {Welcome}
})
注意: 组件名称必须以大写字母开头。
组合组件
组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。按钮,表单,对话框,甚至整个屏幕的内容:在React应用程序中,这些通常都会以组件的形式表示。
例如,我们可以创建一个可以多次渲染WelCome组件的App组件:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>
}
function App() {
return (
<div>
<Welcome name="Sara"/>
<Welcome name="Cahal"/>
<Welcome name="Edite"/>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById('root'))
而使用Vue,则是下面这样:
const Welcome = {
props: {
name: {
type: String,
default: null
}
},
template: `<h1>Hello, {{name}}</h1>`
}
const App = {
components: {
Welcome
},
template: `
<div>
<Welcome name="Sara"/>
<Welcome name="Charle"/>
<Welcome name="Hole"/>
</div>
`
}
new Vue({
el: '#root',
render: h => h(App)
})
提取组件
将组件拆分为更小的组件。
例如,参考如下Comment组件:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
首先我们提取Avatar组件:
function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
)
}
Avatar不需要知道它在Comment组件内部是如何渲染的。因此,我们给它的props起了一个更通用的名字:user,而不是author
我们建议从组件自身的角度命名props,而不是依赖于调用组件的上下文命名。
我们现在针对Comment做一些小调整:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author}/>
</div>
<div clssName="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
)
}
接下来,我们将提取UserInfo组件,该组件在用户名旁渲染Avatar组件:
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user}/>
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
)
}
进一步简化Comment组件:
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author}/>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{props.date}
</div>
</div>
)
}
Props的只读性
组件无论是使用函数声明还是通过class声明,都决不能修改自身的props,来看下这个sum函数:
function sum(a, b) {
return a + b
}
这样的函数被称为纯函数,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。
相反,下面这个函数则不是纯函数,因为它更改了自己的入参:
function widthdraw(account, amount) {
account.total -= amount
}
React非常灵活,但它也有一个严格的规则:
所有React组件都必须像纯函数一样保护它们的props不被更改。
State & 生命周期
请参考前面时钟的例子,在元素渲染中,我们只了解了一种更新UI界面的方法。通过调用ReactDOM.render()来修改我们想渲染的元素:
function tick() {
const element = (
<div>
<h1>Hello,World</h1>
<h2>It is {new Date().toLocaleTimeString()}</h2>
</div>
)
ReactDOM.render(element, document.getElementById('root'))
}
setInterval(tick, 1000)
改写成组件的形式:
function Clock(props) {
return (
<div>
<h1>Hello World</h1>
<h2>It is {props.date.toLocaleTimeString()}</h2>
</div>
)
}
function tick() {
ReactDOM.render(<Clock date={new Date()}/>, document.getElementById('root'))
}
setInterval(tick, 1000)
然而,它忽略了一个关键的技术细节:Clock组件需要设置一个计时器,并且需要每秒更新UI。
理想情况下,我们希望只编写一次代码,便可以让Clock组件自我更新:
ReactDOM.render(<Clock/>, document.getElementById('root'))
我们需要在Clock组件中添加state来实现这个功能。
state与props类似,但是state是私有的,并且完全受控于当前组件。
将函数组件转换成class组件
通过以下五步将Clock的函数组件转换成class组件
- 1.创建一个同名的ES6 class,并且继承于React.Component。
- 2.添加一个空的render()方法
- 3.将函数体移到render()方法之中。
- 4.在render()方法中使用this.props替换props
- 5.删除剩余的空函数声明
Class Clock extends React.Component {
render(){
return (
<div>
<h1>Hello, world</h1>
<h2>It is {this.props.date.toLocaleTimeString()}</h2>
</div>
)
}
}
用state替换props
class Clock extends React.Component {
constructor(props) {
super(props)
this.state = {
date: new Date()
}
}
render() {
return (
<div>
<h1>Hello, World</h1>
<h2>It is {this.state.date.toLocaleTimeString()}</h2>
</div>
)
}
}
ReactDOM.render(<Clock />, document.getElementById('root'))
将生命周期方法添加到Class中
在具有许多组件的应用程序中,当组件被销毁时释放所占用的资源是非常重要的。
当Clock组件第一次被渲染到DOM的时候,就为其设置一个计时器。这在React中被称为挂载(mount)
同时,当DOM中Clock组件被删除的时候,应该清除计时器。这在React中被称为卸载(unmount)
我们可以为class组件声明一些特殊的方法,当组件挂载或者卸载时就会去执行这些方法:
class Clock extends React.Component {
constructor(props) {
super(props)
this.state = {
date: new Date()
}
}
componentDidMount() {
}
componentWillUnmount() {
}
render() {
return (
<div>
<h1>Hello, World</h1>
<h2>It is {this.state.date.toLocaleTimeString()}</h2>
</div>
)
}
}
ReactDOM.render(<Clock />, document.getElementById('root'))
这些方法叫做生命周期方法。
componentDidMount()方法会在组件已经被渲染到DOM中后运行,所在最好在这里设置定时器:
class Clock extends React.Component {
constructor(props) {
super(props)
this.state = {
date: new Date(),
timerId : null
}
}
componentDidMount() {
this.timerId = setInterval(() => this.tick(), 1000)
}
componentWillUnmount() {
clearInterval(this.timerId)
}
tick() {
this.setState({
date: new Date()
})
}
render() {
return (
<div>
<h1>Hello, World</h1>
<h2>It is {this.state.date.toLocaleTimeString()}</h2>
</div>
)
}
}
ReactDOM.render(<Clock />, document.getElementById('root'))
在Vue中也有生命周期,而且比React多,常见的有beforeCreated, created, beforeMounted, mounted, beforeUpdated, updated, beforeDestoryed, destroyed
正确地使用State
关于setState()你应该了解三件事:
不要直接修改State
this.state.comment = 'Hello' // 这是不正确的
而是应该使用setState()
this.setState({
comment: 'Hello'
})
构造函数是唯一可以给this.state赋值的地方
在vue中,也有一个类似state的东西叫data,用来存放组件的私有数据,而在Vue中,没有setState()方法,要修改data中的数据,直接就是:
this.comment = 'Hello'
State的更新可能是异步的
出于性能考虑,React可能会把多个setState()调用合并成一个调用。
因为this.props和this.state可能会异步更新,所以不要依赖他们的值来更新下一个状态。
例如,此代码可能会无法更新计数器
this.setState({
counter: this.state.counter + this.props.increment
})
要解决这个问题,可以让setState()接收一个函数而不是一个对象。这个函数用上一个state作为第一个参数,将此次更新应用时的props做为第二个参数:
this.setState((state, props) => {counter: state.counter + props.increment})
State的更新会被合并
当你调用setState()的时候,React会把你提供的对象合并到当前的state.
例如,你的state包含几个独立的变量:
contructor(props) {
super(props)
this.state = {
posts: [],
comments: []
}
}
然后你可以分别调用setState()来单独更新它们:
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
})
})
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
这里的合并是浅合并,所以setState({comments})完整保留了this.state.posts,但完全替换了this.state.comments
数据是向下流动的
不管是父组件或者是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也不关心它是函数组件还是class组件
这不是为什么称state为局部的或者是封装的原因。除了拥有并设置它的组件,其他组件都无法访问。
组件可以选择把它的state作为props向下传递到它的子组件中:
<h2>It is {this.state.date.toLocaleTimeString()}</h2>
这对于自定义组件同样适用:
<FormattedDate date={this.state.date} />
事件处理
React元素的事件处理和DOM元素的很相似,但是有一点语法上的不同:
- React事件的命名采用小驼峰式(camelCase),而不是纯小写。
- 使用JSX语法时你需要传入的一个函数作为事件处理函数,而不是一个字符串。
例如,传统的HTML:
<button onclick="activateLasers()">
Activate Lasers
</button>
在React中略微不同:
<button onClick={activateLasers}>
Activate Lasers
<button>
在Vue中,跟DOM元素的更相似,传入的就是一个字符串,事件名称就是W3C规定的事件名称,非常好记,处理事件是通过v-on(简写@)指令来实现:
<button @click="activateLasers">
Activate Lasers
<button>
在React中另一个不同点是你不能通过返回false的方式阻止默认行为。你必须显式地使用preventDefault.例如,传统HTML中阻止默认打开一个新页面,你可以这样写:
<a href="#" onclick="console.log('The link was clicked.'); return false">
Click me
</a>
在React中,可以是这样的:
function ActionLink() {
function handleClick(e) {
e.preventDefault()
console.log('The link was clicked')
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
)
}
在这里,e是一个合成事件。React根据W3C规范来定义这些合成事件,所以你不需要担心跨浏览器的兼容性问题。
使用React时,你一般不需要使用addEventListener为已创建的DOM元素添加监听器。事实上,你只需要在该元素初始渲染的时候添加器即可。
当你使用ES6 class 语法定义一个组件的时候,通常的做法是将事件处理函数声明为class中的方法。例如:下面的Toggle组件会渲染一个让用户切换开关状态的按钮:
class Toggle extends React.component {
constructor(props) {
super(props)
this.state = {
isToggleOn: true
}
// 为了在回调中使用this,这个绑定是必不可少的
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState((state) => {
isToggleOn: !state.isToggleOn
})
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
)
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
利用Vue实现时:
const Toogle = {
data() {
return {
isToggleOn: true
}
},
template: `
<button @click="handleClick">
{{isToggleOn ? 'ON' : 'OFF'}}
</button>
`,
methods: {
handleClick(){
this.isToggleOn = !this.isToggleOn
}
}
}
new Vue({
el: '#root',
render: h => h(Toogle)
})
从这里可以看到实现同样一个Toggle功能,Vue的代码更简洁,逻辑上更清晰,不需要关心this的指向,this始终指向vue的实例,改变state也不需要手动地调用setState这样的方法,而是直接通过赋值的操作就可以实现,内部通过双向数据绑定,同步到页面上。
你必须谨慎对待JSX回调函数中的this,在JavaScript中,class的方法默认不会绑定this。如果你忘记绑定this.handleClick并把它传入了onClick,当你调用这个函数的时候this的值为undefined。
这并不是React特有的行为;这其实与JavaScript函数工作原理有关,通常情况下,如果你没有在方法后面添加(),例如:onClick={this.handleClick},你应该为这个方法绑定this.
如果你觉得使用bind很麻烦,这里有两种方式可以解决。可以使用class fields正确的绑定回调函数:
handClick = () => {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
// 注意: 这是 *实验性* 语法。
console.log('this', this)
}
你也可以在回调函数中使用箭头函数:
render() {
return (
<button onClick={(e) => this.handleClick(e)}>
Click me
</button>
)
}
此语法问题在于每次渲染时都会创建不同的回调函数。在大多数情况下,这没有什么问题,但如果 该回调函数作为prop传入子组件时,这些组件可能会进行额外的重新渲染。
我们通常建议在构造器中绑定或者使用class fields语法来避免这类性能问题,。
向事件处理程序传递参数
在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id 是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
上述两种方式是等价的,分别通过箭头函数和 Function.prototype.bind 来实现。
在这两种情况下,React的事件对象e会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显示的进行传递,而通过bind的方式,事件对象以及更多的参数就会被隐式的进行传递。
而Vue向事件处理程序传递参数是这样的,跟原生DOM很相似:
<button @click="deleteRow(id, $event)">Delete Row</button>
Vue中通过$event来表示事件对象
条件渲染
在React中,你可以创建不同的组件来封装各种你需要的行为。然后,依据应用的不同状态,你可以只渲染对应状态下的部分内容。
React中的条件渲染和JavaScript中的一样,使用JavaScript运算符if或者条件运算符去创建元素来表达当前的状态,然后让React根据它们来更新UI。
观察这两个组件:
function UserGreeting(props) {
return <h1>Welecome back!</h1>
}
function GuestGreeting(props) {
return <h1>Please sign up!</h1>
}
再创建一个Greeting组件,它会根据用户是否登录来决定显示上面的哪一个组件。
function Greeting (props) {
const isLoggedIn = props.isLoggedIn
if (isLoggedIn) {
return <UserGreeting />
}
return <GuestGreeting />
}
ReactDOM.render(
<Greeting isLoggedIn={false} />,
document.getElementById(''root)
)
使用Vue来实现是可能是这样的:
const UserGreeting = {
template: `<h1>Welcome back</h1>`
}
const GuestGreeting = {
template: `<h1>Please sign up</h1>`
}
const Greeting = {
props: {
isLoggledIn: {
type: Boolean
}
},
components: {GuestGreeting, UserGreeting},
template: `
<div>
<template v-if="isLoggledIn">
<UserGreeting />
</template>
<template v-else>
<GuestGreeting />
</template>
</div>
`
}
new Vue({
el: '#root',
render: h => h(Greeting, {props: {isLoggledIn: false}})
})
在这里可能看到有些许不一样,React中的props,可能直接通过父组件的属性,如:
<Greeting isLoggedIn={false} />
而在Vue中如果使用template语法,更React类似,如果使用render函数,props是被再包装了一层,父组件要通过类似这样才可以:
render: h => h(Greeting, {props: {isLoggledIn: false}})
元素变量
你可以使用变量来储存元素。它可以帮助你有条件地渲染组件的一部分,而其他的渲染部分并不会因此而改变。
观察这两个组件,它们分别代表了注销和登录按钮:
function LoginButton(props) {
return (
<button onClick={props.onClick}>
Login
</button>
)
}
function LogoutButton(props) {
return (
<button onClick={props.onClick}>
Logout
</button>
)
}
在下面的示例中,我们将创建一个名叫LoginControl的有状态的组件。
它将根据当前的状态渲染<LoginButton />或者<LogoutButton />。同时它还会渲染上一个示例中的<Greeting />
class LoginControl extends React.Component {
constructor(props) {
super(props)
this.handleLoginClick = this.handleLoginClick.bind(this)
this.handleLogoutClick = this.handleLogoutClick.bind(this)
this.state = {isLoggledIn: false}
}
handleLoginClick() {
this.setState({isLoggledIn: true})
},
handleLogoutClick() {
this.setState({isLoggledIn: false})
}
render() {
const isLoggledIn = this.state.isLoggledIn
let button
if (isLoggledIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />
} else {
button = <LoginButton onClick={this.handleLoginClick} />
}
return (
<div>
<Greeting isLoggledIn={isLoggedIn} />
{button}
</div>
)
}
}
ReactDOM.render(
<LoginControl />,
document.getElementById('root')
)
通过vue实现,可能是这样的:
const UserGreeting = {
template: `<h1>Welcome back</h1>`
}
const GuestGreeting = {
template: `<h1>Please sign up</h1>`
}
const Greeting = {
props: {
isLoggledIn: {
type: Boolean
}
},
components: {GuestGreeting, UserGreeting},
template: `
<div>
<template v-if="isLoggledIn">
<UserGreeting />
</template>
<template v-else>
<GuestGreeting />
</template>
</div>
`
}
const LoginButton = {
props: {
onClick: {
type: Function
}
},
template: `<button @click="onClick">Login</button>`
}
const LogoutButton = {
props: {
onClick: {
type: Function
}
},
template: `<button @click="onClick">Logout</button>`
}
const LoginControl = {
components: {
LoginButton,
LogoutButton,
Greeting
},
template: `
<div>
<Greeting :isLoggledIn="isLoggledIn"/>
<template v-if="isLoggledIn">
<LogoutButton :onClick="handleLogoutClick"/>
</template>
<template v-else>
<LoginButton :onClick="handleLoginClick">
</tempalte>
</div>
`,
data() {
return {
isLoggledIn: false
}
},
methods: {
handleLoginClick() {
this.isLoggledIn = true
},
handleLogoutClick() {
this.isLoggledIn = false
}
}
}
new Vue({
el: '#root',
render: h => h(LoginControl)
})
声明一个变量并使用if语名进行条件渲染是不错的方式,但有时你可能会想使用更为简洁的语法。接下来,我们将介绍几种在JSX中内联条件渲染的方法。
与运算符 &&
通过花括号包裹代码,你可以在中JSX中嵌入任何表达式。这也包括JavaScript中的逻辑与运算符(&&),它可以很方便地进行元素的条件渲染。
function Mailbox(props) {
const unredadMessage = props.unreadMessage
return (
<div>
<h1>Hello!</h1>
{unreadMessage.length > 0 &&
<h2>
You have {unreadMessage.length} unread messages
</h2>
}
</div>
)
}
const message = ['React', 'Re: React', 'Re: Re: React']
ReactDOM.render(
<Mailbox unreadMessage={message}/>,
document.getElementById('root')
)
用Vue实现,可能是这样的:
const Mailbox = {
props: {
unreadMessage: Array
},
template: `
<div>
<h1>Hello!</h1>
<h2 v-if="unreadMessage.length > 0">You have {{unreadMessage.length}} unread messages</h2>
</div>
`
}
new Vue({
el: '#root',
render: h=> h(Mailbox, {props: {unreadMessage: ['react', 're:react', 're:re:react']}})
})
三目运算符
另一种内联条件渲染的方法是使用JavaScript中的三目运算符condition ? true : false。
在下面的示例中,我们用它来条件渲染一小段文本。
render() {
const isLoggedIn = this.state.isLoggedIn
return (
<div>
The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in
</div>
)
}
同样的,它也可以用于较为复杂的表达式中,虽然看起来不是很直观:
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn ? (
<LogoutButton onClick={this.handleLogoutClick} />
) : (
<LoginButton onClick={this.handleLoginClick} />
)}
</div>
);
}
阻止组件渲染
在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,你可以让render方法直接返回null,而不进行任何渲染。
function WarningBanner(props) {
if (!props.warn) {
return null
}
return (
<div className="warning">
Warning
</div>
)
}
class Page extends React.Component {
constructor(props) {
super(props)
this.state = {showWarning: true}
this.handleToggleClick = this.handleToggleClick.bind(this)
}
handleToggleClick() {
this.setState({
showWarning: !state.showWarning
})
}
render() {
return(
<div>
<WarningBanner warn={this.state.showWarning} />
<button onClick={this.handleToggleClick}>
{this.state.showWarning ? 'Hide' : 'Show'}
<button>
)
}
}
ReactDOM.render(
<Page />,
document.getElementById('root')
)
Vue中实现,可是能是这样:
const WarningBanner = {
props: ['warn'],
template: `
<div class="warning" v-if="warn">Warning!</div>
`
}
const Page = {
components: {
WarningBanner
},
template: `
<div>
<WarningBanner :warn="showWarning" />
<button @click="handleToggleClick">
{{showWarning ? 'Hide' : 'Show'}}
</button>
</div>
`,
data() {
return {
showWarning: true
}
},
methods: {
handleToggleClick() {
this.showWarning = !this.showWarning
}
}
}
new Vue({
el: '#root',
render: h => h(Page)
})
列表 && Key
首先,让我们看下在JavaScript中如何转化为列表。
如下代码,我们使用map()函数让数组中的每一项变双倍,然后我们得到了一个新的列表doubled并打印出来:
const numbers = [1, 2, 3, 4, 5]
const doubled = numbers.map(number => number * 2)
console.log(doubled)
在React中,把数组转化为元素列表的过程是相似的。在Vue中,是通过v-for指令来实现的。
渲染多个组件
你可以通过使用{}在JSX内构建一个元素集合。
下面,我们使用JavaScript中的map()方法来遍历number数组。将数组中的每个元素变成
- 标签,最后我们将得到的数组赋值给listItems:
-
const numbers = [1, 2, 3, 4, 5] const listItems = numbers.map(number => <li>{number}</li> )
我们把整个listItems插入到
- 元素中然后渲染进DOM:
ReactDOM.render( <ul>{listItem}</ul>, document.getElementById('root') )
基础列表组件
通常你需要一个组件中渲染列表,我们可以把前面的例子重构成一个组件,这个组件接收numbers数组作为参数,并输出一个元素列表。
function NumberList(props) { const numbers = props.numbers const listItems = numbers.map(number => <li>{number}</li>) return ( <ul> {listItems} </ul> ) } const numbers = [1, 2, 3, 4, 5] ReactDOM.render(<NumberList numbers={numbers} />, document.getElementById('root'))
Vue的实现可能是这样的:
const App = { template: `<ul><li v-for="item in numbers">{{item}}</li></ul>`, data() { return { numbers: [1, 2, 3, 4, 5] } } } new Vue({ el: '#root', render: h => h(App) })
key
key帮助React识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。Vue当中也有key,key的作用是帮助虚拟dom快速定位节点位置。
const numbers = [1, 2, 3, 4, 5] const listItems = numbers.map(number => <li key={number.toString}> {number} </li> )
一个元素的key最好是这个元素在列表中拥有的一个独一无二的字符串,通常,我们使用数据中的id作为元素的key:
const todoItems = todos.map(todo => <li key={todo.id}> {todo.text} </li> )
当元素没有确定id的时候,万不得已你可以使用元素索引index作为key:
const todoItems = todos.map((todo, index) => <li key={index}> {todo.text} </li> )
用key提取组件
元素的key只有放在就近的数组上下文才有意义。
比如说,如果你提取出一个ListItem组件,你应该把key保留在数组中的这个<ListItem />元素上,而不是放在ListItem组件中的
- 元素上。
-
例子:不正确的使用key的方式
function ListItem (props) { const value = props.value return ( // 错误!你不需要在这里指定 key: <li key={value.toString()}> {value} </li> ) } function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => // 错误!元素的 key 应该在这里指定: <ListItem value={number} /> ); return ( <ul> {listItems} </ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render( <NumberList numbers={numbers} />, document.getElementById('root') );
例子:正确的使用key的方式
function ListItem (props) { // 正确!这里不需要指定key: return <li>{props.value}</li> } function NumberList(props) { const numbers = props.numbers const listItems = numbers.map(number => // 正确!key应该在数组上上下文中被指定 <ListItem key={number.toString()} value={number} /> ) return ( <ul> {listItems } </ul> ) } const numbers = [1, 2, 3, 4, 5] ReactDOM.render( <NumberList numbers={numbers}/>, document.getElementById('root') )
一个好的经验法则是:在 map() 方法中的元素需要设置 key 属性。
key只是在兄弟节点之间必须唯一
数组元素中使用的key在其兄弟节点之间应该是独一无二的,然而,它们不需要是全局唯一的,当我们生成两个不同的数组时,我们可以使用相同的key值:
function Blog(props) { const sidebar = ( <ul> {props.posts.map(post => <li key={post.id}> {post.title} </li> )} ) const content = props.posts.map((post) => <div key={post.id}> <h3>{post.title}</h3> <p>{post.content}</p> </div> ); return ( <div> {sidebar} <hr /> {content} </div> ); } const posts = [ {id: 1, title: 'Hello World', content: 'Welcome to learning React!'}, {id: 2, title: 'Installation', content: 'You can install React from npm.'} ]; ReactDOM.render( <Blog posts={posts} />, document.getElementById('root') );
key会传递信息给React,但不会传递给你的组件。如果你的组件中需要使用key属性的值,请使用其他属性名显示传递这个值
表单
在React里,Html表单元素的工作方式和其他的DOM元素有些不同,这是因为表单元素通常会保持一些内部的state。例如这个纯HTML表单只接受一个名称:
<form> <label> 名字: <input type="text" name="name" /> </label> <input type="sumbit" value="提交" /> </form>
此表单具有默认的HTML表单行为,即在用户提交表单后浏览到新页面。如果你在React中执行相同的代码,它依然有效。但大多数情况下,使用JavaScript函数可以很方便的处理表单的提交,同时还可以访问用户填写的表单数据。
受控组件
在HTML中,表单元素(如<input>, <textarea>和<select>)之类的表单元素通常维护state,并根据用户输入进行更新,而在React中,可变状态通常保存在组件的state属性中,并且只能通过setState()来更新。
我们可以把两者结合起来,使React的state成为唯一数据源。渲染表单的React组件还控制着用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素就叫做爱控组件。
例如,如果我们想让前一个示例在提交时打印出名称,我们可以将表单写成受控组件:
class NameForm extends React.component { constructor(props) { super(props) this.state = {value: ''} this.handleChange = this.handleChange.bind(this) this.handleSubmit = this.handleSubmit.bind(this) } handleChange(e) { this.setState( { value: e.target.value }) } handleSubmit(e) { e.preventDefault() console.log('your name: ', this.state.value) } render() { return ( <form onSubmit={this.handleSubmit}> <label> 名字: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="提交" /> </form> ) } }
利用Vue实现,可能是这样的:
const NameForm = { template: ` <form @submit="handleSubmit"> <label> 你的名字: <input type="text" :value="value" @change="handleChange"/> </label> <input type="submit" value="提交"/> </form> `, data() { return { value: '' } }, methods: { handleChange(e) { this.value = e.target.value }, handleSubmit(e) { e.preventDefault() console.log('your name: ', this.value) } } } new Vue({ el: '#root', render: h => h(NameForm) })
由于在表单元素上设置了value属性,因此显示的值始终为this.state.value,这使得React的state成为唯一数据源。由于handleChange在每次按键时都会执行并更新React的state,因此显示的值将随着用户输入而更新。
对于受控组件来说,每个state突变都有一个相关的处理函数。这使得修改或者验证用户输入变得简单。例如,如果我们要强制要求所有名称都用大写字母书写,我们可以将handleChange改写为:
handleChange(event) { this.setState({value: event.target.value.toUpperCase()}) }
textarea标签
在HTML中,<textarea>元素通过其子元素定义其文本:<textarea> 你好,这是在textarea 里的文本 </textarea>
而在React中,<textarea>使用value属性代替。这样,可以使得使用<textarea>的表单和使用单行input的表单非常类似:
class EssayForm extends React.Component { constructor(props) { super(props) this.state = { value: '请撰写一篇关于你喜欢的 DOM 元素的文章.' } this.handleChange = this.handleChange.bind(this) this.handleSubmit = this.handleSubmit.bind(this) } handleChange(event) { this.setState({ value: event.target.value }) } handleSubmit(event) { console.log('提交的文章:', this.state.value) event.preventDefault() } render() { return ( <form onSubmit={this.handleSubmit}> <label> 文章: <textarea value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="提交" /> </form> ); } }
请注意,this.state.value初始化于构造函数中,因此文本区域默认有初值。
select标签
在HMTL中,<select>创建下拉列表标签。例如,如下HTML创建了水果相关的下拉列表:
<select> <option value="grapefruit">葡萄</option> <option value="lime">酸橙</option> <option selected value="coconut">椰子</option> <option value="mango">芒果</option> </select>
请注意,由于slected属性的缘故,椰子选项默认会被选 中。React并不会使用selected属性,而是在根select标签上使用value属性。这在受控组件上更便捷,因为您只需要在根标签中更新它。例如:
class FlavorForm extends React.Component { constructor(props) { super(props) this.state = {value:'coconut'} this.handleChange = this.handleChange.bind(this) this.handleSubmit = this.handleSubmit.bind(this) } handleChange(event) { this.setState({value: event.target.value}) } handleSubmit(event) { console.log('你喜欢风味是:', this.state.value) event.preventDefault() } render() { return ( <form onSubmit={this.handleSubmit}> <label> 选择您喜欢的风味: <select value={this.state.value} onChange={this.handleChange}> <option value="grapefruit" >葡萄柚</option> <option value="lime">酸橙</option> <option value="coconut">椰子</option> <option value="mango">芒果</option> </select> </label> <input type="submit" value="提交" /> </form> ) } }
注意
你可以将数组传递到value属性中,以支持在select标签中选择多个选项:<select multiple={true} value={['B', 'C']}
文件input标签
在HMTL中,<input type =“file”>允许用户从存储设备中选择一个或者多个文件,将其上传到服务器,或者通过使用JavaScript的File API进行控制。
<input type="file">
因为它的value只读,所以它是React中的一个非受控组件。
处理多个输入
当需要处理多个input元素时,我们可以给每个元素添加name属性,并让处理函数根据event.target.name的值选择要执行的操作。
例如:class Reservation extends React.Component { constructor(props) { super(props); this.state = { isGoing: true, numberOfGuests: 2 }; this.handleInputChange = this.handleInputChange.bind(this); } handleInputChange(event) { const target = event.target; const value = target.type === 'checkbox' ? target.checked : target.value; const name = target.name; this.setState({ [name]: value }); } render() { return ( <form> <label> 参与: <input name="isGoing" type="checkbox" checked={this.state.isGoing} onChange={this.handleInputChange} /> </label> <br /> <label> 来宾人数: <input name="numberOfGuests" type="number" value={this.state.numberOfGuests} onChange={this.handleInputChange} /> </label> </form> ); } }
这里使用了 ES6 计算属性名称的语法更新给定输入名称对应的 state 值:
this.setState({[name]: value})
状态提升
多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同组件中去。让我们看看它是如何运作的。
举个例子,我们试着做一个用于计算水在给定温度下是否会沸腾的温度计算器。
我们将从一个名为BoilingVerdice的组件开始,它接受celsius温度作为一个prop,并据此打印出该温度是否中以将水煮沸的结果。
function BoilingVerdict(props) { if (props.celsius >= 100) { return <p>The water would boil</p> } return <p>The water would not boil</p> }
接下来,我们创建一个名为Calculator的组件。它渲染一个用于输入温度<input>,并将其值保存在this.state.temperature中。
另外,它根据当前输入值渲染BoilingVerdict组件。
class Calculator extends React.Component { constructor(props) { super(props) this.handleChange = this.handleChange.bind(this) this.state = {temperature: ''} } handleChange(e) { this.setState({temperature: e.target.value}) } render() { const temperature = this.state.temperature return ( <fieldset> <legend>Enter temperature in Celsius: </legend> <input value={temperature} onChange={this.handleChange} /> <BoilingVerdict celsius={parseFloat(temperature)} /> </fieldset> ) } }
状态提升
通常,多个组件需要要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。让我们看看它是如何运作的。
我们将从一个名为BoilingVerdict的组件开始,它接受celsius温度作为一个props,并据此打印出该温度是足以打印出该温度是否足以将水煮沸的结果。
function BoilingVerdict(props) { if (props.celsius >= 100) { return <p>The water would boil.</p> } return <p>The water would not boil.</p> }
接下来,我们创建一个名为Calculator的组件。它渲染一个用于输入温度的<input>,并将其值保存在this.state.temperature中。
另外,它根据当前输入值渲染BoilingVerdict组件。
class Calculator extends React.Component { constructor(props) { super(props) this.handleChange = this.handleChange.bind(this) this.state = {temperature: ''} } handleChange(e) { this.setState({ temperature: e.target.value }) } render() { const temperature = this.state.temperature return ( <fieldset> <legend>Enter temperature inCelsius</legend> <input value={temperature} onChnage={this.handleChange}/> <BoilingVerdict celsius={parseFloat(temperature)}/> </fieldset> ) } }
使用Vue实现,可能是这样的:
const BoilingVerdict = { props: { celsius: Number }, template: ` <template v-if="celsius >= 100"> <p>The water would boil</p> </template> <template v-else> <p>The water would not boil</p> </template> ` } const Calculator = { components: {BoilingVerdict}, template: ` <fieldset> <legend>Enter temperature in Celsius:</legend> <input :value="temperature" @input="handleChange"/> <BoilingVerdict :celsius="parseFloat(temperature)"/> </fieldset> `, data() { return { temperature: '' } }, methods: { handleChange(e) {this.temperature = e.target.value} } } new Vue({ el: '#root', render: h => h(Calculator) })
这里有一点不一样,需要提出来说明一下:**React事件对象是一个合成对象,有些事件跟原生事件表现并不完全一致,如这里的onChange事件,跟Vue中的@input事件的表现一致!
当输入非英文时,React onChange事件会存在bug添加第二个输入框
我们的新需求是,在已有摄氏温度输入框的基础上,我们提供华氏度的输入框,并保持两个输入框的数据同步。
我们先从Calculator组件中抽离出TemperatureInput组件,然后为其添加一个新的scale prop它可以是"c"或者 是"f":
const scaleNames = { c: 'Celsius', f: 'Fahrenheit' } class TemperatureInput extends React.Component { constructor(props) { super(props) this.handleChange = this.handleChange.bind(this) this.state = {temperature: ''} } handleChange(e) { this.setState({temperature: e.target.value}) } render() { const temperature = this.state.temperature const scale = this.props.scale return ( <fieldset> <legend>Enter temperature in {scaleNames[scale]}</legend> <input value={temperature} onChange={this.handleChange}/> </fieldset> ) } }
我们现在可以修改Calculator组件让它渲染两个独立的温度输入框组件:
class Calculator extends React.Component { render( return ( <div> <TemperatureInput scale="c" /> <TemperatureInput scale="f" /> </div> ) ) }
编写转换函数
首先,我们将编写两个可以在摄氏度与华氏度之间相互转换的函数:
function toCelsius(fahrenheit) { return (fahrenheit - 32) * 5 / 9 } function toFahreheit(celsius) { return (celsius * 9 / 5) + 32 }
上述两个函数仅做数值转换。而我们将编写另一个函数,它接受字符串类型的temperature和转换函数作为参数并返回一个字符串。我们将使用它来依据一个输入框的值计算出另一个输入框的值。
当输入temperature的值无效时,函数返回空字符串,反之,则返回保留三位小数并四舍五入后的转换结果:
function tryConvert(temperature, convert) { const input = parseFloat(temperature) if (Number.isNaN(input)) { return '' } const output = convert(input) const rounded = Math.round(output * 1000) / 1000 return rounded.toString }
状态提升
到目前为止,两个TemperatureInput组件均在各自内部的state中相互地保存着各自的数据。
然而,我们希望两个输入框内的数值彼此能够同步,当我们更新摄氏度输入框内的数值时,华氏度输入框内就当显示转换后的华氏温度,反之亦然。
在React中,将多个组件中需要共享的state向上移动到它们的最近共同组件中,便可实现共享state。这就是所谓的状态提升。接下来,我们将temperatureInput组件中的state移动至Calculator组件中去。
如果Calculator组件拥有了共享的state,它将成为两个温度输入框中当前温度的数据源。
它能够使得两个温度输入框的数值保持一致。由于两个TemperatureInput组件的props均来自共同的父组件Calculator,因此两个输入框中的内容将始终保持一致。首先,我们将TemperatureInput组件中的this.state.temperature替换为this.props.temperature.
我们知道props是只读的。当temperature存在于TermperatureInput组件中的state中时,组件调用this.setState()便可修改它。然而,temperature是由父组件传入的prop,TemperatureInput组件便失去了对它的控制权。
在React中,这个问题通常是通过使用“受控组件”来解决的,与DOM中的<input>接受value和onChange一样,自定义的TempratureInput组件接受temperature和onTemperatureChange这两个来自父组件Calculator的props。
顼在,当TemperatureInput组件想要更新温度时,需调用this.props.onTemperatureChange来更新它:
handleChange(e) { this.props.onTemperatureChange(e.target.value) }
任何可变数据应当只有一个相对应的唯一“数据源”,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流
组合 vs 继承
React有十分强大 的组合模式。我们推荐使用组合而非继承来实现组件间的代码重用。
包含关系
有些组件无法提前知晓它们子组件的具体内容。在Sidebar(侧边栏)和Dialog(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。
我们建议这些组件使用一个特殊的children prop来将他们的子组件传递到渲染结果中:
在Vue中是通过slot(插槽)来实现的,slot分为具名slot与匿名slot
function FancyBorder(props) { return ( <div className={'FancyBorder FancyBorder-' + props.color}> {props.children} </div> ) }
这使得别的组件可以通过JSX嵌套,将任意组件作为子组件传递给它们。
function WelcomeDialog() { return ( <FancyBorder color="blur"> <h1 className="Dialog-title"> Welcome </h1> <p className="Dialog-message"> thank you for visiting our spacecraft </p> </FancyBorder> ) }
注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。
如果你想要在组件间复用非 UI 的功能,我们建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。 这个也Vue中的mixin相似