导语:
最近组内打算对部分项目的前端进行重构,新的前端框架打算抛弃传统的JQuery,使用这几年非常火的React,于是从头开始学习React,写篇博客记录下学习的过程,也可以加固对React的理解。本文主要是记录入门的经历,所以侧重于实践开发,所以就不啰嗦介绍背景什么的了。主要介绍原理、优势和实际应用。
Why React?
我们知道,浏览器通过Http请求从各个异构服务器获取到html文档。会根据包含相关信息的请求头和请求体,将其解析并构建成一个DOM树。同时,根据文档获取到相关的css文档,这些文档里面包含了许许多多的CSSOM。最后,这颗DOM树和这些CSSOM会在浏览器内存中形成一个Render树,浏览器就是根据这个Render树渲染出我们最后看到的页面的。而这些过程都是发生在渲染引擎中的,这与负责执行动态逻辑的JavaScript引擎是相分离的。因此,为了JS能够方便操作DOM结构,渲染引擎会暴露一些接口供JavaScript调用
问题就在这里,虽然通过暴露的接口,JS可以操作到DOM树中的节点。但是性能其实不是很高,特别是对于一些复杂的网页,添加删除节点会导致DOM节点的更新,这个开销是很大的。在之前,普遍都是通过JQuery来和DOM进行交互:
在网页设计越来越丰富,逻辑交互越来越复杂的情况下,频繁地进行DOM操作组件逐渐成为了性能的瓶颈。而以直接操作DOM的JQuery也不再像之前那么大一统。许许多多前端框架如雨后春笋般涌现,如AngularJS,React,Vue等。其中最火的当属React,它提供了一套不同的,高效的方案来更新DOM。这种全新的解决方案就是“Virtual DOM”:
如上图所所示,React会在内存中根据DOM创建一个虚拟的DOM树。基于React的开发并不直接操作DOM,而是通过操作这棵虚拟DOM进行的,每当数据变化的时候,React会重新构建整个DOM树,然后将当前DOM树和上个DOM树进行对比,得到DOM结构的区别,然后仅仅将需要变化的部分进行实际的浏览器DOM更新。既然最后还是会通过React来进行对DOM的更新,那为何还会有性能的提升呢?原因在于React并不总是马上对DOM树所做的更改进行更新,换而言之,就是你在虚拟DOM树上做的操作,不保证马上会产生实际的效果,它只会在你需要产生DOM树更新的时候进行更新。这样的一个机制就使得React能够等到一个事件循环的结尾,将若干个由数据影响的节点合并在一起,和实际DOM进行比较,只操作Diff部分,而不是像传统的js那样需要更新DOM操作,就更新DOM树一次,因而能达到提高性能的目的。同时,在保证性能的同时,React通过组件化的抽象概念,让开发者将不需要关注某个数据的变化该如何体现在DOM树上,只需要关系某个数据更新时,页面是如何Render的。
React 使用
本文的所有例子均来自于React官方教程,不过做了些许改动,可以不需要搭设服务器即可运行,所以也不需要引入JQuery。
需要引入的文件
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core5.6.16/browser.js"></script>
上面一共调用了三个库:react.js,react-dom.js,browser.js,它们必须优先加载。其中react.js是react的核心库,react-dom.js 提供与DOM相关的功能,browser.js的作用实际是一种运行时的编译,当执行的代码是jsx的语法,其实浏览器是无法识别然后报错的,但是引入了它以后就可以解析了,这也是为什么有时候并不需要将jsx的语法转成js语法也能直接在浏览器中运行。不过这个文件本身挺大的,而且jsx在客户端解析成js语法需要一段时间,并且造成不必要的性能损耗。所以其实这个过程应该由服务端完成。即我们在开发完成后应该用gulp或者webpack这些工具将其解析打包后才发到生产,这样就不再需要引入browser.js这个文件了。
JSX语法
使用JSX语法,可以定义简洁而且较为熟知的树状语法结构。其实它的基本语法规则也很简单:遇到HTML标签(<开头,并且第一个字母是小写,如<div>),就用HTML规则解析;遇到代码块(以{开口)就用JavaScript规则解析,遇到组件(<开头,并且第一个字母是大写,如<Comment>),就是我们的React组件的类名了,所以写组件类的时候,别忘了类名以大写字母开头。
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>
);
}
});
Components
Component就是其实就是React的核心思想,它通过把代码封装成组件的形式,然后每调用一次就会通过React的工厂方法来生成这个组件类的实例,并且根据注入的props或者state的值来输出组件。
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
Hello, world! I am a CommentBox.
</div>
);
}
});
ReactDOM.render(
<CommentBox />,
document.getElementById('content')
);
ReactDOM.render是React的最基本用法,用于将模版转为HTML语言,并插入指定的DOM节点中。下面两小节代码将展示如何将值传到组件中,组件如何获取。
Props
通过Props属性,组件能够读取到从父组件传递过来的数据,然后通过这些标记渲染一些标记,所有在父组件中传过来的属性都可以通过this.props.propertyName来获取到,其中有一个特殊的属性this.props.children,通过它你可以获取到组件的所有子节点,如下例所示:
var data = [
{author: "Pete Hunt", text: "This is one comment"},
{author: "Jordan Walke", text: "This is *another* comment"}
];
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
{this.props.children}
</div>
);
}
});
var CommentList = React.createClass({
render: function() {
return (
<div className="commentList">
<Comment author="Pete Hunt">This is one comment</Comment>
<Comment author="Jordan Walke">This is *another* comment</Comment>
</div>
);
}
});
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
</div>
);
}
});
ReactDOM.render(
<CommentBox data={data} />,
document.getElementById('content')
);
绿色框是由Comment组件负责生成的,它被它的父组件CommentList(图中的蓝色框)调用了两次,所以根据props中获取的不同的数据实例化了两次。接着组件CommentList又被顶层组件CommentBox所包括(图中的红色框)。React就是通过这样的方式,将组件与组件之间的关系建立起来的,通过组合,可以做出各式各样的我们需要的页面。同时,由于这些模块化的组件,使得我们可以只关注传入组件和改变组件的数据,基本数据对了,组件对数据的渲染也就对了。同时我们也可以在后续的开发中,将一些通用的组件抽出来,代码结构清晰有调理。随着开发的不断深入和代码的不断累积,这种优势就会越来越明显。
State
在上一节的例子中,在组件CommentList中传给Comment组件是写死的。我们知道,可以通过父组设置属性,然后子组件中通过props获取。但是如果子组件中的数据会不断地改变(或者通过定时器,或者通过回调,或者通过Ajax),子组件如何通过数据的变化来不断地重新渲染呢?答案是State。
state和props一样,都是用来描述组件的特性。只不过不同的是,对于props属性,组件只会在对象实例的时候渲染并返回render函数,而对state的设置则在组件的生命周期内都有效,只要setState了,组件就会重新渲染并返回render。换而言之,就是如果你在组件实例以后再对props进行更新,react并不能保证你的更新会反应到VDOM甚至DOM上,而setState就可以。所以我们一般将哪些定义了以后就不再改变的特性放在props中,而随着用户交互或者定时触发产生变化的一些特性,那放在state中将是更好的选择。
现在,我们添加一个可以供用户输入的两个输入框和一个按钮,让用户来输入自己的名字和评论内容,点击提交后页面将会显示他们的评论。
//修改CommentList
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function (comment) {
return (
<Comment author={comment.author}>
{comment.text}
</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
});
//创建CommentForm组件,用于用户输入提交
var CommentForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var author = this.refs.author.value.trim();
var text = this.refs.text.value.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});
this.refs.author.value = "";
this.refs.text.value = "";
alert("Submit!");
return;
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
});
var data = [
{author: "YYQ", text: "这是一条评论"},
{author: "wuqke", text: "这是另外一条评论"}
];
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
<h3 className="commentAuthor">
{this.props.author}说:
</h3>
<span>{this.props.children}</span>
</div>
);
}
});
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function (comment) {
return (
<Comment author={comment.author}>
{comment.text}
</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
});
var CommentForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var author = this.refs.author.value.trim();
var text = this.refs.text.value.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});
this.refs.author.value = "";
this.refs.text.value = "";
alert("Submit!");
return;
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
});
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},handleCommentSubmit: function(comment) {
var ndata = this.state.data;
ndata.push(comment);
this.setState({data:ndata});
},
componentDidMount: function() {
this.setState({data:this.props.data})
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit}/>
</div>
);
}
});
ReactDOM.render(
<CommentBox data={data} />,
document.getElementById('content')
);
//修改CommentBox组件,当回调函数被触发的时候,将comment添加到data中并且setState更新data
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},handleCommentSubmit: function(comment) {
var ndata = this.state.data;
ndata.push(comment);
this.setState({data:ndata});
},
componentDidMount: function() {
this.setState({data:this.props.datas})
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit}/>
</div>
);
}
});
----------朴实无华的前后效果分割线---------------
上面的例子,在CommentBox的render中添加了一个CommentForm组件,用于获取用户的输入,同时添加了一个函数handleCommentSubmit(comment),函数接收comment参数,做的事情很简单,就是和原来的数据data合并,并通过setState()更新数据data,该组件发现state变化以后,就会去重新渲染组件,最后在执行render函数,最终将变化反映在VDOM和DOM上。这让我们可以只关注数据的变化,而不必去考虑太多DOM节点是否被更新的问题。
组件的生命周期
React为其组件定义了生命周期的三个状态。针对这三个状态提供了7种钩子函数,方便使用者在不同状态之前或者之后设置一些事件监听或者逻辑处理。
- Mounting:组件正在被插入到DOM节点中
- Updating:组件正在被重新渲染,是否被更新取决于该组件是否有改变
- Unmouting:组件正在从DOM节点中移出
针对以上三个状态,都分别提供了两种钩子函数,用于在进入这个状态之前(will函数),活着离开这个状态之后(did函数)调用,理解了上面的状态,就会非常容易明白函数名和函数的调用时机了:
Mounting:
- componentWillMount()
- componentDidMount()
Updating:
- shouldComponentUpdate(object nextProps, object nextState)
- componentWillReceiveProps(object nextProps)
- componentWillUpdate(object nextProps, object nextState)
- componentDidUpdate(object prevProps, object prevState)
Unmouting:
- componentWillUnmount
总结
在理解React的思想和相关的一些概念后,其实很容易就可以使用React开始开发。个人总结了一下,只要了解好React会在内存中创建一个Virtual DOM,所有的React组件都是在更新这棵VDOM(通过ref获得DOM节点更新除外),然后React才会根据这棵VDOM和DOM运用一个加速Diff算法,做一个差异覆盖。
接着,可以通过Props和State来传递数据(但是数据的传递是单向的)。其中,setState会使得组件重新计算并执行render函数,从而做到组件随着数据变化渲染。最后,理解了组件的生命周期的三个状态,我们就可以在这三个状态之前或者之后调用的钩子函数中绑定事件,处理相关逻辑,也可以从父组件中传入回调函数,在子组件中调用该函数,做到子组件和父组件的通信,解决单数流单向传递的一些问题。
感觉真正难的,是如果使用React及周边生态搭建起一套行之有效的架构。虽然React入门比较简单,但是真正开发起来,其实是一个漫长的过程,不仅仅是思维的转变,整个技术栈可能也要配合着学习。但这也是许多的前端开发者相信正是因为这些,它可能是未来前端的方向。
参考资料
- 书籍:《React:引领未来的用户界面开发框架》 电子工业出版社
- 阮一峰老师的 React入门实例教程
- 本文的例子绝大部分来自 React官方文档 并作了小量的修改。