React的历史与应用
应用场景
React的设计思路
- UI编程的特点
- 状态更新的时候,
UI不会自动更新,需要手动调用DOM接口进行更新 - 欠缺基本的代码层面的封装和隔离,代码层面没有组件化
UI之间的数据依赖关系,需要手动维护,如果依赖链路长,则会遇到回调地狱
React的出现,就是为了解决这三大痛点,他做到了:
- 状态更新,
UI也会进行更新 - 前端代码组件化,可复用,可封装
- 状态之间的互相依赖关系,只需声明即可
- 响应式系统:
它使用了响应式编程的思想,通过监听事件,由消息驱动,需要有一个监控系统去关注事件,并对事件做出响应,更新UI界面:
- 组件化
可以用树状结构表示组件之间的关系:
- 组件是组件的组合/原子组件
- 组件内拥有自己的状态,外部不可见
- 父组件可将状态传入组件内部
组件的设计:
- 组件有 props (外部传入的)和 state(内部定义的) 两种状态
- 组件的根据状态来返回一个 UI
- 组件可以由其他组件拼装而成
- 状态归属和更新
React是单项数据流
如果想要两个组件的状态共享的话,他们的状态归属于最近的公共祖先,如上的例子中,当前价格属于根节点,因为所有的组件都需要可能影响到它。
当需要改变一个状态时,由于在js中,函数是一等公民,所以可以将函数也作为属性传递给子组件,那么就可以在Root组件中定义一个修改当前价格的函数,然后将这个函数传给子组件,当子组件需要修改当前价格时,就调用该函数即可。
React的生命周期
1.Mounting 挂载时 ,就是初始化的时候,把我们定义组件对应的UI 挂载到真实的 dom 上
2.Updating 更新时 ,当状态更新的时候,怎么样更新组件,重新渲染再挂载上去
3.Unmounting 销毁时
React (Hooks) 的写法
- 类组件和函数组件
根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
类组件,顾名思义,也就是通过使用ES6类的编写形式去编写组件,该类必须继承React.Component,如果想要访问父组件传递过来的参数,可通过this.props的方式去访问,constructor 的存在是为了让我们获取 this,这是一个固定写法 :
class Welcome extends React.Component {
constructor(props) {
super(props)
}
render() {
return <h1>Hello, {this.props.name}</h1>
}
}
函数组件,顾名思义,就是通过函数编写的形式去实现一个React组件,是React中定义组件最简单的方式,函数第一个参数为props用于接收父组件传递过来的参数:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
在 Hooks 出现之前,函数组件都是无状态组件,不能拥有自己的状态,hooks 的出现使得函数组件成为了主流,以下是几个常用的 hooks :
useState useState可以用来定义一个状态。useState返回的是一个数组,第一个是当前状态的实际值,第二个用于更改该状态的函数,类似于setState。更新函数与setState相同的是都可以接受值和函数两种类型的参数,与useState不同的是,更新函数会将状态替换(replace)而不是合并(merge)。
import React, { useState } from 'react'
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={()=> setCount(count + 1)}>+</button>
<button onClick={() => setCount((count) => count - 1)}>-</button>
</div>
);
}
函数组件中如果存在多个状态,既可以通过一个useState声明对象类型的状态,也可以通过useState多次声明状态。
// 声明对象类型的状态
const [count, setCount] = useState({
count1: 0,
count2: 0
});
// 多次声明
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
- useEffect
在函数式思想的React中,生命周期函数是沟通函数式和命令式的桥梁,你可以在生命周期中执行相关的副作用(Side Effects),例如: 请求数据、操作DOM等。React提供了useEffect来处理副作用。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`
return () => {
console.log('clean up!')
}
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
我们会发现每次组件更新时,useEffect中的回调函数都会被调用,因此我们可以认为useEffect是componentDidMount和componentDidUpdate结合体,
useEffect为我们提供了第二个参数,如果第二个参数传入一个数组,仅当重新渲染时数组中的值发生改变时,useEffect中的回调函数才会执行。因此如果我们向其传入一个空数组,则可以模拟生命周期componentDidMount。
//仅执行一次
useEffect(() => {
document.title = `You clicked ${count} times`
return () => {
console.log('clean up!')
}
},[]);
//只在count变化时调用
useEffect(() => {
document.title = `You clicked ${count} times`
return () => {
console.log('clean up!')
}
},[count]);
你可以在useEffect中定义 return 方法,它只在组件被销毁之前才会执行,常用于清理以下遗留垃圾,比如订阅或计时器 ID 等占用资源的东西。相当于生命周期的 componentWillUnMount:
useEffect(()=>{
console.log('我执行了')
return ()=>{
console.log('我销毁了')
}
},[])
要注意的是 useEffect 的函数会在组件渲染到屏幕之后执行
而 useLayoutEffect 则是在DOM结构更新后、渲染前执行,相当于有一个防抖效果,他们的用法是一样的,不一样是只是执行的时机
- 父子组件交互
react 中,父组件可以把状态或者函数方法传递给子组件:
//父组件传参
<Hearders name={name} />
//子组件获得参数
function Hearders(props) {
const {name} =props
}
父组件也可以传递一个方法给子组件,子组件可以通过这个方法来修改子组件的值:
//父组件
const Parent = () => {
const onClick = (value) => {
console.log(value,'点击了')
}
return(
<div>
<Child
click={onClick}
/>
</div>
)
}
//子组件
const Child = (props) => {
const handleClick = (value) => {
props.click(value)
}
return(
<div onClick={()=>{handleClick(1)}}>
子组件
</div>
)
}
借助Hook useContext可以帮助我们跨越组件层级直接传递变量,实现数据共享。
import React,{useContext, useState, createContext} from 'react';
import {Button} from 'antd';
import '../../App.css';
const CountContext = createContext();
const TestContext = () =>{
const [count, setCount] = useState(0);
return(
<div>
<p>父组件点击次数:{count}</p>
<Button type={"primary"} onClick={()=>setCount(count+1)}>点击+1</Button>
<CountContext.Provider value={count}>
<Counter/>
</CountContext.Provider>
</div>
)
};
不止在子组件,在子组件的下一级孙子组件,再下一级中,都可以获取到响应的值,只要是被 Context.Provider 包裹的内容中,都可以如下方法使用 Context 里的值:
const CountContext = createContext();
const Counter = () => {
const count = useContext(CountContext);
return (
<div>
<p>子组件获得的点击数量:{count}</p>
</div>
);
};
-
useRef
useRef可以用来拿到 dom 节点的引用,拿到引用后可以进行一系列操作
function Example() {
const inputEl = useRef();
const onButtonClick = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useRef 也可以接受一个默认值,并返回一个含有current属性的可变对象,该可变对象会将持续整个组件的生命周期。它有什么用处呢,例子如下:
在like为6的时候, 点击 alert , 再继续增加like到10, 弹出的值为 6, 而非 10.当我们更改状态的时候,React会重新渲染组件,每次的渲染都会拿到独立的like值,并重新定义个handleAlertClick函数,每个handleAlertClick函数体里的like值也是它自己的,所以当like为6时,点击alert,触发了handleAlertClick,此时的like是6,哪怕后面继续更改like到10,但alert时的like已经定下来了。可见不同渲染之间无法共享state状态值
import React, { useState } from "react";
const LikeButton: React.FC = () => {
const [like, setLike] = useState(0)
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like}`)
//形成闭包,所以弹出来的是当时触发函数时的like值
}, 3000)
}
return (
<>
<button onClick={() => setLike(like + 1)}>{like}赞</button>
<button onClick={handleAlertClick}>Alert</button>
</>
)
}
export default LikeButton
采用useRef,在like为6的时候, 点击 alert , 再继续增加like到10, 弹出的值为10。useRef 在更新的时候不会使得组件重新渲染, useRef 每次都会返回相同的引用
import React, { useRef } from "react";
const LikeButton: React.FC = () => {
// 定义一个实例变量
let like = useRef(0);
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like.current}`);
}, 3000);
}
return (
<>
<button
onClick={() => {
like.current = like.current + 1;
}}
>
{like.current}赞
</button>
<button onClick={handleAlertClick}>Alert</button>
</>
);
};
export default LikeButton;
import React, { useRef } from "react";
const LikeButton: React.FC = () => {
// 定义一个实例变量
let like = useRef(0);
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like.current}`);
}, 3000);
}
return (
<>
<button
onClick={() => {
like.current = like.current + 1;
}}
>
{like.current}赞
</button>
<button onClick={handleAlertClick}>Alert</button>
</>
);
};
export default LikeButton;
useImperativeHandle用于自定义暴露给父组件的ref属性。需要配合forwardRef一起使用。
function Example(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
}
export default forwardRef(Example);
class App extends Component {
constructor(props){
super(props);
this.inputRef = createRef()
}
render() {
return (
<>
<Example ref={this.inputRef}/>
<button onClick={() => {this.inputRef.current.focus()}}>Click</button>
</>
);
}
}
- useCallback 和 useMemo
都是react可用于性能优化的内置hooks。两者的区别在于:useCallback缓存的是一个函数,而useMemo缓存的是计算结果。
// useCallback
// 第一个参数是一个回调函数,useCallback会缓存这个函数,返回缓存的回调函数
// 第二个参数是依赖项,只有当依赖项改变时,才会重新创建这个函数
const memorizedCallback = useCallback(()=>{
doSomething(a,b);
},[a,b])
// useMemo
// 第一个参数是一个函数,useMemo会缓存函数运行返回的值,返回缓存的值
// 第二个参数是依赖项,只有当依赖改变时,才会重新计算这个值
const memorizedValue = useMemo(()=>computeValue(a,b),[a,b])
他们的用处是:在函数式组件中,每次UI的变化,都是通过重新执行整个函数来完成的,这和传统的类组件有很大区别:函数组件中并没有一个直接的方式在多次渲染之间维持一个状态。在重新执行整个函数组件的过程中,其中的函数和引用类型的变量会创建新的(指向新的引用),函数组件在重新渲染前后,其中函数和引用类型变量是不相等的,这又会导致其他非必要的重新渲染。
如下:当Counter组件因为其他数据(非count)发生变化而导致重新渲染的时候,重新执行整个Counter函数,会创建新的 handleIncrement 函数,而子组件 Button 会由于 props-handleClick 传入的 handleIncrement 函数改变而重新渲染,但其实这个渲染是不必要的,因为只有在count发生变化时,才应该导致Button组件的渲染。
// 需要做到:只有当count发生变化时,才需要重新定一个回调函数-useCallback
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(
() => setCount(count + 1),
[count]
)
// 只有当依赖项count改变时,才会重新生成函数,不然都是返回的缓存的回调函数,不会触发Button子组件的重绘
return <Button handleClick={handleIncrement}/>
}
- 自定义 hooks
React 允许我们创建自定义Hook来封装共享状态逻辑。所谓的自定义Hook是指以函数名以use开头并调用其他Hook的函数。
// 自定义一个hook 功能判断当前的网络情况
// 函数名要以use开头
// 函数中必须要用到内置hook函数
const useOnline = () => {
const [online, setOnline] = useState(navigator.onLine)
// 让它在第1次挂载时执行
useEffect(() => {
const onlineFn = () => setOnline(true)
const offlineFn = () => setOnline(false)
// js提供的监听事件
window.addEventListener('online', onlineFn, false)
window.addEventListener('offline', offlineFn, false)
return () => {
window.removeEventListener('online', onlineFn, false)
window.removeEventListener('offline', offlineFn, false)
}
}, [])
return online
}
const App = () => {
const online = useOnline()
return (
<div>
{
online
?
<div style={{ color: 'green' }}>在线</div>
:
<div>离线</div>
}
</div>
);
}