本篇文章内容迁移自老博客, 虽然写了很多年,但中间有调整或插入内容, 是对React基础知识点比较完善的梳理。
React
是什么?
ReactNative
中可以使用React
语法进行移动端开发虚拟DOM
+优秀的Diff算法
,尽量减少与真实DOM
的交互React的组成
React
负责逻辑控制,数据 -> VDOM
ReactDOM
渲染实际DOM
,VDOM
-> DOM
JSX
React
使JSX
来描述UI@babel/preset-react
把JSX 编译成相应的 JS 对象, JSX -> React.creactElement(tagName, props, children)
React.createElement
再把这个JS对象构造成React需要的VDOM。JSX
是⼀种JavaScript
的语法扩展,其格式比较像模版语言,但事实上完全是在JavaScript
内部实现的。JSX
可以很好地描述UI,能够有效提⾼开发效率,体验JSX
。JSX
仅仅是React.cloneElement(component, props, ...children)
的语法糖JSX
的语法规则
.
引用React
必须在作用域内 import React from 'react'
props
有两个特例
class => className
for => htmlFor
<components[props.storyType] />
props
默认true
<Comp {...props} />
babel
编译时处理的JSX
可以无缝融合到JS
中
JSX
可以赋值给变量,作为函数参数返回值等JSX
中可以写JS
逻辑,比如条件、分支、循环、运算语句等。JSX
的{}
语法
&&
表达式 如果返回 true
、false
、null
、undefined
将被忽略{{}}
表示一个对象 比如style={{ width: 100 }}
key
diff
时候,首先比较type
, 然后是key
,所以同级同类型元素,key
值必须得唯一CSSModule
的写法jsx/* index.module.css内容如下
.app {
.logo {
width: 100px;
}
}
*/
import styles from "./index.module.css";
const jsx = (
<div className={styles.app}>
<span
className={styles.logo}
style={{ width: "50px", height: "30px" }}
/>
</div>
);
css modules
只是加了一些网页组件最急需的功能 (局部作用域、全局作用域、定制hash、组合、定义变量), 可以看我的另一篇文章 CSS模块化方案
组件,从概念上类似于 JavaScript
函数。它接受任意的入参(即 “props”
),并返回用于描述页面展示 内容的 React
元素。
组件有两种形式: class
组件 和 function
组件。
函数组件通常无状态,仅关注内容展示,返回渲染结果即可。从React16.8
开始引⼊了hooks
,函数组件也能够拥有状态。
function
组件非常灵活,比如 useEffect
jsxuseEffect(() => {
// 相当于componentDidMount + componentDidUpdate
const timer = setInterval(() => {
setDate(new Date());
}, 1000);
return () => clearInterval(timer); // componentWillUnmount的集合
},
[] // 依赖项,哪些state发生改变触发更新 相当于 shouldComponentUpdate
);
Hook
是什么?
Hook
是⼀个特殊的函数,它可以让你“钩入” React
的特性。例如, useState
是允许 你在React
函数组件中添加 state
的 Hook
。
什么时候用Hook
?
如果你在编写函数组件并意识到需要向其添加⼀些state
,以前的做法是必须将其它转化为class
。现在你可以在现有的函数组件中使用 Hook
。
注意事项
Hook
。不要在循环、条件判断或者子函数中调⽤React
的函数组件中 或 ⾃定义的 Hook
中 调用Hook
组件复合 - Composition
Vue
中slot
能力setState(updater[, callback])
this.setState({...})
浅层merge 类似Object.assign
this.setState(state, (newState)=> {/*更新后的回调*/})
this.setState(currentState => { return newState;})
批处理,可以避免闭包引起的变量不是最新值的情况javascript class App extends React.Component {
state = {
count: 0,
sex: '男'
}
add = () => {
// this.setState({count: this.state.count+1});
// this.setState({count: this.state.count+1});
// 此时 页面显示 1
this.setState(state => ({ count: state.count + 1 }));
this.setState(state => ({ count: state.count + 1 }));
// 现在页面显示2
}
render() {
return <h1 onClick={this.add}>{this.state.count}</h1>
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);
setState
是同步还是异步?
< 18
React
可能会把多个 setState()
调用合并成一个调用。 合成事件和⽣命周期函数中是异步的, (合成事件是指一个方法调用另一个方法, 这⾥的异步其实是批量更新。)> 18
ReactDOM.flushSync
注意事项
不要直接修改 State
,因为不会触发渲染组件:
在react15.5
之前react
内置prop-types
,后来将prop-types
独立出去了
class
组件类型校验、必填校验、默认值设置的示例代码
jsxclass Person extends React.Component{
//对传给Person组件的props进行类型的限制
static propTypes = {
name: PropTypes.string, // 限制name必须为字符串类型
sex: PropTypes.string.isRequired,// 限制sex必须为字符串类型,且是必要属性
age: PropTypes.number,// 限制age必须为数值类型
address: PropTypes.string, // 限制address必须为字符串类型
}
//对传给Person组件的props进行默认值的设置
static defaultProps = {
address: '中国'
}
render(){
// return ...
}
}
// 下面的...p1,并不是原生ES8的对象解构,
// 而是babel+react环境提供的能力,支持展开运算符展开一个对象,
// 但是仅仅适用于传递标签属性!!
ReactDOM.render(<Person {...p1}/>,document.getElementById('test2'))
function组件类型校验及默认值的代码示例
jsxfunction Person(props){
const {name,age,sex,address} = props
// return ...
}
Person.propTypes = {
name: PropTypes.string,
sex: PropTypes.string.isRequired,
address: PropTypes.string,
}
Person.defaultProps = {
address: '中国'
}
组件实例的三大属性: ref
、state
、props
ref的三种使用方式
jsxclass Demo extends React.Component{
showData = ()=>{
const {input1} = this.refs
alert(input1.value)
}
render(){
return (
<div>
<input ref="input1" type="text" placeholder="点击按钮提示输入"/>
<button onClick={this.showData}>点我提示数据</button>
</div>
)
}
}
jsxclass Demo extends React.Component{
showData = ()=>{
const {input1} = this
alert(input1.value)
}
render(){
return (
<div>
<input ref={c => this.input1 = c} type="text" placeholder="点击按钮提示输入"/>
<button onClick={this.showData}>点我提示数据</button>
</div>
)
}
}
ref的值不会因为组件的刷新而重新声明,它是专门用来存储组件级别的信息的。
React官方推荐有三种场景我们可以使用它
ReactDOM.render()
触发---初次渲染
constructor()
componentWillMount()
render()
componentDidMount()
=====> 常用一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
更新阶段: 由组件内部this.setSate()
或父组件render
触发this.forceUpdate()
shouldComponentUpdate()
注意:强制更新不走“阀门”componentWillUpdate()
render()
componentDidUpdate()
卸载组件: 由ReactDOM.unmountComponentAtNode()
触发
componentWillUnmount()
=====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息注意
constructor
中 super()
与super(props)
的区别
super()
构造函数里不能使用state
推荐super(props)
写法componentWillReceiveProps
V17 可能会废弃的三个生命周期函数用getDerivedStateFromProps替代,
componentWillMount
componentWillReceiveProps
componentWillUpdate
如果你还想使用的话要加上UNSAFE_
例如 UNSAFE_componentWillMount
还可以通过命令自动添加 UNSAFE_
前缀
npx react-codemod rename-unsafe-lifecycles
为什么标记unsafe ?
官方解释
- 这些生命周期经常被误解和滥用;
- 此外,我们预计,在异步渲染中,它们潜在的误用问题可能更大
- 我们将在即将发布的版本中为这些生命周期添加“UNSAFE_”前缀。这里并不是指安全性,而是表示使用这些生命周期的代码在React的未来版本中可能更有可能出现bug,尤其是在启用异步渲染之后。
具体是什么Bug就没有再说了, 查了一下资料是这样说的
constructor
里面, 因为componentWillUnmout
可能不会触发(只有触发了componentWillMount
才会触发willUnmount
), 如果渲染被中断了导致willMount
没有触发,而开发者在constructor
添加了监听,销毁的时候就没有触发,导致内存泄漏。V16.4 引⼊两个新的生命周期函数:
static getDerivedStateFromProps(props, state)
render
方法之前调用,并且在初始挂载及后续更新时都会被调用。state
,如果返回 null
则不更新state
,但render
还是会执行。UNSAFE_componentWillReceiveProps
形成对比,后者仅在⽗组件重新渲染时触发,⽽不是在内部调用 setState
时getSnapshotBeforeUpdate(prevProps, prevState)
render
之后,在componentDidUpdate
之前。componentDidUpdate(prevProps, prevState, snapshot)
。static getDerivedStateFromError
ComponentDidCatch
jsxclass ErrorLifePage extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) { // 副作用处理
// "组件堆栈" 例⼦:
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
console.error(info.stack)
}
static getDerivedStateFromError(error) {
// 更新 state 使下⼀次渲染可以显示降级 UI
return { hasError: true };
}
render() {
return (<div>
<h3>ErrorLifePage</h3>
{this.state.hasError ?
(<h1>Something went wrong.</h1>)
: (<Clock></Clock>)
}
</div>);
}
}
function Clock() {
const [date, setDate] = React.useState(new Date());
React.useEffect(() => {
const timer = setInterval(() => {
if(new Date().getMilliseconds() % 5 === 0) {
setDate('not Date');
}
setDate(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
return <h1>{date.toLocaleTimeString()}</h1>
}
onXxx
属性指定事件处理函数(注意大小写)
dispatchEvent(new CustomEvent('myClick', {detail: ...}))
event.target
得到发生事件的元素解决this指向的三种方案
注意事项
JSX中应避免 onClick={() => this.handleClick()}
这种写法。
每次render都会创建不同的回调函数,如果该回调函数作为props传⼦组件,每次⼦组件都要re-render
前面说到react
有个生命周期 shouldComponentUpdate
它返回一个boolean
值来决定是否执行render
,这个是性能优化的关键
jsximport React, { Component, PureComponent } from 'react'
export default class PureCompUsage extends PureComponent {
constructor(props) {
super(props);
this.state = {
count: 1
}
}
handleClick = () => {
this.setState({
count: 10
})
}
render() {
// console.log('点击一次,这里会执行一次,其实count不变不需要重复渲染')
console.log('使用了PureComponent 对props和state进行浅比较,数据相同不会再次渲染')
return (
<>
<p>{this.state.count}</p>
<button onClick={this.handleClick}>ADD to 10</button>
</>
)
}
}
思考: 如果function组件怎么避免不必要的渲染呢?
state
,我们判断是否需要setState
props
,则使用useMemo
const NewChild = React.useMemo(Child);
useMemo
还可以用来避免复杂的运算Effect Hook
Effect Hook 可以让你在函数组件中执行副作用操作。
数据获取,设置订阅以及⼿动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或 是“副作⽤”这个名字,应该都在组件中使⽤过它们。
jsximport React, { useState, useEffect } from "react";
export default function UseEffectUsage() {
const [date, setDate] = useState(new Date().toLocaleTimeString());
useEffect(
() => {
// 相当于componentDidMount + componentDidUpdate(有依赖项的情况)
const timer = setInterval(() => {
setDate(new Date().toLocaleTimeString());
}, 1000);
// return 相当于 componentWillUnmount
return () => {
clearInterval(timer);
console.log('定时器已被清除')
};
},
// 依赖项,哪些state发生改变触发更新
// 可类比 shouldComponentUpdate
[]
);
return <div>{date}</div>;
}
自定义hook
有时候我们会想要在组件之间重⽤一些状态逻辑。
目前为⽌,有两种主流方案来解决这个问题: ⾼阶组件 和 render props。
⾃定义Hook可以让你在不增加组件的情况下达到同样的目的。
⾃定义Hook是一个函数,其名称以 “use” 开头,函数内部可以调⽤其他的Hook。
hook封装定时器案例
jsimport React, { useState, useEffect } from "react";
export default function UseEffectUsage() {
const date = useClock();
return <div>{date}</div>;
}
function useClock() {
const [date, setDate] = useState(new Date().toLocaleTimeString());
useEffect(() => {
const timer = setInterval(() => {
setDate(new Date().toLocaleTimeString());
}, 1000);
return () => {
clearInterval(timer);
console.log("定时器已被清除");
};
}, []);
return date;
}
Hook 就是 JavaScript 函数,但是使⽤它们会有两个额外的规则:
把“创建”函数和依赖项数组作为参数传入 useMemo ,它仅会在某个依赖项改变时才重新计算memoized 值。这种优化有助于避免在每次渲染时都进⾏高开销的计算。
useMemo
有两个用途
jsx// 缓存某个计算结果,避免不必要的重复运算
import {useState, useMemo} from 'react';
export default function UseMemoUsage() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
/* const expensive = () => {
console.log("compute"); // 输入框 value 改变 也会触发
let sum = 0;
for (let i = 0; i < count; i++) {
sum += i;
}
return sum;
}; */
const expensive = useMemo(() => {
console.log("compute");
let sum = 0;
for (let i = 0; i < count; i++) {
sum += i;
}
return sum;
}, [count]); // 只有count改变才进行expensive计算;
return (
<div>
<p>{count} -- {expensive} </p>
<button onClick={() => setCount(count + 1)}>ADD</button>
<input type="text" defaultValue={text} placeholder='试试输入内容' onInput={e => {
setText(e.target.value)
}}/>
</div>
);
}
把内联回调函数及依赖项数组作为参数传入
useCallback
,它将返回该回调函数的memoized
版本, 该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使⽤引用相等性去避免非必要渲染(例如shouldComponentUpdate )的子组件时,它将非常有用。
使用场景: 子组件某个事件触发依赖调用父组件的方法进行计算,这种场景通常是父组件通过props
将函数传给子组件,但是这样会有个问题,父组件state
发生改变,会导致子组件重新渲染,使用useCallback
可以避免子组件的重复渲染
看个案例
setState
会触发子组件的渲染jsxconst { useState, useCallback } = React;
function UseCallBackUsage() {
const [value, setValue] = useState({ text: "" });
console.log("父组件渲染");
const onChange = (e) => {
setValue({
text: e.target.value,
});
};
return (
<div>
<p>{value.text}</p>
<input
type="text"
placeholder="输入内容试试看"
value={value.text}
onChange={onChange}
/>
<hr />
<Child />
</div>
);
}
function Child(props) {
// 父组件setState会触发子组件渲染
console.log("子组件渲染");
return (
<div>
<span>子组件</span>
</div>
);
}
React.memo(Comp)
包裹使用注意事项:
class
组件,则要继承PureComponent
function
组件,则要使用React.memo()
包裹子组件jsximport React, {useReducer} from 'react';
export default function UseReducerUsage() {
function counter (state, action) {
switch(action.type) {
case 'ADD':
return state + 1;
default :
return state;
}
}
const [state, dispatch] = useReducer(counter, 0);
return <div>
<p>{state}</p>
<button onClick={() => dispatch({type: 'ADD'})}>ADD</button>
</div>
}
jsx// function组件用法
import {useRef} from 'react';
export default function FuncRef () {
const inputRef = useRef();
return <div>
<input type="text" ref={inputRef}/>
<button onClick={handleClick}>focus</button>
</div>
function handleClick() {
inputRef.current.focus();
}
}
// class组件用法
import React, { Component } from "react";
export class RefClassUasge extends Component {
constructor() {
super();
this.inputRef = React.createRef();
}
render() {
return <div>
<input type="text" ref={this.inputRef}/>
<button onClick={() => this.inputRef.current.focus()}>focus</button>
</div>
}
}
使用子组件Ref -- ref转发
jsximport React from "react";
export default function Forward() {
const ref = React.createRef();
return (
<div>
<MyInputW ref={ref} disabled placeholder="this is disabled input">
MyInputW
</MyInputW>
<button onClick={() => console.log(ref.current)}>get MyInputW</button>
</div>
);
}
const MyInput = (props, ref) => (
<input ref={ref} placeholder={props.placeholder} disabled={props.disabled}></input>
)
const MyInputW = React.forwardRef(MyInput);
通过ref暴露子组件的方法给父组件调用
jsximport React, { forwardRef, useImperativeHandle, useRef } from 'react'
export default function Expose() {
const exposeRef = useRef();
return (
<div>
<FancyInputW ref={exposeRef} />
<button onClick={() => {
exposeRef.current.focus();
}}>focus</button>
</div>
)
}
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
}));
return <input ref={inputRef} />
};
const FancyInputW = forwardRef(FancyInput);
useState获取不到最新值问题
jsximport React, { useState, useEffect } from 'react';
// export default function UseStateUsage () {
// const [arr, setArr] = useState([0]);
// useEffect(() => {
// console.log(arr);
// }, [arr]);
// const handleClick = () => {
// Promise.resolve().then(() => {
// setArr([...arr, 1]);
// }).then(() => {
// setArr([...arr, 2]); // 时赋值前 arr 为旧状态仍然为:[0]
// })
// }
// return <div>
// <button onClick={handleClick}>click</button>
// </div>
// }
// 解决方案1 setState(function)
// export default function UseStateUsage () {
// const [arr, setArr] = useState([0]);
// useEffect(() => {
// console.log(arr);
// }, [arr]);
// const handleClick = () => {
// Promise.resolve().then(() => {
// setArr((arr) => [...arr, 1]);
// }).then(() => {
// setArr((arr) => [...arr, 2]);
// })
// }
// return <div>
// <button onClick={handleClick}>click</button>
// </div>
// }
// 解决方案2 使用UseReducer模拟强制刷新
// export default function UseStateUsage() {
// const [,forceUpdate] = React.useReducer(x => x+1, 0);
// const [arr] = useState([0]);
// const handleClick = () => {
// Promise.resolve().then(() => {
// arr.push(1);
// }).then(() => {
// arr.push(2);
// forceUpdate();
// })
// }
// return <div>
// <h1>{arr.toString()}</h1>
// <button onClick={handleClick}>click</button>
// </div>
// }
// 解决方案3 ref
export default function UseStateUsage() {
const [arr, setArr, getArr] = useGetState([0]);
const handleClick = () => {
Promise.resolve().then(() => {
setArr([...getArr(), 1]);
}).then(() => {
setArr([...getArr(), 2]);
})
}
return <div>
<h1>{arr.toString()}</h1>
<button onClick={handleClick}>click</button>
</div>
}
const useGetState = (initVal) => {
const [state, setState] = useState(initVal);
const ref = React.useRef(initVal);
const _setState = (newVal) => {
ref.current = newVal;
setState(newVal)
}
const getState = () => {
return ref.current;
}
return [state, _setState, getState];
}
// 参考资料 https://www.cnblogs.com/hymenhan/p/14991789.html
父组件渲染会引发子组件也跟着渲染,怎样避免不必要的子组件渲染呢?
PureComponent
,它是利用shouldComponentUpdate
**浅层对比 prop 和 state **,从而决定是否渲染。PureComponent
只能用于class
组件,那么function
组件怎么办呢?可以使用React.memo(Comp)
useMemo
缓存useCallback
包裹该回调函数 (需要memo
配合)使用自定义Hook 进行代码优化(类似vue3关注点分离思想)
本章节完整案例 点这里
本人一直以来是vue2玩家,最近在学react,试着对比两种框架语法糖之间的异同,以便快速掌握react的基本使用。
react | vue2 | |
---|---|---|
在线使用 | 支持 要使用React.createElement 代替JSX | 支持 要引入含有编译器的包 |
官方文档 | https://react.docschina.org/ | https://cn.vuejs.org/v2/guide/ |
cli 脚手架 | create-react-app 如果想像vue-cli一样灵活可以考虑umi | vue-cli 非常智能灵活,如 vue server sfc.vue 运行组件vue creact project 根据问卷生成脚手架vue ui 图形化界面管理项目vue add package 智能安装依赖代码帮你处理好支持处理跨域、mock配置等 |
devtools | https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi | https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd |
路由 | react-router | vue-router |
状态管理 | redux /react-redux mobx | vuex |
服务端渲染 | https://apollographqlcn.github.io/react-docs-cn/server-side-rendering.html | https://v3.cn.vuejs.org/guide/ssr/introduction.html |
原生开发 | react-native | weex |
本文作者:郭敬文
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!