react入门三
ReactDOM.render(<MyComponent/>, document.getElementById("root"))发生了什么
react解析MyComponent标签找到了MyComponent组件,发现组件是类定义的,随后new出该类的实例,并通过该实例调用到原型上的render方法
将render方法返回的虚拟dom转为真实dom,随后呈现在页面中,呈现后还会调用一个componentDidMount
ref
-
字符串的形式(不推荐使用,存在性能问题)
-
回调的方式
<input ref={(currentNode) => {this.input = currentNode}}/>, 该回调一上来就会调用一次
PS: 向上面这种回调的方式(内联的方式)在更新的时候会调用两次,注意是更新的时候,第一次currentNode为null(因为每次render都会创建一个新的内联函数,当更新的时候第一次调用是为了清空就得函数,所以返回一个null),第二次才会把节点传入. 如果非得解决这个问题,react也提供了方案,通过类绑定的方式:<input ref={this.saveInput}/>,
在class里面定义saveInput
saveInput = (c) => { // c就是传入的node节点
this.input1 = c;
}
createRef
class Demo extends React.Compoent{
myRef = React.createRef(); // createRef返回一个容器,该容器用于存放被ref所标识的节点,该容器专人专用只能绑定一个,因此后放进去的会覆盖之前的,如果想使用多个就需要createRef多次
render() {
return (
<input ref={this.myRef}/> {/*这里相当于将input存放到了myRef中了,input节点可以通过this.myRef.current访问*/}
)
}
}
react中的事件
-
1 通过onXXX属性指定事件处理函数
react中使用的是自定义事件(合成事件,而不是使用原生的dom事件)--- 是为了更好的兼容性react中的事件是通过事件委托的方式处理的(委托给组件最外层元素)--- 事件委托的原理是事件冒泡,使用事件委托高效
-
2 通过event.target得到发生事件的dom元素对象
收集表单数据
-
非受控组件
现用现取,通过ref可以完成
-
受控组件
输入类的元素在随着输入将值维护到状态中
高阶函数和柯里化
高阶函数:
若函数A接受的参数是一个函数,那么A就称之为高阶函数
若函数A调用的返回值依然是一个函数,那么A就称之为高阶函数
常见的高阶函数:如Promise, setTimeout, 数组的常用方式
函数柯里化
部分求值,函数的调用仍旧返回函数,实现多次接受参数最后统一处理的函数编码
生命周期
组件从创建到死亡会经历一些特定的阶段
React组件中包含一系列钩子函数,会在特定的时刻调用
在定义组件时,会在特定的声明周期回调函数中做特定的工作
- 旧版的生命周期钩子
组件挂载的执行顺序
constructor ---> componentWillMount ---> render ---> componentDidMount ---> componentWillUnmount
组件更新执行顺序(可以分为3条线)
父组件渲染子组件,当传入的数据更新时的执行路线
componentWillReceiveProps ---> shouldComponentUpdate ---> componenWillUpdate ---> render ---> componentDidUpdate ---> componentWillUnmount
当组件setState更新的时候
setState() ---> shouldComponentUpdate ---> componentWillUpdate ---> render ---> componentDidUpdate ---> componentWillUnmount
当强制更新组件的时候(不更改状态数据就想让组件更新就可以走这条线)
forceUpdate() ---> componentWillUpdate ---> render ---> componentDidUpdate ---> componentWillUnmount
ps: shouldComponentUpdate是一个阀门钩子,如果不写这个钩子默认返回true,接下来的流程正常执行,如果写了并且返回false则接下来的流程不会执行,只能返回true或者false,
componentWillReciveProps 这个钩子第一次不调用,只有接受的props变化了才会调用,也就是父组件重新渲染导致子组件渲染才执行该钩子,接受新的props。
总结:
初始化阶段
constructor ---> componentWillMount ---> render() ---> componentDidMount
更新阶段: 由组件内部this.setState() 或者父组件更新触发
shouldComponentUpdate ---> componentWillUpdate ---> render ---> componentDidUpdate
卸载阶段:由reactDOM.unmountComponentAtNode(node)触发
componentWillUnmount()
常用的钩子:
componentDidMount: 一般做初始化操作例如开启定时器,发送网络请求,订阅消息。。。
componentWillUnmount: 做一些收尾工作如取消定时器,取消订阅等
render: 必须用,需要掉1+n次
- 新的声明周期 (react最新版本)
所有带will的钩子在新版版中都加了UNSAFE_前缀, 除了卸载的钩子,为什么这么做,因为react在设计异步渲染,这些组件可能会出现bug,以后可能会被废弃
新版本废弃了componentWillMount, componentWillUpdate, componentWillReciveProps,新增了getDerivedStateFromProps 和getSnapshotBeforeUpdate
需要注意的是 getDerivedStateFromProps不能在实例上调用,需要声明成静态的需要加static, 而且该方法必须要有返回值,要么你返回状态对象,要么返回null,不能返回其他的。需要注意的是只要返回一个对象,那么状态的更新就没有意义了,因为一旦返回状态,此状态就不能被改了,该方法接收一个参数props, 他会接收props属性并且会派生出一个取决于props的状态。可以接收第二个参数state,使用场景罕见,基本不用。
getSnapshotBeforeUpdate: 该钩子处于render和componentDidUpdate之间,用于在更新之前获取快照,因为更新后之前的状态就不见了,在更新之前再看一眼之前的状态。
componentDidUpdate() 该钩子会接收三个参数,第一个是更新前的props,第二个是更新前的state,第三个是一个快照值(即getSnapshotBeforeUpdate返回的值。)
挂载时:
constructor ---> getDerivedStateFromProps ---> render (react更新dom和refs) ---> componentDidMount
更新:
(new props | setState() | forceUpdate)时 ---> getDerivedStateFromProps(得到一个派生的状态) ---> shouldComponentUpdate
dom的diff算法
class Person extends React.Component{
state = {
persons: [
{id: 2, name: '小张', age: 23},
{id: 3, name: '小李', age: 19}
]
}
render() {
return (
<ul>
{
this.state.persons.map((person,index) => {
return <li key={index}>{person.name}</li>
})
}
</ul>
)
}
}
虚拟dom中的key有什么作用?
简单说key是虚拟dom对象的标识,在更新显示是key起着极其重要的作用,,
详细说,当状态中的数据发生变化的时候,react会根据新的数据生成新的虚拟dom,随后react进行新虚拟dom和就虚拟dom的diff对比,比较规则如下:
- 就虚拟dom中找到了与新虚拟dom中相同的key:
1) 若虚拟dom中内容没有变化,直接使用之前的真实dom
2) 若虚拟dom中内容变了,则生成新的真实dom,随后替换页面中之前的真实dom
- 旧虚拟dom中没有找到与新虚拟dom相同的key
根据数据创建新的真实dom,随后渲染到页面。
用index作为key可能会引发的问题:
- 若对数据进行逆序添加,逆序删除等破坏顺序操作,会产生没有必要的真实DOM更新 ==》 界面效果没问题,但是效率低
- 如果页面中还包含输入类的dom:
会产生错误dom更新==》 界面有问题
注意,如果不存在对数据逆序添加删除等破坏顺序的操作,仅用于渲染列表,展示列表,使用index是没有问题的
<input type="checkbox"/> 这样的输入内容在react中有一个defaultChecked属性,可以代替checked属性,因为使用checked,则必须使用onchange来修改其值
react组件通讯
-
父子组件 通过props可以通信
-
兄弟组件 可以提取共同状态到公共父组件或者使用消息的发布订阅,或者使用redux之类的状态管理库
消息的发布订阅实现兄弟组件之间的通信常用的三方库有pubsubjs
react路由
-
前端路由的工作原理
依托的是浏览器历史记录,可以借助history这个库来完成操作
-
react-router-dom
路由组件
会自动给组件传入路由相关的props
NavLink可以实现路由的高亮,通过activeClassName指定样式名
标签体内容是一个特殊的标签属性
通过this.props.children可以获取标签体内容
路由组件一般放在pages目录
一般组件
一般放在components目录-
Switch组件
该组件会提高性能,如果不适用他,那么在匹配到目标组件后还会继续往下匹配,使用了他,当匹配到目标组件后就不会网下匹配其他组件了
-
解决多级路径页面刷新样式丢失的问题
- public/index.html中引入样式时不写./而是写/
- public/index.html中引入样式时不写./ 而是%PUBLIC_URL%
- 不要使用HistoryRouter而是使用HashRouter
-
路由的严格模式与模糊匹配
不是非必要情况下不要用严格模式,比如有二级路由的情况下使用了严格模式就会出问题,如/home /home/mian 当使用了严格模式, 那么/home/main永远匹配不到 -
Redirect的使用
Redirect放在注册路由的最后,当所有路由没有匹配的时候提供一个默认的路由
-
嵌套路由
- 注册子路由的时候要写上父路由的path
- 路由的匹配是按照路由的注册顺序进行的
-
向路由组件传递params
路由导航向路由组件传递params参数 <Link to={`home/message/detail/${id}`}> // 可以传递多个参数 <Link to={`home/message/detail/${id}/${title}`}> // 相当于传递了一个id,一个title 注册的路由 <Route path="/home/message/detail/:id" component={Detail}> // 可以接收多个参数 <Route path="/home/message/detail/:id/:title" component={Detail}> Detail组件通过props参数就能接收到参数,在接收到的match里面就有传递的参数
-
- 向路由组件传递search参数
```
<Link to={`home/message/detail/?id=${id}&title=${title}`}> // 相当于传递了一个id,一个title
接受的方式(无需声明正常注册路由即可)
<Route path="/home/message/detail" component={Detail}>
Detail组件通过this.props.location.search获取,获取到的是一个字符串,我们需要的是对象的格式,因此需要特殊处理,安装脚手架的时候其实下载了一个叫做querystring的库,通过这个库可以帮我们完成
import qs from 'querystring';
// 通过qs.stringify(obj) 可以将一个对象转为key=value&key=value的格式
// 通过qs.parse(str) 可以将一个key-value格式的字符串变成对象
```
-
向路由组件传递state参数 (这里的state不是路由状态的state)
无论是params参数还是search参数都在地址栏上,还可以通过对象的方式去传递,此时的to属性只能是一个对象,不再是字符串了,
需要注意的是这种方式传递参数虽然地址栏上没有参数,但是刷新的时候同样不会丢失参数,因为他是存在history中的,如果是HashRouter刷新会丢失
```
<Link to={{pathname:`/home/message/detail`, state: {id: id, title: title}}}>
// 接受state
通过this.props.location.state可以获取
```
- push 与 replace
历史记录是一个压栈模式,默认是push,如果要开启replace需要这么写, replace是替换模式
<Link replace to={{pathname:`/home/message/detail`, state: {id: id, title: title}}}>
- 编程时路由导航
通过脚本的方式进行跳转。路由组件通过this.props.history可以拿到history对象,里面包含操作历史的方法,需要注意的是编程时路由跳转时注册路由需要对应,也就是说通过search跳转的不能以params的方式注册路由,其他同理,params和query的参数在地址栏好理解,state的不再地址栏,但是同理对应的方式也有第二个参数可以接收state, 如this.props.history.push(path, state)
编程时路由导航的几个api:
* this.props.history.push
* this.props.history.replace
* this.props.history.goBack
* this.props.history.goForward
* this.props.history.goBack
* this.props.history.go
- withRouter
路由组件里面可以获取到操作历史的方法,但是再非路由组件里面是没有这些的。可以借助withRouter使一般组件也能获取到history
import {withRouter} from 'react-router-dom'
比如
export default withRouter(Header) ; 通过withRouter将一般组件Header加工后暴露,此时使用header组件就会有history相关的内容
- BrowserRouter与HashRouter的区别
* 底层原理不一样,BrowserRouter使用的是H5的history API,不兼容Ie9及以下版本HashRouter使用的是url的hash值
* url表现形式不一样,HashRouter带#
* 刷新后对路由state的影响
BrowserRouter没有影响,因为state保存在history对象中
HashRouter会丢失state参数
* HashRouter可以用于解决一些路径错误相关的问题
redux
-
redux是什么
- 1.1 redux是专门做状态管理的库不是react插件
- 1.2 可以用在react,vue,angular中,但基本与react搭配使用
- 1.3 专门做集中式状态管理,管理应用中多个组件共享的状态
-
什么情况下用redux
- 1.1 某个组件的状态需要让其他组件随时拿到(共享)
- 1.2 一个组件需要改变另一个组件的状态(通信)
-
redux的工作流程
有一个核心store,里面的值只能通过reducer更改,当用户在组件中派发一个action的时候。action不是特殊的东西,他是一个带有type和数据的对象比如{type: "INCREAMENT", data: {count: 1}}就是一个action,
通过dispatch可以将action交给store,store不干活,他会让reducer去更改状态。reduer将状态加工完毕会返回一个新的状态给store。reducer能加工状态,那个状态哪里来?其实reducer干了两件事,一是初始化状态,二是加工状态。action creator是一个返回action对象的东西。
-
redux的三个核心概念
-
1 action 是一个动作对象包含两个属性
- type 标识属性,值为字符串,唯一,必要属性
- data 数据属性,值类型任意,可选属性
action分同步action(一般对象) 和异步action(是一个函数),一个异步的action一定对应一个同步的action,使用异步action需要一个中间件,因为action需要的是一个对象,而不是一个函数,因此需要安装redux-thunk,引入后 需要在创建store的时候作为第二个参数传递,异步的action是通过store帮助调用的
import {createStore, applyMiddleware} from 'redux' import thunk from 'redux-thunk' createStroe(reducers, applyMiddleware(thunk)) 这里的reducers是个多个reducer,以对象的方式去组织,因为对象的key-value存值和取值更方便。因此reducers的结构应该是这样,是多个reducer合并的结果 首先引入一个函数将所有的reducer合并为一个总的reducer import {combineReducers} from 'redux' const allReducers = combineReducers({ countRecuder, otherReducer, ... });
-
2 reducer 用于初始化状态,加工状态,加工时根据旧的state和action,产生新的state的纯函数
该函数接受两个参数,第一个是之前的状态,第二个是action对象,初始化的时候第一个参数是undefined,在创建store的时候会传递reducer进去,内部会调用reducer进行状态初始化 reducer必须是纯函数,纯函数,一个函数只要接受同样的实参,那么一定得到同样的结果,不能改写参数的数据 ``` 比如: let arr = [1,2,3]; arr.push(4); 虽然arr的内容变了,但是arr的地址引用没有变,这样的变化redux是不认的。这样写才可以 [4, ...arr];这样得到一个新数组 像这样的函数不是纯函数,同样的输入不能得到同样的输出 function demo(a) { return Math.random() + a } function demo1(a) { // 改写了参数的值 a = 9 } 纯函数不会产生任何副作用,例如网络请求,输入和输出设备,不能调用Date.now(), Math.random()等不纯的方法 ```
-
3 store 将state,action, reducer联系到一起的对象
通过store.getState() 可以得到状态
通过store.dispatch(action对象) 可以更改状态,注意不是直接更改,其实每一个action对应一个reducer,最终是通过reducer来更改,需要注意的是redux只是在维护状态,在react中派发action导致转台改变,是不能引起页面的渲染的。因为redux只承诺维护状态,没承诺渲染页面,如果要渲染需要我们检测状态,手动去调用render. 注意手动调用不是this.render. 可以通过this.setState({}) 来重新渲染
store.subscribe() 可以检测redux的状态,只要redux中的任意状态发生改变,指定的回调都会执行,因此页面一挂载就可以检测redux的状态了
componentDidMount() {
store.subscribe(() => {
this.setState({})
})
}在组件中写可能写很多这样重复的代码,因此可以将检测的逻辑放在入口的index.js中,这样只要redux的状态一发生变化,真个App就会重新渲染,对应的子组件也会渲染。因为有diff算法的存在,不会引起大面积的重绘重排,因此不用担心性能问题。
-
react-redux
react-redux需要注意的几个点:
- 所有的ui组件都应该包裹一个容器组件,他们是父子关系
- 容器组件是真正和redux打交道的,里面可以随意使用redux的api
- ui组件中不能使用任何redux的api
- 容器组件会传给ui组件redux中所保存的状态,用于操作状态的方法。
- 容器给ui传递状态,操作状态的方法均通过props传递
- redux的状态变化并不会引起ui的更新,除非手动去检测状态变化。但是用了react-redux就不需要检测也能更新ui, 因为使用connect的时候他已经帮我们检测了。
容器组件就是一个桥梁,用于连接ui组件和redux,因此在容器组件中需要引入要包装的ui组件,要引入store(redux的核心就是store),需要注意的是容器组件不能亲自引入store,而是通过props的方式传入store,比如有一个容器组件Count,此时可以通过props传递
<Count store={store}/>
需要注意的是页面中不止这么一个容器组件,他都需要store,那么react-redux提供了一个批量提供store的方法
import {Provider} from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
)
需要引入连接的方式
import {connect} from 'react-redux'
// 得到一个容器组件
const ContainerComponent = connect()(ui组件);
容器组件和父子组件通信通过props的方式传递状态和操作状态的方法,正常的组件是通过标签属性的方式给子组件传递参数的,但是容器组件是由connect方法形成的,并没有和ui组件有这明确的父子关系,因此需要注意的是connect的第一个()需要两个参数。第一个参数是一个函数,用于生成传给ui组件的状态,需要返回一个对象,既然是操作状态,那么该函数一定能接受store中的状态,该函数接受一个参数state,第二个参数同样是一个函数,(当然也可以是一个对象,里面是一系列action,是精简版的写法,这这种写法,react-redux会自动分发对应的action),接受一个分发action的dispatch方法,返回一个对象,对象封装的是操作状态的方法。从语义上来讲,第一个参数就是一个mapStateToProps,第二个参数就是mapDispatchToProps
示例代码
funciton mapStateToProps(state) {
return {
count: state
}
}
function mapDispatchToProps(dispatch) {
return {
increament: (number) => dispatch(createInreamentAction(number))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ui组件)
mapStateToProps映射状态 mapDispatchToProps映射操作状态的对象, 这两个函数都返回一个对象
mapDispatchToProps 也可以是一个对象,对象是一系列action,react-redux会自动分发这个action
注意事项:当一个ui组件对应一个容器组件的时候,如果这样编码会导致组件的数量成倍增加,可以同过一个文件将其整合,定义ui组件,最终以容器组件暴露
redux的开发工具
一个项目一般都由多个人写,可能都会用到redux存数据,但是彼此之间不知道状态是如何存的,都存了哪些状态,这时可以通过redux的开发工具来快速追踪状态的变化。需要引入一个库redux-devtools-extension
在store.js中引入
import {composeWithDevTools} from redux-devtools-extension
export default createStore(allReducers,composeWithDevTools(applyMiddleware(thunk)))
关于setState
setState更新状态的两种写法
- setState(stateChange, [callbacnk]); // 对象式的写法,更改状态需要手动获取原来的state
eg: this.setState({count: this.state.count+1}) - setState(updater, [callback]); // 函数式的写法,不需要获取原来的state,因为会作为参数传入
eg: this.setState((state, props) => { // 不仅能拿到state, 还可以拿到props
return {count: state.count+1}
})
两种写法都有个可选的回调,因为react是异步更细的。需要注意的是setState是一个同步方法,由setState引起react更新状态是一个异步的操作,这个回调在状态更新完成并且在render后才调用的.
lazyLoad
路由组件的懒加载,不适用懒加载,所有的路由组件都会一次性被加载,如果有成百上千个路由组件,这是非常恐怖的。路由组件应该在跳那个路由就加载那个路由组件,这就是懒加载,在需要的时候才加载组件
hooks
-
React.useState(状态初始值)
该方法返回一个数组,数组第一位是当前状态,第二位是更新状态的函数,因为数组的解构赋值是按下标来的,所以如下:
let [count, setCount] = React.useState(0);
需要注意的是每次调用setCount更新,函数组件都会执行,setCount的参数可以是一个函数,
-
React.useEffect(fn,[arr]) 可以在函数式组件里模拟生命周期钩子,fn就相当于一个声明周期钩子至于是哪一个需要看情况
如果不指定第二个参数arr,那么所有的状态的改变都会执行回调fn。如果arr是空,表示全都不监测,如果不写第二个参数那么表示全部进行监测,如果要监听指定的状态,就需要在数组中指明。fn可以返回一个函数。
-
React.useRef()
和createRef一样的作用 -
React.createContext
-
创建context容器对象
const xxxContext = React.createContext(); -
渲染子组件时,外面包裹xxxContext.Provider,通过value属性给后代组件传递数据
<xxxContext.Provider value={数据}>
子组件
</xxxContext.Provider>
3)后代组件读取数据
方式一,仅适用于类组件
首先声明接收 static contextType = xxxContext 然后才能通过this.context.value拿到方式二,函数式组件和类组件都能接收到 <xxxContext.Consumer> { value => ( // value就是context中的value数据 要显示的内容 ) } </xxxContext.Consumer>
注意,在应用开发中一般不用context,一般都用它来封装react插件。
-
组件优化
- Component的两个问题
1) 只要执行setState(), 即使不改变状态数据,组件也会重新render() ,也就是setState({}) 也会render.
2) 只要当前组件重新render,就会自动重新render子组件 ,效率低。
高效的做法应该是
只有当前组件的state或者props数据发生改变时才重新render
之所以有这样的问题是因为Component中的shouldComponentUpdate()默认总是返回true. 通过重写这个钩子可以解决这个问题
shouldComponentUpdate(nextProps, nextState){
console.log(this.props, this.state) // 当前的props和state
console.log(nextProps, nextState); // 目标props和state
// 根据当前数据和目标数据对比来决定返回ture|false ,至于怎么比有人做了,react中提供了PureComponet这个组件,他会重写 shouldComponentUpdate, 里面的对比逻辑是写好了的
reuturn true;
}
- PureComponent
该组件其实也没干啥,就是重写了shouldComponentUpdate, PureComponent也会有一点小瑕疵。因为他的底层进行了浅对比,因此如果是下面的这种写法同样会有问题
const obj = this.state;
obj.xxx = 'XXX'; // 改了某一个属性
this.setState(obj); // 因为obj和state指向了同一个引用,因此在使用PureComponent比较时认为state是没有变化的,所以不会render.因此一定注意使用PureComponent,在更新状态时不要和原来的状态发生任何关系。使用时额外注意数组的那些方法。
renderProps
场景,比如有两个组件A,B,不应该关系不大,但是因为某些逻辑的原因可能需要这么写,
<A>
<B/>
</A>
对于这种格式要个可以通过this.props.children来获取,但是有个问题如果B组件需要A组件内的数据是做不到的,因此就有了下面的这种格式
<A render={(parmas) => <B {...params}/>}/> // 就相当于传递了一个函数,该函数返回一个组件,函数名不一定叫render.
在A组件中预留一个位置this.props.render(可以传递A中的状态到B),类似于vue中插槽,
组件通信的总结
* 组件间的关系
1) 父子组件
2) 兄弟组件 (非嵌套组件)
3) 祖孙组件(跨级组件)
* 几种通信方式
1) props
1.1) children props
1.2) render props
2) 消息的发布订阅
pubsub ,event等
3) 集中式管理如redux, dva等
4) context:
生产者和消费者模式
快速预览打包后的程序
全局安装serve,会快速开启一个服务。
错误边界
一个页面有多个组件构成,但是其中的某个组件因为某些不可控因素导致出错,此时整个页面都会出问题,边界错误就是处理这类问题的,就是当某个组件发生了错误,不要导致整个页面崩溃,而是将错误控制在最小的范围了,出错了给一个友好的提示。需要注意的是错误边界只有在生产环境有效,就是打包后才生效,开发环境是不生效的。总的来说错误边界就是用来捕获后代组件错误,渲染出备用页面,只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件,定时器中产生的错误。
当Parent的子组件出错时会走这个钩子,定义在父组件,并且携带错误信息err
state = {
error: '' // 默认空,当发生了错误就会有值,可以根据这个值给一个友好的提示
}
static getDerivedFromError(err) {
return {error: err}
}
当然还有一个钩子componentDidCatch() {
console.log('渲染组件出错');
} 当组件发生错误的时候就会触发该钩子,这里可以统计发生错误的次数