题目 25 - 题目 32
25. 子组件是一个 Portal,发生点击事件能冒泡到父组件吗?
React 的 Portal 通过 React 的 context 和事件冒泡的机制工作。
在理解这个问题之前,首先要了解一些基本知识:
-
React Context:React 使用 context 来存储组件树的一些信息,比如事件处理程序。当组件使用 Portal 时,Portal 在 React 内部仍然保持在父组件树中,即使在 DOM 上渲染到其他地方。也就是说,Portal 的 context 依然从其父组件继承而来。
-
DOM 事件冒泡:DOM 中的事件(例如点击事件)通常会从触发事件的元素开始,然后逐步向上冒泡到父元素,直到 document 元素。在这个过程中,事件会按照 DOM 树的层级一层层地向上传递。
-
React 的事件代理:React 使用事件代理模式将所有事件都代理到顶层(document 或者 root DOM 节点)进行处理。这意味着当在子组件中触发一个事件时,无论子组件是否使用了 Portal,React 都会将事件传递到其父组件,然后逐级往上冒泡,直到到达代理事件的顶层。
在 React 中,当一个子组件使用 Portal 将其内容渲染到其他 DOM 节点时,尽管在 DOM 结构上子组件不再是父组件的直接子节点,但在 React 的组件树中,子组件仍然是父组件的子节点。这意味着 React 在监听和处理事件时,会沿着组件树的路径(而不是 DOM 树的路径)冒泡事件。因此,子组件中触发的事件仍然会冒泡到父组件。
总结:Portal 在 DOM 结构上将子组件渲染到其他位置,但在 React 的组件树中,它仍然是父组件的子组件。这使得事件可以从子组件沿着组件树冒泡到父组件。
26. React 为什么要废弃 componentWillMount、componentWillReceiveProps、componentWillUpdate 这三个生命周期钩子?它们有哪些问题呢?React 又是如何解决的呢?
我们知道 React 的更新流程分为:render 阶段和 commit 阶段。
componentWillMount、componentWillReceiveProps、componentWillUpdate 这三个生命周期钩子都是在 render 阶段执行的。
在 fiber 架构被应用之前,render 阶段是不能被打断的。当页面逐渐复杂之后,就有可能会阻塞页面的渲染,于是 React 推出了 fiber 架构。在应用 fiber 架构之后,低优先级任务的 render 阶段可以被高优先级任务打断。
而这导致的问题就是:在 render 阶段执行的生命周期函数可能被执行多次。
componentWillMount、componentWillReceiveProps、componentWillUpdate 这三个生命周期钩子,如果我们在其中执行一些具有副作用的操作,例如发送网络请求,就有可能导致一个同样的网络请求被执行多次,这显然不是我们想看到的。
而 React 又没法强迫开发者不去这样做,因为怎么样使用 React 是开发者的自由,所以 React 就新增了一个静态的生命周期 getDerivedStateFromProps,来解决这个问题。
用一个静态函数 getDerivedStateFromProps 来取代被废弃的几个生命周期函数,这样开发者就无法通过 this 获取到组件的实例,也不能发送网络请求以及调用 this.setState。它就是强制开发者在 render 之前只做无副作用的操作,间接强制我们无法进行这些不合理不规范的操作,从而避免对生命周期的滥用。
27. 为什么 react 需要 fiber 架构,而 Vue 却不需要?
以下是 React 选择 Fiber 架构,而 Vue 没有类似架构的原因:
- 异步渲染和任务优先级:React 的 Fiber 架构支持异步渲染和任务优先级,这对于复杂的用户界面和大规模应用中的性能优化非常重要。
- 更好的中断和恢复机制:Fiber 架构提供了一种更灵活的中断和恢复机制,允许 React 在渲染过程中暂停、中断,然后根据优先级恢复。
- 增量更新:Fiber 允许 React 实现增量更新,即只更新变化的部分而不必重新渲染整个组件树。
Vue 的设计上采用了不同的响应式系统和渲染机制,不需要像 React 那样进行复杂的中断和任务调度。Vue 的设计目标可能更注重简洁性和开发体验,而 React 的目标之一是提供更灵活和强大的性能优化工具。每个框架在设计上都有权衡和取舍,选择适合其目标和使用场景的策略。
28. useMemo 和 useCallback 有什么区别?
在 React 中,useMemo 和 useCallback 都是用来优化性能的钩子函数,但它们的用途和作用稍有不同。
- useMemo: useMemo 的主要作用是在组件重新渲染时,用来缓存计算结果,以避免不必要的重复计算。它接收两个参数:一个回调函数和一个依赖数组。回调函数用于进行计算,而依赖数组用于指定在数组中列出的依赖项发生变化时,才重新计算并返回新的值,否则会返回上一次缓存的值 。
const memoizedValue = useMemo(() => {
// 进行耗时的计算
return someValue
}, [dependency1, dependency2])
- useCallback: useCallback 的作用是在组件重新渲染时,返回一个记忆化的回调函数,以避免不必要的函数重新创建。它也接收两个参数:一个回调函数和一个依赖数组。当依赖项发生变化时,会返回一个新的回调函数,否则会复用之前的回调函数。
const memoizedCallback = useCallback(() => {
// 处理事件的回调函数
}, [dependency1, dependency2])
- useMemo 主要用于缓存计算结果,适用于任何需要缓存值的场景。
- useCallback 主要用于缓存回调函数,适用于需要传递给子组件的事件处理函数,以避免不必要的重新渲染。
29.如果在 useEffect 的第一个参数中 return 了一个函数,那么第二个参数分别传空数组和传依赖数组,该函数分别是在什么时候执行?
在 React 中,当 useEffect 第一个参数中返 回一个函数时,这个函数会在组件卸载时执行。当传递空数组 [] 时,useEffect 只会在组件挂载和卸载时调用一次,因此返回的函数也只会在组件卸载时执行一次。
useEffect(() => {
// 在挂载时执行
return () => {
// 在卸载时执行
}
}, [])
当传递依赖数组时,useEffect 会在组件挂载和依赖项更新时调用,因此返回的函数也会随着组件更新而执行。每次组件重新渲染时都会检查依赖项列表是否有变化,如果有变化则重新执行 useEffect,并在执行新的 useEffect 前先执行上一个 useEffect 返回的函数(如果存在)。
useEffect(() => {
// 在挂载、依赖列表变化及卸载时执行
return () => {
// 在下一次 useEffect 执行前执行
}
}, [dep1, dep2])
需要注意,这个函数的作用通常是清除 effect 留下的副作用,例如取消定时器、取消订阅等等。在函数中应该清理掉之前设置的任何 effect,在组件卸载时避免不必要的内存泄漏和资源浪费。
30.在 React 中如何避免不必要的组件渲染?
import React from 'react'
const MyComponent = () => {
return <div>Hello, world!</div>
}
const elem = <MyComponent />
console.log(React.isValidElement(elem)) // true
console.log(React.isValidElement({})) // false
31.React 中,怎么实现父组件调用子组件中的方法?
- 在子组件中,创建一个公开的方法。这可以通过在子组件类中定义一个方法或者使用 React Hooks 中的 useImperativeHandle 来实现。
如果是类组件,可以在子组件类中定义一个方法,并将其挂载到实例上。例如:
class ChildComponent extends React.Component {
childMethod() {
// 子组件中需要执行的操作
}
render() {
// 子组件的渲染逻辑
}
}
如果是函数式组件,可以使用 useImperativeHandle Hook 将指定的方法暴露给父组件。例如:
import { forwardRef, useImperativeHandle } from 'react'
function ChildComponent(props, ref) {
useImperativeHandle(ref, () => ({
childMethod() {
// 子组件中需要执行的操作
}
}))
// 子组件的渲染逻辑
}
export default forwardRef(ChildComponent)
- 在父组件中,首先引用或创建对子组件的引用。可以使用 ref 对象来保存对子组件的引用。
如果是类组件,可以使用 createRef 创建一个 ref 对象,并将其传递给子组件的 ref prop。例如:
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>
)
}
}
如果是函数式组件,可以使用 useRef 创建一个 ref 对象,并将其传递给子组件的 ref prop。例如:
function ParentComponent() {
const childRef = useRef(null)
const handleClick = () => {
// 调用子组件的方法
childRef.current.childMethod()
}
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleClick}>调用子组件方法</button>
</div>
)
}
32.taro 的实现原理是怎么样的?
-
JSX 转换:Taro 使用 Babel 插件将类似 HTML 的语法转换为 React 组件。在编译过程中,Taro 还会对 JSX 语法进行优化和压缩,以避免生成不必要的代码。
-
多端适配:Taro 通过封装原生 API 和提供不同的 Polyfill 实现多端适配。例如,在微信小程序中,Taro 封装了 wx 对象,使得可以使用类似 React Native 的组件化开发方式;在 H5 中,Taro 则提供了针对浏览器的 Polyfill。
-
跨端样式处理:Taro 通过 CSS Modules 技术和 PostCSS 插件来处理 CSS 样式。在编译过程中,Taro 会将样式文件转换为 JavaScript 对象,并按需导入到组件中。同时,Taro 提供了 @import 指令或 scss 语法等方式来支持复杂的样式表达。
-
构建系统:Taro 使用 webpack 构建工具来打包编译后的代码,并提供了一系列开箱即用的插件、规则和配置项,例如自动化导入组件、静态资源压缩、TypeScript 支持等。
-
运行时性能优化:Taro 在运行时对代码进行了一些优化,例如使用字典树实现 JSX 解析、避免使用内置事件监听器、减少对原生 API 的调用等方式来优化性能。