React
简介因为之前做了挺久的Vue
开发。现在开始学习React
也是并不难的。首先要对React
的整体脉络要有个大概的了解。(注:这里只做学习的记录和总结,有些可能会遗漏,后期有了新的感悟也会慢慢完善,一些基础的语法的使用还是要看官方网站为主)。
从上图可以得知React
分为三大体系:
React.js
ReactNative
ReactVR
学习的顺序也是React.js
->ReactNative
->ReactVR
循序渐进的。
React
的基础大体包括下面这些概念:JSX
Virtual DOM
Data Flow
React.js
不是一个框架,它只是一个库。它只提供UI
(view)层面的解决方案。在实际的项目当中,它并不能解决我们所有的问题,需要结合其它的库,例如Redux
、React-router
等来协助提供完整的解决方法。
React
开发环境的搭建React
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <!--https://cdnjs.com/--> <script src="./js/react.development.js"></script> <script src="./js/react-dom.development.js"></script> <script src="./js/browser.min.js"></script> <script src="./js/prop-types.min.js"></script> </head> <body> <div id="root"></div> <!--凡是使用 JSX 的地方,都要加上 type="text/babel"--> <script type="text/babel"> class Hello extends React.Component { render() { return (<h1>Hello,{this.props.name}</h1>) } } ReactDOM.render( <Hello name="zhangsan" />, document.getElementById('root') ); </script> </body> </html> 复制代码
上面代码一共用了四个库:react.js
、react-dom.js
、browser.js
和prop-types.min.js
,它们必须首先加载。
react.js
:React
的核心库。react-dom.js
:负责Web
页面的DOM
操作。browser.js
:将JSX
语法转为JavaScript
语法。prop-types.min.js
:是传入的props
类型的验证。自React v15.5
起,React.PropTypes
已移入另一个包中,使用prop-types
库代替。需要注意的是从
Babel 6.0
开始,不再直接提供浏览器直接编译的JS
版本,而是要用构建工具构建出来。这里只做演示用。建议学习开发的时候也不要使用浏览器直接引入的方式。
node.js
npm
create-react-app
,类似于Vue
的vue-cli
npm install -g create-react-app // 在任意文件夹中使用create-react-app新建项目 create-react-app demo 复制代码
安装成功之后,
npm start
或者yarn start
就可以启动项目了。
create-react-app
生成的目录结构如下:
demo/ README.md package.json yarn.lock .gitignore node_modules/ public/ favicon.ico index.html ... src/ App.css App.js App.test.js index.css index.js logo.svg serviceWorker.js 复制代码
package.json
安装的React
依赖介绍如下:
从package.json
的dependencies
可以看出来脚手架工具默认安装了React
需要的依赖。下面就介绍这些核心依赖的作用:
react
:是React
的核心库。react-dom
:负责Web
页面的DOM
操作。react-scripts
:生成项目所有的依赖。例如babel
,css-loader
,webpack
等从开发到打包前端工程化所需要的react-scripts
都帮我们做好了。现在就可以在App.js
里编写我们自己的代码了,如下(已修改生成的源码):
在项目根目录执行npm start
(若安装了yarn
可使用yarn start
),打开http://localhost:3000
就可以看到首页了:
建议大家参考Create React App 中文文档
要想理解JSX
的由来,就要先介绍一下Virtual DOM
。
一个真实页面对应一个DOM
树。在传统页面的开发模式中,每次需要更新页面时,都要手动操作DOM
来进行更新。DOM
操作非常昂贵。而且这些操作DOM
的代码变得难以维护。
React
把真实DOM
树转换成JavaScript
对象树,也就是Virtual DOM
。如下图:
每次数据更新后,重新计算Virtual DOM
,并和上一次生成的Virtual DOM
做对比,对发生
变化的部分做批量更新。VirtualDOM
不仅提升了React
的性能,而且它最大的好处其实还在于方便和其他平台集成(比如react-native
是基于Virtual DOM
渲染出的原生控件)。
因此在
Virtual DOM
输出的时候,是输出Web DOM
,还是Android
控件,还是iOS
控件,由平台本身决定。
Web
页面是由一个个HTML
元素嵌套组合而成的。当使用JavaScript
来描述这些元素的时候,这些元素可以简单地被表示成纯粹的JSON
对象。比如,我们现在需要描述一个按钮(button
),用HTML
语法表示非常简单:
<button class="primary"> <em>submit!</em> </button> 复制代码
其中包括了元素的类型和属性。如果转成JSON
对象,会包括元素的类型以及属性:
{ type: 'button', props: { className: 'primary', children: [{ type: 'em', props: { children: 'submit' } }] } } 复制代码
上面的JS
对象表达了一个按钮功能。在表达还不怎么复杂的结构时,书写就已经很难受了。这让我们想起使用HTML
编写结构时的简洁。所以JSX
语法为此应运而生。假如我们使用JSX
语法来重新表达上述button
元素,只需下面这么写:
ReactDOM.render( <button className="primary"> <em>submit!</em> </button> , document.getElementById('root')) 复制代码
JSX
将HTML
语法直接加入到JavaScript
代码中,会让代码更加直观并易于维护。通过编译器转换到纯JavaScript
后由浏览器执行。JSX
在产品打包阶段都已经编译成了纯JavaScript
。
注意:
JSX
是JavaScript
语言的一种语法扩展,长得像HTML
,但并不是HTML
。尽管JSX
是第三方标准,但这套标准适用于任何一套框架。现在已全部采用Babel
的JSX
编译器来实现对JSX
语法的编译。
class Test extends React.Component { constructor(props) { super(props) } render() { return ( <div> <h1 className='title'>Hello,React</h1> </div> ) } } ReactDOM.render(<Test />, document.getElementById('root')) 复制代码
上面的代码经过编译以后会变成如下:
class Test extends React.Component { constructor(props) { super(props) } render() { return ( React.createElement( 'div', null, React.createElement( 'h1', { className: 'title' }, 'Hello,React' ) ) ) } } ReactDOM.render(<Test />, document.getElementById('root')) 复制代码
React.createElement
会构建一个JavaScript
对象来描述HTML
结构的信息,包括标签名、属性、还有子元素等。这样的代码就是合法的JavaScript
代码了。
所以从JSX
到页面经过了如下图的过程:
ReactDOM.render(<div className="foo">Hello</div>, document.getElementById('root')) 复制代码
HTML
里的class
在JSX
里要写成className
,因为class
在JS
里是保留关键字。同理某些属性比如for
要写成htmlFor
。
JavaScript
表达式JSX
遇到HTML
标签(以<
开头),就用HTML
规则解析。遇到代码块(以 {
开头),就用 JavaScript
规则解析。
const names = ['zhangsan', 'lisi', 'wangwu'] ReactDOM.render( <div> { names.map((name,key) => { return <div key={key}>Hello,{name}</div> }) } </div>, document.getElementById('root') ) 复制代码
JSX
允许直接在模板插入JavaScript
变量。如果这个变量是一个数组,则会展开这个数组的所有成员。
const names = [<div key="1">zhangsan</div>, <div key="2">lisi</div>] ReactDOM.render( <div> {names} </div>, document.getElementById('root') ) 复制代码
在JSX
里使用注释也很简单,就是沿用JavaScript
,唯一要注意的是在一个组件的子元素位置使用注释要用 {}
包起来。
const App = ( <Nav> {/* 节点注释 */} <Person /* 多行 注释 */ name={window.isLoggedIn ? window.name : ''} /> </Nav> ) 复制代码
HTML
转义React
会将所有要显示到DOM
的字符串转义,防止XSS
。所以如果JSX
中含有转义后的实体字符比如 ©
(©) 最后显示到 DOM
中不会正确显示,因为React
自动把©
中的特殊字符转义了。可以使用dangerouslySetInnerHTML
来实现。
ReactDOM.render( <div dangerouslySetInnerHTML={{ __html: '© 2020' }} />, document.getElementById('root') ) 复制代码
如果在JSX
中使用的属性不存在于HTML
的规范中,这个属性会被忽略。如果要使用自定义属性,可以用data-
前缀。可访问性属性的前缀aria-
也是支持的。
ReactDOM.render(<div data-attr="abc">content</div>, document.getElementById('root')) 复制代码
React
认为组件是和模板紧密关联的,组件模板和组件逻辑分离让问题复杂化了。React
允许将代码封装成组件(component
),然后像插入普通 HTML
标签一样,在网页中插入这个组件。一个React
应用就是构建在React
组件之上的。Component
(组件)可以是类组件(class component
)、函数式组件(function component
)。
组件有三个核心概念,React
组件基本上由组件的构建方式
、组件内的属性状态
与生命周期方法
组成。如下图:
props
(属性)states
(状态)注意:组件生成的
HTML
结构只能有一个单一的根节点。在React
中,数据是自顶向下单向流动的,即从父组件到子组件。
官方在React
组件构建上提供了3
种不同的方法:React.createClass
、ES6 classes
和无状态
函数(stateless function
)。
官方在
React@15.5.0
后不推荐用React.createClass
,建议使用ES6 class
,这里不做具体介绍。
ES6 classes
class Test extends React.Component { constructor(props) { super(props) } render() { return (<h1>Hello,React</h1>) } } 复制代码
可以用纯函数来定义无状态的组件(stateless function
),这种组件没有状态,没有生命周期,只是简单的接受props
渲染生成DOM
结构。无状态组件非常简单,开销很低。比如使用箭头函数定义:
const Hello = (props) => <div>Hello {props.name}</div> ReactDOM.render(<Hello name="张三" />, document.getElementById('root')) 复制代码
props
就是组件的属性,由外部通过JSX
属性传入设置,一旦初始设置完成,就可以认为this.props
是不可更改的,所以不要轻易更改设置this.props
里面的值。
class Hello extends React.Component { constructor(props) { super(props) } render() { return ( <h1>Hello,{this.props.name}</h1> ) } } // PropTypes 验证,若传入的props type不是string将提示错误 Hello.propTypes = { name: PropTypes.string } // Prop 初始值,如果 prop 没有传入值將會使用 default 值 lisi Hello.defaultProps = { name: 'lisi' } ReactDOM.render(<Hello name="张三" />, document.getElementById('root')) 复制代码
state
state
是组件的当前状态,用this.state
来存取state
。一旦状态(数据)更改,组件就会自动调用 render
重新渲染UI
,这个更改的动作会通过this.setState
方法来触发。
下面代码是一个1000
毫秒就會加一的累加器:
class Timer extends React.Component { constructor(props) { super(props) // 需要自行绑定 this context this.tick = this.tick.bind(this) this.state = { count: 0 } } // 累加器方法,每一秒会使用 setState() 更新内部 state,让 Component 重新 render tick() { this.setState({ count: this.state.count + 1 }) } // 生命周期函数 componentDidMount() { this.interval = setInterval(this.tick, 1000); } // 生命周期函数 componentWillUnmount() { clearInterval(this.interval); } render() { return ( <div>Count: {this.state.count}</div> ) } } ReactDOM.render(<Timer />, document.getElementById('root')) 复制代码
每个组件都包含都包含组件生命周期方法,在运行过程中特定的阶段执行这些方法。
组件的生命周期分为四类,共有10个方法:
下面会依次介绍这几类组件的生命周期所调用的函数。
当组件实例被创建并插入DOM
中时,其生命周期调用顺序如下:
constructor()
static getDerivedStateFromProps()
:会在调用render
方法之前调用,并且在初始挂载及后续更新时都会被调用。render()
:是class
组件中唯一必须实现的方法。componentDidMount()
:在组件挂载后(插入DOM
树中)立即调用。当组件的props
或state
发生变化时会触发更新。组件更新的生命周期调用顺序如下:
static getDerivedStateFromProps()
shouldComponentUpdate()
:当props
或state
发生变化时,shouldComponentUpdate()
会在渲染执行之前被调用。render()
getSnapshotBeforeUpdate()
:在最近一次渲染输出(提交到DOM
节点)之前调用。componentDidUpdate()
:会在更新后会被立即调用。首次渲染不会执行此方法。当组件从DOM
中移除时会调用如下方法:
componentWillUnmount()
:在组件卸载及销毁之前直接调用。当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:
static getDerivedStateFromError()
:此生命周期会在后代组件抛出错误后被调用。componentDidCatch()
:此生命周期在后代组件抛出错误后被调用。这里只做简单介绍,后面会单独写文章详细记录生命周期的使用方式。
React
里面绑定事件的方式和在HTML
中绑定事件类似,使用驼峰式命名指定要绑定的onClick
属性为组件定义的一个方法。下面是一个数字累加的小例子:
class AddCount extends React.Component { constructor(props) { super(props) this.state = { count: 0 } } handleClick(e) { this.setState({ count: this.state.count + 1 }); } render() { return ( <div> <p>Count: {this.state.count}</p> <button onClick={this.handleClick.bind(this)}>Click me</button> </div> ) } } ReactDOM.render(<AddCount />, document.getElementById('root')) 复制代码
注意要显式调用
bind(this)
将事件函数上下文绑定要组件实例上,这也是React
推崇的原则:没有黑科技,尽量使用显式的容易理解的JavaScript
代码。
DOM
操作有时候我们避免不了要直接操作DOM
。React
也提供了几种我们可以直接操作DOM
的方式。
findDOMNode
当组件加载到页面上之后(mounted
),可以通过react-dom
提供的findDOMNode()
方法拿到组件对应的DOM
元素。
class Test extends React.Component { constructor(props) { super(props) } componentDidMount() { const el = ReactDOM.findDOMNode(this) // 123456 console.log(el.textContent) } render() { return ( <div>123456</div> ) } } ReactDOM.render(<Test />, document.getElementById('root')) 复制代码
注意:
findDOMNode()
不能用在无状态组件上。findDOMNode
仅在组件始终返回永不更改的单个DOM
节点时才起作用。在<React.StrictMode>
严格模式下,这个方法已经被官方弃用,所以在开发中不要使用这个方法。
Refs
另外一种方式就是通过在要引用的DOM
元素上面设置一个ref
属性指定一个名称,然后通过 this.refs.name
来访问对应的DOM
元素。
class Test extends React.Component { constructor(props) { super(props) } componentDidMount() { this.refs.textInput.focus() } render() { return ( <div> <input ref="textInput" /> </div> ) } } ReactDOM.render(<Test />, document.getElementById('root')) 复制代码
不要在 render
或者render
之前访问refs
。不要滥用 refs
。比如只是用它来按照传统的方式操作界面UI:找到 DOM -> 更新 DOM
。
https://zh-hans.reactjs.org/
http://huziketang.mangojuice.top/books/react/
https://www.bilibili.com/video/BV1g4411i7po?p=2
https://www.jianshu.com/p/c6040430b18d
https://www.zhihu.com/question/336664883/answer/790855896
https://book.douban.com/subject/26918038/
https://www.kancloud.cn/digest/babel/217110
http://www.ruanyifeng.com/blog/2015/03/react.html
https://kdchang.gitbooks.io/react101/content/
https://github.com/mocheng/react-and-redux/issues/99
https://juejin.im/entry/587de1b32f301e0057a28897
https://www.html.cn/create-react-app/docs/getting-started/
本文使用 mdnice 排版