1. 虚拟DOM
(1) 虚拟DOM说明
- 本质是Object类型的对象
- 虚拟DOM比较‘轻’(包含的属性较少),真实DOM比较‘重’。因为虚拟DOM是React内部在用,无需真实DOM上那么多属性。
- 虚拟DOM最终会被react转化为真实DOM呈现在页面中。
(2) 构建虚拟DOM的方法
- 使用jsx语法直接写虚拟dom
- React.createElement(标签名,标签属性,标签内容)方法创建
React.createElement('h1', {id: 'title'}, 'hello React')
(3) 虚拟DOM中的key的作用
当状态中的数据发生变化时,react会根据新数据生成【新的虚拟DOM】,随后进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则:
a. 新虚拟DOM中找到了与旧虚拟DOM中相同的key
若虚拟DOM中内容没变,直接使用之前的真实DOM
若虚拟DOM中内容变了,则生成新的真实DOM替换掉页面中之前的真实DOM
b. 新虚拟DOM中没找到与旧虚拟DOM中相同的key
根据数据创建新的真实DOM放到页面中
3. jsx语法规则
- 定义虚拟DOM时不用加引号
- 标签中混入js表达式时要用{},注释也需要用大括号包裹{}
- 样式类名指定使用className非class,
- 内联样式要用style={{key:value}}的形式写
- 虚拟DOM只有一个根标签
- 标签必须闭合;标签首字母:
(1)若小写字母开头,则将该标签转换为html中的同名标签,若html中无该标签对应的同名元素则报错
(2)若大写字母开头,React则去渲染对应的组件,若组件没有定义则报错
- 标签体内容是一个特殊的标签属性,可以使用属性形式书写(key:children,value:xxx),并且在子组件中可以通过this.props.children获取到父组件的标签体
const myStyle = {
background: blue,
fontSize: 30
}
const vDom = <span className={myclass} style={myStyle}>{mycontent}</span>
ReactDOM.render(
<div>
{/*我是注释*/}
{vDom}
</div>
)
import Child1 from './component/child1'
import Child2 from './component/child2'
<Child1>我是标签体内容</Child1>
<Child2 children="我是标签体内容" />
<h1>{this.props.children}</h1>
<h1 children={this.props.children} />
const arr = [
{ name: 'zhangsan', age: 18 },
{ name: 'lisi', age: 12 }
]
const vDom = (
<ul>
{
arr.map((item, index) =>
<li key={index}>姓名:{item.name} 年龄:{item.age}</li>
)
}
</ul>
)
4. 组件化(函数式组件、类式组件)
执行了ReactDOM.render(<MyComponent...)之后发生了什么?
- react解析组件标签,找到了MyComponent组件,发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM呈现在页面中
- react解析组件标签,找到了MyComponent组件,发现组件是使用类定义的,随后New出来该类的实例,并通过该实例调用到原型上的render方法,将render返回的虚拟DOM转为真实DOM呈现在页面中
函数式组件:没有this,无法使用state、refs,后期可以通过hooks访问
function Demo(props) {
const {name, age} = props
return (
<h1>我是函数式组件</h1>
)
}
ReactDOM.render(<Demo name="tom" age={18} />, document.getElmentById('root'))
类式组件
render是放在组件的原型对象上,供组件实例使用
(1)组件props传值
字符串等简单类型数据直接传,对象、数组等用{}包裹,组件中获取到传入值为简单数据类型的数组,自动将数组项提取成字符串加空格拼接展示
import React from 'react';
import PropTypes from 'prop-types';
class MyComponent extends React.Component {
static defaultProps = {
name: 'a',
age: 18
}
static propTypes = {
name: PropTypes.string.isRequired,
age: Proptypes.oneOfType([PropTypes.string, PropTypes.number])
}
render() {
return (
<div>
<p>name:{this.props.name}</p>
<p>name:{this.props.obj.name} age:{this.props.obj.age}</p>
<p>arr:{this.props.arr}</p>
</div>
)
}
}
ReactDom.render(
<MyComponent name="qing" age={8} obj={{name:'qing',age:18}} arr={['haha','lala']} />,
document.getElementById('app')
)
const obj = {name: 'qing', age: 8, obj: {name: 'qing', age: 18}, arr: ['haha', 'lala']}
ReactDom.render(
<MyComponent { ...obj } />,
document.getElementById('app')
)
(2)onXxxx属性指定事件处理函数
React使用的是自定义(合成)事件而非DOM的原生事件
React中的事件是通过事件委托的方式处理的,委托给组件最外层元素
(3)组件ref的使用
- 创建容器存储ref:使用React.createRef()方法创建容器,绑定的dom元素存在容器的current中
- 回调函数式ref:ref={currentNode => this.inputNode = currentNode }
- 类绑定式ref,类似回调函数式
class RefExample extends React.Component {
constrctor(props) {
super(props)
}
inputRef = React.createRef()
setInputRef = (c) => {
this.input3 = c
}
render() {
return (
<div>
{/* 创建容器存储ref */}
<input ref={this.inputRef} type="text" />
<button onClick={() => this.focusInput()}>绑定事件方法一</button>
{/* 回调函数式ref;与class绑定式的区别是,内联运行完会释放,视图更新时会再次调用且调用两次,第一次接收到的参数为null(清空) */}
<input ref={currentNode => this.input2 = currentNode} type="text" />
<button onClick={this.inputFocus}>绑定事件方法二</button>
{/* class绑定式ref */}
<input ref={this.setInputRef} type="text" />
</div>
)
}
focusInput() {
this.inputRef.current.focus()
}
inputFocus = () => {
this.input2.focus()
}
}
4⃣️组件生命周期(新版:17.0.1起)
卸载组件
ReactDOM.unmountComponentAtNode(卸载的dom根节点)
初始化阶段:由ReactDOM.render()触发
constructor
componentWillMount(旧) --> UNSAFE_componentWillMount(新):
组件将要挂载
getDerivedStateFromProps(新)
组件的静态方法,接收props,state参数,必须指定返回值(限制返回组件状态对象或null),state的值取决于返回的状态对象值无法通过修改状态值变更
render:
初始化渲染调用一次
componentDidMount
组件挂载完毕调用一次
更新阶段:由组件内部this.setState()或父组件重新render触发
componentWillReceiveProps(旧)-->UNSAFE_componentWillReceiveProps(新)
组件将要接收新的props,可接收到props参数;(父组件重新render后调用,第一次收到传值不调用)
shouldComponentUpdate:
setState更新状态数据后,控制组件更新的‘阀门’,可以接收nextProps、nextState两个参数,返回布尔值。 注:调用组件实例的forceUpdate方法,在不更新状态里面数据的情况下强制组件更新,可跳过‘阀门’钩子
componentWillUpdate(旧) --> UNSAFE_componentWillUpdate(新)
组件将要更新
render
每次状态更新后调用
getSnapshotBeforeUpdate(新):
在页面更新前获取快照;必须指定返回值,返回值作为第三个参数传递给下一个钩子
componentDidUpdate
组件更新完成,收到两(三:新版)个参数preProps,preState,snapshotValue
卸载阶段:由ReactDOM.unmountComponentAtNode(container)触发
componentWillUnmount
组件将要被卸载
componentDidCatch
错误处理:一般用于统计错误,反馈给服务器,通知开发人员处理bug
5. Dom
(1)input框双向绑定
class MyInput extends React.Component {
constructor () {
this.state = {
userName: '',
userPassword: ''
}
}
render() {
const { userName, userPassword } = this.state
return (
<form onSubmit={() => this.submit()}>
<input type="text" value={userName} onChange={(e) => this.userChange(e)}/>
<input type="password" value={userPassword} onChange={(e) => this.pwdChange(e)}/>
</form>
)
}
submit() {}
userChange(e) {
this.setState({ userName: e.target.value })
}
}
6.Redux
(1)简介
三方库redux,类似Vue中的Vuex,集中式管理react应用中多个组件共享的状态;action、store、reducer三个核心概念
- action:
同步action为对象,包含type和data两个属性({type: 'ADD_COUNT', data: 2});
异步action为函数,依赖redux-thunk库,store读到action为函数时,先调用函数执行一下,而非直接交给reducer处理;异步action中一般会调用同步action,在相应的时间处理数据
- reducer:初始化、加工状态,加工时根据旧的state和action,产生新的state的纯函数,combineReducers用来合并reducers;
*注:redux中的reducer必须是一个纯函数*
- store:将state、action、reducer联系在一起的对象
----------------得到store对象----------------------
import {createStore, combineReducers} from 'redux'
import countReducer from './reducers/count'
import personReducer from './reducers/person'
const allReducers = combineReducers(
{count: countReducer, person: personReducer})
const store = createStore(allReducers)
--------------支持异步action的store对象-----------
import {createStore, applyMiddleware} from 'redux'
import reducer from './reducers'
import thunk from 'redux-thunk'
const store = createStore(reducer, applyMiddleware(thunk))
---------------store对象的功能---------------------
getState():得到state
dispatch(action):分发action,触发reducer调用,产生新的state
subscribe(listener):注册监听,产生新的state时候自动调用;通常在index.js文件中使用,监听到变化时重新render整个app;
store.subscribe(ReactDOM.render(<App/>,document.getElementById('root')))
(2)redux文件结构
src
│ components
│ static
| App.js
| index.js
│ ...
└───store
│ index.js
| constant.js 放actions的type变量
└───actionCreators
│ count.js
│ person.js
└───reducers
│ count.js
│ person.js
(3)使用官方react-redux对状态管理进行优化
src
│ components 放一般组件
│ container 放容器组件,内部定义对应UI组件
│ static
| App.js
| index.js
│ ...
└───store
│ index.js
| constant.js
└───actionCreators
│ count.js
│ person.js
└───reducers
│ count.js
│ person.js
- 将组件区分为容器组件与UI组件,容器组件负责与store做交互,UI组件中通过属性调用的方式实现交互
- 在容器组件中引入react-redux中的connect方法来连接UI组件与redux
import React, {Component} from 'react'
import {connect} from 'react-redux'
import {xxxxAction} from '../../store/actionCreators'
class Count extends Component {
render() {
return ...
}
}
export default connect(
state => ({key: value}),
{ key: xxxxAction }
)(Count)
import React, {Component} from 'react'
import Count from './container/Count'
import store from './store'
export default class App extends Component {
render() {
<div>
<Count store={store} />
</div>
}
}
...
import store from './store'
import {Provider} from 'react-redux'
ReactDOM.render(
{}
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
* 结合react-redux使用状态管理工具的优点
- 无需在index.js中自己通过store.subscribe()检测redux中状态的改变,connect内部自动完成这个工作
- 无需自己给容器组件中传入store,给App包裹一个\<Provider store={store}>即可
7.react-router(web端使用react-router-dom库)
- 项目路由用路由器BrowserRouter/HashRouter管理,整个项目统一使用一个路由器,通常包裹在入口index.js文件中
- BrowserRouter:Bom中history的应用
- HashRouter:url中的哈希运用,刷新后会导致路由state参数的丢失,可以解决一些路径错误相关的问题
- 路由链接Link、NavLink负责切换组件,指定跳转的路由地址
- 路由Route负责注册路由,指定路由组件
- 路由组件可以收到路由器传递的三个props属性history、location、match
history ---> go、goBack、goForward、push、replace
location ---> hash、pathname、search、state
match ---> params、path、url
- 嵌套路由:注册子路由时要写上父路由的path值
- 路由的匹配值按照注册路由的顺序进行
- withRouter:加工一般组件,让一般组件中具备路由组件特有的属性api
{}
import { BrowserRouter } from 'react-router-dom'
import App from './App'
ReactDom.render(<BrowserRouter><App/></BrowserRouter>, document.getElementById('root'))
import { Switch, Link, NavLink, Route, Redirect } from 'react-router-dom'
import About from './pages/About'
import Home from './pages/Home'
<Link className="xxx" to="/about">About</Link>
<Link className="xxx" to="/home">Home</Link>
<NavLink activeClassName="xxx" className="xxx" to="/about/a/b" exact>About</NavLink>
<NavLink activeClassName="xxx" className="xxx" to="/home/a/b" children="Home" />
<Switch>
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
<Redirect to="/about"/>
<Switch/>
给路由组件传值的三种方式
- 通过params给路由组件传值:路由链接中传入参数,注册路由时声明接收
- 通过search给路由组件传值:路由链接中传入参数,注册路由无需声明接收
- 通过state给路由组件传值:路由链接中传入参数,注册路由无需声明接收;地址栏无信
息,state信息保存在history对象中,刷新时依然保留住参数,清理history缓存后参数
无法保留(HashRouter的方式刷新后会丢失state参数)
<Link to={`/home/message/detail/${参数1变量}/${参数2变量}`}>Message</Link>
<Route path="/home/message/detail/:id/:title" component={Detail}/>
<Link to={`/home/message/detail?id=${参数1变量}&title=${参数2变量}`}>Message</Link>
<Route path="/home/message/detail" component={Detail}/>
<Link to={{pathname:'/home/message/detail', state: {id:参数1变量,title:参数2变量}}}>Message</Link>
<Route path="/home/message/detail" component={Detail}/>
- 路由包括push与replace模式:默认push模式
- 编程式路由导航
<Link to="/home/message/detail">Message</Link>
<Route replace path="/home/message/detail" component={Detail}/>
render() {
return (
<button onClick={() => {this.replaceShow(id参数,title参数)}}>replace跳转</button>
{}
<Route path="/home/message/detail" component={Detail}/>
)
}
replaceShow = (id,title) => {
this.props.history.replace(`/home/message/detail/${id}/${title}`)
this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`)
this.props.history.replace('/home/message/detail', {id,title})
}
import React, {Component} from 'react'
import {withRouter} from 'react-router-dom'
class Header extends Component {
render() {
const {history, params, match} = this.props
}
}
export default withRouter(Header)
React浏览器调试插件
React Developer Tools、Redux DevTools
Redux DevTools依赖redux-devtools-extension插件,在store的代码中如下配置:
此处省略部分引用
...
import {composeWithDevTools} from 'redux-devtools-extension'
export default createStore(reducers, composeWithDevTools(ApplyMiddleware(thunk)))
补充知识点
css模块化引入:
- 文件名中加入module(如index.module.css),组件中以import Xxx from 'xxxx'形式引入,模板中样式可以用Xxx.xxx形式访问定义的类名等
setState:是同步方法,但是调用之后引起react后续更新状态的动作是异步的
- 对象式:setState(stateChange, [callback]) 不依赖于原状态时推荐使用
callback是在状态更新完毕、界面也更新后(render调用后)才被调用
- 函数式:setState(updater, [callback]) 依赖原状态时推荐使用
updater为返回stateChange对象的函数,可以接收到state和props参数
组件间通信方式整理
- props(childrenProps、renderProps):父子组件
- 集中式状态管理工具(redux):兄弟组件、祖孙组件(跨级组件)
- context(生产者--消费者模式):祖孙组件(跨级组件)
- 消息订阅与发布机制,依赖库pubsub-js:兄弟组件、祖孙组件(跨级组件)
```
import Pubsub from 'pubsub-js'
Pubsub.subscribe('订阅的消息名', (msg, data) => {
)
Pubsub.public('发布的消息名', 发布的消息内容)
Pubsub.unsubscribe('取消订阅的消息名')
```
Context:一种组件间通信方式,常用于祖组件与后代组件通信;在react应用中一般不用context,一般用来封装react插件
```
const MyContext = React.createContext()
class A extends Component {
state = {username: 'tom', age: 18}
render() {
return (
<div>
{}
<MyContext.Provider value={this.state.username}>
<B/>
</MyContext.Provider>
</div>
)
}
}
class B extends Component {
static contextType = MyContext
render() {
return (
<div>
<h1>我是B组件<h1>
<div>我接收到A组件人名为:{this.context}</div>
</div>
)
}
}
class C extends Component {
render() {
return (
<div>
<h1>我是C组件<h1>
<div>我接收到A组件人名为:
{}
<MyContext.Consumer>
{
value => {
return <span>{value}</span>
}
}
</MyContext.Consumer>
</div>
</div>
)
}
}
```
lazyload:路由组件懒加载,依赖react中的lazy、Suspense
- 引入懒加载的组件:const Home = lazy(() => {import('./xx')})
- 注册路由时用Suspense包裹,指定等待加载时展示的内容,fallback可以传虚拟dom也可以传组件,传组件时不可以是懒加载组件。
```js
<Suspense fallback={<h1>Loading</h1>}>
<Route path="/home" component={Home}/>
</Suspense>
```
hooks: ^16.8.0版本,在函数式组件中使用state及其他react特性
- stateHook:state管理功能,通过React.useState(initState值)实现
- effectHook:生命周期功能,通过React.useEffect()实现
- refHook:类似React.createRef()功能,通过React.useRef()实现
```
function Demo() {
const [count, setCount] = React.useState(1)
const [name, setName] = React.useState('tom')
const myRef = React.useRef()
React.useEffect(() => {
return () => {
}
},[count])
function add() {
setCount(count + 1)
}
function changeName() {
setName('jerry')
}
return (
<div>
<h2>当前数字为:{count}</h2>
<button onClick={add}>点我加1</button>
<h2>我的名字是:{name}</h2>
<button onClick={changeName}>点我改名</button>
<input ref={myRef} type="text" />
</div>
)
}
```
Fragment:类似Vue中的template元素,Fragment标签可以拥有一个key属性
```
import {Fragment} from 'react'
class Demo extends Component {
render() {
return (
<Fragment>
<h2>...</h2>
<input type="text"/>
</Fragment>
)
}
}
```
Component的两个问题:
- 只要执行setState(),即使不改变状态数据,组件也会重新render()
- 只要当前组件重新render(),就会重新render所有的子组件,即使子组件没有用到当前组件的任何数据
解决方案1: 利用shouldComponentUpdate钩子,对props/state是否变化进行判断,确定是否需要重新render组件
解决方案2: 使用react中的PureComponent组件替代Component组件创建类式组件,PureComponent重写了shouldComponentUpdate,只有props或state发生变化时才返回true;
注意:PureComponent只对props及state进行了浅比较,如果只是改了数据对象内部的数据,对象指针没有变化,返回false。
renderProps:组件插槽,类似Vue中的slot
```
export default class Parent extends Component {
render() {
return (
<div>
<h1>我是Parent组件</h1>
<A render={name => <B name={name} />} />
</div>
)
}
}
class A extends Component {
state = { name: 'tom' }
render() {
return (
<div>
<h1>我是A组件</h1>
{this.props.render(this.state.name)}
</div>
)
}
}
class B extends Component {
render() {
return (
<div>
<h1>我是B组件</h1>
<div>我收到的名字是:{this.props.name}</div>
</div>
)
}
}
```
ErrorBoundary:错误边界,只适用于生产环境
- 用于捕获后代组件错误,渲染出备用页面
- 只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
```
export default class Parent extends Component {
state = {childHasError: ''}
static getDerivedStateFromError(error) {
return {childHasError: error}
}
render() {
return (
<Fragment>
{this.state.childHasError ? <h1>当前网络不稳定,请稍后再试</h1> : <Child/>}
</Fragment>
)
}
}
```