安装
安装脚手架:yarn add global create-react-app
进入项目所在目录
使用脚手架快速搭建react项目:create-react-app
认识React
理念:
函数式、数据不可变
import React from 'react';
import ReactDOM from 'react-dom';
React元素和React函数组件
App1 = React.createElement(‘div’,null,n) 👉App1是一个 React元素
App2 = () => React.createElement(‘div’,null,n) 👉App2是一个 React函数组件
React元素:
React.createElement的返回值element可以代表一个div元素,但不是真正的div(DOM对象),不能直接插入DOM中,所以我们一般称返回值element为虚拟DOM对象
React函数组件:() => React元素
() =>React.createElement返回值element也可以代表一个div,但这个函数可以多次执行,每次得到最新的虚拟div
React会对比每次的虚拟div,找出不同,局部更新视图,找不同的算法叫做DOM Diff算法,避免跨线程操作DOM,先在内部虚拟DOM对象进行统一操作,提高效率(可在页面调试通过闪烁辨别是否整体替换DOM对象还是局部替换)
类组件和函数组件
首先明确类的本质也是函数
关注:
外部数据props
内部数据state(数据的:初始化、读、写)
事件绑定(书写格式的选择)
最大区别:
类组件有this,函数组件没有this
外部数据props
props认识:<B name="frank" onClick={this.onClick}>hi</B>
B 组件接收到的 props 的值是{name:"frank", onClick:this.onClick, children: 'hi'}
类组件:
this.props.xxx
class App extends React.Component {
constructor(props){ //可在此处传入外部数据
super(props);
this.state={ //内部数据初始化
n:0;
m:0
z:{
name:"npc";
age:18
}
}
};
父组件向子组件传递数据
天然地使用props
子组件向父组件传递数据
也可以使用props
通过在父组件传递props函数给子组件
步骤:
- 在父组件中创建函数,使用props绑定给子组件
<Child addFun={this.addFun}/>
- 子组件从props解构出对应的函数名
const { addFun } = props
- 子组件将自身数据作为实参传入接收的函数中调用,此处是在抽离出的数据处理逻辑代码中调用的,通过
this.props.addFun(xxx)
,jsx部分的代码中没有直接调用,也不推荐直接在jsx中直接调用函数,但有些场景下只能在jsx中拿到实参,如何做到不自执行又能拿到实参?可以在jsx中使用匿名箭头函数,在箭头函数的函数体中直接调用调用函数,方便直接原地传入实参
<button onClick={deleteContent(id)}>删除数据</button>
- 父组件在自身的函数中设置形参接收子组件传递过来的参数
const addFun =(xxx)=>{...可在父组件中接收子组件传来的数据做处理}
内部数据state
类组件:
this.state:{xxx:yyy;…}初始值
this.state.xxx读
this.setState(()=>{…})写 //setState()是异步更新,其中优先放置函数,手动返回最新值
函数组件:
const [n,setN] = React.useState(初始值) 第一项读,第二项写
复杂的state:
当state对象中出现多个属性需要修改时:
不要依赖react的shallow merge自动合并,需要自己手动使用…操作符 合并
shallow merge:React 只会检查新 state 和旧 state 第一层的区别,并把新 state 缺少的数据从旧 state 里拷贝过来
举例:
类组件:
第一层属性会自动合并
第二层属性开始不会自动合并,需要手动使用…操作符拷贝原有state对象
class App extends React.Component {
constructor(){
super();
this.state={ //数据初始化
n:0;
m:0
z:{
name:"npc";
age:18
}
}
};
add(){
this.setState( {...this.state,n:this.state.n+=1} ) //数据的写(优先写函数)
};
}
函数组件:
使用setN回调不会自动合并,需要使用setSate回调手动合并
const App = () => {
// const [n, setN] = React.useState(0); //数据初始化
// const [m,setM] = React.useState(1) //可以分开写出多个对象,灵活控制
const [state,setState] = React.useState(0) //可以统一写进state对象,往后需要解构
return (
<div className="App">
n:{n} //数据的读
<button
onClick={() => {
//setN(n + 1); //数据的写
//setM(m + 2)
setState(...state,state.n + 1)
}}
>
+1
</button>
</div>
);
}
修改state的最佳实践
在需要需要修改state的地方,最好统一先将原始state使用…扩展运算符拷贝一份下来
eg.因为通常情况下state的数据较多,可能state对象中包含一个选项,但是选项又属于object[],需要对每个object做操作是不利于state整体更新的,办法是先将原始state拷贝下来赋值给新声明的变量(一般与原先state的选项同名),再在setState回调中统一覆盖原先的state
addTodo = (todo)=>{
let todos = [...this.state.todos , todo] //一般同名
this.setState({
todos:todos
})
}
setState异步更新解决机制
setState回调是异步更新的,会在页面UI更新完成后再更新数据,并不会同步渲染到页面上,若出现多个setState回调则无法正常渲染
解决方法:
利用setState(fn,callback)第二个回调参数(只适合两个修改的情况)(不推荐)
使用多个setState回调传函数,不要传对象,具体修改多少个就写多少个setState回调
class Son extends React.Component{
//addN = () => this.setState( {n: this.state.n + 1} );改为以下:
onClick = () =>{
this.setState((state) => ({ n:state.n + 1}) ) //函数返回值若为对象则用()包裹
this.setState((state) => ({ m:state.m + 1}) )
}
render(){
return <button onClick={ this.addN }>n+1</button>
}
}
注意:函数返回值若为对象则用()包裹,如this.setState( ()=> ({…}) )
//踩坑:当input框输入值回车或确认按钮后,input框重置状态失败
//分析:setState是异步更新的,会在页面UI更新完成后再更新数据,此时页面UI没有触发重新渲染,故不会更新数据
//解决:this.state.content数据已重置,只需在jsx模板中添加绑定value值,react自动会进行UI绑定更新
handleSubmit = (e) =>{
e.preventDefault()
//重置状态
this.setState({
content:''
})
}
render(){
return (
...
<input type = 'text' onChange={this.handleChange} value={this.state.content}>//绑定value
)
}
事件绑定
类组件:
class Son extends React.Component{
addN = () => this.setState( {n: this.state.n + 1} );
render(){
return <button onClick={ this.addN }>n+1</button>
}
}
思考箭头函数和普通函数区别
普通函数有原型,箭头函数没有原型(深拷贝用法处)
普通函数挂在原型上,而不是实例对象上
箭头函数挂在每次的实例对象上,而不是原型上
函数组件:onClick={ () => {setN(n + 1)} }
直接内部写箭头函数onClick={ onClick }
或者写函数名,然后外部声明该函数:
const onClick = ()=> {
setN(n + 1)
}
+1案例:
类组件:
熟悉书写格式
class App extends React.Component {
constructor(){
super();
this.state={ //数据初始化
n:0
}
};
add(){
this.setState({n:this.state.n+=1}) //数据的写(优先写函数)
};
render(){
return (
<div className="App">
n:{this.state.n} //数据的读
<button onClick={()=>{ this.add()} }>+1</button> //事件绑定
</div>
);
}
}
函数组件:
const App = () => {
const [n, setN] = React.useState(0); //数据初始化
return (
<div className="App">
n:{n} //数据的读
<button
onClick={() => {
setN(n + 1); //数据的写
}}
>
+1
</button>
</div>
);
};
有状态组件和无状态组件
也称:容器组件和UI组件,本质就是类组件和函数组件,只是区分应用场景,作为“容器是有状态的”作为“UI只是被动的接收数据和处理展示数据”
容器组件:
- 包含state状态
- 拥有生命周期钩子
- 不包含UI
- 使用类创建组件
UI组件:
- 不包含state状态
- 从props接收数据
- 只包含UI
- 使用函数创建组件
案例:
- 容器组件模板中存放两个子组件;
- AddContact.js子容器组件通过props函数传递给App.js父容器组件的内容,也会通过父容器组件绑定给Contact.js子UI组件的props进行实时的动态传递,更新子UI组件展示的数据,此时的App.js父容器组件相当于共享的状态组件
常用的数组API
react理念提倡不修改原数据,则state数据中常用数组api应该返回新数组:
map遍历,结合if判断语句,常用于筛选遍历,返回新数组
filter过滤,返回值为true的数据项组成的数组,常用于删除操作,保留不是目标id的数据项,返回新数组
认识JSX
抽离数据逻辑处理内容
此处举例过滤筛选的数据处理操作
数据的逻辑处理可写入return()的jsx内容中,但逻辑容易混乱,一般将数据的逻辑处理部分单独抽离出来为一个函数复制给变量,再将数据处理的变量结果放置于jsx部分
本质
本质是React.createElement()
即使用babel会自动将return(
注意
jsx中label标签不能使用for属性,改为htmlFor
表单数据提交
提交事件可以添加在form标签上,也可以添加在button标签上
添加在form标签上的事件使用onSubmit事件,添加在button上的事件为onClick事件
- onSubmit事件默认为form表单提交,可以提交表单内容
- onClick事件则是指定处理函数,也可以完成表单提交工作,但通常用作异步提交,可以走后台代码
虚拟DOM
内容是怎么在react当中进行渲染的?
并不是每次都完整地更新我们所渲染的整个内容
而是根据新状态state得出新的jsx模板内容,和当前状态state得到的jsx模板内容做对比,找到不同再更新到真实的DOM中,加快浏览器的运行效率
类组件生命周期
组件初始化阶段:
constructor() 初始化props、state,一般初始化state,因为props固定,其中不能调用setState回调,
可以用来bind this,但一般使用箭头函数并写在外部
shouldComponentUpdate() return false阻止更新,return true 同意更新(勿忘)
面试:shouldComponentUpdate()有什么用?
答案:它允许我们手动判断是否要进行组件更新,我们可以根据应用场景灵活地设置返回值,以避免不必要的更新
class CounterButton extends React.Component {
onClick = () => {
this.setState(state => ({ n:state.n +1})) //两个不同地址的对象,尽管渲染结果一样,
this.setState(state => ({ n:state.n -1})) //react依然会render,
//但是虚拟DOM在diff的时候发现UI没有变化,于是UI不变
}
shouldComponentUpdate(newProps,newState) {
if(newState.n === this.state.n) {
return false
}else{
return true
}
}
}
class CounterButton extends React.PureComponent {
...
//只需要将React.Component 改为 React.PureComponent
}
//只是浅比较,适用于大部分时候
PureComponent 会在 render 之前对比新 state 和旧 state 的每一个 key,以及新 props 和旧 props 的每一个 key。如果所有 key 的值全都一样,就不会 render;如果有任何一个 key 的值不同,就会 render。
render() 创建虚拟DOM
展示视图
只能有一个根元素,多个则用
render里面可以写if else
可以写三元表达式
不能直接写for循环因为for循环第一次return就是结束了,除非声明一个变量,每次循环都将结果push进变量,最后return变量
循环一般用数组方法遍历如array.map(注意所有循环都要加key)
render(){
return this.state.array.map(n=><span key={n}>{n}</span>)
}
componentDidMount() 组件已出现在页面
在元素插入页面后执行代码,这些代码依赖DOM
比如需要获取DOM高度、发起网络请求加载数据
首次DOM渲染会执行此钩子
补充:快速获取DOM:Refs的使用
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
//访问当前DOM
const node = this.myRef.current;
组件更新阶段:
componentDidUpdate() 组件已更新
在视图更新后执行代码
也可以发起网络请求更新数据
首次渲染不会执行此钩子,第二次才开始渲染
在此处setState可能会引起死循环,除非放在if判断语句中,但不推荐此用法
若shouldComponentUpdate()返回false,则不触发此钩子
组件销毁阶段:
componentWillUnmount() 组件将要死
组件将要 被移出页面然后被销毁 的时候执行代码(移出页面是UI层次,销毁是内存层次)
常用于路由中触发,一般很少去手动销毁东西
unmount 过的组件 不会再次mount,因为内存数据已经销毁
做一些收尾的操作:比如:关闭定时器,取消订阅消息等
举例:
在 DidMount里面监听了window.scroll,那么就要在WillUnmount里面取消监听
在DidMount里面创建了Timer计数器,那么就要在WillUnmount里面取消Timer
在DidMount里面创建了网络请求,那么就要在WillUnmount里面取消请求
即 谁污染了谁终要被清理
函数组件声明周期
创建方式
箭头函数、普通函数
可以代替类组件
没有state?使用useState
初始化阶段
模拟constructor即函数组件 return 之前
模拟shouldComponentUpdate,使用React.memo和useMemo
模拟render,return就是render
模拟componentDidMount,useEffect( () => { console.log('第一次渲染')}, [] )
传空数组默认首次渲染
更新阶段
模拟componentDidUpdateuseEffect( ()=>{ console.log('任意属性变更')} )
不传数组则每次都渲染useEffect( ()=>{console.log('n变了')}, [n,m,...])
[n,m,…]表示前面函数依赖、依据的变量发生改变才渲染
遇到问题:注意此处的用法在首次也进行了渲染,如何避免首次渲染?
解决方法:使用自定义的hook,完整版见下
const [count, setCount] = useState(0) //重新声明一个变量
useEffect(() => {
setCount(count + 1) //每次n变化的时候UpdateCount+1
}, [n]) //针对n变化
useEffect(() => {
if (count > 0) { //通过外置变量判断是否屏蔽首次渲染操作
console.log('nnn')
}
}, [n]); //针对n变化
自定义 更新hook
自定义 屏蔽首次渲染 的hook,实现完整模拟componentDidUpdate
//定义hook(可抽离到单独文件)
const useUpdate = (fn, array) => {
const [count, setCount] = useState(0)
useEffect(() => {
setCount(count+1)
}, array)
useEffect(() => {
if (count > 0) {
fn()
}
}, array);
}
//调用hook
useUpdate(() => { console.log('变了') }, [n])
销毁阶段
模拟componentWillUnmountuseEffect( ()=>{ console.log('第一次渲染') return ()=>{console.log('组件将要死了') } })
蓝字部分,return一个箭头函数,函数将在组件即将销毁前执行
useState的原理
解决副作用问题
useState剖析
实现简易useState
理解setN不会直接作用于n:
App() 类比于
每次setN后App()会重新执行即render,useState(0)也会得到新的值,但是setN并不是直接改变n
setN可以理解为 一个中介,存放着对n数据的修改,即每个组件都有自己的数据x,将其命名为state,而useState会从x即state从读取n最新值
//声明
let _state //外部声明变量不会被myUseState重置
const myUseState = (initialValue) => {
_state = _state === undefined ? initialValue : _state //精确控制
const setState = (newValue) => {
_state = newValue
render()
}
return [_state,setState]
}
const render =() => {
ReactDOM.render(<App/>,rootElement)
}
//调用
const App = () =>{
const [n,setN] = myUseState(0)
}
思考:为什么_state声明在外部,因为声明在函数内部则每次调用函数均会重置_state,没有缓存
避坑:_state = _state || initialValue
错误,万一_state为0(falsy值),则走了initialValue
同一组件两个useState冲突
let _state = [];
let index = 0;
const myUseState = (init) => {
const currentIndex = index //-----------重点:外设变量的作用
_state[currentIndex] = _state[currentIndex] === undefined ? init : _state[currentIndex]
const setState = (newState) => {
_state[currentIndex] = newState
render()
}
index += 1; //--------------重点:在return 之前+1
return [_state[currentIndex], setState]
}
const render = () => {
index = 0 //--------------每次render需要重置index
ReactDOM.render( < App / > , document.getElementById('root'))
}
//调用:
const App = () => {
console.log('App运行了')
const [n, setN] = myUseState(0) //同一组件使用两个或多个useState
const [m, setM] = myUseState(0)
return ( <
div > { n } <
button onClick = {
() => setN(n + 1)
} > +1 < /button> {m} <
button onClick = {
() => setM(m + 1)
} > +1 < /button> < /
div >
)
}
解决冲突思路:state 设为数组类型,默认index初始值为0;return之前+1,以便下次进来定位到数组第二+个元素
重点:
- 理解外设变量的意义,若没有外设currentIndex变量,只依赖index变量,那么return之前index+1则影响了数组第一位元素,index放return之后却没有意义,故外设不可修改值currentIndex作为中介
- 每次render的时候需要重置index,否则保留了上次的缓存
缺点:
useState调用顺序
若第一次渲染时,n是第一个,m是第二个,z是第三个
则往后渲染时必须保持顺序完全一致,因为index值由 useState 出现的顺序决定的
以防万一,react不允许 if 等判断语句中使用useState
(vue3此处做了改进和优化)
总结
- 每个函数组件对应一个React节点
- 每个节点保存着_state和 index
- useState会读取state[index]
- index 由 useState 出现的顺序决定的
- setState 会异步地修改state(此时不会及时渲染到页面中),并触发更新re-render
useRef 解决分身
出现问题:n的分身
出现场景:设置onClick+1和异步log(3秒后log值),先+1再log和 先log再+1出现的结果不同,因为onClick触发re-render出现新的App组件,新的App组件会有新的n,此时异步log仍是旧的n,因为log在旧的App组件上,针对的仍是旧的n(react理念,数据不可变,函数特性会创建新数据)
解决问题:希望有一个贯穿始终的state状态,即保持onClick和异步log的都是同一数据,避免因为频繁渲染组件产生新的组件新的state状态(useState固有缺陷:每次render都会产生新的state)
useRef补充:
useRef返回的是一个可变的ref对象,其 xxx.current 属性也就是 useRef(inital)中的inital初始化值。
所以注意 const xxxRef = useRef(0) 中, 此处的 xxxRef 实际是个对象为:{current:0}
思考:refs?
useRef另一用处:保持可变变量状态
如果需要一个值,在组件不断render时保持不变,返回的 ref对象 在组件的整个生命周期内保持不变,不会在render的时候重新生成新的组件新的ref对象
const App = () => {
const nRef = React.useRef(0)
const update = React.useState(null)[1] //定义自动render函数 (小技巧)
const log = () => { return setTimeout(() => { console.log(`n:${nRef.current}`) }, 1000) }
return (
<div >
{nRef.current}
<button onClick={
() => {nRef.current += 1;
update(nRef.current) } //难点:同时触发页面更新(即强制更新)
}>+1</button>
<button onClick={log}>log</button>
</div>
)
}
为何要同时触发页面更新?因为不触发 update(即setN) 页面不会渲染更新(不推荐)
大部分情况useRef适用,而useContext较重
useContext 解决分身
这也是实现 保持可变变量状态 的另一种方式
全局变量 是全局的“上下文”
useContext 不仅能贯穿 始终,保持可变变量的状态,还能贯穿不同的组件
//全局创建上下文Context:
const xxxContext = React.createContext(initialValue)
//外面包裹xxxContext.Provider, 通过value属性给作用域内组件传递state内部数据
//包裹标签内相当是 xxxContext 的作用域
<xxxContext.Provider value = { {yyy,zzz} }>
<div>
<ChildA /> //不管Child组件多深,value始终能传递给子组件
<ChildB />
</div>
<xxxContext.Provider>
//作用域内子组件 用useContext(xxxContext)使用上下文
const ChildA = () => {
const {yyy,zzz} = React.useContext(xxxContext)
...
}
总结
针对useState,在每次任一数据变动后都会 re-render
- 每次re-render,组件函数就会重新执行
- 对应的组件所有state都会出现“分身”,没用的分身会被垃圾回收
- 不希望出现分身则使用useRef / useContext等
React Context API
- React Context API 是针对 状态管理 而设计的API,将计算的共享数据 保存在一个通用的顶层父组件上(provider component),和redux作用类似,无需手动每层组件添加props传输数据,独立于整个组件树之外创建一个共享的数据中心以供使用;
- 以一种更直接有效的方式解决了早期使用 props来处理嵌套UI 的 状态共享问题
基本使用
创建Context上下文
- React.createContext
const MyContext = React.createContext(defaultValue);
只有当组件所处的组件树中没有匹配到 Provider 时,其 defaultValue 参数才会生效
- Context.Provider
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化
//Provider 接收一个 value 属性,传递给消费组件
class MyContextProvider extends Component {
state ={...}
render(){
return (
<MyContext.Provider value = { {yyy,zzz} }>
<div>
<ChildA /> //不管Child组件多深,value始终能传递给子组件
<ChildB />
</div>
<MyContext.Provider>
)
}
}
- 子组件(消费组件)订阅/访问 Provider React 组件(如此以来才能真正生效)
方法一:使用Class.contextType
可以让你使用 this.context 来获取最近 Context 上的值。你可以在任何生命周期中访问到它,包括 render 函数中
class Navbar extends Component {
static contextType = ThemeContext
render(){
let value = this.context;
/* 基于这个值进行渲染工作 */
}
}
方法二:使用Context.Consumer
此方法和Context.Provider相对应
<MyContext.Consumer>
{(context) => {const {xxx,yyy} = context} /* 基于 context 值进行渲染*/}
//此处为函数作为 子元素 进行渲染
</MyContext.Consumer>
注意:Class.contextType 和 Context.Consumer 区别
Class.contextType 只能订阅单一context、用于类组件
Context.Consumer 可以订阅多个 context、用于类组件、函数组件
更新Context上下文
本质上是父子组件通信
子组件(消费组件)想要更新父组件的Context上下文数据,如何做?类比Vue,同样是通过调用父组件的对应方法来改变父组件Context,只是使用API具体不同
//1.父组件内部定义函数
//2.父组件通过value对象暴露方法给子组件
class MyContextProvider extends Component {
state ={...}
+ toggleTheme =()=>{...} //一般通过函数来修改状态,父组件定义函数
render(){
return (
<MyContext.Provider value = { {yyy,zzz}, toggleTheme:this.toggleTheme}>//父组件暴露方法给子组件
<div>
<ChildA /> //不管Child组件多深,value始终能传递给子组件
<ChildB />
</div>
<MyContext.Provider>
)
}
}
可以将修改数据的逻辑抽离出来为一个单独的组件,如 ThemeToggle 组件
//1.子组件 接收 父组件传过来的方法
//2.子组件 调用 父组件传过来的方法
class ThemeToggle extends Component {
static contextType = ThemeContext
render(){
const {toggleTheme} = this.context; //从父组件中传过来的方法
return (
<button onClick={toggleTheme}>切换</button>
)
/* 基于这个值进行渲染工作 */
}
}
获取多个Context上下文
函数的返回值若 是基于Context渲染的 jsx模板,故使用 圆括号直接包括函数体
//在提供初始 context 值的 App 组件中:可嵌套包裹多个provider
function App() {
return (
<AuthContext.Provider value = {...}>
<ThemeContext.Provider value = { {yyy,zzz} }>
<div>
<ChildA /> //不管Child组件多深,value始终能传递给子组件
<ChildB />
</div>
</MyContext.Provider>
</AuthContext.Provider>
)
}
//子消费组件中:可嵌套包裹多个consumer
function Nav() {
return (
<AuthContext.Consumer>
{(authContext) => (
<ThemeContext.Consumer>
{(themeContext) => {
const {isAuthenticated,toggleAuth} = authContext
const {...} = themeContext
return (...)/* 基于这个值进行渲染工作 */
}}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}