1.使用
1.html中通过CDN引入React相关文件。
2.html中给一个节点(div)作为容器。
3.使用ReactDOM.render()方法修改容器里的内容。
// 第3步代码
const domContainer = document.querySelector('#like_button_container');
ReactDOM.render(element, domContainer,callback);
// element:填充的react组件,callback:渲染完后执行的回调函数。
2.数据
1.组件的state 是私有的,并且完全受控于当前组件。
2.不要直接修改state,而要用setState,setState会导致当前组件重新渲染,当然也包括该组件的子组件。
3.props应该是一个只读数据,而且是单向数据流。
4.父组件reRender的时候,也会导致子组件reRender。
如果你把一个以组件构成的树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动
3.生命周期
shouldComponentUpdate()会在渲染执行之前被调用,默认为true。可以通过它来进行优化(返回false),跳过reRender。
4.setState更新原理
前置知识
1.setState的参数可以有两种:对象和函数。
setState对state对象的修改是一种浅合并(浅合并:对象第一层属性,没有的添加上,有的简单的覆盖。深合并:对象的每一层都递归合并,而不是仅第一层做简单的覆盖),参数是函数时,函数会返回一个对象,也是用来浅合并的。
可以使用展开运算符([...prevState, ...updatedValues ])或者Object.assign来达到这种合并的效果。
2.合成事件
首先明确定义,在 React 中为元素添加的事件被叫做合成事件。
说人话就是,在react中类似我们写的onClick、onFocus等事件,实际不是纯原生的,react会帮我们代理,做些优化处理。
合成事件的好处有两个:
-
屏蔽了浏览器之间关于事件处理的兼容性问题,为合成事件对象内部提供了统一的 API;
-
性能的提升, 事件都被委托给根标签上了,绑定的事件数也少了。之前帮三个onclick,现在只用在root上绑一个onClick,也方便统一管理。
合成事件的实现:简单点说,就是render时在顶层(root元素)注册事件监听(例如click,此时注册应该也是记录下,真正的挂载也得等到root节点挂载后)。顶层监听到click时,通过e.target找到触发事件的真实dom,然后映射到对应fiber。然后从这个fiber从下往上遍历所有父节点,把click回调push到数组。然后倒叙执行模拟捕获流程,顺序执行就是模拟冒泡。至于事件对象e,也是react合成的,包括e.stopPropagation()这种也是react自己实现的,这个数组里所有的回调函数的e都是同一个。
注:js事件流程中,捕获->目标节点->冒泡,这个目标节点不是指绑定click的那个最底层节点,而是点击的最里层节点(哪怕这个节点没有绑定任何事件)。e.target指的就是这个点击的最里层节点,e.currentTarger指的是绑定了这个回调函数的节点。(这也是react实现事件代理的基础,找到了最里层的点击节点,模拟捕获和冒泡也就轻松了)
3.批量更新模式 react通过isBatchingUpdates来控制是否开启批量更新模式。
批量更新时:setState调用后不会立马去更新组件,而是会将状态放入队列存储,其他事都做完后,最后再来统一更新state状态,reRender组件。
非批量更新状态:setState调用后,会直接更新state状态,然后reRender、diff、更新dom。
同步与异步
先说结论:
同步和异步主要取决于它被调用的环境
如果 setState 在 React 能够控制的范围被调用,它就是异步的。比如合成事件处理函数, 生命周期函数, 此时会进行批量更新, 也就是将状态合并后再进行 DOM 更新。
如果 setState 在原生 JavaScript 控制的范围被调用,它就是同步的。比如原生事件处理函数中, 定时器回调函数中, Ajax 回调函数中, 此时 setState 被调用后会立即更新 DOM 。
细节探讨:
拿合成事件举例,当触发合成事件时, 在事件处理函数执行之前,会先将批量更新模式设置为 true,然后执行事件处理函数收集状态。当事件处理函数执行完成后,执行批量更新操作,即从更新队列中获取组件更新器并调用。组件更新器调用完成后再将批量更新模式设置为 false。
说人话就是:setState 在 React 能够控制的范围被调用,react会设置isBatchingUpdates为true,所有state状态改变会被缓存,等其他事做完了。最后统一更新state,然后reRender,最后把isBatchingUpdates设置为false,关闭批量更新模式。
再来看看setState参数为对象和函数的区别:批量更新时,如果参数都是对象,那么最后会进行对象的统一浅合并。拿到的旧state也是统一一样的。如果是函数,统一合并时,是依次调用函数。下一个函数的入参state,是上个函数调用完返回的新state。这就是为啥函数式参数,即使在异步更新情况下,依旧可以实现数据准确更新的原因。
当然react也提供了相应的方法,可以在一些原生场景内,强行开启批量更新模式。
在react18中,所有更新都将自动开启批量处理模式。包括promise、setTimeout、原生事件处理中的状态更新。
5.常见知识点
函数组件
官方说明:如果你想写的组件只包含一个 render 方法,并且不包含 state,那么使用函数组件就会更简单。
说白了,函数组件就是只保留了class组件的render方法,然后props作为入参。
高阶组件
高阶组件是参数为组件,返回值为新组件的函数。重点是高阶组件不是组件,而是函数。高阶组件约定不会去修改组件本身,而是加强组件的能力,是组件加工器。
受控组件
在React中,所谓受控组件和非受控组件,是针对表单而言的。
类似Vue中的双向绑定,像input的值(value)实际是受react的state控制的,这是受控组件。
而在基础dom中,input随用户输入,本来就会自己维护和存储value,如果我们只是用ref去取这个value,这就是非受控组件。但是随着输入的变化,无法做到时刻校验输入值,这点受控组件就可以做到。
React.memo()
React.memo为高阶组件,如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,React会缓存渲染结果,下次直接用,提高性能。
当然有useState、useReducer、 useContext 这些Hook,且state 或 context 发生变化时,组件依然会重新渲染。
Context
组件树中想用全局数据(例如当前认证的用户、主题或首选语言),不想一层层通过props往下传的时候,采用Context。
使用:
1. 创建一个context对象,给一个默认value
const MyContext = React.createContext(defaultValue);
2. 每个 Context 对象都会返回一个 Provider React 组件,给子组件们用context值,value没有才会用默认值。可嵌套使用,里面的覆盖外面的值。
一旦Provider的这个值发生变化,里面用到这个值的组件都会更新
<MyContext.Provider value={/* 某个值 */}>
3.组件接受这个值
方案一:把MyContext赋值给class组件的contextType属性(构造函数属性),class内部可以通过this.context访问了。
方案二:使用静态属性state声明contextType(说白了一个意思)
useContext(这也一个hook) 接受一个context对象,返回当前值(距离最近的provider值),一旦context对象更新,使用了useContext的组件就会reRender().
Portal
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
render() {
// React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
// `domNode` 是一个可以在任何位置的有效 DOM 节点。
return ReactDOM.createPortal(
this.props.children,
domNode );
}
一般作为子组件,在DOM树中会挂载到父组件的节点下。但是使用ReactDOM.createPortal时,你可以将子组件(自己)挂到父组件外任意DOM节点。
使用Portal时,对于事件冒泡,还是会以react元素的父子层级进行冒泡,而不是真实的Dom父子层级。因为react的合成事件中,找到e.target后是按照react层级来搜寻回调的。
Refs
操作DOM或者react元素(组件)的一种方式(仅限class组件)。
使用方式:
1.React.createRef()创建一个refs。
2.给Dom元素或者组件传递ref属性(值为1中创建的refs)
3.挂载后通过refs的current属性,访问该Dom节点(组件)。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef(); // 放在实例属性上,便于在整个组件中引用refs
console.log(this.myRef.current) // 挂载后,通过current属性访问
}
render() {
return <div ref={this.myRef} />; // 通过ref属性,给dom元素或组件传递refs值。
}
}
或者使用refs回调:说白了就是第二步中传一个回调函数给Dom,在render过程中,react会把Dom节点(组件实例)传递给回调函数的参数,我们只要在函数体内操作,把参数储存到我们想要的属性上就好了(结合词法作用域考虑,逻辑就很简单了)。
注意点:
1.如果绑的是html标签,拿到的就是dom元素。绑的是class组件,拿到的就是组件实例。函数组件本身是不能绑ref的,除非是用React.forwardRef创建的组件,往下透传用。
2.针对回调的方式,如果传入的回调函数没加useCallback。意味每次render,回调函数都是新的。那么在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。React 清空旧的 ref 并且设置新的。但如果用了useCallback,还是原来的那个回调,组件更新时回调函数都不会被调用。
Hook
官方定义:Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。
说白了: 就是让函数组件拥有和class组件一样的生命周期、状态管理、逻辑复用等能力。
hook的使用规则:
1个约定:所有hook必须以use开头。
2个只在: 只在函数组件中使用(普通函数不行)、只在顶层使用(而不要在循环,条件或嵌套函数中调用)。
1.只在函数组件中使用:
首先内置hooks,很多功能都与组件本身息息相关的,例如组件生命周期,调用组件render函数更新组件等,所以一旦在组件外调用,肯定是有很多error的。自定义hook,它只是一种逻辑抽离,倘若它里面用了内置hook,理所当然的,它也只应该在组件内调用。
2.只在顶层使用:
首先hook本质是函数,每次更新reRender,这些hook都是会被执行的。也就是说一模一样的useState、useEffect函数会被多次调用(当然参数可能是不同的),特别是当参数也一模一样时,react是怎么将他们一一对应起来的呢?
const [num, setNum] = useState(0)
const [count, setCount] = useState(0)
我们知道num和count在实际使用时是完全独立的,并不会相互干扰。但同样的两个useState(0),react是怎么把他们和num、count一一对应起来的呢?答案只有一个,那就是顺序。同理useEffect也是通过顺序来一一对应的,实际上所有的hook都是通过统一的顺序来一一对应起来的,也就是说useState和useEffect用的是统一序列号。所以为了保证hooks们能准确地一一对应,调用顺序千万不能乱,否则就全乱套了。只在顶层调用hooks能很好的保证他们的调用顺序不变,当然,你要是足够牛x,能保证它们顺序不乱,你用条件,循环甚至嵌套都行。
自定义hook:
主要是把组件中的重复逻辑抽离到函数中,自定义hook其实和普通函数没区别,自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性,官方文档这句话说的很好。因此如果hook中用到了state,state也是相互独立的。但有一点,你用了这个hook约定后(前缀use),react会自动检查你是否满足hook条件(两个只在)。
使用感受:使用起来很爽,包括之前的vue的hook,仿佛hook返回的就是一个响应式数据,一旦这个数据变化,组件就会自动更新。
6.常用Hook
useState
useState和useEffect可以说是react最重要、最常用的两个hook了。
setState(),值没有变化时(object.is比较),不会reRender。
setState(),参数是函数时,该函数只会被调用一次(用来设置初始值)。
简单模拟useState实现
import React from "react";
import ReactDOM from "react-dom";
const states = [];
let cursor = 0;
function useState(initialState) {
const currenCursor = cursor;
states[currenCursor] = states[currenCursor] || initialState; // 检查是否渲染过
function setState(newState) {
states[currenCursor] = newState;
render();
}
cursor+=1; // 更新游标
return [states[currenCursor], setState];
}
function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(1);
return (
<div>
<div>count1: {count1}</div>
<div>
<button onClick={() => setCount1(count1 + 1)}>add1 1</button>
<button onClick={() => setCount1(count1 - 1)}>delete1 1</button>
</div>
<hr />
<div>num2: {num2}</div>
<div>
<button onClick={() => setCount2(count2 + 1)}>add2 1</button>
<button onClick={() => setCount2(count2 - 1)}>delete2 1</button>
</div>
</div>
);
}
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
cursor = 0; // 重置cursor
}
render(); // 首次渲染
从代码上看,useState主要使用了闭包,返回的setState是个内部函数,setState可以使用外部函数(useState)的内部变量(currenCursor)。最根本的还是根据useState的调用顺序将不同的state一一对应起来,到最后更新render的时候,顺序index又置零,方便下次更新再一一对应起来。
useEffect
执行顺序:
componentDidMount:effect。
componentDidUpdate:上一轮effect的清除函数,然后本轮effect。
componentWillUnmount:上一轮effect清除函数。
依赖:
依赖没有变化,本轮componentDidUpdate将被跳过。
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
像computed一样,通过函数返回一个记忆值。
函数是在渲染期间执行,所以并没有依赖监听,只是一种优化手段,一个值不用每次render都重新计算,缓存了而已,render时发现依赖变了才重新计算。
useCallback
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
语法糖,缓存函数,依赖不变不用重新创建函数。
场景:父组件更新时,通过props传递给子组件的函数也会重新创建,然后这个时候使用 useCallBack 就可以缓存函数不使它重新创建。
useRef
首先功能上,它具有React.createRef()的功能,可以获取Dom元素或者React组件。
它还有另外一个功能,就是存储变量。这个变量就是放在{current:‘’}盒子中的current属性上,在该组件的整个生命周期中(无论reRender()多少次,但不包括卸载,卸载了该轮生命周期就结束了),useRef返回的都是同一个引用对象。
你要用const a = {current:‘’},这样存,每次reRender,a都是重新创建的。感觉useState也可以起到一样的作用,但是state修改值时会reRender,用这个useRef则不会。