React
React (also known as React.js or ReactJS) & Meta
快速上手
React 主要负责 MVC 中视图层 View 的渲染。库 react 和 react-dom 分别用于创建和渲染元素。
- React.createElement 的参数分别是元素名、元素属性和子节点|文本节点
- render 的参数是待渲染元素和页面元素挂载点;返回值描述页面的内容
import React from 'react'; // 提供 React.StrictMode
import ReactDOM from 'react-dom/client';
...
const title = React.createElement('h1', null, 'Hello React') // 创建 react 元素
ReactDOM.render(title, document.getElementById('root')) // 渲染 react 元素到页面
JSX => JavaScript XML
React embraces the fact that rendering logic is inherently coupled with other UI logic => 事件处理、状态改变、数据渲染 -> 互相影响
Since JSX is closer to JavaScript than to HTML, React DOM uses camelCase property naming convention instead of HTML attribute names.
class => className in JSX, tabindex => tabIndex, and for => htmlFor
/* input 标签点 label 获取焦点 */
<label for="username">username:</label> // Native
<label htmlFor="username">username:</label> // React
<input type="text" id="username"></input>
作为 React.createElement 的语法糖,JSX 是经 @babel/preset-react 包下的 Babel 配置完成转义。React.createElement 调用后会产生称作 React 元素的对象。
const element = ( <h1 className="greeting"> Hello, world! </h1> ); // 转义前
const element = React.createElement( 'h1', {className: 'greeting'}, 'Hello, world!' );
// React 通过读取这些对象来构建 DOM 以及保持随时的更新
const element = { type: 'h1', props: { className: 'greeting', children: 'Hello, world!' } };
- JSX 是 React 声明式的体现,React.createElement 是命名式的体现
- JSX 中可以使用花括号嵌入 JavaScript 表达式,不可以嵌入语句
语句会产生能用变量接收结果值,可以放在任何需要值的位置。不同于依靠框架提供的语法增强 HTML 结构,React 利用语言自身能力编写结构的需要。
由于没有指令的概念,条件渲染需要通过标识变量,列表渲染需要在花括号中使用数组原型的 map 方法。
- React 在样式处理中推荐行内样式而非类名,开发中推荐使用 className
// 行内样式可以具有多个 => 对象形式
var styleObj = {color: 'black', backgroundColor: 'white'}
<h1 style={styleObj}> JSX 样式 </h1>
<h1 style={{color: 'black', backgroundColor: 'white'}}> JSX 样式 </h1> // 双花括号不是语法 => 只有单花括号是
Components
- 函数组件和类组件 Function and Class Components
函数组件没有状态,接收唯一带有数据的 props 属性对象,并返回一个 React 元素。函数名必须用大写字母开头,以区分组件与普通 React 元素
function Hello(props) { return <h1>Hello, {props.name}</h1>; }
const Hello = (props) => <div>Hello, {props.name}</div>
类组件使用 ES6 的 class 定义,普通类继承 React.Component 才能成为组件类
class Hello extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }
类组件的具体渲染效果依靠渲染函数,所以渲染函数必须有表示该组件结构的返回值。解析类组件标签时,会先 new 出该类的实例,并调用原型上的 render 方法。
ReactDom.render(<App>, document.getElementById("#root")) // 自动转换以下写法
ReactDom.render(new App({name: react}).render(), document.getElementById("#root"))
函数组件和类组件在不需要渲染内容时可直接返回 null。
除非要对实例初始化指定属性,类组件的构造器不是必须的。当子类继承父类,且子类指定构造器时,子类构造器中的 super 函数必须调用。因为子类继承父类时没有 this,需通过 super 绑定父类的 this,同 sup.prototype.constructor.call(sub)。
- 组件实例属性 state & props
自定义组件会将 JSX 所接收的属性 attributes 以及子组件 children 转换为单个对象传递给组件,这个对象被称之为 props。
组件无论是使用函数声明还是通过 class 声明,都绝不能修改自身的 props。
function Hello(props) { return <h1>Hello, {props.name}</h1>; }
// class Hello extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }
const root = ReactDOM.createRoot(document.getElementById('root'));
const element = <Hello name="IcedAmericano" />;
root.render(element);
state 与 props 类似,但是 state 是私有的,并且完全受控于当前组件,是组件的内部状态。
直接修改 state 并不会重新渲染组件,state 修改应该使用 setState()。setState() 的作用是修改 state 并更新页面。构造函数是唯一能给 this.state 赋值的位置。
this.state.comment = 'Hello'; // Wrong
this.setState({comment: 'Hello'}); // Correct
state 的更新可能是异步的。出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。因为 this.props 和 this.state 可能会异步更新,所以不要依赖这些值来更新下一个状态。要解决此问题,可以让 setState() 接收函数而不是对象。
传入对象参数是传入函数参数的语法糖。若新状态不依赖于原状态,可以使用对象参数;若新状态依赖于原状态,需要使用函数参数。
this.setState({ counter: this.state.counter + this.props.increment }); // Wrong
this.setState((state, props) => ({ counter: state.counter + props.increment })); // Correct
this.setState(function(state, props) { return { counter: state.counter + props.increment }; }); // Correct
setState() => 是主线程调用的同步方法,但其更新数据状态的动作是异步的。
函数 render 中不能调用 setState()。渲染函数作为纯函数,其返回结果完全取决于 this.state 和 this.props,期间不应该造成任何副作用(副作用即状态改变)。
React 单向数据流 => 任何的 state 总是所属于特定的组件,且从该 state 派生的任何数据或 UI 只能影响树中"更低"的组件。
Handling Events
React 事件的命名采用 camelCase,而不是纯小写;在 JSX 语法中需要传入函数作为事件处理函数,而不是字符串。
<button onclick="btnFunc()">Click me</button> // 原生标签内调用函数
类组件绑定事件函数时需要用到 this,代表指向当前的类的引用;函数组件中不需要调用 this。
<button onClick={btnFunc}>Click me</button> // React
元素在 JSX 中所绑定的事件先会经 Babel 转换成 React.createElement 的形式,再转成 fiber 对象。对象属性 memoizedProps 和 pendingProps 保存具体事件。
<button onClick={ this.handerClick }>Click Me</button>
React.createElement ("button", { onClick: this.handerClick }, "\u{43}\u{6c}\u{69}\u{63}\u{6b}\u{20}\u{4d}\u{65}")
child: FiberNode { memoizedProps: {children: "Click Me", onClick: f}, pendingProps: {children: "Click Me", onClick: f} }
React 17 不再将事件添加于 document,而是绑定根节点容器。方便局部升级。
const rootNodeContainer = document.getElementById('root');
ReactDOM.render(<App />, rootNodeContainer);
事件不会注册在具体元素节点,而是采取事件代理模式,利用冒泡指定事件处理程序,等冒泡到根节点再通过 event.target 找到真实触发的事件源。
// 真实节点上的处理函数被替换为空函数 noop
elementName
useCapture: false
passive: false
once: false
handler: f noop()
并非在初始时将所有事件绑定在根节点容器上,而是采取按需绑定,在发现具体合成事件后,再去绑定根节点容器的原生事件。事件触发时,通过调度离散事件函数 dispatchDiscreteEvent 将指定的函数执行。
React 合成事件 SyntheticEvent 是模拟原生 DOM 事件所有能力的事件对象,即浏览器原生事件的跨浏览器包装器。
此外不能通过返回 false 的方式阻止默认行为。必须显式地使用 preventDefault。
// 传统的 HTML 中阻止表单的默认提交行为
<form onsubmit="console.log('You clicked submit.'); return false">
<button type="submit">Submit</button>
</form>
// React
function Form() {
function handleSubmit(e) { e.preventDefault(); console.log('You clicked submit.'); }
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}
- this for the methods in JSX
render 中 this 指向类组件实例,与 render 平级的方法中 this 为 undefined。
处理事件回调函数中 this 的指向时,通常用箭头函数与 bind 绑定。
/* 方式一 => 在 render 中使用行内箭头函数 */
class ArrowFunctionBindTest extends React.Component {
// handleFunc() { console.log('this is:', this); };
handleFunc = () => { console.log('this is:', this); };
render() {
return (
// 事件处理函数用箭头函数表示 => 回调函数可使用箭头函数表示或函数声明表示
// 箭头函数 this 指向外部函数的 this => 回调函数因自主调用 this 指向类组件实例
<button onClick={() => this.handleFunc()}>
Click me
</button>
);
}
}
/* 方式二 => 组件内使用箭头函数定义方法 */
class ArrowFunctionBindTest extends React.Component {
handleFunc = () => { console.log('this is:', this); }; // 回调函数用箭头函数表示
render() {
return (
<button onClick={ this.handleFunc }>
Click me
</button>
);
}
}
/* 方式三 => 在 render 中对事件处理函数绑定 this */
class ArrowFunctionBindTest extends React.Component {
handleFunc() { console.log('this is:', this); }
// handleFunc = () => { console.log('this is:', this); };
render() {
return (
// 事件处理函数用 bind 绑定 => 回调函数可使用箭头函数表示或函数声明表示
<button onClick={ this.handleFunc.bind(this) }>
Click me
</button>
);
}
}
/* 方式四 => 在构造器中绑定 this */
class ArrowFunctionBindTest extends React.Component {
constructor(props) {
super(props)
// 原型 handleFunc 中 this 指向实例 => bind 的函数挂载到实例自身并命名为 handleFunc
this.handleFunc = this.handleFunc.bind(this)
}
handleFunc() { console.log('this is:', this); }
// handleFunc = () => { console.log('this is:', this); };
render() {
return (
// 事件处理函数用 bind 绑定 => 回调函数可使用箭头函数表示或函数声明表示
<button onClick={ this.handleFunc }>
Click me
</button>
);
}
}
/* 方式五 => 事件处理函数用箭头函数表示且直接写回调函数 */
class ArrowFunctionBindTest extends React.Component {
render() {
return (
<button onClick={ () => { console.log('this is:', this); } }>
Click me
</button>
);
}
}
自行封装的组件进行 props 校验:
- 安装:
yarn add prop-types
- 导入 PropTypes:
import PropTypes from 'prop-types'
- 给组件属性添加 props 校验:
组件.propTypes = {
属性1: PropTypes.string.isrequired,
属性2: PropTypes.func
}
React Hooks => React16.8
Hook 可以在不编写类组件的情况下使用状态以及其他的 React 特性。
React 复用组件的逻辑通常会采用 render prop 或 HOC,但这会存在嵌套地狱的问题。Hook 从组件中提取状态逻辑,即可在无需修改组件结构的情况下复用状态逻辑。
Hook 本质是 JavaScript 函数。Hook 只在最顶层使用、只在 React 函数中调用。
自变量 Hook
- 纯函数组件没有状态 => 通过 useState 为函数组件引入状态
setXxx(newValue) or setXxx(value => newValue)
useState 接受状态的初始值作为参数,并返回一个数组。useState 的返回值通过数组解构创建状态变量和更新状态变量的函数。
import React, { useState } from "react";
function UseStateHook() {
const [name, setName] = useState("hello useState");
const [age, setAge] = useState(22);
const [work, setWork] = useState("frontendenv");
return (
<div>
<span>{name}</span>
<span>{age}</span>
<span>{work}</span>
<br />
<button
onClick={() => {
setName((name) => name.split("").reverse().join(""));
}}
>
change my name
</button>
<button
onClick={() => {
setAge(age + 1);
}}
>
click me add age
</button>
<button
onClick={() => {
setWork("fullstack");
}}
>
what's my field
</button>
</div>
);
}
export default UseStateHook;
若使用单个状态变量,每次更新状态时需要合并之前的状态。类组件 setState 会把更新的字段自动合并到状态对象,而 useState 返回的 setXxx 会替换原来的值。
const [state, setState] = useState({ key01: val01, key02: val02, key03: val03, key04: val04 });
setState(state => ({ ...state, key01: val001, key02: val002 }));
不相关的状态推荐拆分为多组状态变量;相互关联或互相依赖的的状态建议合并为单组状态变量。把独立的状态变量拆分开还有另外的好处。在后期将一些相关的逻辑抽取到一个自定义 Hook 变得更容易。
function Box() {
const position = useWindowPosition();
const [size, setSize] = useState({ width: 100, height: 100 });
// ...
}
function useWindowPosition() {
const [position, setPosition] = useState({ left: 0, top: 0 });
useEffect(() => {
// ...
}, []);
return position;
}
- 组件之间共享状态 Hook => useContext
Context 上下文状态分发用于替代 React 逐层以 props 传递的全局数据。
Context 常用 API => React.createContext、Context.Provider、Context.Consumer、Class.contextType、Context.displayName
当组件上层最近的 <XxxContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递的数据。不受限于 React.memo 或 shouldComponentUpdate。
上下文对象创建时即可初始化传递默认值作参数。实际以 <XxxContext.Provider> 标签中 value prop 决定当前值。
const XxxContext = React.createContext(_defaultValue);
...
<XxxContext.Provider value=_finalValue>
<Isolation />
</XxxContext.Provider>
useContext Hook 的参数必须是上下文对象本身 => useContext(XxxContext)。
const _xxx = useContext(XxxContext);
...
_variable01: _xxx.attr01
import React, { useState, createContext, useContext } from "react";
const CountContext = createContext();
function UseContexthook() {
const [count, setCount] = useState(6);
return (
<div>
<p>You clicked {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
click me
</button>
<CountContext.Provider value={count}>
<Isolation />
</CountContext.Provider>
</div>
);
}
function Isolation() { // 隔离组件
return (
<Counter />
)
}
function Counter() {
const count = useContext(CountContext); //一句话就可以得到count
return <h3>{count}</h3>;
}
export default UseContexthook;
- 状态关联复杂的 useState 替代方案 => useReducer
useReducer 适用于多方式更新 state,或依赖于 oldState 的情况。dispatch 可以向深层级的子组件传递。
const [state, dispatch] = useReducer(reducer, initialArg, init);
reducer => 接收状态 state 和动作 action,返回与 dispatch 配套的状态更新逻辑。
function reducer(state, action) {
switch (action.type) {
case 'type01':
return {xxx: state.xxx + operation01};
case 'type02':
return {xxx: state.xxx + operation02};
default:
throw new Error();
}
}
...
<button onClick={() => dispatch({type: 'type01'})}>operation01</button>
惰性初始化创建初始 state => 将 init 函数作为 useReducer 的第三个参数传入,初始 state 将被设置为 init(initialArg)。
这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供便利。
import React, { useReducer } from "react";
const initCount = 0;
const init = (initCount) => {
return { count: initCount };
};
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return init(action.payload || 0);
default:
throw new Error();
}
};
const UseReducerHook = () => {
const [state, dispatch] = useReducer(reducer, initCount, init);
return (
<div className="App">
<div>useReducer Count:{state.count}</div>
<button
onClick={() => {
dispatch({ type: "decrement" });
}}
>
useReducer 减少
</button>
<button
onClick={() => {
dispatch({ type: "increment" });
}}
>
useReducer 增加
</button>
<button
onClick={() => {
dispatch({ type: "reset", payload: 999 });
}}
>
resetTo 999
</button>
</div>
);
};
export default UseReducerHook;
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)
需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。
因变量 Hook
- 生命周期替代 => useEffect Hook
类组件通过生命周期函数处理副作用操作,但是存在编写重复代码的弊端。此时可以在组件内部调用 useEffect 直接访问 state 变量或其他 props。
默认 useEffect 在渲染(第一次渲染之后和每次更新)之后都会执行。
渲染后执行的副作用函数称为 effect,在 DOM 更新之后执行。
useEffect 可看做生命周期函数 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合。
无需清除的 effect => 发送网络请求,手动变更 DOM,记录日志。
需要清除的 effect => 订阅外部数据源,监听。
useEffect Hook 的第一个参数是函数,内部注册副作用操作和监听器,返回函数用于清除监听的逻辑。useEffect Hook 的第二个参数是依赖数组,只有当所依赖的状态或 prop 改变时才会调用 useEffect Hook。
传入空数组和不传入数组的效果不同。前者是只在首次更新时执行,后续更新不执行;后者是渲染即执行。
[] => componentDidMount、componentWillUnMount
不传入 [] => componentDidMount、componentDidUpdate 和 componentWillUnmount
SOF => state 或 props 变更时,函数组件的 useEffect 会先执行 effect 的 return 所返回的函数,再执行 effect 函数体中的副作用内容。
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Route, Link, Routes } from "react-router-dom";
function Index() {
useEffect(() => {
console.log("useEffect=>Indexpage");
return () => {
console.log("Bye, Index page");
};
}, []);
return <p>indexPage</p>;
}
function OtherPage() {
useEffect(() => {
console.log("useEffect=>otherPage");
return () => {
console.log("Bye, Other page");
};
}, []);
return <p>otherPage</p>;
}
function useEffectHook() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`useEffect=>You clicked ${count} times`);
return () => {
console.log("DONE");
};
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
click me
</button>
<br />
<Router>
<Link to="/">indexPage</Link> & <Link to="/otherPage">otherPage</Link>
<Routes>
<Route path="/" exact element={<Index />} />
<Route path="/otherPage" element={<OtherPage />} />
</Routes>
</Router>
</div>
);
}
export default useEffectHook;
- 减少组件的更新频率 => useCallback
在函数组件中,定义在组件内部的函数会随着状态的更新而重新渲染,即函数中定义的函数会被频繁定义。在父子组件通讯的情况中,是特别消耗性能的。
useCallback 接收一个回调函数和依赖数组,返回一个 memoized 回调函数。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
函数组件通讯时,父组件的状态变化会导致内部的函数被重新定义,那么在将此被重新定义的函数作为传入子组件的 props 的值时,会导致子组件重新渲染。
React.memo 只能保证在相同 props 的情况下跳过渲染组件的操作并直接复用最近一次渲染的结果。
import React, { useState, useCallback } from "react";
function UseCallbackHook() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
const subClick01 = () => {};
const subClick02 = useCallback(() => {}, []);
return (
<div>
<span>{count}</span>
<button onClick={handleClick}>Sup Increment</button>
<Sub01 click={subClick01} />
<Sub02 click={subClick02} />
</div>
);
}
// React.memo 包裹子组件避免触发 => 不传入变化的 props 时可行
const Sub01 = React.memo(function Sub01() {
console.log("Sub01 Component is triggered");
return (
<>
<p>Hello Sub01</p>
</>
);
});
const Sub02 = React.memo(function Sub02() {
console.log("Sub02 Component is triggered");
return (
<>
<p>Hello Sub02</p>
</>
);
});
export default UseCallbackHook;
- 类似计算属性的监听 => useMemo
useMemo is a React Hook that lets you cache the result of a calculation between re-renders.
useMemo 传入的函数内部必须有返回值;useMemo 只能声明在函数组件内部。
useMemo 接收一个具有返回值的回调函数和依赖数组,返回一个 memoized 值。
React.memo 的使用位置处于函数式组件的外围,不能直接用 useMemo 替代。
import React, { useState, useMemo } from "react";
function UseMemoHook() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [d, setD] = useState(0);
const handleClick = (action) => {
switch (action.type) {
case "a":
setA(a + 1);
break;
case "b":
setB(b + 1);
break;
case "d":
setD(d + 1);
break;
default:
return false;
}
};
const c = useMemo(() => {
console.log("useMemo");
return (
<>
<span>{a + b}</span>
<span> - DOM 输出</span>
</>
);
}, [a, b]);
return (
<>
<p>a: {a}</p>
<p>b: {b}</p>
<p>c: {c}</p>
<p>d: {d}</p>
<button
onClick={() => {
handleClick({ type: "a" });
}}
>
+a
</button>
<button
onClick={() => {
handleClick({ type: "b" });
}}
>
+b
</button>
<button
onClick={() => {
handleClick({ type: "d" });
}}
>
+d
</button>
</>
);
}
export default UseMemoHook;
额外的 Hook
- useRef => 返回 ref 对象,该对象在组件的整个生命周期中保持不变
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
createRef 返回的 ref 对象会随着函数组件的渲染而重新初始化,useRef 返回的 ref 对象在组件的整个生命周期内都会持续存在。
useRef() 可以方便地保存任何可变值,类似于在 class 中使用实例字段的方式。
import React, { useRef, useState, useEffect } from "react";
function UseRefHook() {
const [renderTimes, setRenderTimes] = useState(0);
const refByUseRef = useRef();
let thisVal = "hello useRef";
const refLikeThis = useRef(thisVal);
const refByCreateRef = React.createRef();
if (!refByUseRef.current) {
refByUseRef.current = renderTimes;
}
if (!refByCreateRef.current) {
refByCreateRef.current = renderTimes;
}
useEffect(() => {
console.log(refLikeThis);
});
return (
<>
<span>renderTimes: {renderTimes}</span>
<span>refByUseRef: {refByUseRef.current}</span>
<span>refByCreateRef: {refByCreateRef.current}</span>
<br />
<button
onClick={() => {
setRenderTimes((time) => time + 1);
}}
>
render now
</button>
</>
);
}
export default UseRefHook;
- useLayoutEffect => DOM 变更之后同步调用 effect
useEffect
会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 更新。useLayoutEffect
在渲染的内容更新到 DOM 上之前执行,会阻塞 DOM 的更新,可解决使用 useEffect
所导致页面闪动的问题。
创建并调用函数组件 => 更新 DOM => useLayoutEffect => 渲染视图 => useEffect => 侦测到状态改变重新执行函数组件 => 和 Virtual DOM 比较后更新 DOM => 调用 useLayoutEffect => 渲染视图 => useEffect => 组件被移除 => useLayoutEffect => 调用 useEffect。
useInsertionEffect 在所有 DOM 突变之前同步触发,可将样式注入 DOM。由于其执行的时机,不能访问 refs,也不能安排更新。
import React, { useEffect, useLayoutEffect, useState } from "react";
function UseLayoutEffectHook() {
const [state, setState] = useState(0);
console.log("render", state);
useEffect(() => {
console.log("useEffect render", state);
}, [state]);
useLayoutEffect(() => {
console.log("useLayoutEffect render", state);
}, [state]);
return (
<>
<button
onClick={() => {
setState(state + 1);
}}
>
state: {state}
</button>
</>
);
}
export default UseLayoutEffectHook;
- useImperativeHandle => 使用 ref 时自定义暴露给父组件的部分功能
在 React 中,通常 useRef
、forwardRef
和 useImperativeHandle
会结合使用,以实现类似于 Vue 中直接设置子组件的 ref 属性并通过 this.$refs.xxx 访问子组件实例的功能。
在 React 中给子组件设置 ref
属性时会出现报错,这是因为在 React 中,ref
是一种特殊的属性,不能直接用于函数式组件或者普通的 HTML 元素上。只有在类组件中才可以使用 ref
属性。
forwardRef
和 useImperativeHandle
组合起来可以帮助控制子组件暴露哪些方法给父组件,从而实现方法的保护或封装。
input & focus 需求可通过 useRef 实现。若将 input & focus 再封装一层,Sup 组件也需要对这个输入框执行聚焦相关的操作时,考虑 useImperativeHandle。
useImperativeHandle 可以让父组件获取子组件的数据或者调用子组件里声明的函数。
import React, { useRef, useImperativeHandle, forwardRef } from "react";
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
defaultVal: "hello useImperativeHandle"
}));
return (
<input ref={inputRef} type="text" placeholder="hello ..." {...props} />
);
}
export default forwardRef(FancyInput);
import React, { useRef } from "react";
import FancyInput from "./FancyInput";
export default function UseImperativeHandleHook() {
const supInputRef = useRef(null);
console.log(supInputRef);
const foucsFancyInput = () => {
supInputRef.current.focus();
console.log(supInputRef.current.defaultVal);
};
return (
<>
<h4 onClick={foucsFancyInput}>Click Me</h4>
<FancyInput ref={supInputRef} />
</>
);
}
- useDeferredValue => 返回传入值的副本
const [queryStr, setQueryStr] = useState('');
const deferredQueryStr = useDeferredValue(queryStr);
queryStr 是常规的 state,deferredQueryStr 是 queryStr 的延迟值。
设置延迟值后每次调用 setState 都会触发两次组件的重新渲染。deferredQueryStr 的值首次是 queryStr 修改前的值,第二次是修改后的值。延迟值相较于 state 总会慢一步更新。
当指定状态需要在多个组件中使用时,不同组件会有不同的渲染效率。渲染较快的组件使用正常的状态,渲染较慢的组件使用该状态的延迟值。可结合 React.memo 或 useMemo。
- useTransition => 处理非紧急更新
默认所有更新都是具有相同优先级的阻塞渲染很可能会降低页面效率,在并发模式中,渲染不是阻塞的,而是可中断的。
返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。
const [isPending, startTransition] = useTransition();
作为过渡任务的函数,会在其他优先级更高的方法执行完毕后运行。启动该过渡任务的函数 startTransition 可以在不需要 isPending 时直接使用。
import { useState, useTransition } from "react";
export default function App() {
const [isPending, startTransition] = useTransition();
const [input, setInput] = useState("");
const [list, setList] = useState([]);
const LIST_SIZE = 1000;
function handleChange(e) {
setInput(e.target.value);
startTransition(() => {
const l = [];
for (let i = 0; i < LIST_SIZE; i++) {
l.push(e.target.value);
}
setList(l);
});
}
return (
<>
<input type="text" value={input} onChange={handleChange} />
{isPending
? "Loading..."
: list.map((item, index) => {
return <div key={index}>{item}</div>;
})}
</>
);
}
- useId => 生成唯一 id,不适用于列表的 key
当封装组件在被复用时,可能存在标识符重复的异常。
每次组件渲染,useId 都会返回 unique id。这可以避免出现相同 id 所致的错误。
每次页面的渲染(多次刷新),固定位置所产生的随机 id 都会相同。
既使用 SSR,又使用 CSR 渲染部分页面时,可能会导致 ids 不匹配的情况出现 => Math.random 所产生的数据是大概率不同的。
useId 可以保证客户端渲染与服务端渲染产生一致的 id。
useId 产生的 id 不能用 querySelector 所捕获,欲拿到 DOM,可以使用 useRef。
// EmailForm.js 修改前
export default function EmailForm(){
return (
<>
<label htmlFor="email">Email</label> // 给 label 添加 htmlFor 属性 => 点击 label -> Input 框自动聚焦
<input id="email" type="email" /> // 复用多次会产生相同 id => 点击后面的输入框不会聚焦 -> 默认聚焦最前方输入框
</>
)
}
// EmailForm.js 修改后
import { useId, useRef } from "react";
export default function EmailForm(){
const id = useId();
const ref = useRef();
return (
<>
<label htmlFor={`${id}-email`}>Email</label>
<input ref={ref} id={`${id}-email`} type="email" />
<br />
<label htmlFor={`${id}-name`}>Name</label>
<input ref={ref} id={`${id}-name`} type="text" />
</>
)
}
自定义 Hook
自定义 Hook 用作共享组件之间的状态逻辑,是 props render 和 HOC 的替代。
自定义 Hook 是一个函数,其名称以 use 开头,函数内部可以调用其他的 Hook。
Hook 注意事项
Sup Func Component 会因 setState 导致其内部的 Sub Func Component 被重新渲染。通常使用 memo 包裹 Sub Func Component 以达到类组件中 pureComponent 的效果。
React.memo 高阶组件仅通过检查 props 的变更以判断是否直接复用最近一次渲染的结果。被 React.memo 所包裹的 Func 组件的实现中拥有 useState,useReducer 或 useContext 的 Hook 时,state 或 context 变化依旧会导致组件的重新渲染。
React Router
Router Principle
接受 props:一般组件根据标签中所传递的内容确认 props,路由组件默认接受被路由器固定传递的三个属性。
history => action、block、createHref、go、goBack、goForward、length、listen、location、push、replace
location => hash、key、pathname、search、state
match => isExact、params、path、url
- 严格模式开启时,可能会导致该级路由下的所有子路由无法正常匹配
注册嵌套路由时要写上前一级路由的路径,路由的匹配是按照注册路由的顺序进行的。注册路由即展示区的 <Route path="" component={}>
。
// Sup 组件开启严格模式
<Route exact path="/sup" component={Sup}/>
// Sub 组件无法正常匹配
<NavLink to="/sup/sub"> Sub </NavLink>
<Route path="/sup/sub" component={Sub}/>
向路由组件传参
- params 传参 => 路由链接携带的参数需在注册路由时接收
// 传递参数
<Link to={`/a/b/c/${xxx.id}/${xxx.name}`}>hello</Link>
// 声明接受
<Route path="/a/b/c/:id/:name" component={Hello}/>
// 组件获取
const {id, name} = this.props.match.params
- search 传参 => 无需声明接收,类似 Ajax 的 query 参数
获取到的 search 是 urlencoded 编码字符串,需借助 querystring 解析。
// 传递参数
<Link to={`/a/b/c/?id=${xxx.id}&name=${xxx.name}`}>hello</Link>
// 声明接受
<Route path="/a/b/c" component={Hello}/>
// 组件获取
import qs from 'querystring'
const {search} = this.props.location
const {id, name} = qs.parse(search.slice(1))
- state 传参 => 传递的数据在地址栏隐藏,且无需声明接收
在 Vue 中使用 params 参数且未在路由配置里指明时,会出现刷新所导致的参数丢失问题。React 路由中的 state 传参虽没有显示在地址栏,但不会出现此类情况。
react-router-dom 中的 BrowserRouter 维护浏览器的 history,location 是 history 中的一个属性。考虑到会有清除浏览器缓存并刷新的情况,故添加空对象以处理不存在 state 的问题。state 默认为 undefined。
// 传递参数
<Link to={{pathname:"/a/b/c", state:{id:xxx.id, name:xxx.name}}}>hello</Link>
// 声明接受
<Route path="/a/b/c" component={Hello}/>
// 组件获取
const {id, name} = this.props.location.state || {}
- BrowserRouter 和 HashRouter
底层原理:前者使用 H5 的 history,不兼容 IE9- 版本,后者使用 URL 哈希值。
路径表现:前者无 #,后者包含 #。
刷新对路由 state 参数的影响:前者无影响,state 存储于 history;后者 state 会因刷新而丢失。
编程式路由导航
业务场景有时需要点击按钮进行页面的跳转,此时就应该从声明式路由导航转向使用编程式路由导航。考虑到需要传递相关参数,事件处理函数可以使用高阶函数或进行函数包裹。
编程式路由导航主要使用的是 history 对象,即 React Router 的 history 依赖包。
push(path, [state]) - (function 类型) 在 history 堆栈添加一个新条目
replace(path, [state]) - (function 类型) 替换在 history 堆栈中的当前条目
- params 参数 & 编程式路由导航
<button onClick={() => this.pushHandle(xxx.id, xxx.name)}>push</buttton>
<button onClick={() => this.replaceHandle(xxx.id, xxx.name)}>replace</buttton>
pushHandle = (id, name) => {this.props.history.push(`/a/b/c/${id}/${name}`)}
replaceHandle = (id, name) => {this.props.history.replace(`/a/b/c/${id}/${name}`)}
<Route path="/a/b/c/:id/:name" component={Xxx}/>
- search 参数 & 编程式路由导航
<button onClick={() => this.pushHandle(xxx.id, xxx.name)}>push</buttton>
<button onClick={() => this.replaceHandle(xxx.id, xxx.name)}>replace</buttton>
pushHandle = (id, name) => {this.props.history.push(`/a/b/c/?id=${id}&name=${name}`)}
replaceHandle = (id, name) => {this.props.history.replace(`/a/b/c/?id=${id}&name=${name}`)}
<Route path="/a/b/c" component={Xxx}/>
import qs from 'querystring'
const {search} = this.props.location
const {id, name} = qs.parse(search.slice(1))
- state 参数 & 编程式路由导航
<button onClick={() => this.pushHandle(xxx.id, xxx.name)}>push</buttton>
<button onClick={() => this.replaceHandle(xxx.id, xxx.name)}>replace</buttton>
pushHandle = (id, name) => {this.props.history.push(`/a/b/c/`, {id, name})}
replaceHandle = (id, name) => {this.props.history.replace(`/a/b/c/`, {id, name})}
<Route path="/a/b/c" component={Xxx}>
const {id, name} = this.props.location.state || {}
- withRouter 函数 => 使一般组件获得路由组件的相关 API
可通过 withRouter 高阶组件访问 history 对象的属性和最近 <Route> 的 match。
当路由渲染时,withRouter 会将已经更新的 match,location 和 history 属性传递给被包裹的组件。
// withRouter => 使一般组件具有路由组件 API
import React, {Component} from 'react'
import {withRouter} from 'react-router-dom'
class Demo extends Component{...}
export default withRouter(Demo)
React Router v6
- React Router 以三个不同的包发布到 npm
react-router => 路由核心库,提供组件和钩子;react-router-dom => react-router 所有内容都被包含,添加专门用于 DOM 的组件,如 <BrowserRouter> 等。react-router-native => 含 react-router 所有内容,额外增加用于 ReactNative 的 API,如 <NativeRouter> 等。
- <Routes/> 与 <Route/>
通过引入 <Routes>
替代移除的 <Switch>
。<Routes>
、<Route>
应配合使用,前者将后者包裹。
path 属性用于定义路径,element 属性用于定义当前路径所对应的组件。
<Route>
相当于 if 语句,若其路径与当前 URL 匹配,则呈现对应的组件。
<Route>
中的 caseSensitive 属性用于指定匹配时是否区分大小写,默认 false。
当 URL 改变时,<Routes>
会查看所有子 <Route>
以匹配并呈现组件。
<Route>
可嵌套使用,可配合 useRoutes()
配置路由表,但需 <Outlet>
组件以渲染其子路由。
<Routes>
// 一级路由 demo 所对应的路径为 /demo*/
<Route path="demo" element={<Demo/>}>
// inner01 和 inner02 是二级路由 => 对应路径为 /demo/inner01 和 /demo/inner02
<Route path="inner01" element={<Inner01/>}></Route>
<Route path="inner02" element={<Inner02/>}></Route>
</Route>
</Routes>
- React Router6 实现自定义的类名时,需要把 className 的值写成函数
// React Router5
<NavLink className="default-style" activeClassName="additional-style" to="/demo">Demo</NavLink>
// React Router6
<NavLink className={({isActive})=>{return isActive ? "default-style additional-style" : "default-style"}} to="/demo">Demo</NavLink>
- useRoutes => 将应用路由的层级进行统一管理
import React from 'react'
import {NavLink, useRoutes} from 'react-router-dom'
import routes from './routes'
export default function App(){
const element = useRoutes(routes)
return (
...
{element}
)
}
// routes/index.js
import Dashboard from '../pages/Dashboard'
import Info from '..pages/Info'
import {Navigate} from 'react-router-dom'
export default [
{path: "/dashboard", element:<Dashboard/>},
{path: "/info", element:<Info/>},
{path: "/", element:<Navigate to="/dashboard"/>}
]
NavLink
If the
end
prop is used, it will ensure this component isn't matched as "active" when its descendant paths are matched. v6.3
-
this.props.match.params 通过 useParams 或 useMatch 替代
-
this.props.location 的 search 参数通过 useSearchParams 或 useLocation 替代
import {useSearchParams} from 'react-router-dom'
export default function Xxx(){
const [search, setSearch] = useSearchParams();
const id = search.get("id");
const msg = search.get("msg");
...
return (<>...</>)
}
-
state 参数使用 useLocation 替代
-
编程式路由导航
-
v6 中没有 this,可使用 useNavigate
useNavigate hook 返回一个以编程方式导航的函数,例如在提交表单之后。
declare function useNavigate(): NavigateFunction;
interface NavigateFunction {
(
to: To,
options?: { replace?: boolean; state?: any }
): void;
(delta: number): void;
}
withRouter
函数在 v6 中被 useNavigate
替代,传入 delta
以对 history 进行操作。
import { useNavigate } from "react-router-dom";
function eventHandle() {
let navigate = useNavigate();
async function handleSubmit(event) {
event.preventDefault();
await submitForm(event.target);
navigate("../success", { replace: true });
}
return <form onSubmit={handleSubmit}>{/* ... */}</form>;
}
- useInRouterContext => 判断组件是否在
<Router>
上下文呈现
通过调用该 hook 可以判断当前组件是否被 <XxxRouter>
之流包裹。开发中若直接将 <App>
组件以路由包裹,则
封装组件时判断使用者是否在路由环境下使用。
- useNavigationType => 返回当前的导航类型 -> POP|PUSH|REPLACE
来到当前页面的跳转方式。POP
为直接在浏览器打开该路由组件(刷新页面)。
- useOutlet => 呈现当前组件中要渲染的嵌套路由
嵌套路由挂载则展示嵌套的路由对象,没有挂载则返回 null。
const result = useOutlet(); // 嵌套路由没有挂载则返回 null
-
useResolvedPath => 给定 URL 值,解析其中的 path|search|hash
-
<Navigate>
<Navigate>
组件在渲染时会修改路径并切换视图。replace 属性用于控制跳转模式,可选 push 或 replace,默认为 push。
import React, {useState} from 'react'
import {Navigate} from 'react-router-dom'
export default function Demo(){
const [sum, setSum] = useState(1)
return (
<>
{sum === 1 ? <h4>sum 的值为 {sum}</h4>:<Navigate to="/about" replace/>}
<button onClick={() => setSum(2)}>Click Change Sum to 2</button>
</>
)
}
- <Outlet> => 当
<Route>
产生嵌套时,渲染其对应的后续子路由
const element = useRoutes([
{path:'/about', element:<About/>},
{
path:'/info',
element:<Info/>,
children:[
{path:'news', element:<News/>},
{path:'msg', element:<Msg/>}
]
}
])
import React from 'react'
import {NavLink, Outlet} from 'react-router-dom'
export default function Home(){
return (
<>
<h2>Home...</h2>
<div>
<ul className="nav nav-tabs">
<li><NavLink className="list-group-item" to="news">News</NavLink></li>
<li><NavLink className="list-group-item" to="msg">Msg</NavLink></li>
</ul>
<Outlet/>
</div>
</>
)
}
- useRoutes => 根据路由表动态创建 <Routes> 和 <Route>
Parsing The Source Code
v18 事件系统
- 事件注册
DOMPluginEventSystem 中调用各类 XxxEventPlugin 的 registerEvents()
注册事件。registerEvents 即从 DOMEventProperties 导入的 registerSimpleEvents 函数。如 SimpleEventPlugin 的 registerSimpleEvents。
// registerSimpleEvents 即 registerEvents
export function registerSimpleEvents() {
for (let i = 0; i < simpleEventPluginEvents.length; i++) {
const eventName = ((simpleEventPluginEvents[i]: any): string);
const domEventName = ((eventName.toLowerCase(): any): DOMEventName);
const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
}
// Special cases where event names don't match.
registerSimpleEvent(ANIMATION_END, 'onAnimationEnd');
registerSimpleEvent(ANIMATION_ITERATION, 'onAnimationIteration');
registerSimpleEvent(ANIMATION_START, 'onAnimationStart');
registerSimpleEvent('dblclick', 'onDoubleClick');
registerSimpleEvent('focusin', 'onFocus');
registerSimpleEvent('focusout', 'onBlur');
registerSimpleEvent(TRANSITION_END, 'onTransitionEnd');
}
在 registerSimpleEvents()
内有调用 registerSimpleEvent()
函数,后者内部又调用 registerTwoPhaseEvent()
以分别注册捕获和冒泡阶段的事件。
function registerSimpleEvent(domEventName, reactName) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}
export function registerTwoPhaseEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
): void {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
}
export function registerDirectEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
) {
...
registrationNameDependencies[registrationName] = dependencies;
}
- 事件绑定
createRoot 在 ReactDOM.js 与 client.js 中视作 createRootImpl 进行调用,即在创建根节点之后会执行 listenToAllSupportedEvents。
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
...
listenToAllSupportedEvents(rootContainerElement);
}
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
allNativeEvents.forEach(domEventName => {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
const ownerDocument =
(rootContainerElement: any).nodeType === DOCUMENT_NODE
? rootContainerElement
: (rootContainerElement: any).ownerDocument;
if (ownerDocument !== null) {
// The selectionchange event also needs deduplication
// but it is attached to the document.
if (!(ownerDocument: any)[listeningMarker]) {
(ownerDocument: any)[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
}
}
调用 addTrappedEventListener 进行真正的事件绑定,绑定在document上,dispatchEvent 为统一的事件处理函数。
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
target: EventTarget,
): void {
if (__DEV__) {
if (nonDelegatedEvents.has(domEventName) && !isCapturePhaseListener) {
console.error(
'Did not expect a listenToNativeEvent() call for "%s" in the bubble phase. ' +
'This is a bug in React. Please file an issue.',
domEventName,
);
}
}
let eventSystemFlags = 0;
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
}
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
let listener = createEventListenerWrapperWithPriority( // 创建具有优先级的监听函数
targetContainer,
domEventName,
eventSystemFlags,
);
...
targetContainer =
enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
? (targetContainer: any).ownerDocument
: targetContainer;
let unsubscribeListener;
if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
const originalListener = listener;
listener = function(...p) {
removeEventListener(
targetContainer,
domEventName,
unsubscribeListener,
isCapturePhaseListener,
);
return originalListener.apply(this, p);
};
}
// TODO: There are too many combinations here. Consolidate them.
if (isCapturePhaseListener) { // 事件捕获阶段处理函数 => 节点上添加事件
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
}
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind( // 绑定 dispatchDiscreteEvent
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
- 事件触发
React 事件注册时,dispatchDiscreteEvent 为统一的事件处理函数,即触发事件首先执行 dispatchDiscreteEvent 函数,因 dispatchDiscreteEvent 前三个参数已经被 bind 绑定,故事件源对象 event.target 被默认绑定成最后参数 nativeEvent。
function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
const previousPriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
try {
setCurrentUpdatePriority(DiscreteEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
if (!_enabled) {
return;
}
if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
} else {
dispatchEventOriginal(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
}
}
import {dispatchEventForPluginEventSystem} from './DOMPluginEventSystem';
...
function dispatchEventOriginal(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
) {
// TODO: replaying capture phase events is currently broken
// because we used to do it during top-level native bubble handlers
// but now we use different bubble and capture handlers.
// In eager mode, we attach capture listeners early, so we need
// to filter them out until we fix the logic to handle them correctly.
const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
if (
allowReplay &&
hasQueuedDiscreteEvents() &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
// If we already have a queue of discrete events, and this is another discrete
// event, then we can't dispatch it regardless of its target, since they
// need to dispatch in order.
queueDiscreteEvent(
null, // Flags that we're not actually blocked on anything as far as we know.
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
return;
}
const blockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (blockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer,
);
if (allowReplay) {
clearIfContinuousEvent(domEventName, nativeEvent);
}
return;
}
if (allowReplay) {
if (isDiscreteEventThatRequiresHydration(domEventName)) {
// This this to be replayed later once the target is available.
queueDiscreteEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
return;
}
if (
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
return;
}
// We need to clear only if we didn't queue because
// queueing is accumulative.
clearIfContinuousEvent(domEventName, nativeEvent);
}
// This is not replayable so we'll invoke it but without a target,
// in case the event system needs to trace it.
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}
HostComponent 常量为 5。
import { HostRoot, HostPortal, HostComponent, HostText, ScopeComponent, } from 'react-reconciler/src/ReactWorkTags';
...
export function dispatchEventForPluginEventSystem(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
let ancestorInst = targetInst;
if (
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
(eventSystemFlags & IS_NON_DELEGATED) === 0
) {
const targetContainerNode = ((targetContainer: any): Node);
// If we are using the legacy FB support flag, we
// defer the event to the null with a one
// time event listener so we can defer the event.
if (
enableLegacyFBSupport &&
// If our event flags match the required flags for entering
// FB legacy mode and we are processing the "click" event,
// then we can defer the event to the "document", to allow
// for legacy FB support, where the expected behavior was to
// match React < 16 behavior of delegated clicks to the doc.
domEventName === 'click' &&
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0 &&
!isReplayingEvent(nativeEvent)
) {
deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
return;
}
if (targetInst !== null) {
// The below logic attempts to work out if we need to change
// the target fiber to a different ancestor. We had similar logic
// in the legacy event system, except the big difference between
// systems is that the modern event system now has an event listener
// attached to each React Root and React Portal Root. Together,
// the DOM nodes representing these roots are the "rootContainer".
// To figure out which ancestor instance we should use, we traverse
// up the fiber tree from the target instance and attempt to find
// root boundaries that match that of our current "rootContainer".
// If we find that "rootContainer", we find the parent fiber
// sub-tree for that root and make that our ancestor instance.
let node = targetInst;
mainLoop: while (true) {
if (node === null) {
return;
}
const nodeTag = node.tag;
if (nodeTag === HostRoot || nodeTag === HostPortal) {
let container = node.stateNode.containerInfo;
if (isMatchingRootContainer(container, targetContainerNode)) {
break;
}
if (nodeTag === HostPortal) {
// The target is a portal, but it's not the rootContainer we're looking for.
// Normally portals handle their own events all the way down to the root.
// So we should be able to stop now. However, we don't know if this portal
// was part of *our* root.
let grandNode = node.return;
while (grandNode !== null) {
const grandTag = grandNode.tag;
if (grandTag === HostRoot || grandTag === HostPortal) {
const grandContainer = grandNode.stateNode.containerInfo;
if (
isMatchingRootContainer(grandContainer, targetContainerNode)
) {
// This is the rootContainer we're looking for and we found it as
// a parent of the Portal. That means we can ignore it because the
// Portal will bubble through to us.
return;
}
}
grandNode = grandNode.return;
}
}
// Now we need to find it's corresponding host fiber in the other
// tree. To do this we can use getClosestInstanceFromNode, but we
// need to validate that the fiber is a host instance, otherwise
// we need to traverse up through the DOM till we find the correct
// node that is from the other tree.
while (container !== null) {
const parentNode = getClosestInstanceFromNode(container);
if (parentNode === null) {
return;
}
const parentTag = parentNode.tag;
if (parentTag === HostComponent || parentTag === HostText) {
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}
batchedUpdates(() =>
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
),
);
}
dispatchEventsForPlugins()
中 extractEvents()
生成 SyntheticEvent 合成事件,而 processDispatchQueue()
执行事件队列。
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
FAQ & Bugs
React Fiber
Fiber 是 React 16 中新的协调引擎 => 使 Virtual DOM 可以进行增量式渲染。
incremental rendering 增量式渲染能够将渲染工作分块并将其分散到多个帧上。
测试一下测试一下测试一下测试一下测试一下测试一下测试一下测试一下测试一下测试一下测试一下
页面元素较多,且频繁刷新的情况下,v15 会出现掉帧的现象。由于采用的是全量渲染,渲染过程不可中断。
协调器 reconciler 会调用组件的 render 决定是否进行挂载,更新或是卸载操作。
从 v15 到 v16,React 团队花了两年时间将源码架构中的 Stack Reconciler 重构为 Fiber Reconciler。
页面节点多,层次深会导致递归渲染的耗时增加,由于 UI 线程与单线程的 JS 线程互斥,影响响应。
Fiber 其实是一种数据结构,可以用纯 JS 对象表示。fiber 也是一个执行单元,每次执行完一个执行单元,React 就会检查还剩多少时间,若没有时间就将控制权让出去。
Fiber 四个关键特性 => 增量渲染;暂停、中止、复用渲染任务;优先级更新;并发能力
- 帧 & JS 阻塞渲染
主流刷新频率为 60HZ,即 60 帧每秒。每帧中都会包括样式计算、布局和绘制。
Chrome has a multi-process architecture and each process is heavily multi-threaded.
主线程用于浏览器处理用户事件和页面绘制等。默认情况下,浏览器在一个线程中运行一个页面中的所有 JavaScript 脚本,以及呈现布局,回流,和垃圾回收。
Performance insights 面板开启录制后分析:Main 栏中的灰色块 Run Task 是主线程中执行的任务,绿色块 First Contentful Paint 表示首次绘制。
在首次绘制之前会有 Parse HTML 和 Evaluate Script,而后者这类阻塞 DOM Tree 生成的 Script 会延长 Parse HTML 的耗时。
随后是紫色块:输出 styleSheets 的 Recalculate Style 和用作布局的 Layout。
最后是 Details 中查看到的 Composite Layers。
具体的绘制操作会将 Composite Layers 交给合成线程 Compositor。
合成线程并不会与主线程互斥。
Script 的执行和 Paint 图层的绘制都发生在主线程 Main。
渲染被阻塞的原因是由于 JS 执时间过长,导致这一帧没有时间执行 Paint 任务。
- Fiber 执行流程
React 中可通过 this.setState、this.forceUpdate、ReactDOM.render 等 API 触发更新。更新发生时,Reconciler 会调用组件的 render 方法,将返回的 JSX 转化为虚拟 DOM;将虚拟 DOM 和上次更新时的虚拟 DOM 对比;通过对比找出本次更新中变化的虚拟 DOM;通知 Renderer 将变化的虚拟 DOM 渲染到页面上
Stack reconciler 是 v15 及更早的解决方案。Fiber 从 v16 开始成为默认的 reconciler。
React15 架构可以分为 Reconciler 协调器和 Renderer 渲染器两层。
Reconciler 协调器 => 负责找出变化的组件;
Renderer 渲染器 => 根据 Reconciler 为虚拟 DOM 打的标记,同步执行对应的 DOM 操作
在React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,递归更新时间超过了16ms,用户交互就会卡顿。
为了解决这个问题,React16将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生。
每次渲染有两个阶段:Reconciliation(协调render阶段〕和Commit(提交阶段〕
协调的阶段:可以认为是Diff阶段,这个阶段可以被终止,这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等,这此变更React称之为副作用。
提交阶段:将上一阶段计算出来的需要处理的副作用(effects)一次性执行了。这个阶段必须同步执行,不能被打断。
故障解除
- React 使用 antd-mobile 开发移动端项目时,在部分界面用嵌套路由(不同页面渲染内容 + TavBar恒固定)时出现点击无效的情况
经检测是 antd-mobile 中 TabBar 组件带有一个检测缩放满屏属性 fullScreen 的盒子将 TabBar 元素包裹。即如下代码:
<div style="position: fixed; height: 100%; width: 100%; top: 0px;">
...
</div>
这里看到一个解决方案是设置 z-index:-1
。当然在 JSX 行内元素设置务必根据驼峰写法来设置成 zIndex:-1
,但是经本人实践,次方法会将 TabBar 直接隐藏,故不可取。
实际这一处的代码只是为了演示根据点击标签而更改 TabBar 的放置位置,若不需要还是直接去除这个包裹容器即可。
- 校验报错 => Typo in static class property declaration react/no-typos
检查大小写 => 组件.propTypes = {}
不要写成 组件.PropTypes = {}
,前者的 propTypes 是 React.Component 的特殊属性。
- CSS Modules
在配置路由时,组件都被导入到项目中,那么组件的样式也就被导入到项目中了。如果组件之间样式名称相同,那么一个组件中的样式就会在另一个组件中也生效,从而造成组件之间样式相互覆盖的问题。
CSS 仅是网页样式的描述方法。Less、SASS 到 PostCSS 都是为了让 CSS 更像一门编程语言,这也导致使用者增加更多的学习成本。是否存在一种规则少,又保证某个组件的样式不会影响到其他组件的方法—— CSS Modules 通过只加入了局部作用域和模块依赖解决组件样式冲突。
React 项目在用 npx create-react-app my-app
创建后需要使用 CSS Modules 需保证项目存在 css-loader 插件。这里解释一下为什么需要 css-loader 插件。webpack 是用 JavaScript 编写,运行在 Node 环境里的打包工具,所以默认 webpack 打包的时候只会处理JS之间的依赖关系。如果在 .js
文件中导入了 css,那么就需要使用 css-loader 识别这个导入的 css 模块,通过特定的语法规则进行转换内容最后导出这个模块数组。因为是个页面无法直接识别的数组,这时就需要用到另外一个插件 style-loader 来创建一个style标签去包含处理这些样式。否则会出现报错:
index.module.css (./node_modules/css-loader/dist/cjs.js??ref--5-oneOf-5-1!./node_modules/postcss-loader/src??postcss!. ... not found babel-loader
确保上述依赖完成后即可使用 CSS Modules 。由于 React 已内置 CSS Modules ,只需把要保证独立样式的样式提出再注入保证规范名的样式文件( [name].module.css)即可,最后 .ts
文件中通过自定义对象名引入则可以拿到经 CSS Modules 演化后生成的 css 对象。
- 百度地图 BMapGL 未定义
React 项目中,使用百度地图 API 在位于 BMapGL 命名空间下的 Map 类通过 new 操作符创建地图实例时,出现了 'BMapGL' is not defined no-undef
的报错。
这里是因为 React 的生命周期中 render() 阶段负责创建虚 DOM,进行 diff 算法,更新 DOM树。而 render 及之前的阶段,并没有将组件渲染为实际的 DOM 节点,所以不能获取 window 对象。
这种情况下可以通过在组件外,进行声明拿到 window 对象下的 BMapGL (推荐),解决脚手架中全局变量访问的问题。再在 componentDidMount 生命周期中通过 new 方法获取实例。
// 方法一
const BMapGL = window.BMapGL
// 方法二
var map = new window.BMapGL.Map("container");
//创建地址解析器实例
var myGeo = new window.BMapGL.Geocoder();
...
- TypeError: Class extends value undefined is not a constructor or null
src/components/demo.js:2
1 | import React from 'react'
> 2 | export default class Demo extends React.component{ // 别写成小写
3 | showData = () => {
4 | const {inputt} = this
5 | alert(inputt.value)
- Functions are not valid as a React child. This may happen if you return a Component instead of
from render.
react-router-v6 => 标签 Route 的属性 component 替换为 element,element 的属性值要写成 JSX 组件的形式。SOF
<Route path="/movies/list" exact element={ MoviesList } />
- Type '{ ref: RefObject<ChildHandle>; }' is not assignable to type 'IntrinsicAttributes'. Property 'ref' does not exist on type 'IntrinsicAttributes'.ts(2322)
错误原因:在 JSX 中,ref
属性是一个特殊的属性,不能直接通过对象字面量传递给组件。要在 JSX 中传递 ref
属性,必须使用 React.forwardRef
函数将组件包装起来。
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
interface ChildProps {}
interface ChildHandle { focusInput: () => void; }
const ChildComponent = forwardRef<ChildHandle, ChildProps>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
// 定义子组件的方法,可以通过 ref 被父组件调用
const focusInput = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};
// 使用 useImperativeHandle 暴露给父组件的方法和属性
useImperativeHandle(ref, () => ({
focusInput
}));
return (
<input ref={inputRef} type="text" />
);
});
const ParentComponent = () => {
const childRef = useRef<ChildHandle>(null);
const handleButtonClick = () => {
if (childRef.current) {
childRef.current.focusInput();
}
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleButtonClick}>Focus Input</button>
</div>
);
};
export default ParentComponent;
结束
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!