用了大概12天时间对react有了一个初步的认识,相比vue感觉还是复杂一些尤其是redux,学的时候挺懵逼的.....可能是我太菜了。建议学react的小伙伴还是要结合官方文档,而且不要看一些很老的视频,尤其是16版本以前的,因为那时候还没有hooks,对于菜鸡的我来说hooks真是救命了!
本人参考的是coderwhy老师的视频,感觉讲的很好还有源码的细节等等。只是自己的吸收能力有点弱,还需要多巩固一下!
学了有半年多的前端,这是随手写的笔记,很多地方肯定还有不足,希望大佬们多指正
如果这篇文章能帮到你,那写的也有点意义了哈哈哈哈
1、Hello React
UI = render(state)
图片加载优化:后端传图片的时候要传大小,虽然样式能改,但是大图消耗性能
命令式编程:每做一个操作,都会给计算机一步步的命令去执行
声明式编程:
1.1 JSX语法
注意事项:使用jsx代码,必须在script必须添加text/babel
render函数里只能有一个根节点,JSX语法也是,外层包一个小括号
单标签必须以/>结尾,/不能省略
写注释必须在 {/我是一段注释/} 里
在{}里不能显示的属性:null、undefined、boolean都不显示
如果想显示null可用tostring(),或者在后面加一个空字符串
也不能在{}里放一个对象
可以放运算符表达式列入字符串拼接 加减乘除
可以放 三元表达式
可以放函数表达式,逻辑与或 都可以
1.2 JSX绑定属性
可以在标签的属性也用{}来绑定
但是不能给标签加class,因为js里有class怕冲突,所以必须要写成className
如果要动态添加class属性,需要用{},如果加样式需要再嵌套一个对象
<div className={"box title" + (this.state.active ? "active" : "")}>
<div style={{color:"red", fontSize:"50px"}}>
1.3 JSX绑定事件
react内部使用按钮点击,自身会绑定一个call(undefined),所以直接在render用this找方法是不行的,必须用Bind call apply来绑定this。
第一种方案:在constructor里直接绑定this this.click = this.click.bind(this)
第二种方案:箭头函数,永远不绑定this,会自己找上层作用域,就是类组件本身
第三种方案(推荐):直接传入一个箭头函数,在箭头函数里调用需要执行的函数
函数传参就使用第三种方案,在() => {onclick(参数)}
1.4 条件渲染
第一种方案可以用if else判断
第二种用三元表达式
第三种用逻辑与&&
1.5 JSX本质
通过babel把 React.createElement("div",{ className:"header"}, "hello world")转为JSX,所以写JSX代码必须需要依赖babel
1.6 虚拟DOM的创建过程
React.createElement的本质最终创建出一个对象,利用这个对象组成了一个JS对象树,Rea.crea里面又有很多其他Rea.crea对象。
再通过render函数映射到真实DOM
1.7 为什么使用虚拟DOM
很难跟踪状态发生的改变
操作真实DOM性能很低:document.createElement本身创建的是一个非常复杂的对象
而且会引起浏览器的回流和重绘
例如:真实DOM在ul后面加五个li,循环遍历,那就要操作5次真实DOM。
2、React组件化
组件化思想:树结构
类组件:不能写大写的标签,大写的会被当成组件。
类组件必须继承React.Component
类组件必须实现render函数
2.1 组件传参
函数式组件用 ChildCpn.propTypes={name:PropTypes.string.isRequired}来声明参数类型,
用 ChildCpn.defaultProps ={name:"why"}来设置默认值
类组件用内部的 static propTypes={} 和 static defaultProps={}来声明和设置默认
如果声明了参数类型,但没有传入,可能会因为NaN报错,react里的属性{}里不允许有NaN
组件通信发送自定义事件时要注意普通函数this指向的是undefined,三种方法:
使用箭头函数,会自己寻找this
在子组件标签里传递事件使用箭头函数 {e => this.increment()}
使用bind绑定this
2.2 跨组件通信
第一种:一层一层往上传,用props,在子组件标签后面{...props}
第二种:1.1、使用context,先创建context实例
const UserContext = React.createContext({
nickname: "aaaa", //默认值
level: -1 //默认值
})
1.2、使用provider API将需要传值的子组件放里面,相当于一个大容器
<UserContext.Provider value={this.state}>
<Profile />
</UserContext.Provider>
1.3、子组件使用自身contextType API来指向需要传值的context,取值直接用this.context
ProfileHeader.contextType = UserContext;
<h2>用户昵称: {this.context.nickname}</h2>
<h2>用户等级: {this.context.level}</h2>
2.1、以上是类组件,函数式组件就需要,再子组件需要context的地方包一层consumer,返回值是一个回调 ,就不需要contextType了。
function ProfileHeader() {
return (
<UserContext.Consumer>
{
value => {
return (
<div>
<h2>用户昵称: {value.nickname}</h2>
<h2>用户等级: {value.level}</h2>
</div>
)
}
}
</UserContext.Consumer>
)
}
2.2、多个context嵌套调用的话,就先创建context,然后在App组件里再包一层就行了,需要调用值的子组件需要再返回一个回调。
return (
<UserContext.Consumer>
{
value => {
return (
<ThemeContext.Consumer>
{
theme => {
return (
<div>
<h2 style={{color: theme.color}}>用户昵称: {value.nickname}</h2>
<h2>用户等级: {value.level}</h2>
<h2>颜色: {theme.color}</h2>
</div>
)
}
}
</ThemeContext.Consumer>
)
}
}
</UserContext.Consumer>
)
3、setState解析
3.1 setState异步更新
1、可以显著提升性能,因为会调用很多setstate,每次render函数会频繁调用,效率很低
最好是应该获取到多个更新,放入到一个队列,之后进行批量更新
2、如果同步更新state,但是还没有执行render函数(虚拟DOM和Diff算法会慢一点),那么state和props不能保持同步。
3、如果想拿到异步结果,setState第二个参数是一个回调,可以在里面拿,类似于nextTick
4、执行完setState完,会执行componentDidUpdate生命周期,里面也可也拿更新的数据
3.2 setState同步更新
1、放到定时器里会变成同步
2、放到componentDidMount() 里原生DOM会变成同步,以及合成事件
3.3 数据合并
原理是 使用object.assign({},{this.state},{this.setState}),更新属性会覆盖没有的不会清除
setState是会合并的,调用多次会合并成一次
setState((prevState, props) => {return {} }),就不会合并,会累加
3.4 组件嵌套的render调用
一般情况下,父组件内有多个子组件,但是调用其中一个子组件的方法,比如按钮点击执行setState,那么整个父组件会重新调用render,里面每个子组件都会重新执行render,浪费性能
3.5 ShouldComponentUpdate
该生命周期可以阻断页面的render重新调用,使用if判断,直接返回true或false
shouldComponentUpdate(nextProps, nextState) {
if (this.state.counter !== nextState.counter){return true;}
return false;
}
但是每个组件去调用,里面还有if,如果有十个属性值改变,冗余代码太多。
3.6 PureComponent
在类组件里,所以类组件继承时候,不再继承Component,而是继承PureComponent,里面会自动调用shouldComponentUpdate生命周期
PureComponent的原理是根据类组件本身是否依赖和改变props和state,如果改变return true,重新渲染render,反之亦然
不要在shouldComponentUpdate做深层比较,非常消耗性能,做浅层比较就可以
3.7 Memo
函数式组件里,memo是一个函数也是一个高阶组件(可以对其他组件操作),使用方法
const MemoHeader = memo(函数组件)
本质和PureComponent操作一样,根据函数式组件本身是否依赖和改变props和state,如果改变return true,重新渲染render,反之亦然
3.8 SetState不可变的力量
1、不要这样做
const newData = {name:"Tom", age:33}
this.state.friends.push(newData)
this.setState({friends:this.state.friends})
2、推荐做法
const newFriends = [...this.state.friends]
newFriends.push({name:"tom" , age:33})
this.setState({friends:newFriends})
原因是因为,第二种重新开辟了一个内存空间,有点类似于深拷贝,此时shouldComponentUpdate在比较的时候,是两个对象在比较,如果是第一种,那么是两个相同的对象比较。
4、高阶组件
高阶组件本质是一个函数,参数是一个组件,相当于包装、强化组件,增强复用性,可以应用在登录授权、生命周期劫持得到组件执行时间等等。
内部render内容一般为,传过来的组件
render() {
return <WrappedComponent {...this.props }/>}
4.1 Ref转发
类组件:通过ref来传递参数,使用createRef函数通过props接受,在父组件的constructor里去声明
this.titleRef = createRef()
函数组件:通过forwardRef函数(本质是一个高阶组件,所以返回值是一个组件),调用时会多一个参数ref,就可以在标签里使用ref
const Profile = forwardRef(function(props, ref) {
return <p ref={ref}>Profile</p>
})
4.2 Portal的使用
把当前组件可以渲染当前任意一个位置,默认挂载root上
class Modal extends PureComponent {
render() {
return ReactDOM.createPortal(
this.props.children,
document.getElementById("modal")
)
}
}
4.3 Fragments的使用
可以把多余没用的div节点在删掉,如果fragment没有属性就可以直接省略,但是有的话不能省略
return (
<Fragment key={item.name}>
<div>{item.name}</div>
<p>{item.age}</p>
<hr/>
</Fragment>
)
4.4 StrictMode的使用
开启严格模式,为后代元素触发额外的检查和警告
1、识别不安全的生命周期
2、使用过时的ref API 例如字符串声明ref="title"的形式
3、constructor会被调用两次,找副作用
4、使用早期废弃的findDOMNode方法,后面ref已经代替
5、检测过时的context API
5、React中的css样式
1、可以编写局部css,具备自己的作用域,不会污染其他组件
2、编写动态的css,例如根据不同布尔值显示不同样式
5.1 内联样式
优点:样式不会冲突,可以获取state中的状态
缺点:需要驼峰标识、没代码提示,代码冗余,不能写伪类元素
5.2 普通css
写单独的css文件,然后直接引入使用
缺点:容易产生冲突的覆盖、为了不冲突需要写多个选择器去选中一个className
5.3 Css modules
声明css文件为style.module.css,组件内部引入之后在render里className={}当成对象直接取对应的css类名,类名必须是驼峰
缺点:难以获取动态样式,比如在state里有动态的 color,还需要结合内联样式
5.4 Css in JS(推荐)
5.5 Styled-components
先引入,使用标签模板字符串,返回的是一个组件,直接用组件代替对应的标签就可以了
const HYButton = styled.button`
padding: 10px 20px;
border-color: red;
color: red;
`
const HYPrimaryButton = styled(HYButton)`
color: #fff;
background-color: green;
`
const HYInput = styled.input.attrs({
placeholder: "coderwhy",
bColor: "red"
})`
background-color: lightblue;
border-color: ${props => props.bColor};
color: ${props => props.color};
`
使用主题属性,先引入,在父组件定义,然后子组件可以通过props直接获取到
<ThemeProvider theme={{themeColor: "red", fontSize: "30px"}}/>
export const TitleWrapper = styled.h2`
text-decoration: underline;
color: ${props => props.theme.themeColor};
font-size: ${props => props.theme.fontSize};
`
5.6 Classnames库
可以类似于vue绑定动态属性,只要返回值是false都不会添加到标签属性中
const {isActive} = this.state;
const isBar = false;
const errClass = "error";
const warnClass = 10; //传数字的话会直接添加转化为字符串 添加一个 10的类名
<h2 className={"foo bar active title"}>我是标题1</h2>
<h2 className={"title" + (isActive ? " active": "")}>我是标题2</h2>
<h2 className={["title", (isActive ? "active": "")].join(" ")}>我是标题3</h2>
{/* classnames库添加class */}
<h2 className="foo bar active title">我是标题4</h2>
<h2 className={classNames("foo", "bar", "active", "title")}>我是标题5</h2>
<h2 className={classNames({"active": isActive, "bar": isBar}, "title")}>我是标题6</h2>
<h2 className={classNames("foo", errClass, warnClass, {"active": isActive})}>我是标题7</h2>
<h2 className={classNames(["active", "title"])}>我是标题8</h2>
<h2 className={classNames(["active", "title", {"bar": isBar}])}>我是标题9</h2>
6、AntDesign组件库
craco:配置修改antd的默认蓝色主题样式,在craco.config.js里改webpack配置
引入样式文件要换成less
const CracoLessPlugin = require('craco-less');
const path = require("path");
const resolve = dir => path.resolve(__dirname, dir);
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { '@primary-color': '#1DA57A' },
javascriptEnabled: true,
},
},
},
}
],
webpack: {
alias: {
"@": resolve("src"),
"components": resolve("src/components")
}
}
}
###7、Css过渡动画
#### 7.1 CssTransition
先引入,在需要使用动画的标签外面包一层,里面的timeout不能省略,再根据className,写对应的样式,分为三个,enter是进入,active是执行中,end是结束后,unmountOnExit是隐藏时卸载。如果需要第一次加载组件需要动画时,需要添加appear属性,并且在css写对应的动画。
import { CSSTransition } from 'react-transition-group';
<CSSTransition in={isShow}
classNames="card"
timeout={5000}
unmountOnExit={true}
appear
/>
.card-enter, .card-appear {
opacity: 0;
transform: scale(.6);
}
.card-enter-active, .card-appear-active {
opacity: 1;
transform: scale(1);
transition: opacity 300ms, transform 300ms;
}
7.2 SwitchTransition
这是切换效果,cssTransition是直接用来显示和隐藏。
一般用法为,在cssTransition外面包一层SwitchTransition。out-in是先隐藏再出现,而且这里面in换成了key,记得也要有timeout,外面的timeout属性是切换class需要的时间,css里的timeout才是决定动画执行时间
<SwitchTransition mode="out-in">
<CSSTransition key={isOn ? "on": "off"}
classNames="btn"
timeout={1000}>
<button onClick={e => this.setState({isOn: !isOn})}>
{isOn ? "on": "off"}
</button>
</CSSTransition>
</SwitchTransition>
.btn-enter {
opacity: 0;
transform: translateX(100%);
}
.btn-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 1000ms, transform 1000ms;
}
.btn-exit {
opacity: 1;
transform: translateX(0);
}
.btn-exit-active {
opacity: 0;
transform: translateX(-100%);
transition: opacity 1000ms, transform 1000ms;
}
7.3 TransitionGroup
主要做给某一组元素添加动画,例如列表,用法是在最外层把div换成TransitionGroup组件。
注意:如果是循环必须给一个key,不添加会因为diff算法,会把class添加到diff算法比较后,删除的位置。例如,三个数据删除第一个,diff算法比较后前两个能找到对应的值,最后一个被删掉,所以会给最后一个添加class动画。
<TransitionGroup>
{
this.state.names.map((item, index) => {
return (
<CSSTransition key={item}
timeout={500}
classNames="item">
<div>
{item}
<button onClick={e => this.removeItem(index)}>-</button>
</div>
</CSSTransition>)})
}
</TransitionGroup>
<button onClick={e => this.addName()}>+name</button>
.item-enter {opacity: 0;transform: scale(.6);}
.item-enter-active {opacity: 1;transform: scale(1);transition: opacity 300ms, transform 300ms;}
.item-enter-done { color: red;}
.item-exit {opacity: 1;transform: scale(1);}
.item-exit-active {opacity: 0;transform: scale(.6); transition: opacity 300ms,transform 300ms}
.item-exit-done {opacity: 0;}
8、Redux状态管理
8.1 JavaScript纯函数
确定的输入,一定会产生确定的输出,例如输入5,返回得到10,不能改变必须是10
函数在执行过程中,不能产生副作用,例如传入一个对象,不能对对象进行修改
function sum(num1, num2) {
return num1 + num2;
}
sum(20, 30);
sum(20, 30); //这是一个纯函数
let foo = 10;
function add(num) {
return foo + num;
}
add(5); // 15
foo = 20;
add(5); // 25 //不是一个纯函数,每次输入是5但是值变了
//能否将上面函数改成一个纯函数?
const bar = 10;
function add2(num) {
return bar + num;
}
bar = 11;
const baz = {
count: 10
}
function add3(num) {
return baz.count + num
}
baz.count = 20; //不是一个纯函数,baz里的值能改,输入相同值结果可能不一样
所有React组件都必须像纯函数一样保护他们的Props不被更改
比如说,父组件用props给子组件传数据,子组件只能用,但是不能改例如props.info.name="why"
8.2 Redux的三大原则
单一数据源:整个应用程序state都被存储在一个object tree中
State是只读的:唯一修改State的方法一定是触发了action,不要在其他地方改
使用纯函数来执行修改:通过reducer将旧state和 action联系在一起,改变时返回新的state
8.3 基本使用流程
1、定义一个唯一的store的库,里面的index.js为唯一出口,这里做的事是把总的仓库导入出去,别的组件想使用时可以直接导入
import {createStore} from 'redux'
import reducer from './reducer.js'
const store = createStore(reducer)
export default store
2、声明对应的reducer文件,先声明默认的state值,里面的reducer方法是为了根据不同的派发事件action的类型type来执行对应的操作(例如对数据的增删改查,类似于回调),记得要用拷贝的方式保证纯函数。
const defaultState ={
counter: 0
}
function reducer(state = defaultState, action){
switch(action.type){
case ADD_NUMBER:
return {...state, counter: state.counter +action.num};
case SUB_NUMBER:
return {...state, counter: state.counter -action.num};
default:
return state
}
}
3、在actionCreators文件里,根据类型,声明对应的派发事件action,记得要返回的是对象
export const addAction = (num) => {
return {
type:ADD_NUMBER,
num
}
}
export const subAction = num => ({
type:SUB_NUMBER,
num
})
4、然后在需要用的组件里,直接派发定义好的事件,用store.dispatch,同时要在加载完的声明周期里用store.subscribe来监听派发事件的结果
store.subscribe(() => {
console.log(store.getState());
})
store.dispatch(addAction(10))
store.dispatch(addAction(10))
store.dispatch(subAction(15))
8.4 在React中使用Redux
1、在需要用的组件引入redux和actioncreator,在组件内部state临时保存store的state
this.state = {
counter: store.getState().counter
}
2、声明函数里面写对应的action事件用dispatch派发,这些action是已经在actioncreator定义好的
3、派发完事件后会在reducer根据类型执行对应的方法
4、在didmount声明周期监听state的改变
componentDidMount(){
this.unsubscribe = store.subscribe(() => {
this.setState({
counter:store.getState().counter
})
})
}
5、在willunmount生命周期取消订阅监听
componentWillUnmount(){
this.unsubscribe()
}
8.5 后端数据存到Redux思路
前提:先封装对应的react-redux
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')); //在总出口文件添加context,传入store
需要使用的地方引入以及添加connect函数的高阶组件
import { connect } from 'react-redux';
export default connect(mapStateToProps, mapDispatchToProps)(Home);
1、先用axios在componentDidMount里请求数据
2、在reducer里的默认数据里,临时保存需要到存store的数据
3、在constants里声明action的type变量名
4、在actionCreators里先导入tyoe变量名,再设置对应的action函数,记得返回的是一个对象
5、在reducer里,根据action的type添加case,返回的是纯函数
6、在请求数据的组件里,引入刚刚声明好的action
7、在mapDispatchToProps函数返回的对象里添加映射,添加dispatch里面是派发的事件
changeBanners(banners) {
dispatch(changeBannersAction(banners));
},
changeRecommends(recommends) {
dispatch(changeRecommendAction(recommends));
}
8、在componentDidMount里存储用axios请求到的数据
this.props.changeBanners(data.banner.list);
this.props.changeRecommends(data.recommend.list);
9、此时数据已经存到store里的state了,在另一个组件就可以使用
const mapStateToProps = state =>({
banners: state.banners,
recommends: state.recommends
})
8.6 Redux-thunk中间件
请求数据也是一个状态管理的地方所以最好放到redux里,在dispatch和reducer之间添加一个中间件,目的是让dispatch直接返回一个函数而不是对象
1、在创建store时传入应用了middleware的enhance函数 2、通过applyMiddleware来结合多个Middleware, 返回一个enhancer; 3、将enhancer作为第二个参数传入到createStore中;
const storeEnhancer = applyMiddleware(thunkMiddleware, sagaMiddleware);
const store = createStore(reducer, storeEnhancer);
// redux-thunk中定义的action函数
export const getHomeMultidataAction = (dispatch, getState) => {
axios({
url: "http://123.207.32.32:8000/home/multidata",
}).then(res => {
const data = res.data.data;
dispatch(changeBannersAction(data.banner.list));
dispatch(changeRecommendAction(data.recommend.list));
})
} //先在组件派发,再到请求数据里派发,然后找对应的reducer进行处理
componentDidMount() {
this.props.getHomeMultidata();
}
如果要用redux-devtools,先引入compose,需要把强化后的中间件做一个合并
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
const store = createStore(reducer, composeEnhancers(storeEnhancer));
8.7 Redux-saga中间件
import createSagaMiddleware from 'redux-saga' //引入函数
const sagaMiddleware = createSagaMiddleware() //创建中间件
const storeEnhancer = applyMiddleware(thunkMiddleware, sagaMiddleware)//应用中间件
sagaMiddleware.run(saga) //拦截action,传入生成器函数
function* mySaga() {
// takeLatest takeEvery区别:
// takeLatest: 依次只能监听一个对应的action
// takeEvery: 每一个都会被执行
yield all([
takeLatest(FETCH_HOME_MULTIDATA, fetchHomeMultidata),
// takeLatest(ADD_NUMBER, fetchHomeMultidata),
]);}
//监听对应的action type,监听到会执行第二个参数 生成器函数
function* fetchHomeMultidata(action) {
const res = yield axios.get("http://123.207.32.32:8000/home/multidata");
const banners = res.data.data.banner.list;
const recommends = res.data.data.recommend.list;
// yield put(changeBannersAction(banners));
// yield put(changeRecommendAction(recommends));
yield all([
yield put(changeBannersAction(banners)),
yield put(changeRecommendAction(recommends))
])
} //生成器函数内具体的逻辑
8.8 CombineReducer
如果我们自己封装的reducer,返回一个对象,里面为子reducer
缺点在于如果传过来的action值里的state没有更改,那还要重新return一个相同的新的子reducer合并的对象,浪费性能。
combineReducer的好处就是会判断action,如果是有数据改变的时候返回新的对象,如果数据没改变就返回原来的对象
const reducer = combineReducers({
counterInfo: counterReducer,
homeInfo: homeReducer
});
8.9 单向数据流
vue和react都有单向数据流,指的是通过props进行数据的传递。
ui视图区域 -> 发送一个事件 -> 发送dispatch执行action ->修改state ->修改ui
redux中的单向数据流:
ui组件发生事件 -> dispatch一个action -> action传递给reducer返回新的state ->state的改变引起ui的改变
不能跨级传递数据,例如直接store.getState() = newValue 这是不符合单向数据流的规范
9、React路由
第一阶段:后端路由
一个页面有自己对应的网址, 也就是URL. URL会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个Controller进行处理. Controller进行各种处理, 最终生成HTML或者数据, 返回给前端. 这就完成了一个IO操作. 上面的这种操作, 就是后端路由. 当我们页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户顿. 这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化.
第二阶段:前后端分离
每次请求涉及到的静态资源都会从静态资源服务器获取; 这些资源包括HTML+CSS+JS,然后在前端对这些请求回来的资源进行渲染;
然后在另一台API服务器上写接口请求对应的数据,拿到数据做一个展示就可以了
第三阶段:单页面富应用
整个Web应用只有实际上只有一个页面,当URL发生改变时,并不会从服务器请求新的静态资源; 而是通过JavaScript监听URL的改变,并且根据URL的不同去渲染新的页面; 如何可以应用URL和渲染的页面呢?前端路由 前端路由维护着URL和渲染页面的映射关系; 路由可以根据不同的URL,最终让我们的框架(比如Vue、React、Angular)去渲染不同的组件;
9.1 前端路由的原理
1、改变URL,但是页面不要强制刷新(a元素不行)
2、自己来监听URL的改变,并且改变之后自己改变页面的内容
hash路由
window.addEventListener("hashchange", () => {
switch (location.hash) {
case "#/home":
routerViewEl.innerHTML = "首页";
break;
case "#/about":
routerViewEl.innerHTML = "关于";
break;
default:
routerViewEl.innerHTML = "";}})
history路由
el.addEventListener("click", e => {
e.preventDefault(); //阻止默认事件
const href = el.getAttribute("href");
history.pushState({}, "", href);
urlChange();//点击改变URL
}) //history接口六种模式改变URL不会刷新页面
// 执行返回操作时, 依然来到urlChange
window.addEventListener('popstate',urlChange);
// 监听URL的改变
function urlChange() {
switch (location.pathname) {
case "/home":
routerViewEl.innerHTML = "首页";
break;
case "/about":
routerViewEl.innerHTML = "关于";
break;
default:
routerViewEl.innerHTML = "";
}
}
9.2 switch组件
默认情况下,react-router中只要是路径被匹配到的Route对应的组件都会被渲染; 但是实际开发中,我们往往希望有一种排他的思想: 只要匹配到了第一个,那么后面的就不应该继续匹配了; 这个时候我们可以使用Switch来将所有的Route进行包裹即可
NavLink是在active情况下是否显示特殊的内容,link是要加额外的active属性
9.3 按钮点击路由跳转
如果是app的子组件,因为这个组件是app通过路由创造出的组件所以有history,push,go等属性,子组件可以直接通过this.props.history.push可以直接拿到history属性然后跳转
但是APP不行,因为APP外层是index.js是没有路由来帮APP创造组件,此时需要引入withRouter
并且在最外层包Browser组件,才能调用this.props.history
10、React Hooks
10.1 useState
对于简单的state,直接用解构赋值定义数据
注意:如果想要添加state的value,不能直接push,必须拷贝,因为usestate会判断state是否更新来渲染render,就跟setState类似
<button onClick={e => setFriends([...friends, "tom"])}>添加朋友</button>
####10.2 useEffect
代替类组件里,一些生命周期的执行
useEffect(() => {
console.log("订阅事件"); //类似于componentDidMount生命周期
return () => {
console.log("取消订阅"); //类似于componentWillUnmount
}
},[]) //第二个参数为数组,为了优化
一般情况下,只要组件渲染或者更新,useEffect就会重新执行一遍,但有些请求我们只需要执行一次就可以了类似于订阅事件、网络请求
第二个参数为数组,里面可以放想要处理的state,只是在该state执行后才会重新调用该useEffect
10.3 useContext
类组件可以通过 类名.contextType = MyContext方式,在类中获取context; 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;
使用useContext后
export default function Cpn1(props) {
const user = useContext(UserContext);
const theme = useContext(ThemeContext)
console.log(user, theme);
return (
<div>
<h2>ContextHookDemo</h2>
<h2>{user.name}</h2>
<h2>{user.age}</h2>
</div>
)
}
注意:当组件上层最近的<Context.provider/> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值
10.4 useReducer
在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分
或者这次修改的state需要依赖之前的state时,也可以使用,reducer是纯函数,所以要拷贝
export default function Home() {
// const [count, setCount] = useState(0);
const [state, dispatch] = useReducer(reducer, {counter: 0});
return (
<div>
<h2>Home当前计数: {state.counter}</h2>
<button onClick={e => dispatch({type: "increment"})}>+1</button>
<button onClick={e => dispatch({type: "decrement"})}>-1</button>
</div>
)
}
export default function reducer(state, action) {
switch(action.type) {
case "increment":
return {...state, counter: state.counter + 1};
case "decrement":
return {...state, counter: state.counter - 1};
default:
return state;
}
}
10.5 useCallback
useCallback实际的目的是为了对函数 进行性能的优化
使用场景:在将一个组件中的函数,传递给子组件进行回调使用时,使用useCallback对函数进行处理,
子组件不会重新渲染
const increment1 = () => {
console.log("执行increment1函数");
setCount(count + 1);
}
const increment2 = useCallback(() => {
console.log("执行increment2函数");
setCount(count + 1);
}, [count]);
const HYButton = memo((props) => {
console.log("HYButton重新渲染: " + props.title);
return <button onClick={props.increment}>HYButton +1</button>
});
此时increment2,值是相同的,memo浅层比较后组件就不会重新渲染
10.6 useMemo
目的也是性能优化,针对于state,也可也是函数,主要看返回值
如果子组件引用着父组件的state,那么就算包层memo还是会重新渲染,因为每次传过来的对象是一个新值,此时需要用useMemo
const info = { name: "why", age: 18 };//子组件会重新渲染
const info = useMemo(() => {
return { name: "why", age: 18 };}, []); //子组件不会重新渲染
10.7 useRef
1、操作DOM
const titleRef = useRef();
const inputRef = useRef();
const testRef = useRef();
const testRef2 = useRef();
function changeDOM() {
titleRef.current.innerHTML = "Hello World";
inputRef.current.focus();
console.log(testRef.current);
console.log(testRef2.current);
}
2、记录上一次的值,按钮点击完执行render后会调用useEffect,会把执行+10后的count传给numRef的current,改变numRef是不会重新渲染界面的
export default function RefHookDemo02() {
const [count, setCount] = useState(0);
const numRef = useRef(count);
useEffect(() => {
numRef.current = count;
}, [count])
return (
<div>
{/* <h2>numRef中的值: {numRef.current}</h2>
<h2>count中的值: {count}</h2> */}
<h2>count上一次的值: {numRef.current}</h2>
<h2>count这一次的值: {count}</h2>
<button onClick={e => setCount(count + 10)}>+10</button>
</div>
)
}
10.8 useImperativeHandle
通过useImperativeHandle可以只暴露固定的操作,而不是子组件把整个ref暴露给父组件: 通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起; 所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象; 比如我调用了 focus函数,甚至可以调用 printHello函数
const HYInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}), [inputRef])
return <input ref={inputRef} type="text"/>
})
//上面是子组件
export default function UseImperativeHandleHookDemo() {
const inputRef = useRef();
return (
<div>
<HYInput ref={inputRef}/>
<button onClick={e => inputRef.current.focus()}>聚焦</button>
</div>
)
}
父组件调用focus实际上是调用子组件里useImperativeHandle的focus函数
10.9 useLayoutEffect
当render渲染完,在这个hook里面执行完,再次判断state是否变化,如果变化就把最新的state进行渲染。
export default function EffectCounterDemo() {
const [count, setCount] = useState(10);
useEffect(() => {
if (count === 0) {
setCount(Math.random() + 200)
}
}, [count]);
return (
<div>
<h2>数字: {count}</h2>
<button onClick={e => setCount(0)}>修改数字</button>
</div>
)
}
正常执行过程是:
1、先点击修改数字把10set为0
2、一旦调用render函数,且count改变那么就会执行useEffect hook
3、判断count === 0 ,返回一个随机数
4、调用render,count又改变又调用useEffect,但此时不是0,直接渲染页面渲染的结果是会先闪一下变成0,然后变为随机数,这时把useEffect换成useLayoutEffect,就不会闪一下的过程
10.10 自定义hook
自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性
声明的函数必须是use开头
11、Fiber作用
Fiber可以理解为执行单元(碎片),浏览器要在一帧之内加载js单线程所有的任务,浏览器一有空余时间就会执行fiber碎片,根据函数requestIdleCallback()