@gyyin
2019-11-24T09:06:29.000000Z
字数 12294
阅读 1376
慕课专栏
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.jsxconst 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 中使用变量的时候,不管是在属性上面还是元素节点里面,都需要用花括号来包裹。如果不用花括号,就一律当做原始类型的值来处理。
// goodconst App = () => {const text = 'hello, world'return (<h1>{ text }</h1> // 输出hello, world)}// badconst 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-colorconst 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 和 <></> 来代替。
// badconst App = () => {return (<p></p><p></p>)}// goodconst App = () => {return (<div><p></p><p></p></div>)}// goodconst App = () => {return (<><p></p><p></p></>)}// goodconst 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 有着很多优秀的理念和设计。在掌握了基本语法之后,如何设计好的组件更需要我们去不断探索。