@gyyin
2019-11-24T17:06:29.000000Z
字数 12294
阅读 1162
慕课专栏
React 最早是 Facebook 的内部项目,当时 Facebook 对市场上的 MVC 框架都不满意,于是自己做了一套新的框架。
在 React 诞生之前,Facebook 内部已经有了 React 的雏形项目。在2010年,Facebook 开源了 XHP 项目,支持在 PHP 中书写模板,这种模板语法和现在 React 中的 JSX 非常相似。
2011年,jordwalke 开源了一个名为 FaxJS 的项目,支持组件化、服务端渲染等等,这是 React 最初的灵感之一。
现在 FaxJS 的 GitHub 上已经建议开发者去使用 React。
This is an old, experimental project: React is much better in every way and you should use that instead. This project will remain on Github for historical context.
2013年,Facebook 将 React 开源,虽然刚开始很多开发者认为这是一种倒退,但随着越来越多人使用,React 获得了越来越多的肯定,现在已经是最流行的前端框架之一。
相比传统的 jQuery 和原生 JS,React 带来了数据驱动、组件化的思想。
前面在讲解模块化的时候,有提到过组件这个概念。如果将应用看做一个玩具,那么组件就是组成这个玩具的一块块积木。
在现代化的前端框架之前,也有过一些组件化的思想,比如 jQuery 丰富的插件。但这些大都不够彻底,都只是对逻辑层的封装,很多插件都还需要用户配合写要求格式的 HTML 结构。
举个简单的例子,比如有这么一个 Slider 插件,如果想要使用它,就必须按照插件规定的 HTML 格式来编写,不然它就无法在插件内部准确地获取到 DOM。
<div class="slider">
<div class="slider-item"></div>
<div class="slider-item"></div>
<div class="slider-item"></div>
</div>
$(".slider").start({
showDot: true,
animate: true
})
React 中提出了 JSX,可以将 HTML 放到 JS 中编写,将 UI 和 逻辑都封装到了组件中,这样做到了更彻底的组件化。
在 jQuery 中,用户点击了某个 DOM-A 元素,来修改另一个 DOM-B 中相应的内容。
一般的做法是对这个 DOM-A 绑定事件,之后在事件里面用 $
来查询获取到 DOM-B,然后修改 DOM-B 的 innerHTML 值。
$("DOM-A").click(function() {
$("DOM-B").html('xxx');
})
只是这样的确比较简单,如果不仅仅要修改 DOM-B 呢?还要同时修改 DOM-C、DOM-D等等呢?
$("DOM-A").click(function() {
$("DOM-B").html('xxx');
$("DOM-C").html('xxx');
$("DOM-D").html('xxx');
})
如果修改 DOM-B 的来源不仅仅是点击 DOM-A 呢?也可能是点击了 DOM-C 和 DOM-D,这就会让应用中的数据流动很不清晰。
除此之外,频繁的 DOM 操作会让性能变得比较低,尤其是涉及到 重排 的时候。
React 中则采用了数据驱动的思想,那就是我能不能把这个页面中渲染的数据放到某个地方来管理呢?
只要我修改了这个数据,就会引发页面的重新渲染,这样就不需要直接修改页面。
React 中提供了 state 来管理这些数据。可以参考下面这个 Toggle 组件:
// 只要点击 div 就会修改 state.show 的值,从而触发重新渲染
class Toggle extends React.Component {
state = {
show: false
}
toggle() {
this.setState({
show: !this.state.show
})
}
render() {
<div class="toggle">
<div class="notice">click for toggle</div>
<span>{this.state.show}</span>
</div>
}
}
讲了这么多,那么接下来开始进入正题。
在开始开发 React 之前,这里提供了几种方式来运行 React 应用。
Babel REPL
可以直接在 Babel REPL 中编写代码,实时预览 babel 编译后的结果。
codesandbox
codesandbox 是一个在线网站,可以直接创建 React/Vue/Typescript 等项目,不需要自己去配置 babel。
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<script type="text/babel">
const Header = () => <h1>hello, world</h1>
ReactDOM.render(
<Header />,
document.querySelector("#app")
)
</script>
一般编写 React 的应用,除了需要引入 react 库之外,还需要引入 react-dom 这个库。react-dom 是 react剥离出的涉及 DOM 操作的部分。
react-dom 中最常用的 API 就是 render,在将项目的根组件插入到真实 DOM 节点中时常常要用到。这一步是必要的,不然你的 React 应用就无法挂载到浏览器的 DOM 中。
// index.html
<body>
<div id="app"></div>
</body>
// App.jsx
const App = () => <h1>hello, world</h1>
ReactDOM.render(
<App />,
document.querySelector("#app")
)
JSX 就是 JavaScript XML 的缩写,遵循 XML 的语法规则,语法格式很像模板,但是由 Javascript 实现的。
编写 React 组件的时候,文件名常常以 .jsx
结尾,首字母保持大写。这是为了和原生的 JS 做区分,让别人一眼就看出来这个文件是一个 React 组件。
当然也可以直接用 .js
结尾的文件,这里全部统一为以 .jsx
结尾且首字母大写的格式作为标准。
+ components
- Header.jsx
- Footer.jsx
- Button.jsx
- index.js
jsx 语法类似我们熟悉的 html,允许我们使用尖括号语法来描述组件。
const App = () => {
return (
<div id="app">
<Header />
<div className="body">
</div>
<Footer />
</div>
)
}
这里的 div 就是普通的 html 标签,Header 和 Footer 则是 React 组件。
在 React 中,使用一个组件的时候,可以使用自闭合,也可以使用标签闭合,这点儿和 html 依然类似。
// 对于一个Header组件来说,怎样闭合都可以
<Header />
<Header></Header>
由于 jsx 本质上也是 js,所以保留字 class 和 for 无法在 jsx 直接使用,需要用 className 和 htmlFor 来代替。例如:
const Header = () => <h1 className="header"></h1>
const App = () => {
return (
<div>
<input type="text" />
<label htmlFor=""></label>
</div>
)
}
在 jsx 中使用变量的时候,不管是在属性上面还是元素节点里面,都需要用花括号来包裹。如果不用花括号,就一律当做原始类型的值来处理。
// good
const App = () => {
const text = 'hello, world'
return (
<h1>{ text }</h1> // 输出hello, world
)
}
// bad
const App = () => {
const text = 'hello, world'
return (
<h1> text </h1> // 输出text
)
}
花括号中不仅可以存放变量,甚至还可以存放一段 js 表达式。
// 根据number的值来渲染不同的文本
const App = (props) => {
const number = 100;
return (
<h1>{ number > 100 ? 'big' : 'small' }</h1>
)
}
在 jsx 中想要实现 if...else 和 for 循环也很简单,只要在花括号里面写入表达式就行了。
在 jsx 中可以使用三目运算符来代替 if...else。
const App = () => {
const isShowButton = false;
return (
<div id="app">
{isShowButton ? <button></button> : null}
</div>
)
}
如果想输出一个列表,那么可以在 jsx 中使用 map 函数。但要切记,当使用循环输出列表的时候,需要给子项设置 key。至于 key 的作用是什么,后面我们会讲到。
const List = () => {
const list = [1, 2, 3, 4, 5]
return (
<ul>
{
list.map((item, i) => {
return <li key={i}>{item}</li>
})
}
</ul>
)
}
如果你想直接设置元素的 style 属性,那么 jsx 要求传给 style 一个对象,这个对象里面的属性和原生 DOM 保持一致,规定为必须是驼峰式的。
// 我们将对象styles传给了style属性,styles里面设置了背景颜色background-color
const Button = (props) => {
const styles = {
backgroundColor: props.color
}
return (
<button style={styles}>{ props.text }</button>
)
}
由于 jsx 本质上也是 js,因此在 jsx 里面也可以直接使用 js 的注释。但是要注意,这里一般只能用 /* */
这种注释。
const App = () => {
return (
<p>
{/* 单行注释 */}
{
/*
多行注释符号
多行注释符号
*/
}
</p>
)
}
如果你以前有用过 jQuery 的插件,那么一定对这种结构很清楚吧。
function Employee ( name, position, salary, office ) {
this.name = name;
this.position = position;
this.salary = salary;
this._office = office;
this.office = function () {
return this._office;
}
};
$('#example').DataTable( {
data: [
new Employee( "Tiger Nixon", "System Architect", "$3,120", "Edinburgh" ),
new Employee( "Garrett Winters", "Director", "$5,300", "Edinburgh" )
],
columns: [
{ data: 'name' },
{ data: 'salary' },
{ data: 'office()' },
{ data: 'position' }
]
} );
这是 jQuery DataTable 官网的一个使用例子。DataTable 方法接收了一个对象,这个对象包括 data 和 columns 属性,这两个属性分别代表了这个表格要展示的数据和表格的头部。
这个 DataTable 就是一个组件,这个组件接收了一些数据,输出了对应的界面。
组件化的作用是为了复用相同的界面。比如我们每个页面中都有一个 button 按钮,但每个按钮的颜色都不一样,如果我们在每个页面都编写一个按钮,那么就得不偿失。这时候就应该将按钮封装成一个 Button 组件,根据传入的颜色来展示不同的按钮。
一般来说,React 中的组件有两种,一种是原生组件,即用原生 HTML 标签的组件,一种 React 自定义组件。
原生组件:
const Header = () => <header>hello, world</header> // header就是原生组件
自定义组件:
const App = () => <Header></Header> // 上面创建的 Header 组件就是 React 自定义组件
同时,React 的每个组件必须要有一个根节点,如果有多个根节点就会导致报错,这是因为 React 在创建虚拟 DOM 树的时候,需要有个根节点。
因此,经常会出现不得不在多个 div 外面再套一个 div 的情况,有时候就会使得样式错乱。
所幸的是,React16 之后可以用 React.Fragment 和 <></>
来代替。
// bad
const App = () => {
return (
<p></p>
<p></p>
)
}
// good
const App = () => {
return (
<div>
<p></p>
<p></p>
</div>
)
}
// good
const App = () => {
return (
<>
<p></p>
<p></p>
</>
)
}
// good
const App = () => {
return (
<React.Fragment>
<p></p>
<p></p>
</React.Fragment>
)
}
在 React 中,组件的定义方式有两种。一种是纯函数组件,这种组件本质上是一个函数,它接受一个 props,返回一个 view,只是单纯负责展示的,像 Button 就属于这种组件。
// Button组件接收了一个props,返回了对应的button按钮
const Button = (props) => {
return <button>{ props.text }</button>
}
而另一种组件是类组件,在类组件中会提供更多的功能,比如 state 和生命周期,这种组件允许在内部控制状态,像轮播图、选项卡就属于这种组件。
定义函数组件的时候需要继承 React.Component 这个类。
class Calculation extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
handlePlus = () => {
this.setState({
count: this.state.count + 1
})
}
handleMinus = () => {
this.setState({
count: this.state.count - 1
})
}
render() {
return (
<div className="calculation">
<button onClick={this.handlePlus}>+</button>
<span>{this.state.count}</span>
<button onClick={this.handleMinus}>-</button>
</div>
)
}
}
可以看下效果:
我们来一步步去讲解这个例子。
首先,我们在 Calculation 的构造函数中,手动用 super 调用了 React.Component 这个构造函数并传入了 props,这一步是为了初始化组件中的 props。
其次,在构造函数中定义了 state,react 中的 state 是一个对象。在 render 的时候将 this.state.count
展示了出来。
这里对加减两个符号绑定了点击事件。首先要注意,这里的 handlePlus
和 handleMinus
是用箭头函数定义的,这是为了避免 handlePlus
和 handleMinus
中的 this
丢失。
当然,也可以不用箭头函数来定义 handlePlus 和 handleMinus,这里还有其他几种方法,比如使用 bind 函数。
class Calculation extends React.Component {
constructor(props) {
this.handlePlus = this.handlePlus.bind(this)
this.handleMinus = this.handleMinus.bind(this)
}
handlePlus() {
}
handleMinus() {
}
render() {
}
}
还可以使用双冒号的语法,双冒号相当于 bind,但无法传值。
class Calculation extends React.Component {
constructor(props) {
}
handlePlus() {
}
handleMinus() {
}
render() {
return (
<div className="calculation">
<button onClick={::this.handlePlus}>+</button>
<span>{this.state.count}</span>
<button onClick={::this.handleMinus}>-</button>
</div>
)
}
}
其次,这里通过 this.setState 方法修改了 state 的值。在 React 中,state 的值无法直接修改,只能通过 setState 来修改。
setState 接收一个新的对象或者函数(函数也要返回一个新的对象),最终会合并到现在的 state 中,这一个过程类似 Object.assign(this.state, newState)
。
在我们每次修改 state 的时候,都会重新执行一次 render 方法,将组件进行重新渲染,因此我们看到每次展示出来的都是最新的 state。
上面我们举了 jQuery-DataTable 的例子来说明什么是组件。DataTable 接收了 data 和 columns 两个数据,返回了一个表格。
因此,在 React 的设计理念中,view = f(data)。我们给组件传入对应的数据,最后组件返回了我们想要展示出来的 view。
在 React 中,组件也是接收数据,返回一个 view,只不过这个数据叫 props。
我们来设计一个 Button 组件,这个组件可以根据传入的 props 来展示不同的文字和颜色。
const Button = (props) => {
const styles = {
backgroundColor: props.bgColor
}
return (
<button style={styles}>
{props.text}
</button>
)
}
那么我们在调用的时候可以直接给Button组件传值。
const App = () => {
return <Button bgColor="red" text="submit"></Button>
}
在组件内部通过 props 来拿到外部传给这个组件的值,比如上面的 bgColor 值为 'red' 是 App 组件从外部传给 Button 组件的,Button 组件在内部通过 props.bgColor 又拿到了这个背景颜色。
props 除了可以传递值,也可以传递对象、数组甚至函数等等。
比如我们想给 Button 组件绑定点击事件,允许我们在外部调用,那就可以把事先写好的 onClick
函数传入。在点击 button 按钮的时候,会去执行这个 onClick
函数。
const Button = (props) => {
return (
<button style={{backgroundColor: props.color}} onClick={props.onClick}>
{props.children}
</button>
)
}
const App = () => {
const handleClick = (event) => {
console.log(event.target);
}
return <Button color="red" onClick={handleClick}>submit</Button>
}
props 还提供了类似插槽的功能,我们可以在组件内部通过 props.children
获取到组件的子节点。这种渲染方式可以让我们设计出非常灵活的组件。
假设有个轮播图组件,你想它既能展示图片轮播,又能放入一个 div 展示文字,甚至你还想让它展示表格、视频等等,这个组件该怎么来设计呢?
考虑一下,如果只封装这个组件的各种切换状态,而里面的内容可以让使用者自行插入,这样是不是就很完美了?不管用户插入什么内容,我都会原封不动展示出来。
可以参照下方的这个 Slider:
class Slider extends React.Component {
render() {
return (
<div className="slider">{ this.props.children }</div>
)
}
}
// 调用方式
<Slider>
<img src="1.jpg" />
<Video />
<Table />
</Slider>
最终,被 Slider 组件包裹着的三个组件最终会被渲染到 this.props.children
的位置。
最终渲染出来的结果如下:
<div className="slider">
<img src="1.jpg" />
<Video />
<Table />
</div>
注意,props.children
有可能是 undefined(没有children),有可能是个对象(只有一个children),也有可能是个数组(有多个children)。这取决于传入的 children 数量。
因此,如果需要遍历 children 的时候,需要注意为另外两种值的可能性。直接遍历对象和 undefined 会导致报错。
React 中提供了 React.Children
这个 API,使用 React.Children.map
或 React.Children.forEach
就可以直接遍历了。
function App() {
return (
<Slider>
<h1>hello</h1>
<p>world</p>
</Slider>
);
}
function Slider(props) {
if (!props.children) {
return null;
}
return React.Children.map(props.children, child => {
return React.cloneElement(child)
})
}
我们在前面讲解类组件的时候已经提到了 state,state 类似一个状态机,可以由一种状态转变为另一种状态。
关于状态机,最形象的理解就是马路上的红绿灯,每隔一段时间可以从红灯切换到绿灯,从绿灯再切换到黄灯,这就是一个典型的状态机。
如果想要深入理解状态机在 JS 中的应用,可以参照一下这篇文章:JavaScript与有限状态机
在 React 中,也是由于 state 的改变,从而引发了组件视图的重新渲染。
在 React 中,state 的改变只能通过 setState 方法,setState 方法可以接收一个对象或者函数。
当 setState 接收一个对象的时候,那么最后会将当前组件内的 state 和这个对象做合并,返回的新对象作为当前组件内的 state。
以最开始的这个 Toggle 组件为例子。在每次点击执行 toggle 方法的时候,会调用 setState 方法。setState 接收一个对象,这个对象会和最开始的 this.state
对象做一个合并,这个合并类似 Object.assign
。
如果不考虑 PureComponent 和 shouldComponentUpdate,那么每次执行 setState 都会引发组件的重新渲染,即重新执行一遍 render 函数。
// 只要点击 div 就会修改 state.show 的值,从而触发重新渲染
class Toggle extends React.Component {
state = {
show: false
}
toggle() {
this.setState({
show: !this.state.show
})
}
render() {
<div class="toggle">
<div class="notice">click for toggle</div>
<span>{this.state.show}</span>
</div>
}
}
当 setState 接收一个函数的时候,这个函数会返回一个参数,这个参数就是前一次的 state,最终函数会返回一个新的对象,也会将这个新对象和组件内的 state 做合并。
this.setState(function(prevState) {
return {
...prevState,
count: prevState.count + 1
}
})
由于 setState 的设计是“异步”的,所以如果想立刻拿到更新后的值,最好是使用传入函数的形式。
关于 setState 的“异步”特性,在后面的文章中会进行深入的讲解。
在 React 类组件中提供了丰富的生命周期,允许你在组件渲染、更新和卸载的时候执行某些操作。
这张图是 React16 之前的生命周期图,一共是三个阶段,分别是首次 mounting 阶段、updation 更新阶段、unmouting 卸载阶段。
constructor 函数接收 props 和 context 两个参数并在组件中进行初始化。如果你想要在构造函数中使用 props 和 context,那么必须调用 super 并传入 props 和 context。
由于 React 组件也是个类,所以在渲染阶段会使用 new
操作符来实例化,这一步是在 React 中做的,我们不需要关心。
class App extends React.Component {
constructor(props, context) {
super(props, context);
console.log('props', this.props);
console.log('context', this.context);
}
}
那有时候我们调用 super,什么都不传,在组件中依然能够拿到 this.props
,这是为什么呢?
这是因为 React 在实例化组件的时候会重新设置一遍 props。
const instance = new App(props);
instance.props = props;
在组件第一次渲染之前调用,如果在此时调用setState,将不会引发多次渲染。
在组件渲染成功(插入到dom树中)调用,不是在组件 render 后就调用,而是当所有子组件都触发 render 之后才会被调用。
依赖 DOM 的操作都应该放在这里,如果需要通过网络请求获取数据,也应当放到这里。
componentDidMount() {
fetch('/getUserList').then(function(res) {
return res.json();
})
}
componentWillReceiveProps 会在组件接收新的 props 之前被调用(一般是更新阶段),参数中返回了新的 props。如果这个时候你有需要比较前后两次 props 后再决定更新 state 的操作,那么就可以在这里。
componentWillReceiveProps(nextProps) {
if (nextProps.count !== this.props.count) {
this.setState({
count: nextProps.count
})
}
}
这个函数是在组件更新之前调用的,接收新的 props 和新的 state,最终需要返回一个布尔类型的值,会根据返回的值来判断当前组件是否需要更新。
如果需要进行性能优化(防止无关组件渲染),那么就可以在此处进行处理。
shouldComponentUpdate(nextProps, nextState) {
// 如果返回true,那就是需要更新。如果返回了false,则组件不会进行更新。
}
这个函数是在组件将要更新之前调用,此时 shouldComponentUpdate 已经返回了true。
切记,在这里不能调用 setState,因为 setState 会造成组件更新,最终将造成死循环。
这个函数是在组件更新之后调用,这个时候组件已经执行过了render 方法。
切记,在这里不能调用 setState,因为 setState 会造成组件更新,最终将造成死循环。
componentWillUnmount 是在组件将要卸载时执行的,如果在 componentDidMount 中绑定了原生事件,那么就需要在这里进行解绑。
componentDidMount() {
window.addEventListener('scroll', this.scroll)
}
componentWillUnmount() {
window.removeEventListener('scroll', this.scroll)
}
scroll() {}
在React 16之后,由于现有的 fiber 架构带来的异步渲染,导致了原有的部分生命周期不再适用,componentWillReceiveProps、componentWillMount、componentWillUpdate 三个生命周期将在 React17 移除。
关于新的生命周期,由于篇幅有限,将会放到下篇文章中进行讲解。
作为当前最火的前端框架之一,React 有着很多优秀的理念和设计。在掌握了基本语法之后,如何设计好的组件更需要我们去不断探索。