虚拟DOM
如果对DOM节点进行修改操作,一般情况下,每次修改都会直接操作DOM树。而React的虚拟DOM技术不一样,它会对比开始状态与最后的状态,当状态一致时,便不会对DOM树进行操作。最后的渲染结果是有diff算法控制的,React只对真正有改变的节点进行渲染。
搭建开发环境
方式一:React CDN资源
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!--编译JSX-->
<script src="https://cdn.bootcss.com/babel-standalone/6.26.0/babel.min.js"></script>
方式二:npm安装React
npm install --save react react-dom
方式三:create-react-app脚手架
npm install -g create-react-app
create-react-app myapp
端口号
在node_modules/react-scripts/scripts/start.js文件中可以修改默认端口号3000
目录说明
- node_modules:放置项目需要的各种依赖模块
- package-lock.json:记录实际安装的各个包的来源以及版本号。
- package.json:定义依赖关系树,以及各个依赖模块的版本范围
- public:里面的文件用于被index.html引用。
- src:存放源码等,webpack只识别这个目录
调试
安装React Developer Tools的chrome插件
React事件系统
React事件系统在原生的DOM事件体系上封装了一个“合成事件”层,事件处理程序通过合成事件进行实例传递。它没有把所有事件绑定到对应的真实DOM上,而是使用委托机制实现统一的事件监听器,把所有的事件绑定到了最外层document上,然后再将事件进行分发。
- 组件上绑定
- 构造函数中绑定
- 箭头函数绑定
React原理
JSX
-
JavaScript创建虚拟DOM
-
JSX创建虚拟DOM
书写JSX时,
<script>
的type为“text/babel”,把JSX转为浏览器能够理解的js代码
JSX样式:
<button style="background-color:red">按钮</button>
class Hello extends React.Component{
render(){
var bgColor={backgroundColor:"red"}
return <button style={bgColor}>按钮</button>
}
}
return <button style={{backgroundColor:"red"}}>按钮</button>
dom-diff
UI进行更新时,React会将当前数据和前一状态的数据进行对比,判断哪些数据发生了变化,接着把发生变化的数据渲染在UI上。Web界面实质上是构建的一棵DOM树,当某一节点发生变化时,React会对当前的DOM树和前一状态的DOM树进行比较,这个比较的算法就是dom-diff算法。
React的diff算法的两个假设:
- 不同类的元素会产生不同的DOM树
- 同一层次的一组节点可以通过唯一的key值区分
在React之前有个标准dom-diff算法,针对任意两棵树找最小变化步骤,这个算法时间复杂度为O(n3)。而基于上述两条假设,React的diff算法可以将算法复杂度减少到O(n)。
React的同层次比较:
React对DOM树进行了分层,只会对同一层的节点进行数据比较。如果节点类型发生变化,React会将其删除,然后新建节点到新的DOM树上。如果节点类型相同、属性不同,那么React会进行替换操作。如果遇到同一层级的子节点进行操作时,需要加上key属性来进行唯一区别,否则React会进行告警。key的唯一属性是避免删除、创建等重复操作,减少性能消耗。
用示例解释React的dom-diff算法:
整个变化过程为:C节点从A节点转移到了D节点上。React对整棵DOM树的操作步骤为:
(1)删除C节点。
(2)创建C节点。
(3)更新B节点。
(4)更新A节点。
(5)渲染C节点。
(6)更新D节点。
(7)更新R节点。
setState
React组件可以理解为一个状态机。组件的更新其实就是内部state值的更新,state属性记录着组件的状态。而修改state的方法就setState。(示例)
class Like extends React.Component{
constructor(){
super()
this.state={
isLiked:true
}
this.changeState=this.changeState.bind(this)
}
changeState(){
console.log(this.state.isLiked)//1.打印出true
this.setState({
isLiked:!this.state.isLiked
})
console.log(this.state.isLiked)//2.打印出true 可见是异步操作
}
render(){
return(
<input type="button" onClick={this.changeState} value={this.state.isLiked?"点赞":"取消"}/>
)
}
}
ReactDOM.render(<Like/>,document.getElementById('root'))
setState()方法为异步操作,它是通过一个队列机制来更新state。在执行changeState()方法中的this.setState时,React先把新state值放到了一个状态队列中,并没有及时更新state值。
React中的setState异步原理:
class Like extends React.Component{
constructor(){
super()
this.state={
count:0
}
this.showState=this.showState.bind(this)
}
componentDidMount(){
this.setState({count:this.state.count+1})
this.setState({count:this.state.count+1})
this.setState({count:this.state.count+1})
console.log('setState后:',this.state.count)
}
showState(){
console.log('showState中的state后:',this.state.count)
}
render(){
return(
<input type="button" onClick={this.showState} value="显示state"/>
)
}
}
结果可以看出,虽然在componentDidMount()
方法中执行了三次setState,但是最后就执行了一次,并且没有立即执行。原因是React先把新的state值放到了状态队列里,最后对三次setState进行了批量处理,批处理过程中进行了合并,所以结果就是对setState只执行了一次,这样做的好处是可以避免一些重复渲染setState()可以理解为一个重新渲染的请求,而不是立即更新的一个命令,为了有更好的性能,React可能会推迟执行,然后会批量进行处理。React不会保证在setState操作后立即拿到最新的state值。
如果想在setState后立即得到最新的state值,可以通过函数来实现:
this.setState(
function(state){
return {
count:state.count+1
}
}
);
在实际项目中不要用this.state来修改state值,因为获取到的this.state有可能不是最新的,更新组件state时要用setState()方法。
React组件写法
-
React.Component写法
class HelloWord extends React.Component{ state={message:'你好呀!'} render(){ return <h1>{this.state.message}</h1> } }
-
无状态函数写法(没有state参与,只接受props,以减少耦合度)
const HelloWord=(props)=>{ return <h1>你好啊!{props.name}</h1> }
function HelloWord(props) { return <h1>你好啊!{props.name}</h1> }
React组件分类
React的组件分类,主要是以两方面为标准的:一方面是呈现,一方面是管理。按这种标准可分为木偶组件和智能组件(对比示例)
-
引入:获取数据与ui渲染放在一起,显得很臃肿。如果把获取数据和UI渲染分开,分别放在一个独立的组件中,代码的可读性、可维护性、可复用性更好
-
木偶组件:呈现UI职责,不涉及数据操作
-
智能组件:更多考虑逻辑操作性问题
MVC框架
MVC框架是业界广泛接受的一种前端应用框架类型,这种框架把应用分为三个部分:
- Model(模型)负责管理数据,大部分业务逻辑也应该放在Model中;
- View(视图)负责渲染用户界面,应该避免在View中涉及业务逻辑;
- Controller(控制器)负责接受用户输入,根据用户输入调用对应的Model部分逻辑,把产生的数据结果交给View部分,让View渲染出必要的输出。
MVC框架提出的数据流很理想,用户请求先到达Controller,由Controller调用Model获得数据,然后把数据交给View,但是,在实际框架实现中,总是允许View和Model可以直接通信
在图中,可以看到Model和View之间缠绕着蜘蛛网一样复杂的依赖关系,根据箭头的方向,我们知道有的是Model调用了View,有的是View调用了Model
Flux框架
Mobx框架
Mobx是一个用于状态管理的库,和React是一对很搭的组合。React在View层提供了很好的解决方案,Mobx对状态管理具有极大的简易性和可扩展性,二者搭配,能够很好地管理一些大型前端项目。Mobx的工作原理大概如下图示。在整个数据流中,首先用户的触发事件到达Actions中,然后依据事件属性及事件要求在State中对状态值进行修改,接下来用新的State数据计算所需要的Computed values,最后响应渲染UI视图层:
Redux框架
Redux生态
redux middleware
Redux主要输出createStore、combineReducers、bindActionCreators、applyMiddleware、compose五个接口。
Redux的核心是控制和管理所有的数据输入与输出,使用纯函数dispatch派发action来更改数据,其功能简单且固定。middleware允许我们dispatch一个action之后,在到达reducer之前先做一些额外的处理,可以理解为每一个middleware都在增强dispatch的功能。
redux-logger
能够对所有action发生后生成的state进行记录,即日志中间件
redux-thunk
用于在redux中处理异步action的中间件,异步action的场景包括需要在action中执行setTimeout,以及需要通过fetch方法调用服务端API等,对于异步action的场景可以使用redux-thunk中间件来优化代码流程
redux-saga
与redux-thunk功能类似,也是主要集中管理React应用中的异步操作。redux-saga最大的特点是使用generator(ES6)的形式,采用监听的形式进行工作,可以使用同步的方式编写异步代码,使得项目流程拆分更细,应用的逻辑和视图拆分更加清晰,分工明确。
React架构
React不仅仅是一个库,也不是一个框架,而是一个庞大的生态体系。若希望充分使用React尽可能多的特性,整个技术栈都要相应地配合改造,则我们要学习从后端到前端的一整套解决方案。因此,使用React时,合理的选择是采用React的整个技术栈。
一、文件结构
采用React+Redux+React-Router+Less+ES6+webpack介绍React实现一个完整应用的文件结构。React的CLI脚手架工具create-react-app屏蔽了和React无关的配置,如Babel、webpack。下面我们使用这个工具快速创建一个项目:
npm install -g create-react-app
create-react-app hello-world
二、CSS方案
一些css相关的问题有:全局污染、命名混乱、依赖引入复杂、无法共享变量、代码冗余。
CSS Modules是一种非常优秀的模块化解决方案。CSS Modules结合了CSS生态和JavaScript模块化能力,使用JavaScript来管理样式依赖,加入了局部作用域和模块依赖。CSS Modules发布时依旧编译出单独的JavaScript和CSS。
局部样式
CSS Modules通过使用一个独一无二的class的名字产生局部作用域:
import React from 'react'
import style from './App.css'
export default()=>{
return(
<div className={style.wrapper}>
<h1 className={style.title}>Hello!</h1>
</div>
)
}
其中App.css:
.wrapper{
750px;
margin:0 auto;
}
.title{
color:red;
font-size:48px;
}
webpack.config.js中配置css-loader启用CSS Modules:
module.export={
entry:__dirname+'/index.js',
output:{
publicPath:'/',
filename:'./bundle.js'
},
module:{
loaders:[
{
test:/.jsx?$/,
exclude:/node_modules/,
loader:'babel',
query:{
preset:['es2015','stage-0','react']
}
},
{
test:/.css$/,
loader:"style-loader!css-loader?modules&localIdentName=[name]__[local]-[hash:base64:5]"
}
]
}
}
localIdentName用于设置生成样式的命名规则,css-loader默认的哈希算法是[hash:base64],这会将.title编译成._3zyde4l1yATCOkgn-DBWEL这样的字符串。本示例中我们自定义的编译规则生成的HTML是,App.css也会同时被编译。CSS Modules对CSS中的class名都做了处理,使用对象来保存原class和混淆后class的对应关系:
通过这些处理,CSS Modules可以继续使用CSS编写样式,相当于给每个class名外加了一个:local,使得所有样式都是局部样式,以此来实现样式的局部化,解决了命名冲突和全局污染问题。
全局作用域
CSS Modules使用:global(.className)的语法,声明一个全局规则。凡是这样声明的class,都不会被编译成哈希字符串:
.normal{
color:green
}
//等价
:local(.normal){
color:green
}
:global(.btn){
color:red;
}
//定义多个全局样式
:global{
.link{
color:red;
}
.box{
color:yello
}
}
组合样式
在CSS Modules中,一个选择器可以继承另一个选择器的规则,称为“组合”。让.title继承.titleBase:
.base{
background-color:blue;
}
.title{
composes:base;
color:red
}
.wrapper{
composes:base;
750px
}
PostCSS
PostCSS是一款对CSS进行处理的工具,主要依赖插件来进行操作。PostCSS是一个利用JavaScript插件来对CSS进行转换的工具,较常使用的插件有CSS Modules、Autoprefixer、postcss-cssnext等。
CSS Modules
npm install --save postcss-loader postcss-modules-values
在webpack.config.js中增加postcss-loader配置:
var values=require('postcss-modules-values')
module.export={
entry:__dirname+'/index.js',
output:{
publicPath:'/',
filename:'./bundle.js'
},
module:{
loaders:[
{
test:/.jsx?$/,
exclude:/node_modules/,
loader:'babel',
query:{
preset:['es2015','stage-0','react']
}
},
{
test:/.css$/,
loader:"style-loader!css-loader?modules!postcss-loader"
}
]
},
postcss:{
values
}
}
定义变量
@value blue #0c77f8
@value red #ff0000
引用
@values colors:"./colors.css";
@values blue,red from colors;
.title{
color:red;
background-color:blue
}
Autoprefixer
针对浏览器兼容的处理可以使用Autoprefixer插件。Autoprefixer是一个根据Can IUse(http://caniuse.com)兼容性解析CSS,然后为其添加浏览器厂商前缀的PostCSS插件。
::example{
display:none;
position:relative;
transform:translate(10,10)
}
Autoprefixer将使用基于当前浏览器支持的特性和属性数据来添加前缀,处理之后生成:
.example{
display:none;
position:relative;
-webkit-transform:translate(10,10);
-ms-transform:translate(10,10);
transform:translate(10,10);
}