入门
特点
- 组件化,声明式编程
- RN 移动端开发
- 虚拟dom, diffing算法减少dom交互
虚拟dom创建的两种方式
jsx语法糖的最后会通过编译变成第二种写法
- 利用jsx语法糖
const jsx = <h1>这是一个jsx创建的虚拟dom</h1>;
let app = document.getElementById("app");
ReactDOM.render(jsx, app);
- 利用createElement
const vNode = React.createElement("h1", { id: "title" }, "Hello,react");
let app = document.getElementById("app");
ReactDOM.render(vNode, app);
jsx语法规则
- 关键字class必须用className代替
- 混入js表达式必须用{}
- style必须写在js表达式内
- 单标签必须闭合
- 有且只有一个根标签
- 首字母大写表示组件,小写表示html标签
const className = "test";
const id = 7;
const jsx = (
<div>
<h1 className="red" id={id}>
关键字class需要用className代替
</h1>
<h1 style={{ fontSize: "13px", color: "#887" }}>
样式必须写在对象内
</h1>
<input style={{ marginTop: "50px" }} type="text" />
</div>
);
ReactDOM.render(jsx, document.getElementById("app"));
- jsx 循环需要借助map; (对比v-for)
const users = ["rose", "jock", "lily", "babe"];
const jsx = (
<ul>
{users.map((item) => {
return <li key={item}>{item}</li>;
})}
</ul>
);
ReactDOM.render(jsx, document.getElementById("app"));
组件
组件必须遵循大驼峰规则
函数式组件
函数式组件render过程,检测到组件时一个函数式组件,执行函数返回jsx,调用render方法
function Demo() {
return <h1>这是一个函数式组件名称为Demo</h1>;
}
ReactDOM.render(<Demo />, document.getElementById("app"));
类组件
类组件render过程,检测到组件是一个类组件,创建一个类实例,执行类实例上的render方法返回一个jsx,调用render方法
这就使得类组件中必须包含一个render方法且必须返回一个jsx对象
class ClassDemo extends React.Component {
render() {
return <h1>这是一个名为ClassDemo的类组件,复杂场景下使用</h1>;
}
}
ReactDOM.render(<ClassDemo />, document.getElementById("app"));
深入组件
state
- state存放于组件实例身上,需要重写constructor添加
- react中的事件不同于dom写法,需要符合小驼峰规则,且不需要()
- 事件的回调函数的调用者是jsx而非组件实例,所以需要在constructor中借助bind改变this指向
- 调用setState传递一个对象后,会合并对象,然后通知react重新执行render函数
class ClassDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
isHot: false,
};
this.changeWeather = this.changeWeather.bind(this);
}
render() {
let { isHot } = this.state;
return (
<h1 onClick={this.changeWeather}>今天很{isHot ? "热" : "冷"}</h1>
);
}
changeWeather(e) {
this.setState({
isHot: true,
});
}
}
ReactDOM.render(<ClassDemo />, document.getElementById("app"));
props
- 每一个组件都有props
- 为父组件添加属性后react会自动将其转换为props
- 通过展开运算符…批量传入属性后,react内部会将其转换为属性传递的方式为组件添加props
- 给类添加静态属性propTypes来限制props的类型
- 给类添加静态属性 defaultProps 来设置props默认值
class Student extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
sex: PropTypes.number,
};
static defaultProps = {
sex: 1,
};
render() {
let { name, age, sex } = this.props;
return (
<div style={{ marginTop: "30px" }}>
<div>姓名:{name}</div>
<div>年龄: {age}</div>
<div>性别: {sex === 1 ? "男" : "女"}</div>
</div>
);
}
}
var rose = {
name: "rose",
age: 99,
};
function StuList(props) {
console.log(props);
return (
<ul>
<li>{<Student name="jack" age={18} sex={0} />}</li>
<li>{<Student {...rose} />}</li>
</ul>
);
}
ReactDOM.render(<StuList data="2" />, document.getElementById("app"));
refs
创建ref总共有三种方式,以下将对每种方式进行优缺点对比
字符串 | 回调函数 | createRef |
---|---|---|
操作简单,统一添加在refs中 | 使用最多,写在函数内,添加到实例上 | 未来趋势,结构规整 |
性能消耗大,官方不推荐 | 可读性差,render时调用两次(可忽略) | 需要通过新增的[ref].current取 |
class Student extends React.Component {
sex = React.createRef();
showRef = (e) => {
console.log(this.refs.name, this.age, this.sex.current);
};
render() {
let { name, age, sex } = this.props;
return (
<div style={{ marginTop: "30px" }} onClick={this.showRef}>
<div ref="name">姓名:{name}</div>
<div ref={(c) => (this.age = c)}>年龄: {age}</div>
<div ref={this.sex}>性别: {sex === 1 ? "男" : "女"}</div>
</div>
);
}
}
ReactDOM.render(
<Student name="jack" age={18} sex={0} />,
document.getElementById("app")
);
forwardRef
ref转发
子组件通过forwardRef将内部的dom元素转发出去,从而达父组件操作子组件ref的目的
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
在hoc上的应用
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogProps 包裹的子组件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
事件
- 自定义合成事件“onClick”作用在于处理兼容性
- 事件是通过事件委托在组件最外层触发的,有益于减少性能消耗,e.target获取当前事件标签
- react表单的双向绑定是通过监听onChange事件的修改实现的(非受控组件),通过表单提交(受控组件)
- 在事件中自定义参数有两种方式,函数柯里化或者在回调中执行
class Form extends React.Component {
submit = (e) => {
e.preventDefault();
};
change = (type) => {
return (e) => {
console.log(type, e);
};
};
change2 = (type, e) => {
console.log(type, e);
};
render() {
return (
<div>
<form action="/" method="get" onSubmit={this.submit}>
<input type="text" onChange={this.change("username")} />
<input
type="text"
onChange={(e) => this.change2("password", e)}
/>
<button type="submit">submit</button>
</form>
</div>
);
}
}
生命周期
- 初始化阶段:ReactDOM.render触发 — 初次渲染
1. constructor()
2. componentWillMount()
3. render()
4. componentDidMount()
- 更新阶段:组件内部setState或父组件重新render触发
1. shouldComponentUpdate()
2. componentWillUpdate()
3. render()
4. getSnapshotBeforeUpdate()
5. componentDidUpdate()
- 卸载组件:ReactDOM.unmountComponentAtNode触发
1. componentWillUnmount()
路由
基本使用
- 有两种路由
BrowserRouter
和HashRouter
,所有路由的操作都必须在这两个组件内使用 NavLink
和Link
都是用在跳转,区别在于NavLink
会对当前选中的路由添加active
类,添加replace
属性将更换记录,to
属性指定跳转路径Route
用于指定路由页面,extra
表示精准匹配,path
表示匹配页面的路径,component
用于指定组件Switch
用做路由惰性匹配
,保证只会匹配到第一个路由(默认命中多个展示多个)Redirect
用在路由全部未命中的情况- 当一个组件不是路由组件时,可以在
导出
时使用withRouter
方法转为路由组件,这样才能使该组件可以正常操作路由 - 函数式跳转的方法在路由组件的props中可以看到
import React, { Component } from "react";
import {
Route,
NavLink,
BrowserRouter,
Switch,
Redirect,
} from "react-router-dom";
import Message from "./Message";
import New from "./New";
export default class Home extends Component {
render() {
return (
<div>
<BrowserRouter>
<div>
<NavLink to="/home/message" style={{ paddingRight: "20px" }}>
message
</NavLink>
<NavLink replace to="/home/new">
new
</NavLink>
</div>
<div>
<Switch>
<Route path="/home/message" component={Message}></Route>
<Route exact path="/home/new" component={New}></Route>
<Redirect to="/home/message" />
</Switch>
</div>
</BrowserRouter>
</div>
);
}
}
BrowserRouter与HashRouter
BrowserRouter | HashRouter | |
---|---|---|
原理 | H5的history API (历史记录) | hash锚点 |
path | 优雅 | 带# |
刷新后 | state存在 | state消失 |
路径影响 | 可能影响public文件夹下的某些相对文件路径 | 无 |
hook
useState
作用:使函数组件也具备state的状态,并进行读写操作
参数:初始化的值,并缓存到内部
返回:【状态值,更新状态函数】
更新状态的函数可以有两种写法,直接传值或者写一个返回值的函数
let [count, setCount] = useState(0);
return (
<>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>方式1</button>
<button onClick={() => setCount((state) => state + 1)}></button>
</>
);
useEffect
作用:可以执行副作用操作(生命周期)。副作用操作有ajax、设置订阅/定时器、操作dom等
参数:【副作用操作函数,需要监控的states】
传入的fn参数相当于componentDidMount+componentDidUpdate,他的返回fn相当于componentWillUnmount
let [count, setCount] = useState(0);
useEffect(() => {
let timer = setInterval(() => {
setCount(++count);
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return (
<>
<span>{count}</span>
</>
);
useRef
作用:获取ref对象,用法与createRef相似
场景:1. 获取ref引用,2.ref的转发不会因为render的变化而发生改变,所以可以用来记录上一次的值
const [count, setCount] = useState(0);
let countRef = useRef();
function handleClick(params) {
console.log(countRef);
}
return (
<>
<button ref={countRef} onClick={handleClick}>
ref
</button>
</>
);
useMemo
作用:缓存变量,可以用来自定义computed
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback
作用:缓存函数,常伴随着React.memo一起使用,主要用于解决react中父组件render后,事件的引用发生变化从而导致的不必要性能消耗问题
const Count = () => {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
// 注意这里的变化 使用了useCallback
const handleClick = useCallback(() => {
setOther(other => other + 1);
},[])
return <Button onClick={handleClick}>{count}</Button>;
};
const Button = React.memo((props) => {
console.log("刷新了");
const { onClick, children } = props;
return <button onClick={onClick}>{children}</button>;
});
// 只在初始化时打印了一次 刷新了
// 而后的每次点击 不在打印
触发render更新方法
- 外界props发生变化
- 组件内部调用this.setState
- 组件内部调用forceUpdate
组件通信方式
- 父子组件通过prop+prop回调实现通信
- ref获取组件实例来实现父子组件通信
- 兄弟组件通过将数组源放在同一个父组件上实现通信
- 祖孙组件通过createContext创建上下文容器,通过Provider+Consumer实现通信
- 兄弟组件通过将数组源放在同一个父组件上实现通信
- 以上几种都可以通过第三方emit库添加发布订阅实现通信
export const Context = createContext();
function GrandChild() {
return (
<div className="grand-child">
{/* 函数组件读取祖先组件数据 */}
<Context.Consumer>
{(value) => {
return <div>ParentId:{value.id}</div>;
}}
</Context.Consumer>
</div>
);
}
class Grand extends Component {
// 类组件读取祖先组件数据
static contextType = Context;
state = {
gid: "3",
};
render() {
return (
<div className="grand">
<div>ParentId:{this.context.id}</div>
<GrandChild />
</div>
);
}
}
class Child extends Component {
grand = createRef();
componentDidMount() {
// 回调的形式向父组件传参
this.props.cid(2);
}
render() {
return (
<div className="child">
<div>
ParentId:{this.props.pid},GrandId:
{this.grand.current?.state?.gid}
</div>
{/* 通过ref获取到子组件的值 */}
<Grand ref={this.grand} />
</div>
);
}
}
export default class Parent extends Component {
state = {
id: 1,
cid: "",
};
getCid(cid) {
this.setState({
cid,
});
}
render() {
return (
<div className="parent">
{/* 爷爷组件通过 Provider+value向后代传递数据*/}
<Context.Provider value={this.state}>
<div>ChildId:{this.state.cid}</div>
{/* 通过prop+回调实现父子组件通信 */}
<Child pid={this.state.id} cid={this.getCid.bind(this)} />
</Context.Provider>
</div>
);
}
}
拓展补充
Fragment
概念:类似于template,不需要渲染标签使用
区别:都是类似template,不过Fragment可以加key,<>不可以
<Fragment><Fragment>
<></>
PureComponent
作用:与component不同的是,只有在组件state或props数据发生变化后才会触发render
注意:检测变化属于浅比较
,因此不能通过引用修改值(与redux相似)
拓展:shouldComponentUpdate(preProp,preState)
class StaticComp extends PureComponent {
render() {
console.log("只有props和state变化才会打印");
return <div>静态组件</div>;
}
}
export default class Extra extends Component {
state = {
count: 0,
};
shouldComponentUpdate(preP, preS) {
return preS.count < 5;
}
render() {
let { count } = this.state;
return (
<div>
<span onClick={() => this.setState({ count: count + 1 })}>{count}</span>
<StaticComp />
</div>
);
}
}
插槽
- 默认插槽使用
props.children
获取,但是无法获取到插槽外父组件的值 - render props 通过
props函数回调
的方式获取到父组件传递过来的参数
function Parent(props) {
let [parentName, setParentName] = useState("parent--name");
return (
<div>
{/* 默认插槽渲染 */}
<div>default-slot:{props.children}</div>
{/* 具名自定义插槽渲染 */}
<div>render-slot:{props.render(parentName)}</div>
</div>
);
}
function Child(props) {
return <span>child:{props.data}</span>;
}
export default class Extra extends Component {
render() {
return (
<div>
{/* 通过指定prop传递子组件并获取到父组件传递过来的值(作用域插槽) */}
<Parent render={(d) => <Child data={d} />}>
<span style={{ color: "red" }}>9</span>
</Parent>
</div>
);
}
}
HOC高阶组件(mixins)
- 定义:传入一个组件,对组件进行加工并返回
- 本质:一个函数,传入一个组件,对组件进行加工(将mixinData通过prop进行传递),返回加工后的组件
- 原则:
-
- 只是用作加工
-
- 不能改变原组件
-
- 相当于一个组件的装饰器
-
- 高阶组件名称以with开头,显示名称以With开头
-
- 禁止在render中使用高阶组件
-
- 如果存在静态方法务必手动复制
// hoc高阶组件
const withHoc = (WarpedComponent) => {
return class extends Component {
state = {
data: null,
};
componentDidMount() {
let data = localStorage.getItem("token") || "test";
this.setState({
data,
});
}
render() {
return <WarpedComponent data={this.state.data} {...this.props} />;
}
};
};
class About extends Component {
render() {
return <div>{this.props.data}</div>;
}
}
export default withHoc(About);
错误边界
- 静态方法getDerivedStateFromError的返回值将会被合并到state中
- componentDidCatch钩子会捕获到错误,一般用于发送请求通知后端
class Child extends Component {
render() {
return (
<ul>
{this.props.list?.map((item) => {
return <li key={item}>{item}</li>;
})}
</ul>
);
}
}
export default class Extra extends Component {
state = {
list: "",
// list: ["jack", "rose"],
};
// 返回值将合并到state中
static getDerivedStateFromError(e) {
return {
hasError: true,
};
}
// 统计页面的错误。发送请求发送到后台去
componentDidCatch(error, info) {
console.log(error, info);
}
render() {
let { hasError } = this.state;
return (
<div>{hasError ? "error" : <Child list={this.state.list}></Child>}</div>
);
}
}
redux
核心组成
action
- 概念:将数据从应用传到store的载体,也是store数据的唯一来源,通过
store.dispatch(action)
将action
传递给store
- 本质:一个js
普通对象
,内部必须有一个type属性
来表明需要执行的动作 - 形式:
对象形式
,创建函数形式
(一个函数返回值
是action普通对象
) - 注意点:action只是描述了有事情会发生,起到一个
通知与载体
(传递数据)作用
reducer
- 概念:store的一个
处理工厂
,检测到stroe.dispath
后会自动执行,对原有的state
进行加工(结合传递过来的action
对象)最终达到更新store
的目的 - 本质:一个
纯函数
,根据action
和旧的state
计算出新的state并返回(返回值是新的state
) - 形式:纯函数,
arg1:旧的state
,arg2:action对象
,返回值:新的state
store
概念:维持应用的state,建立起action和reducer的连接
API:
- 通过
createStore(reducer)
创建store对象 - 提供
getState()
方法获取对象 - 提供
dispatch(action)
方法发送action - 通过
subscribe()
方法注册监听 - 通过
subscribe()
返回值来注销监听
react-redux
作用:react中更便捷的读取redux中的store数据,向store中分发action来更新数据
Provider
作用:整个app的store供应者
,包裹在根组件的最外层,使得所有子组件都可以拿到state
使用:接收store作为props,通过context往下传递,这样react中任何组件旧都可以通过context获取到store
connect
作用:通过该方法可以将组件
和store
进行关联
写法类似hoc高阶组件
使用:connect(option)(component)
option参数:mapStateToProps,mapDispatchToProps
- 参数一,【
mapStateToProps
】是个函数,参数1是store中的state
,函数必须有一个对象返回值
,返回的对象属性将会被映射到props中 - 参数二,【
mapDispatchToProps
】【fn
,obj
,undefined
】
- 如果为
空
则会在props中添加一个dispatch属性
- 如果是fn回调,那回调的第一个参数就是dispatch,需要返回一个包含方法的对象,在方法中调用dispatch,函数放回的方法将会被映射到props中
- 如果是对象,直接映射到props中
代码展示
// 容器代码
class Extra extends Component {
render() {
console.log(this.props);
return (
<Provider store={store}>
<div onClick={() => this.props.dispatch(actions.add(3))}>
{this.props.reducer.count}
</div>
</Provider>
);
}
}
export default connect((state) => {
return state;
})(Extra);
---------------------------------------
// redux代码
// 初始化store
import { createStore, applyMiddleware, combineReducers } from "redux";
//引入redux-thunk,用于支持异步action
import thunk from "redux-thunk"; // 引入dev-tools
import { composeWithDevTools } from "redux-devtools-extension";
export const actions = {
add: (num) => ({ type: "add", num }),
// 异步action返回一个函数
asyncAdd: (num) => {
return (dispatch) => {
setTimeout(() => {
dispatch({ type: "add", num });
}, 1000);
};
},
};
const initState = {
count: 0,
};
const reducer = (state = initState, action) => {
let { type } = action;
switch (type) {
// state数据不能使用引用修改
case "add":
state = {
count: state.count + action.num,
};
return state;
default:
return state;
}
};
export default createStore(
// 合并多个reducer
combineReducers({ reducer }),
// 添加到devtool中 => 使用异步插件
composeWithDevTools(applyMiddleware(thunk))
);
对比vuex
核心概念
redux | vuex |
---|---|
action(同步action,或借助中间件实现异步action,action不改变store,仅用于描述store的变更type) | mutation(同步操作)、action(异步操作提交mutation) |
reducer(纯函数),根据action和旧的store计算出新的store | mutation直接修改state |
state(单一数据源) | state(单一数据源) |
combineReducers组合reducer | module |
补充
- redux通过
store.getState()
获取store树,通过store.subscribe(listener)
订阅store的变化,当store变化时会触发subscribe订阅的listener
监听器,在监听器中通知react重新render
- redux可以通过
react-redux
将状态映射到组件,通过mapStateToProps
获取store的状态,vuex则是有mapState
、mapAction
、mapMutations
等API - redux通过
combineReducers
结合多个reducer
,vuex通过module
实现嵌套子关系
使用原则
- redux
- 单一数据源(一个
redux应用
只有一个store
) - state只读(唯一改变state的方法就是通过触发
action
,action时一个用于描述以发生事件的普通对象
) - 使用纯函数(
reducer
)来修改state
- vuex
- 状态集中到
state
中管理 mutation
是更改状态的唯一方式action
中进行异步逻辑并commit mutation
使用图解
redux
vuex
踩坑记录
reducer
中修改state的值不要通过引用
修改,react的坑(引用值地址一致,无法触发render更新)connect
时mapDispath
默认可以不传- 编码上性能细节,jsx模版中的表达式(函数字面量|对象字面量)应当使用变量接受,这样会避免一些不必要的性能消耗。
state
的更新是异步的,可以通过useEffect
使用最新值
性能优化记录
运行层
出发点:1. 减少render数量;2. 减少计算次数
具体:合理的拆分组件
- 类组件通过
shouldComponentUpdate
和PureComponent
减少静态组件的render次数 - 函数组件通过memo