React使用了一种称为“事件代理”(Event Delegation)的机制来处理事件。事件代理是指将事件处理程序绑定到组件的父级元素上,然后在需要处理事件的子元素上触发事件时,事件将被委托给父级元素进行处理。
React的事件代理机制有以下几个特点:
事件委托:React将事件绑定到组件的父级元素上,而不是直接绑定到每个子元素。这样,不论子元素的数量如何,只需要在父级元素上绑定一个事件处理程序,就可以处理所有子元素的事件。
事件冒泡:当子元素上的事件触发时,事件会沿着DOM树从子元素冒泡到父级元素。React利用事件冒泡机制来实现事件代理,将事件从子元素传递到父级元素进行处理。
合成事件:React提供了合成事件(Synthetic Event)来封装底层浏览器的原生事件。合成事件是跨浏览器兼容的,并且提供了一致的接口,使开发者可以方便地处理事件。
组件实例存储:React会在组件实例上存储合成事件,以便在处理事件时能够准确地访问到组件的相关数据和方法。
通过事件代理机制,React实现了高效和灵活的事件处理方式,同时减少了内存消耗。它使得开发者能够更方便地处理事件,并且能够避免一些潜在的性能问题,特别是在处理大量子元素的情况下。
需要注意的是,React的事件代理机制并不是与原生的事件代理完全相同。React并不是通过在父级元素上使用事件委托来实现性能优化,而是使用合成事件和组件实例存储来提供一种更高效和便捷的事件处理方式。
Redux是一个用于JavaScript应用程序状态管理的开源库。在使用Redux时,有几个原则需要遵循:
单一数据源:Redux要求整个应用的状态(state)被存储在一个单一的存储库(store)中。这意味着应用的所有状态都被存储在一个JavaScript对象中。
状态是只读的:Redux中的状态是只读的,即不能直接修改状态。要修改状态,需要通过派发一个动作(action)来触发状态的变化。
使用纯函数进行状态更新:Redux中的状态更新由纯函数(reducer)处理。纯函数接收先前的状态和动作,返回一个新的状态对象。纯函数应该是没有副作用的,即相同的输入永远会得到相同的输出。
使用纯函数处理副作用:对于具有副作用(如异步请求、日志记录等)的操作,应使用Redux中间件来处理。通过使用中间件,可以确保状态更新仍然是纯函数,并且副作用的操作被封装在中间件中。
使用动作类型常量:为了保持一致性和避免拼写错误,建议使用动作类型常量来定义动作的类型。这样可以在应用的不同部分共享常量,减少错误和调试的难度。
使用选择器(Selector)进行状态访问:为了避免在组件中直接访问状态,Redux推荐使用选择器来获取所需的状态。选择器是纯函数,接收状态作为参数,并返回派生的状态值。
这些原则帮助开发者理解和遵循Redux的设计思想,使得Redux的状态管理更加一致、可预测和可维护。
在React中,可以使用错误边界(Error Boundaries)来捕获和处理组件中的异常。错误边界是一种React组件,它可以捕获其子组件中的错误,并进行处理,以避免整个应用程序崩溃。
官网关于错误边界的描述:https://legacy.reactjs.org/docs/error-boundaries.html#introducing-error-boundaries
以下是使用错误边界来捕获异常的示例:
import React, { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
componentDidCatch(error, errorInfo) {
this.setState({
hasError: true,
error: error,
errorInfo: errorInfo
});
// 这里可以进行异常处理,例如发送错误报告到服务器
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>出现了一个错误</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
class MyComponent extends Component {
render() {
// 抛出一个异常
throw new Error('发生了一个错误');
return <div>My Component</div>;
}
}
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}
在上述示例中,我们创建了一个名为ErrorBoundary的错误边界组件。它通过定义componentDidCatch生命周期方法来捕获子组件中的异常。当子组件抛出异常时,componentDidCatch方法会被调用,并将异常信息存储在state中。
在ErrorBoundary组件的render方法中,我们根据hasError的状态来决定渲染什么内容。如果hasError为true,则渲染错误信息,否则渲染子组件。
在MyComponent组件中,我们使用throw new Error()语句抛出一个异常。这个异常会被ErrorBoundary组件捕获,并进行处理。
最后,在App组件中,我们将MyComponent组件包裹在ErrorBoundary组件中,这样ErrorBoundary就可以捕获MyComponent组件的异常。
需要注意的是,错误边界只能捕获其子组件中的异常,无法捕获事件处理器、异步代码、服务端渲染等其他情况中的异常。
通过使用错误边界,我们可以优雅地处理组件中的异常,并提供友好的错误信息给用户,同时避免整个应用程序的崩溃。
import {ErrorBoundary} from 'react-error-boundary'
function ErrorFallback({error, resetErrorBoundary}) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
const ui = (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// reset the state of your app so the error doesn't happen again
}}
>
<ComponentThatMayError />
</ErrorBoundary>
)
遗憾的是,error boundaries 并不会捕捉这些错误:
handleClick() {
try {
// Do something that could throw
} catch (error) {
this.setState({ error });
}
}
题目:useTimeout 是可以在函数式组件中,处理 setTimeout 计时器函数
解决了什么问题?
如果直接在函数式组件中使用 setTimeout ,会遇到以下问题:
function App() {
const [state, setState] = useState(1);
setTimeout(() => {
setState(state + 1);
}, 3000);
return (
// 我们原本的目的是在页面渲染完3s后修改一下state,但是你会发现当state+1后,触发了页面的重新渲染,就会重新有一个3s的定时器出现来给state+1,既而变成了每3秒+1。
<div> {state} </div>
);
};
function App() {
const [count, setCount] = useState(0)
const [countTimeout, setCountTimeout] = useState(0)
useEffect(() => {
setTimeout(() => {
setCountTimeout(count)
}, 3000)
setCount(5)
}, [])
return (
//count发生了变化,但是3s后setTimout的count却还是0
<div>
Count: {count}
<br />
setTimeout Count: {countTimeout}
</div>
)
}
useTimeout 实现
function useTimeout(callback, delay) {
const memorizeCallback = useRef();
useEffect(() => {
memorizeCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const timer = setTimeout(() => {
memorizeCallback.current();
}, delay);
return () => {
clearTimeout(timer);
};
}
}, [delay]);
};
如何使用
// callback 回调函数, delay 延迟时间
useTimeout(callback, delay);
React 18是即将发布的React的新版本,它带来了一些令人期待的新特性,旨在改善开发者体验和性能。以下是React 18的一些主要新特性:
Concurrent Mode(并发模式):Concurrent Mode是React 18中最重要的新特性之一。它引入了一种新的渲染方式,使得React应用能够更高效地处理大型组件树,提高应用的性能和响应能力。
新的渲染器(新的渲染引擎):React 18引入了新的渲染器,名为React Server Components,用于在服务器上渲染React组件。它可以加速服务器渲染,并提供更好的开发体验。
自动批处理(Automatic Batching):React 18引入了自动批处理机制,它可以自动将多个状态更新批处理为一次,减少不必要的重渲染,提高性能。
事件处理改进:React 18改进了事件处理机制,引入了新的API(如useEvent和useEffect),使得事件处理更加灵活和易用。
生命周期改进:React 18引入了新的生命周期方法,如useEffect的onRender回调,用于更精确地控制组件的渲染和副作用。
组件级别的错误边界(Component-level Error Boundaries):React 18允许开发者在组件级别使用错误边界,使得捕获和处理组件中的错误更加灵活和精确。
这些是React 18的一些主要新特性,希望它们能够提供更好的开发体验和性能优化。需要注意的是,以上列出的特性可能会有所变化,具体的特性和用法应以React 18正式发布时的官方文档和更新日志为准。
补充:https://fe.ecool.fun/topic/6f40b143-3941-44c6-ac90-9bf87795ee2c?orderBy=updateTime&order=asc&tagId=13
由于useState是基于浅比较的,所以在连续调用setTest时,直接使用{…test, newValue}的方式更新状态可能会导致值的丢失。这是因为{…test, newValue}仅仅是对原始状态对象的浅拷贝,新的状态对象与旧的状态对象共享相同的引用,所以React无法正确地检测到状态的改变。并且useState是异步执行的,也就是执行 setTest 后,不会立即更新 test 的结果,多次调用时,可能出现了值覆盖的情况。
为了解决这个问题,可以使用函数式的方式更新状态。这样可以确保使用最新的状态值进行更新,而不是依赖于旧的状态对象。可以通过在setTest中传入一个回调函数来实现:
setTest(prevState => ({...prevState, newValue}));
使用这种方式,prevState参数是之前的状态的副本,直接更新这个副本而不是依赖于旧的状态对象。这样可以确保状态的更新是基于最新的状态值进行的,避免了值的丢失。
总结来说,连续调用setTest({…test, newValue})可能会导致值的丢失,是因为这种方式只是浅拷贝了旧的状态对象,新的状态对象与旧的状态对象共享相同的引用。为了避免这个问题,可以使用函数式的方式更新状态,通过回调函数确保基于最新的状态值进行更新。
setState大部分情况下是异步的。在React中,为了提高性能和优化渲染过程,React会对setState进行批处理以减少不必要的渲染。
当你调用setState时,React会将更新添加到一个队列中,并在合适的时机批量处理这些更新。这意味着在调用setState之后,不会立即更新状态和重新渲染组件,而是会等待合适的时机进行批处理。
React使用一种称为"合成事件"的机制来处理用户交互,例如点击按钮。在合成事件中,React会对多个状态更新进行合并,并一次性更新组件的状态。
这种异步的批处理机制有助于提高性能,避免不必要的渲染,并在一次性更新时减少重复渲染的次数。
但是需要注意的是,setState也有一些特殊情况下的同步行为。例如,在React的生命周期方法(如componentDidUpdate)中调用setState时,更新是同步的,不会进行异步批处理。
如果你需要在setState更新之后立即获取更新后的状态,可以使用setState的回调函数来实现:
setState(newState, () => {
// 在这里可以获取更新后的状态
console.log(this.state);
});
使用回调函数可以确保在状态更新完成后执行相应的操作。
需要注意的是,React 18中的Concurrent Mode引入了新的渲染方式,可能会改变setState的异步行为。具体的变化和细节可以参考React 18的官方文档和更新日志。
React Router有两种主要的路由模式:HashRouter和BrowserRouter。
HashRouter:在HashRouter模式下,URL中的路由信息会以哈希值的形式出现,如http://example.com/#/home。这种模式的实现原理是使用浏览器的URL的哈希部分(即#后面的部分)来管理路由。当URL的哈希部分发生变化时,React Router会根据新的哈希值匹配对应的路由,并更新相应的组件。
BrowserRouter:在BrowserRouter模式下,URL中的路由信息会以常规的路径形式出现,如http://example.com/home。这种模式的实现原理是使用HTML5的History API来管理路由。它通过修改浏览器历史记录中的URL来实现路由的切换和更新。
在BrowserRouter模式下,需要服务器的支持,以确保在刷新或直接访问具体URL时,能够正确地渲染对应的组件。
React Router的实现原理是通过使用React的Context功能和React组件生命周期来管理路由。它提供了一组高阶组件(如Router、Route、Switch等),用于定义和匹配路由,并在URL变化时进行相应的渲染和组件更新。
React Router的核心机制是监听URL的变化并更新对应的组件。它提供了一种将URL路径与组件关联起来的方式,使得根据URL来切换和渲染不同组件成为可能。
需要注意的是,React Router还提供了其他功能,如嵌套路由、重定向、路由守卫等,以满足更复杂的路由需求。
React Router是一个用于构建单页应用中路由功能的库。它基于React构建,提供了一组组件和API,用于处理应用程序中的路由和导航。
React Router能够将URL路径与组件进行映射,使得在应用程序中切换和渲染不同的组件成为可能。通过React Router,开发者可以构建具有多个页面和路由功能的单页应用,实现无刷新的页面切换和导航。
常用的React Router组件包括:
BrowserRouter:用于在浏览器环境下使用HTML5的History API来管理路由。
HashRouter:用于在浏览器环境下使用URL的哈希部分(#后面的部分)来管理路由。
Route:用于定义路由规则和匹配URL。通过设置path属性指定URL路径,设置component属性指定对应的组件。
Switch:用于在多个路由规则中选择匹配的第一个。一般用于避免多个路由规则同时匹配的情况。
Link:用于创建导航链接,生成可点击的链接到指定的URL路径。
NavLink:类似于Link组件,但在匹配当前URL时可以添加特定的样式或类名。
Redirect:用于重定向到指定的URL路径。
withRouter:一个高阶组件,用于将路由的相关信息(如history、location、match)注入到组件的props中。
这些组件是React Router中常用的组件,用于构建和管理应用程序的路由。通过组合和配置这些组件,可以实现灵活和高效的路由功能。
在React中,JSX是一种类似HTML的语法扩展,用于描述UI的结构和组件的渲染。当使用JSX编写组件时,React会将JSX代码转换为真实的DOM元素。
下面是React将JSX转换为真实DOM的过程:
解析JSX代码:React使用Babel等工具将JSX代码转换为JavaScript代码。这个过程将JSX中的标签、属性和内容转换为等效的JavaScript语法。
创建虚拟DOM:在运行时,React会使用转换后的JavaScript代码来创建虚拟DOM(Virtual DOM)。虚拟DOM是一个JavaScript对象树,它和真实DOM具有相似的结构。
Diff算法比较变化:当组件的状态或属性发生变化时,React会使用Diff算法比较新的虚拟DOM和旧的虚拟DOM,找出差异(即需要更新的部分)。
更新真实DOM:根据Diff算法的结果,React会生成一系列DOM操作指令,然后将这些指令应用到真实的DOM上,以更新页面的内容。
通过这个过程,React能够高效地更新页面,只更新发生变化的部分,而不是重新渲染整个页面。
需要注意的是,React的虚拟DOM提供了一种高效的方式来描述和操作真实DOM,但它并不是一种直接替代真实DOM的解决方案。虚拟DOM只是React内部的一种数据结构,用于提高渲染性能和优化更新过程。
在前面文章了解中,JSX通过babel最终转化成React.createElement这种形式
例如:
<div>
<img src="avatar.png" className="profile" />
<Hello />
div>
会被babel转化成如下:
React.createElement(
"div",
null,
React.createElement("img", {
src: "avatar.png",
className: "profile"
}),
React.createElement(Hello, null)
);
在转化过程中,babel在编译时会判断 JSX 中组件的首字母:
当首字母为小写时,其被认定为原生 DOM 标签,createElement 的第一个变量被编译为字符串
当首字母为大写时,其被认定为自定义组件,createElement 的第一个变量被编译为对象
最终都会通过RenderDOM.render(…)方法进行挂载,如下:
ReactDOM.render(<App />, document.getElementById("root"));
在react中,节点大致可以分成四个类别:
如下所示:
class ClassComponent extends Component {
static defaultProps = {
color: "pink"
};
render() {
return (
<div className="border">
<h3>ClassComponent</h3>
<p className={this.props.color}>{this.props.name}</p >
</div>
);
}
}
function FunctionComponent(props) {
return (
<div className="border">
FunctionComponent
<p>{props.name}</p >
</div>
);
}
const jsx = (
<div className="border">
<p>xx</p >
< a href=" ">xxx</ a>
<FunctionComponent name="函数组件" />
<ClassComponent name="类组件" color="red" />
</div>
);
这些类别最终都会被转化成React.createElement这种形式
React.createElement其被调用时会传⼊标签类型type,标签属性props及若干子元素children,作用是生成一个虚拟Dom对象,如下所示:
function createElement(type, config, ...children) {
if (config) {
delete config.__self;
delete config.__source;
}
// ! 源码中做了详细处理,⽐如过滤掉key、ref等
const props = {
...config,
children: children.map(child =>
typeof child === "object" ? child : createTextNode(child)
)
};
return {
type,
props
};
}
function createTextNode(text) {
return {
type: TEXT,
props: {
children: [],
nodeValue: text
}
};
}
export default {
createElement
};
createElement会根据传入的节点信息进行一个判断:
ReactDOM.render(element, container[, callback])
当首次调用时,容器节点里的所有 DOM 元素都会被替换,后续的调用则会使用 React 的 diff算法进行高效的更新
如果提供了可选的回调函数callback,该回调将在组件被渲染或更新之后被执行
render大致实现方法如下:
function render(vnode, container) {
console.log("vnode", vnode); // 虚拟DOM对象
// vnode _> node
const node = createNode(vnode, container);
container.appendChild(node);
}
// 创建真实DOM节点
function createNode(vnode, parentNode) {
let node = null;
const {type, props} = vnode;
if (type === TEXT) {
node = document.createTextNode("");
} else if (typeof type === "string") {
node = document.createElement(type);
} else if (typeof type === "function") {
node = type.isReactComponent
? updateClassComponent(vnode, parentNode)
: updateFunctionComponent(vnode, parentNode);
} else {
node = document.createDocumentFragment();
}
reconcileChildren(props.children, node);
updateNode(node, props);
return node;
}
// 遍历下子vnode,然后把子vnode->真实DOM节点,再插入父node中
function reconcileChildren(children, node) {
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (Array.isArray(child)) {
for (let j = 0; j < child.length; j++) {
render(child[j], node);
}
} else {
render(child, node);
}
}
}
function updateNode(node, nextVal) {
Object.keys(nextVal)
.filter(k => k !== "children")
.forEach(k => {
if (k.slice(0, 2) === "on") {
let eventName = k.slice(2).toLocaleLowerCase();
node.addEventListener(eventName, nextVal[k]);
} else {
node[k] = nextVal[k];
}
});
}
// 返回真实dom节点
// 执行函数
function updateFunctionComponent(vnode, parentNode) {
const {type, props} = vnode;
let vvnode = type(props);
const node = createNode(vvnode, parentNode);
return node;
}
// 返回真实dom节点
// 先实例化,再执行render函数
function updateClassComponent(vnode, parentNode) {
const {type, props} = vnode;
let cmp = new type(props);
const vvnode = cmp.render();
const node = createNode(vvnode, parentNode);
return node;
}
export default {
render
};
在react源码中,虚拟Dom转化成真实Dom整体流程如下图所示:
其渲染流程如下所示:
React Router中的组件和HTML中的标签在导航功能上有一些区别。
SPA导航:组件用于在单页应用中实现导航。它通过React Router管理路由,并使用JavaScript来更新应用程序的状态和渲染。相反,标签通常是用于传统多页应用中的导航,会导致整个页面的刷新。
阻止浏览器默认行为:使用组件时,React Router会拦截点击事件,阻止浏览器默认的页面刷新行为,以避免整个页面的重新加载。这样可以实现无刷新的页面切换。而标签会触发浏览器默认行为,导致页面的重新加载。
路由匹配:组件会自动处理路由的匹配和URL的生成。它会根据设置的to属性生成对应的URL,并与当前路由进行匹配以添加活动状态的类名。相反,标签只是一个普通的HTML标签,不会自动处理路由匹配。
动态路由参数:组件可以方便地处理动态路由参数。通过在to属性中传递参数,可以动态生成对应的URL。而标签需要手动拼接URL参数。
总之,组件是React Router提供的导航组件,可以与React Router一起使用来实现无刷新的页面切换和路由导航。而标签是传统HTML中的导航标签,会触发页面的重新加载。
在使用React Router时,建议使用组件来处理导航和路由链接,以便获得更好的性能和用户体验。
在React中,ref是一个特殊的属性,用于获取组件或DOM元素的引用。通过ref,你可以在组件中访问和操作对应的实例或DOM元素。
ref的主要用途有以下几个:
访问组件实例:使用ref可以获取到组件的实例,从而可以直接调用组件的方法、访问组件的属性,或者获取组件的状态。这在某些情况下非常有用,比如需要手动触发组件的方法或访问组件中的数据。
操作DOM元素:使用ref可以获取到渲染后的DOM元素,并对DOM进行操作。你可以使用ref来获取表单元素的值、改变DOM的样式、添加或移除DOM节点等。
需要注意的是,通过ref获取到的DOM元素是真实的DOM,而不是虚拟DOM。因此,应该避免直接操作DOM,而是使用React的状态和属性来管理和更新UI。
除了获取组件实例和DOM元素之外,ref还可以用于存储和访问任意的变量。这种情况下,你可以将ref视为一个普通的变量容器,用于在组件中存储信息。
使用ref的方式有两种:
字符串引用(不推荐):在React早期版本中,可以使用字符串来定义ref,但这种方式已经被废弃,不推荐使用。
回调函数引用:使用函数来定义ref,在组件渲染完成后会调用该函数,并将对应的组件实例或DOM元素传递给该函数。可以通过将ref赋值给一个变量,然后通过该变量来访问和操作对应的实例或DOM元素。
以下是一个使用ref的示例:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
console.log(this.myRef.current); // 访问组件实例或DOM元素
}
render() {
return <div ref={this.myRef}>Hello, World!</div>;
}
}
在上面的例子中,通过React.createRef()创建了一个ref,并将其赋值给myRef属性。在组件的render方法中,将该ref绑定到div元素上。在组件的componentDidMount生命周期方法中,可以通过this.myRef.current来访问和操作对应的DOM元素。
除了获取组件实例和DOM元素之外,ref还可以用于存储和访问任意的变量。这种情况下,你可以将ref视为一个普通的变量容器,用于在组件中存储信息。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
this.myVariable = 'Hello, World!';
}
componentDidMount() {
console.log(this.myVariable); // 访问存储的变量
}
render() {
return <div ref={this.myRef}>Hello, World!</div>;
}
}
在上面的例子中,除了创建一个ref,还创建了一个myVariable变量,用于存储字符串’Hello, World!'。在组件的componentDidMount生命周期方法中,可以通过this.myVariable来访问和使用存储的变量。
需要注意的是,使用ref存储变量在React中并不是最佳实践,因为组件状态(state)和属性(props)通常更适合用于存储和管理组件的数据。ref的主要目的是用于访问组件实例和DOM元素,而不是用于存储组件数据。
在React中,实现父组件调用子组件中方法的方式在函数组件和类组件之间有一些区别。
import React, { useRef } from 'react';
function ParentComponent() {
const childRef = useRef();
const handleClick = () => {
childRef.current.childMethod(); // 调用子组件中的方法
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleClick}>调用子组件方法</button>
</div>
);
}
function ChildComponent() {
const childMethod = () => {
console.log('子组件方法被调用');
};
return <div>子组件</div>;
}
在上述代码中,使用useRef创建了childRef引用,我们将其传递给子组件ChildComponent的ref属性。然后在父组件中的handleClick方法中,通过childRef.current.childMethod()来调用子组件中的方法。
import React from 'react';
class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.childRef = React.createRef();
}
handleClick() {
this.childRef.current.childMethod(); // 调用子组件中的方法
}
render() {
return (
<div>
<ChildComponent ref={this.childRef} />
<button onClick={() => this.handleClick()}>调用子组件方法</button>
</div>
);
}
}
class ChildComponent extends React.Component {
childMethod() {
console.log('子组件方法被调用');
}
render() {
return <div>子组件</div>;
}
}