关于JSX
考虑这样一段代码:
const element = <h1>Hello, world!</h1>;
这段代码既不是字符串也不是HTML,它是JSX,是javascript的拓展。在React里用来描述UI,因为JSX是用来生产React内的“元素”的。以后会介绍它是如何被渲染成DOM的。
我们可以在JSX里嵌入任何的javascript代码,例如下面这个例子就混合了javascript代码:
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') );
需要指出的是,虽然JSX是javascript的拓展,但是实际到最后,JSX还是要被编译成纯粹的javascript对象,所以在if语句、for循环语句里面都可以用,而且还可以把它当成函数参数或返回值。例如:
function getGreeting(user) { if (user) { return <h1>Hello, {formatName(user)}!</h1>; } return <h1>Hello, Stranger.</h1>; }
鉴于JSX相比HTML更接近javascript,所以它里面元素的属性采用驼峰命名法,例如class要写成className,tabindex要写成tabIndex。
JSX自带过滤以避免XSS攻击,所以不必担心注入的问题。
元素渲染
元素是React应用里最小的构造块,一个元素所描述的内容决定了你在屏幕上看到的东西。比如这个:
const element = <h1>Hello, world</h1>;
另外,不同于浏览器DOM元素,React中的元素都是纯粹的对象,创建起来代价很低,而且React会负责更新浏览器DOM以便于和React元素相匹配。
React中的元素是不可变的,一旦创建了一个元素,就无法改变它的属性或者是后代元素,这有点类似电影中的一帧,仅是某个时间点的快照。就目前的知识来说,如果想要更新UI,唯一的方法就是再创建一个新的元素,比如我想制作一个显示时间的程序,要做到内容每秒刷新一次,那么以目前的知识来说,只能这么写代码:
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);
值得注意的是,虽然每秒钟我们创建一个新的元素,但React会十分智能的分辨其中的不同,每次仅仅会改变不同的部分,以上面的代码为例,会发现每次只改变了dom中的文字。
在React程序中大部分代码都是一次性的,不牵扯到动态刷新的问题,不过在后面会介绍如何利用状态组件解决这个问题。
组件
组件给了你将UI切割的能力,以便更好地复用UI代码。从概念上看,组件和javascript中的函数很像,可以接受任意的输入然后返回一个最终绘制在屏幕上的React元素。
创建组件的最简单方式就是写一个javascript函数:
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
之前的React元素都采用的是原生的dom标签,比如div,其实React提供了一种机制来满足我们采用自定义标签,例如:
function Welcome(props) { return <h1>Hello, {props.name}</h1>; } const element = <Welcome name="Sara" />; ReactDOM.render( element, document.getElementById('root') );
解释React对一下上面这段代码干了什么:
- 对<Welcome name="Sara" />元素调用ReactDOM.render()方法。
- React以{name: 'Sara'}为props调用Welcome组件。
- Welcome组件返回了一个<h1>Hello, Sara</h1>元素。
- React高效的更新DOM。
需要注意的是组件均是以大写字母开头,比如<div/>就是一个dom标签,而<Welcome/>就是一个组件,而且需要在作用于中有一个对应的Welcome函数。
需要指出的是,组件可以嵌套,例如:
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') );
注意:组件返回的必须是一个元素,例如上面的代码中,虽然希望的是三个Welcome元素,但是必须用一个div包裹起来,不能直接返回三个元素。
鉴于组件的可嵌套能力, React推荐将一个大组件分割成小组件,例如下面这个组件:
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} /> ); }
然后提取Comment:
function Comment(props) { return ( <div className="Comment"> <UserInfo user={props.author} /> <div className="Comment-text"> {props.text} </div> <div className="Comment-date"> {formatDate(props.date)} </div> </div> ); }
提取工作一开始看起来可能是乏味的,但是在大型应用中,这可以极大的提高代码复用率。一个指导原则是,如果一个UI要素多次出现或者太大,都要做拆分以便复用。
有一点需要说明的是,组件的props都是只读的,不要想在组件中修改props,那样不会起任何作用。
生命周期
思考一下上面演示的时钟程序,到目前为止只介绍了一种更新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);
再介绍新方法前,我们先将这段代码利用React的组件化能力进行封装:
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);
仅仅做到这样是不够的,我们希望将更改时间的逻辑和元素绑定到一起,去除掉setInterval,最终的理想效果应该这样的:
ReactDOM.render( <Clock />, document.getElementById('root') );
为了到达理想的效果,我们需要为组件添加state。要想使用state就需要讲组件用class的写法,因为class写法在定义class时可以提供一些额外的特性,state就是其中之一。
将函数化的组件转化成class的写法,可以按照下面的步骤进行:
- 创建一个同名的ES6 class,该class继承自React.Component
- 添加一个空的render方法。
- 将函数的主体部分挪到render方法内。
- 用this.props替换props。
- 将原来的函数形组件删掉。
上述操作完成后函数就转换为了class:
class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.props.date.toLocaleTimeString()}.</h2> </div> ); } }
接下来就能用state和lifecycle hooks这两项技术,首先用state代替props
class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } }
然后添加一个构造函数去初始化this.state:
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> ); } }
需要注意的是,构造函数的参数是props并且将这个参数传给了基类。
接下来可以将date(prop)从Clock元素上移除了。
ReactDOM.render( <Clock />, document.getElementById('root') );
此时我们的组件是这样的:
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') );
接下来我们将为Clock设置一个定时器,这需要我们了解React组件生命周期的知识,React中的组件会经历从创建到移除的一个周期,暴露给我们的是两个钩子函数:componentDidMount和componentWillUnmount,分别代表创建和移除,在这两个时间点上我们可以做一些工作。例如定义计时器:
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 ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } } ReactDOM.render( <Clock />, document.getElementById('root') );
需要注意的是,在更新date数值的时候,不能直接赋值,而是采用this.setState方法进行赋值。不过在构造函数里给date初始化的时候可以直接赋值。
事件处理
React里的事件名称采用的是驼峰拼写法,比如HTML中是onclick,在React里就是onClick。另外不能通过返回false来阻止默认事件,而应该用preventDefault方法。
感觉不管是React还是Angular,在工具性上都比较差,可能需要其它附件做为拓展吧,总之工具性的都比较差,常常有不够用的感觉。
有条件的渲染
在React里面,可以根据实际情况有选择性的渲染,例如考虑下面两个组件:
function UserGreeting(props) { return <h1>Welcome back!</h1>; } function GuestGreeting(props) { return <h1>Please sign up.</h1>; }
然后创建一个新的组件,来有对上面两个组件做选择:
function Greeting(props) { const isLoggedIn = props.isLoggedIn; if (isLoggedIn) { return <UserGreeting />; } return <GuestGreeting />; } ReactDOM.render( // Try changing to isLoggedIn={true}: <Greeting isLoggedIn={false} />, document.getElementById('root') );
React支持将元素变量化以便根据不同的情况渲染不同的元素,如下:
function LoginButton(props) { return ( <button onClick={props.onClick}> Login </button> ); } function LogoutButton(props) { return ( <button onClick={props.onClick}> Logout </button> ); } class LoginControl extends React.Component { constructor(props) { super(props); this.handleLoginClick = this.handleLoginClick.bind(this); this.handleLogoutClick = this.handleLogoutClick.bind(this); this.state = {isLoggedIn: false}; } handleLoginClick() { this.setState({isLoggedIn: true}); } handleLogoutClick() { this.setState({isLoggedIn: false}); } render() { const isLoggedIn = this.state.isLoggedIn; let button = null; if (isLoggedIn) { button = <LogoutButton onClick={this.handleLogoutClick} />; } else { button = <LoginButton onClick={this.handleLoginClick} />; } return ( <div> <Greeting isLoggedIn={isLoggedIn} /> {button} </div> ); } } ReactDOM.render( <LoginControl />, document.getElementById('root') );
注意其中button变量的用法。
同时,在组件中还可以用javascript逻辑控制语句,如:
function Mailbox(props) { const unreadMessages = props.unreadMessages; return ( <div> <h1>Hello!</h1> {unreadMessages.length > 0 && <h2> You have {unreadMessages.length} unread messages. </h2> } </div> ); } const messages = ['React', 'Re: React', 'Re:Re: React']; ReactDOM.render( <Mailbox unreadMessages={messages} />, document.getElementById('root') );
React的组件也可以用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> ); }
如果不希望组件渲染出内容,可以返回null;