虚拟 DOM 与真实 DOM
关于虚拟DOM:
1. 本质是Object类型的对象(一般对象)
2. 虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。
3. 虚拟DOM最终会被React转化为真实DOM,呈现在页面上。
jsx 语法规则:
1. 定义虚拟DOM时,不要写引号。
2. 标签中混入 JS表达式 时要用{}。
3. 样式的类名不要用class,要用className。
4. 内联样式,要用style={{key:value}}的形式去写。有-的 要用小驼峰写
5. 只有一个根标签
<Fragment key={1}>
<input type="text"/>
<input type="text"/>
</Fragment>
解析时会自动去除 Fragment 标签, Fragment标签只能有一个属性 (key)
6. 标签必须闭合
7. 标签首字母
(1).若小写字母开头,则将该标签转为html中同名元素,若html中无该标签对应的同名元素,则报错。
(2).若大写字母开头,react就去渲染对应的组件,若组件没有定义,则报错。
一定注意区分:【js 语句(代码)】与【js 表达式】
1. 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方
下面这些都是表达式:
(1). a
(2). a+b
(3). demo(1)
(4). arr.map()
(5). function test () {}
2.语句(代码):
下面这些都是语句(代码):
(1).if(){}
(2).for(){}
(3).switch(){case:xxxx}
函数组件
1.创建函数式组件
function MyComponent(){
console.log(this); //此处的this是undefined,因为babel编译后开启了严格模式
return <h2>我是用函数定义的组件<h1>(适用于【简单组件】的定义)</h1></h2>
}
//2.渲染组件到页面
ReactDOM.render(<MyComponent></MyComponent>,document.getElementById('test'))
/*
执行了ReactDOM.render(<MyComponent/>.......之后,发生了什么?
1.React解析组件标签,找到了MyComponent组件。
2.发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。
*/
函数组件 hooks
//count为值setCount为修改值的方法,后面()里的内容为初始化的值
const [count,setCount] = React.useState(0)
//创建ref 和 类式组件使用方法相同
const myRef = React.useRef()
//生命周期 函数体内表示 componentDidMount 和 componentDidUpadte。
函数的返回值是一个函数 对应 componentWillUnmount 生命周期
第二个参数表示监视的属性,不写代表全部监视,空数组表示都不监视,数组里的表示需要监视的。
React.useEffect(()=>{
let timer = setInterval(()=>{
setCount(count => count+1 )
},1000)
return ()=>{
clearInterval(timer)
}
},[])
类式组件
//1.创建类式组件
class MyComponent extends React.Component {
render(){
//render是放在哪里的?—— MyComponent的原型对象上,供实例使用。
//render中的this是谁?—— MyComponent的实例对象 <=> MyComponent组件实例对象。
console.log('render中的this:',this);
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
}
}
//2.渲染组件到页面
ReactDOM.render(<MyComponent/>,document.getElementById('test'))
/*
执行了ReactDOM.render(<MyComponent/>.......之后,发生了什么?
1.React解析组件标签,找到了MyComponent组件。
2.发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法。
3.将render返回的虚拟DOM转为真实DOM,随后呈现在页面中。
*/
对 props 进行限制
引入prop-types,用于对组件标签属性进行限制
函数组件和类式组件对组件标签属性进行限制(通用)
//对标签属性进行类型、必要性的限制
Person.propTypes = {
name:PropTypes.string.isRequired, //限制 name 必传,且为字符串
sex:PropTypes.string,//限制 sex 为字符串
age:PropTypes.number,//限制 age 为数值
speak:PropTypes.func,//限制 speak 为函数
}
//指定默认标签属性值
Person.defaultProps = {
sex:'男',//sex 默认值为男
age:18 //age 默认值为 18
}
类式组件可以直接在 Class 里使用
static propTypes = {
name:PropTypes.string.isRequired, //限制name必传,且为字符串
sex:PropTypes.string,//限制sex为字符串
age:PropTypes.number,//限制age为数值
}
//指定默认标签属性值
static defaultProps = {
sex:'男',//sex默认值为男
age:18 //age默认值为18
}
ref
1. 类式组件
```jsx
<input ref="input1" type="text" placeholder="点击按钮提示数据"/>
showData = ()=>{
const {input1} = this.refs
alert(input1.value)
}
<input ref={c => this.input1 = c } type="text" placeholder="点击按钮提示数据"/>
showData = ()=>{
const {input1} = this
alert(input1.value)
}
<input ref={this.saveInput} type="text" />
saveInput = (c) => {
this.input1 = c;
console.log("@", c);
};
<input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/>
myRef = React.createRef()
showData = ()=>{
alert(this.myRef.current.value);
}
```
2. 函数式组件
```jsx
<input type="text" ref={myRef} />;
const myRef = React.useRef();
```
事件处理
(1).通过 onXxx 属性指定事件处理函数(注意大小写) a.React 使用的是自定义(合成)事件, 而不是使用的原生 DOM 事件 —————— 为了更好的兼容性 b.React 中的事件是通过事件委托方式处理的(委托给组件最外层的元素) ————————为了高效 (2).通过 event.target 得到发生事件的 DOM 元素对象 ——————————不要过度使用 ref
受控组件和非受控组件
在 React 中,所谓受控组件和非受控组件,是针对表单而言的。
React 组件的数据渲染是否被调用者传递的props
完全控制,控制则为受控组件,否则非受控组件。
状态提升
如果有多个组件共享一个数据,把这个数据放到共同的父级组件中来管理
高阶函数_函数柯里化
/*
高阶函数:如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。
2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。
常见的高阶函数有:Promise、setTimeout、arr.map()等等
函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。
function sum(a){
return(b)=>{
return (c)=>{
return a+b+c
}
}
}
*/
生命周期
旧
1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
1. constructor()
2. componentWillMount()
3. render()
4. componentDidMount() =====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
2. 更新阶段: 由组件内部this.setSate()或父组件render触发
1. shouldComponentUpdate()
2. componentWillUpdate()
3. render() =====> 必须使用的一个
4. componentDidUpdate()
3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
1. componentWillUnmount() =====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
新
1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
1. constructor()
2. getDerivedStateFromProps
3. render()
4. componentDidMount() =====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
2. 更新阶段: 由组件内部this.setSate()或父组件重新render触发
1. getDerivedStateFromProps
2. shouldComponentUpdate()
3. render()
4. getSnapshotBeforeUpdate
5. componentDidUpdate()
3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
1. componentWillUnmount() =====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
key 的 作用
经典面试题: 1). react/vue 中的 key 有什么作用?(key 的内部原理是什么?) 2). 为什么遍历列表时,key 最好不要用 index?
1. 虚拟DOM中key的作用:
1). 简单的说: key是虚拟DOM对象的标识, 在更新时key起着极其重要的作用。
2). 详细的说: 当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,
随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:
a. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
(1).若虚拟DOM中内容没变, 直接使用之前的真实DOM
(2).若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM
b. 旧虚拟DOM中未找到与新虚拟DOM相同的key
根据数据创建新的真实DOM,随后渲染到到页面
2. 用index作为key可能会引发的问题:
1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
2. 如果结构中还包含输入类的DOM:
会产生错误DOM更新 ==> 界面有问题。
3. 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,
仅用于渲染列表用于展示,使用index作为key是没有问题的。
3. 开发中如何选择key?:
1.最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
2.如果确定只是简单的展示数据,用index也是可以的。
setState
//对象式的setState
this.setState({count:count+1},()=>{
console.log(this.state.count);
//第二个参数时回调
})
//函数式的setState
this.setState( state => ({count:state.count+1}))
状态必须通过 setState 进行更新,且更新是一种合并,不是替换
配置代理
在项目根目录建立 setupProxy.js
文件 在文件中配置
const proxy = require("http-proxy-middleware");
module.exports = function (app) {
app.use(
proxy("/api1", {
//遇见/api1 前缀的请求,就会触发该代理配置
target: "http://localhost:5000", //请求转发给谁
changeOrigin: true, //控制服务器收到的请求头中 Host 的值
pathRewrite: { "^/api1": "" }, //重写请求路径(必须)
}),
proxy("/api2", {
target: "http://localhost:5001",
changeOrigin: true,
pathRewrite: { "^/api2": "" },
})
);
};
发布订阅
安装 pubsub-js 包
import PubSub from 'pubsub-js'
//发布
PubSub.publish('atguigu',{isFirst:false,isLoading:true})
//订阅
componentDidMount(){
this.token = PubSub.subscribe('atguigu',(\_,stateObj)=>{
this.setState(stateObj)
})
}
//取消订阅
componentWillUnmount(){
PubSub.unsubscribe(this.token)
}
fetch
fetch 实例
async search() {
try {
const response = await fetch(`/api1/search/users2?q=${keyWord}`);
const data = await response.json();
console.log(data);
} catch (error) {
console.log("请求出错", error);
}
}
路由
安装 react-router-dom@5.2.0
网络请求
1.设计状态时要考虑全面,例如带有网络请求的组件,要考虑请求失败怎么办。
2.ES6小知识点:解构赋值+重命名
let obj = {a:{b:1}}
const {a} = obj; //传统解构赋值
const {a:{b}} = obj; //连续解构赋值
const {a:{b:value}} = obj; //连续解构赋值+重命名
3.消息订阅与发布机制
1.先订阅,再发布(理解:有一种隔空对话的感觉)
2.适用于任意组件间通信
3.要在组件的componentWillUnmount中取消订阅
4.fetch发送请求(关注分离的设计思想)
try {
const response= await fetch(`/api1/search/users2?q=${keyWord}`)
const data = await response.json()
console.log(data);
} catch (error) {
console.log('请求出错',error);
}
路由的基本使用
1.明确好界面中的导航区、展示区
2.导航区的a标签改为Link标签
<Link to="/xxxxx">Demo</Link>
3.展示区写Route标签进行路径的匹配
<Route path='/xxxx' component={Demo}/>
4.<App>的最外侧包裹了一个<BrowserRouter>或<HashRouter>
路由组件与一般组件
1.写法不同:
一般组件:<Demo/>
路由组件:<Route path="/demo" component={Demo}/>
2.存放位置不同:
一般组件:components
路由组件:pages
3.接收到的props不同:
一般组件:写组件标签时传递了什么,就能收到什么
路由组件:接收到三个固定的属性
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
location:
pathname: "/about"
search: ""
state: undefined
match:
params: {}
path: "/about"
url: "/about"
NavLink 与封装 NavLink
1.NavLink可以实现路由链接的高亮,通过activeClassName指定样式名
Switch 的使用
1.通常情况下,path和component是一一对应的关系。
2.Switch可以提高路由匹配效率(单一匹配)。
解决多级路径刷新页面样式丢失的问题
1.public/index.html 中 引入样式时不写 ./ 写 / (常用)
2.public/index.html 中 引入样式时不写 ./ 写 %PUBLIC_URL% (常用)
3.使用HashRouter
路由的严格匹配与模糊匹配
1.默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)
2.开启严格匹配:<Route exact={true} path="/about" component={About}/>
3.严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由
Redirect 的使用
1.一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由
2.具体编码:
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Redirect to="/about"/>
</Switch>
嵌套路由
1.注册子路由时要写上父路由的path值
2.路由的匹配是按照注册路由的顺序进行的
向路由组件传递参数
1.params参数
路由链接(携带参数):<Link to='/demo/test/tom/18'}>详情</Link>
注册路由(声明接收):<Route path="/demo/test/:name/:age" component={Test}/>
接收参数:this.props.match.params
2.search参数
路由链接(携带参数):<Link to='/demo/test?name=tom&age=18'}>详情</Link>
注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
接收参数:this.props.location.search
备注:获取到的search是urlencoded编码字符串,需要借助querystring解析
3.state参数
路由链接(携带参数):<Link to={{pathname:'/demo/test',state:{name:'tom',age:18}}}>详情</Link>
注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
接收参数:this.props.location.state
备注:刷新也可以保留住参数
路由懒加载
import React, { Component,lazy,Suspense} from 'react'
const Home = lazy(()=> import('./Home') )
const About = lazy(()=> import('./About'))
<Suspense fallback={<Loading/>}>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</Suspense>
编程式路由导航
借助 this.prosp.history 对象上的 API 对操作路由跳转、前进、后退
-this.prosp.history.push()
-this.prosp.history.replace()
-this.prosp.history.goBack()
-this.prosp.history.goForward()
-this.prosp.history.go()
//replace 跳转+携带 params 参数
//this.props.history.replace(/home/message/detail/${id}/${title}
)
//replace 跳转+携带 search 参数
// this.props.history.replace(/home/message/detail?id=${id}&title=${title}
)
//replace 跳转+携带 state 参数
this.props.history.replace(/home/message/detail
,{id,title})
//非路由组件使用 路由
import {withRouter} from 'react-router-dom'
export default withRouter(Header)
//withRouter 可以加工一般组件,让一般组件具备路由组件所特有的 API
//withRouter 的返回值是一个新组件
BrowserRouter 与 HashRouter 的区别
1.底层原理不一样:
BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。
HashRouter使用的是URL的哈希值。
2.path表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test
3.刷新后对路由state参数的影响
(1).BrowserRouter没有任何影响,因为state保存在history对象中。
(2).HashRouter刷新后会导致路由state参数的丢失!!!
4.备注:HashRouter可以用于解决一些路径错误相关的问题。
antd 的按需引入+自定主题
1.安装依赖:yarn add react-app-rewired customize-cra babel-plugin-import less less-loader
2.修改package.json
....
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
....
3.根目录下创建config-overrides.js
//配置具体的修改规则
const { override, fixBabelImports,addLessLoader} = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
lessOptions:{
javascriptEnabled: true,
modifyVars: { '@primary-color': 'green' },
}
}),
);
4.备注:不用在组件里亲自引入样式了,即:import 'antd/dist/antd.css'应该删掉
rudux
安装包
"react-redux": "^7.2.2",
"react-scripts": "4.0.0",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
目录结构
src
-pages(存放用到router的组件)
-components(存放不用redux的组件)
-containers(存放用到redux的组件)
-redux(redux相关文件)
-store.js 总仓库
-constant.js 存放容易写错的TYPE类型 变量名用大写
-actions 将传过来的数据包装成action对象,并且发送给reducer进行处理
-count.js
-person.js
-reducers 更改数据,只能是纯函数
-count.js
-person.js
-index.js 汇总后的reducer
redux 精简版
(1).去除Count组件自身的状态
(2).src下建立:
-redux
-store.js
-count_reducer.js
(3).store.js:
1).引入redux中的createStore函数,创建一个store
2).createStore调用时要传入一个为其服务的reducer
3).记得暴露store对象
(4).count_reducer.js:
1).reducer的本质是一个函数,接收:preState,action,返回加工后的状态
2).reducer有两个作用:初始化状态,加工状态
3).reducer被第一次调用时,是store自动触发的,
传递的preState是undefined,
传递的action是:{type:'@@REDUX/INIT_a.2.b.4}
(5).在index.js中监测store中状态的改变,一旦发生改变重新渲染<App/>
备注:redux只负责管理状态,至于状态的改变驱动着页面的展示,要靠我们自己写。
项目入口文件 `index.js` 加入 以下代码
store.subscribe(()=>{
ReactDOM.render(<App/>,document.getElementById('root'))
})
redux 完整版
新增文件:
1.count_action.js 专门用于创建action对象
2.constant.js 放置容易写错的type值
redux 异步 action 版
(1).明确:延迟的动作不想交给组件自身,想交给action
(2).何时需要异步action:想要对状态进行操作,但是具体的数据靠异步任务返回。
(3).具体编码:
1).yarn add redux-thunk,并配置在store中
2).创建action的函数不再返回一般对象,而是一个函数,该函数中写异步任务。
3).异步任务有结果后,分发一个同步的action去真正操作数据。
(4).备注:异步action不是必须要写的,完全可以自己等待异步任务的结果了再去分发同步action。
在store
写入以下代码
//引入createStore,专门用于创建redux中最为核心的store对象
import {createStore,applyMiddleware} from 'redux'
//引入为Count组件服务的reducer
import countReducer from './count_reducer'
//引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'
//暴露store
export default createStore(countReducer,applyMiddleware(thunk))
react-redux 基本使用
(1).明确两个概念:
1).UI组件:不能使用任何redux的api,只负责页面的呈现、交互等。
2).容器组件:负责和redux通信,将结果交给UI组件。
(2).如何创建一个容器组件————靠react-redux 的 connect函数
connect(mapStateToProps,mapDispatchToProps)(UI组件)
-mapStateToProps:映射状态,返回值是一个对象
-mapDispatchToProps:映射操作状态的方法,返回值是一个对象
(3).备注1:容器组件中的store是靠props传进去的,而不是在容器组件中直接引入
(4).备注2:mapDispatchToProps,也可以是一个对象
react-redux 优化
(1).容器组件和UI组件整合一个文件
(2).无需自己给容器组件传递store,给<App/>包裹一个<Provider store={store}>即可。
(3).使用了react-redux后也不用再自己检测redux中状态的改变了,容器组件可以自动完成这个工作。
(4).mapDispatchToProps也可以简单的写成一个对象
(5).一个组件要和redux“打交道”要经过哪几步?
(1).定义好UI组件---不暴露
(2).引入connect生成一个容器组件,并暴露,写法如下:
connect(
state => ({key:value}), //映射状态
{key:xxxxxAction} //映射操作状态的方法
)(UI组件)
(4).在UI组件中通过this.props.xxxxxxx读取和操作状态
react-redux 数据共享版
(1).定义一个Pserson组件,和Count组件通过redux共享数据。
(2).为Person组件编写:reducer、action,配置constant常量。
(3).重点:Person的reducer和Count的Reducer要使用`combineReducers`进行合并,
合并后的总状态是一个对象!!!
(4).交给store的是总reducer,最后注意在组件中取出状态的时候,记得“取到位”。
//引入combineReducers,用于汇总多个reducer
import {combineReducers} from 'redux'
//引入为Count组件服务的reducer
import count from './count'
//引入为Person组件服务的reducer
import persons from './person'
//汇总所有的reducer变为一个总的reducer
export default combineReducers({
count,
persons
})
react-redux 开发者工具的使用
(1).yarn add redux-devtools-extension
(2).store中进行配置
import {composeWithDevTools} from 'redux-devtools-extension'
const store = createStore(allReducer,composeWithDevTools(applyMiddleware(thunk)))
react-redux 最终版
(1).所有变量名字要规范,尽量触发对象的简写形式。
(2).reducers文件夹中,编写index.js专门用于汇总并暴露所有的reducer
Context
const MyContext = React.createContext() const {Provider,Consumer} = MyContext
// 祖先组件 发送数据
<Provider value={{username,age}}>
<B/>
</Provider>
// 字组件 接受数据
<Consumer>
{value => `${value.username},年龄是${value.age}`}
</Consumer>
错误边界_ErrorBoundary
子组件出问题,父组件里进行处理
import React, { Component } from "react";
import Child from "./Child";
export default class Parent extends Component {
state = {
hasError: "", //用于标识子组件是否产生错误
};
//当Parent的子组件出现报错时候,会触发getDerivedStateFromError调用,并携带错误信息
static getDerivedStateFromError(error) {
console.log("@@@", error);
return { hasError: error };
}
componentDidCatch() {
console.log("此处统计错误,反馈给服务器,用于通知编码人员进行bug的解决");
}
render() {
return (
<div>
<h2>我是Parent组件</h2>
{this.state.hasError ? <h2>当前网络不稳定,稍后再试</h2> : <Child />}
</div>
);
}
}
react 插槽_renderProps
import React, { Component } from "react";
import "./index.css";
export default class Parent extends Component {
render() {
return (
<div className="parent">
<h3>我是Parent组件</h3>
<A render={(name) => <B name={name} />} />
</div>
);
}
}
class A extends Component {
state = { name: "tom" };
render() {
console.log(this.props);
const { name } = this.state;
return (
<div className="a">
<h3>我是A组件</h3>
{this.props.render(name)}
</div>
);
}
}
class B extends Component {
render() {
console.log("B--render");
return (
<div className="b">
<h3>我是B组件,{this.props.name}</h3>
</div>
);
}
}