React
16.8为什么要升级到18
- 并发渲染: React 18 引入并发模式,可暂停、中断和重启渲染任务。在复杂应用中,当用户与界面交互,高优先级任务(如处理点击事件)可打断低优先级渲染任务,保证界面响应性。例如,在渲染长列表时用户点击按钮,并发渲染能立即处理点击,避免卡顿。
- 更广泛的自动批处理
在react17中,只有react事件会进行批处理,原生js事件、promise,setTimeout、setInterval不会。react18,将所有事件都进行批处理,即多次setState会被合并为1次执行,提高了性能,在数据层,将多个状态更新合并成一次处理(在视图层,将多次渲染合并成一次渲染)handleClick方法中两次调用了setState,但 React 会自动将这两个更新合并成一次。在异步函数中的处理:在异步函数如setTimeout、Promise等中,setState不会自动批处理,需要手动调用ReactDOM.flushSync等方法来强制进行同步更新。 对频繁进行状态更新的场景(如表单验证、多状态联动更新)尤为有益。
** 使用 unstable_batchedUpdates批处理:**
不需要手动调用 unstable_batchedUpdates,React 18 自动批处理状态更新。unstable_batchedUpdates 包裹多个状态更新,确保它们在一次渲染中完成,避免多次不必要的渲染。但要注意,unstable_batchedUpdates 是不稳定 API,未来可能有变化。
React 从 16.8 升级到 18 前后是怎么渲染的?
- React 16.8 渲染机制:React 使用的是 同步渲染(Synchronous Rendering) 模式,所有更新任务都会立即执行并完成,直到整个组件树被更新为止。React 16 开始引入 Fiber 架构,但在 16.8 中,渲染任务仍是同步的。如果组件渲染需要大量时间,UI 会“卡顿”,因为 React 在更新完成之前不会释放主线程。
- React 18 渲染机制:React 不再是一次性完成所有渲染任务,而是可以将任务拆分,并在后台渐进式地完成更新。React 会根据任务的优先级动态调度渲染,重要任务优先,次要任务可以延后。主线程会在需要时释放给浏览器,保证用户交互的流畅性。引入新的调度机制(Scheduler),任务分为高优先级和低优先级,用户交互任务(如点击事件)优先处理,后台任务延后处理。
React 18更新
新的 API 和 Hooks
startTransition :标记非紧急更新,让 React 在后台更新,不阻塞用户交互。如搜索框输入时,用startTransition更新搜索结果,输入过程中界面仍流畅。16.8 无此功能,更新可能影响交互。
工作原理:
高优先级:用户交互(输入、点击等)。
低优先级:复杂的计算、后台任务(如搜索结果、筛选)。
React 16.8 的局限性:主线程阻塞: 搜索框输入时,实时更新搜索结果会触发频繁的重渲染,可能导致输入卡顿。
useDeferredValue 可以将一个值标记为延迟处理的值,React 会在其他更紧急的更新处理完毕后再处理该值的更新。优化搜索结果实时显示: const deferredValue = useDeferredValue(value);如果搜索结果的计算比较复杂或者数据量较大,可能会导致输入卡顿。这时可以使用useDeferredValue来延迟处理搜索结果的显示,让用户能够流畅地输入,而搜索结果会在输入完成后或其他更紧急的任务处理完毕后再显示。
fiber架构
自己理解:可中断、链表、调度优先级。
- 优先级调度:React Fiber 引入了优先级的概念,会根据更新的来源和重要性等因素为不同的更新任务分配不同的优先级。
- 基于链表的数据结构:Fiber 节点通过链表的形式构建起整个组件树的结构。相比于之前基于树结构的递归遍历方式,链表结构方便在遍历过程中随时进行节点的插入、删除以及暂停、恢复操作,为实现可中断的渲染更新流程提供了基础。
- React Fiber 的可中断是指在组件树的渲染和更新过程中,能够暂停当前正在进行的工作,去处理其他更紧急的任务,然后再回来继续之前的工作,而不是像传统的渲染方式那样必须一次性完成整个渲染更新流程。
1.为什么会出现 React fiber 架构?
React 15 Stack Reconciler 是通过递归更新子组件 。由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了 16ms,用户交互就会卡顿。对于特别庞大的DOM树来说,reconciliation过程会很长(x00ms),在这期间,主线程是被 js 占用的,因此任何交互、布局、渲染都会停止,给用户的感觉就是页面被卡住了。网友测试使用React V15,当DOM节点数量达到100000时, 加载页面时间竟然要 7 秒。
总结原因:
(1) 长时间阻塞主线程
(2) 无法分配优先级 高优先级任务(如用户输入)无法抢占低优先级任务(如后台数据更新)。
(3) 缺乏灵活的错误恢复
同步渲染中,如果某一部分出错,整个渲染过程都会中断,无法优雅地处理错误。
2.Fiber是什么?
官方的一句话解释是“React Fiber是对核心算法的一次重新实现” 。
Fiber架构 = Fiber节点 + Fiber调度算法
1.把一个耗时长的任务分解为一个个的工作单元。 在执行工作单元之前,由浏览器判断是否有空余时间执行,有时间就执行工作单元,执行完成后,继续判断是否还有空闲时间。没有时间就终止执行让浏览器执行其他任务(如 GUI 线程等)。等到下一帧执行时判断是否有空余时间,有时间就从终止的地方继续执行工作单元,一直重复到任务结束。
2.要让终止的任务恢复执行,就必须知道下一工作单元对应那一个。所以要实现工作单元的连接,就要使用链表,在每个工作单元中保存下一个工作单元的指针,就能恢复任务的执行。
3.要知道每一帧的空闲时间,就需要使用 requestIdleCallback Api。传入回调函数,回调函数接收一个参数(剩余时间),如果有剩余时间,那么就执行工作单元,如果时间不足了,则继续requestIdleCallback,等到下一帧继续判断。
总结核心概念:
(1)Fiber节点 :Fiber 是 React 中用来表示虚拟 DOM 的一种数据结构。每个 Fiber 节点对应一个 React 元素或组件,Fiber 节点形成一棵链表树,通过 child、sibling 和 return 指针连接起来。
(2)时间切片 :Fiber 允许将渲染任务分成多个小的单元,每次处理一部分任务。如果有更高优先级的任务(如用户输入事件)出现,可以暂停当前任务,先处理高优任务。
(3)** 双缓冲机制** : Current Fiber Tree:表示当前屏幕上渲染的内容。Work-in-progress Fiber Tree:表示正在构建的新 Fiber 树。
(4)优先级调度 :引入了调度器(Scheduler),根据任务的类型和优先级分配资源。高优先级:用户输入(如点击、键盘事件)。中优先级:动画、过渡效果。低优先级:后台数据加载或非关键渲染。
Fiber优点:
- 可中断更新:长任务被分解成小任务,减少主线程阻塞,提升用户体验。
- **优先级调度:**任务可以根据优先级被调度和重新安排。
- **增量渲染:**允许逐步构建UI,避免界面冻结。
- **增强错误边界:**错误可以被捕获,避免整棵树崩溃。
Fiber工作流程?
调度阶段(Scheduling):
当组件的状态或属性发生变化时,React 会通过setState等方式触发更新。把待更新的任务会先放入队列中, 然后通过 requestIdleCallback 请求浏览器调度。
协调阶段(Reconciliation):
现在浏览器有空闲或者超时了就会构建 Fiber 树,workInProgress Fiber树,对比 Fiber 树,通过一种称为 “双缓存” 的技术,将新的workInProgress Fiber树与旧的 Fiber 树进行对比,找出需要更新的节点,这个过程称为 “Diffing”。
渲染阶段(Render):
根据协调阶段对比的结果,生成 DOM 更新操作,移动,更新,删除,这些操作会被记录在一个称为effect list的链表中,批量提交到浏览器的 DOM 中。
提交阶段(Commit):
React 会遍历effect list,按照顺序执行其中的 DOM 更新操作,React 会根据操作的类型调用相应的 DOM API,如createElement、setAttribute、appendChild等。
渲染完成后:React 会根据组件的类型和状态,调用相应的生命周期函数或回调函数。
总的来说:React Fiber 的引入,让 React 从一个“同步渲染引擎”转变为一个“异步可中断渲染引擎”。 增量化、可中断、可恢复
React Fiber 将渲染过程分为两个主要阶段,**即 “render 阶段” 和 “commit 阶段”。**在 render 阶段,主要进行组件的渲染逻辑计算,比如根据新的状态生成虚拟 DOM 树,这个过程是可以被中断的 ,会按照优先级和时间切片等机制逐步处理各个 Fiber 节点的渲染工作;而在 commit 阶段,则是将已经完成计算的更新真正应用到 DOM 上,进行实际的 DOM 操作,这个阶段通常是同步且不可中断的,以保证 DOM 更新的完整性和一致性。
React渲染流程
jsx ---- render function ---- vdom ----- Fiber ------ DOM
在编译阶段,Babel 等工具会将 JSX 代码转换为 React.createElement 函数调用的形式。React.createElement 返回的 React 元素是一个普通的 JavaScript 对象,这些对象构成了虚拟 DOM 树。每个虚拟 DOM 节点遍历创建对应的 Fiber 树。Fiber 树是一种链表结构,每个 Fiber 节点都有指向父、子节点和兄弟节点的指针,方便进行遍历和更新操作。在构建完 Fiber 树后,React 会根据 Fiber 树中的信息,创建实际的 DOM 节点。
对React的理解?
react是一个用于构建用户界面的JS库。 怎么构建界面的呢?UI = fn(data)
- 官网强调 “组件是 React 的基石”。通过组件化,将用户界面拆分成一个个独立的、可复用的组件。一个按钮、一个表单、一个导航栏等都可以是一个组件,每个组件都有自己的状态和生命周期,通过组合不同的组件来构建复杂的应用界面,很好维护。
- 其次在构建界面的时候,在代码的写法采用声明式编程 风格,只需要描述界面应该呈现的样子,而不需要直接操作 DOM 来进行界面更新。当数据发生变化时,React 会自动根据新的数据重新渲染组件。
- 那在这个组件渲染状态变化的过程,React 引入了虚拟 DOM的概念, React 会先在虚拟 DOM 上进行更新操作,然后通过对比新旧虚拟 DOM 的差异,只将实际需要更新的部分高效地应用到真实 DOM 上,从而提高渲染性能。
不建议在if语句内使用 Hook ?
在 React 内部,Hook 是通过链表的数据结构来存储和管理的。当组件首次挂载时,React 会按照 Hook 的调用顺序依次将它们添加到链表中,并为每个 Hook 分配一个唯一的索引。在if语句内使用 Hook,可能会破坏 Hook 的调用顺序, Hook 链表的结构和索引就会与首次渲染时不同。
循环语句 || 函数嵌套 || 异步函数 || 条件渲染组件
声明式编程和命令式编程
声明式编程:更注重描述要做什么,而不是具体怎么做。
命令式编程:则侧重于描述具体的操作步骤和执行顺序,即告诉计算机如何一步一步地完成任务。
// 声明式写法
const DeclarationExample = () => {
return (
<div>
<h1>Hello, World!</h1>
<p>This is a declarative example in React.</p>
</div>
);
};
在上面的例子中,用 JSX 描述了一个简单的 React 组件,它声明了 UI 的结构和内容。React 负责将这个声明传递给虚拟 DOM 并进行渲染,我们不需要直接操作底层的 DOM。
// 命令式写法
const ImperativeExample = () => {
const container = document.createElement('div');
const heading = document.createElement('h1');
heading.textContent = 'Hello, World!';
const paragraph = document.createElement('p');
paragraph.textContent = 'This is an imperative example in plain JavaScript.';
container.appendChild(heading);
container.appendChild(paragraph);
// 将容器添加到页面中
document.body.appendChild(container);
};
使用原生 JavaScript 创建了 DOM 元素,并通过命令式的方式逐步操作这些元素的创建和组装过程。这样的写法更注重详细的步骤和控制。
总体而言,声明式的代码更直观和简洁,而命令式的代码更灵活,可以更精细地控制每个步骤。
react中state和constructor的区别
state: 在组件内部存储和管理数据,专门用于存储组件的可变数据,这些数据通常与组件的用户交互、业务逻辑等相关。例如,一个输入框组件的输入值、一个列表组件的列表数据等都可以存储在state中。可以根据需要多次调用。
**constructor:**本身并不直接存储数据,而是用于初始化组件所需的数据和操作。如绑定this指针等,但它不会直接存储组件的业务数据。在组件实例化时只被调用一次,用于进行组件的初始化操作。之后,无论组件的状态如何变化,constructor都不会再次被调用,除非重新创建组件实例。
react类的生命周期
React 生命周期是指组件从创建到销毁的整个过程中所经历的一系列阶段,每个阶段都有其特定的方法和用途,以下是对 React 生命周期的理解:
创建阶段
当组件实例被创建时,首先会调用constructor构造函数,用于初始化组件的状态this.state和绑定事件处理函数等。
接着render方法会被调用,它是 React 组件中最核心的方法,用于根据组件的状态和属性渲染出对应的虚拟 DOM 结构。
最后componentDidMount方法会在组件挂载到真实 DOM 后立即调用,通常在这里进行一些初始化操作,如发起网络请求获取数据、订阅事件等。
更新阶段
当组件的状态或属性发生变化时,会触发更新过程。首先会调用shouldComponentUpdate方法,它可以根据组件的当前状态和即将更新的状态来决定组件是否需要重新渲染,通过返回true或false来控制渲染流程,以优化性能。
如果shouldComponentUpdate返回true,则会继续执行render方法重新渲染虚拟 DOM,然后调用componentDidUpdate方法,在这里可以对更新后的 DOM 进行操作或进行一些后续处理。
卸载阶段
当组件从 DOM 中移除时,会调用componentWillUnmount方法,通常在这里进行一些清理操作,如取消网络请求、取消事件订阅等,以防止内存泄漏。
函数组件和类组件的区别
- 定义方式: 代表两种不同设计思想和心智模式;
类组件: 使用 ES6 的class定义,需要继承React.Component或React.PureComponent,并且必须用render方法来返回组件的 UI 结构。
函数组件: 是一个Js函数,接收props作为参数,返回一个 React 元素来描述组件的 UI 结构。 - 状态管理:
类组件: 通过this.state 来存储和管理组件的内部状态。具有丰富的生命周期方法。
函数组件: 在hooks出来之前函数组件是无状态的,被称为 “纯函数组件”。在没有 Hooks 之前,函数组件不存在生命周期的概念。引入 Hooks 后,可以使用useEffect等钩子函数来模拟类组件的部分生命周期行为。 - 使用场景
类组件: 在处理复杂表单时,类组件的状态管理和生命周期方法能更好地处理input表单数据的收集、验证和提交。 包括我的组件库搭建时候弹窗组件,动画与过渡效果:需要在组件挂载、更新或卸载时添加动画或过渡效果,类组件的生命周期方法提供了更精细的控制。
函数组件: 实时数据展示与交互:如实时聊天应用中的消息列表组件,使用函数组件结合useEffect和useState可以方便地监听消息数据的变化并实时更新界面。 - 类组件是oop面向对象基于类的编程方式,函数组件是基于函数声明式的编程。
UI 组件库使用类组件的原因:
大多数 UI 组件库是在 React 16.8 之前构建的,采用了类组件。UI 组件库更关注对开发者的封装和抽象,类组件提供了清晰的生命周期管理。
业务仓库组件使用函数组件的原因:
业务逻辑更复杂,函数组件通过 Hooks 更易于管理状态和副作用,减少了维护成本。函数组件的性能优化更加方便,能减少不必要的重渲染。
简洁的语法:相比类组件需要大量的 constructor、render 方法、this 绑定等,函数组件只需要简单的函数形式,避免了冗余的代码。
灵活、可组合: 没有类组件的 this 限制:函数组件不受 this 的限制,状态和行为可以通过 Hooks 和函数参数轻松传递和共享。逻辑复用变得更为简单。
类组件的生命周期 哪些是函数组件实现不了的?
但 class 组件的 componentWillReceiveProps 在函数式组件中没有直接对等的实现。
componentWillReceiveProps在初始化render的时候不会执行,它会在Component接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染。
useeffect可以模拟哪些类的生命周期?
- 模拟 componentDidMount(组件挂载):对应场景获取数据(API 请求)。设置订阅(如事件监听)。手动操作 DOM。
- 模拟 componentDidUpdate(组件更新): 根据状态或属性更新副作用逻辑。
- 模拟 componentWillUnmount(组件卸载)在 useEffect 中返回一个清理函数,清理函数会在组件卸载时或者依赖项更新之前调用。清理定时器或动画。移除订阅(如 WebSocket、事件监听)。
- 模拟 shouldComponentUpdate(控制更新逻辑)但可以通过 React.memo 和 useEffect 结合使用优化更新逻辑。
- 模拟 getSnapshotBeforeUpdate(捕获更新前的快照): 可以使用 useRef 捕获更新前的状态。捕获 DOM 状态或滚动位置。
React 组件通信
- Props: 用于父子组件通信,父组件向子组件传递数据。
- 回调函数: 子组件向父组件传递数据,父组件将回调函数作为 props 传给子组件,子组件调用该函数传递数据。
- createContext 跨多层组件传递数据,避免逐层传递 props(“props 钻取”),适用于共享数据(如深色主题模式)
- Redux
使用props和Redux的分界线在哪?
- 数据作用域: Props:当数据仅在父子组件或少数几个紧密关联的组件间传递时,使用props; Redux:若数据需要在多个不相关组件间共享,或跨越多层组件传递,Redux 更合适。如电商应用中,用户登录状态需在导航栏、商品列表、购物车等多个组件中使用,将登录状态存于 Redux store,各组件都能获取。
- 数据修改频率与复杂度:Props:对于数据修改操作简单且不频繁,并且仅影响局部组件状态的情况,props足够。例如,一个按钮组件,通过props传递初始文本,点击按钮改变文本,这种简单的状态变化在组件内部处理即可; Redux:当数据修改逻辑复杂,涉及多个组件协同改变状态,或需要追踪状态变化历史时,Redux 更具优势。例如,在项目管理应用中,任务的创建、分配、完成等操作会涉及多个组件交互。
- 应用规模
redux的工作流程?
redux本质是一个js状态管理库,把所有的状态集中存储在单一的js对象树里面。通过它独有的工作流程来管理状态数据的更新,这样其实就解决了状态混乱的问题,而且是响应式的。它的工作原理是
Redux 的工作原理如下:
首先,通过动作(action)来触发状态改变,动作是一个包含特定类型(type)及可选负载数据(payload)的普通 JavaScript 对象,用以描述发生了什么操作。当用户进行界面交互等操作时,对应的动作创建函数会生成这些动作对象。
接着,动作会被分发(dispatch)到 Redux store,store 内存储着整个应用的状态。store 会将接收到的动作传递给所有已注册的纯函数形式的 reducer。
reducer 依据接收到的动作以及当前的状态,按照动作类型来决定如何更新状态,并返回一个全新的状态对象。因为它是纯函数,相同输入必定输出相同结果,且无副作用,确保了状态更新的可预测性。
然后,Redux 会把各 reducer 返回的新状态合并起来,更新 store 中的全局状态。
Redux 能实现多组件通信的底层原理?
- 集中式状态存储: Redux 将整个应用的状态存储在一个单一的 Store 对象中。无论组件在应用层级结构的何处,都可以从这个单一的 Store 获取所需状态。这打破了组件间层级限制,使得不同层次、不相关的组件能基于同一数据源工作,为组件间通信奠定基础。
- 发布 - 订阅模式: 组件通过subscribe方法订阅 Store 的状态变化。当 Store 状态改变时,所有订阅的组件都会收到通知。
- 纯函数式状态更新: Reducer 是纯函数,给定相同输入(当前状态和 Action),总会返回相同输出(新状态),且无副作用。不可变性:通过返回新状态,确保状态变化可预测,并且不会引入意外的副作用。
函数组件与闭包的相似性?
在函数组件中,当组件内部使用了外部作用域的变量时,就形成了闭包的特性。 当在函数组件内部使用了父组件传递过来的 props 或者在组件内部定义的状态等变量时,函数组件实际上就是在访问其外部作用域中的这些变量,并且在组件的多次渲染过程中,这些变量的引用会被持续保留,这与闭包中内部函数可以访问外部函数作用域中的变量并保留其引用的特点是一致的。
useEffect 和 useLayoutEffect 区别?
useEffect和useLayoutEffect都是 React 中用于处理副作用的钩子函数
- useeffect是异步的,不会阻塞浏览器的渲染线程,所以不会影响页面的首次渲染速度。useLayoutEffect它是同步执行的,在组件渲染到屏幕之前执行。调用时机
- useeffect: 比如数据获取、订阅事件、手动修改 DOM 等。因为它是异步执行的,所以不会导致页面卡顿,适合处理一些不紧急的操作。useLayoutEffect获取 DOM 元素的尺寸、位置等信息。 使用场景
- useLayoutEffect 相比 useEffect,通过同步执行状态更新可解决一些特性场景下的页面闪烁问题(如果使用 useEffect,可能会出现短暂的页面闪烁,因为 React 先进行了一次渲染,然后再异步执行副作用中的 DOM 操作,导致页面在短时间内呈现出不一致的状态。而 useLayoutEffect 可以确保在浏览器绘制之前,DOM 已经更新到最终状态,从而避免了这种闪烁现象)。
useMemo 和 useCallback 的区别
useCallback 和 useMemo 参数相同,第一个参数是函数,第二个参数是依赖项的数组。
功能侧重点不同
useMemo:主要用于缓存计算结果,避免在每次渲染时都进行重复的计算。它返回的是一个值。
useCallback:主要用于缓存函数,避免在每次渲染时都重新创建函数。它返回的是一个函数。
依赖项变化时的行为不同
useMemo:当依赖项发生变化时,会重新计算函数的结果并返回新的值。
useCallback:当依赖项发生变化时,会返回一个新的函数,否则返回上一次缓存的函数。
使用场景不同
useMemo:适用于在组件渲染过程中需要进行昂贵计算的场景,如对大量数据进行处理、复杂的数学运算等。
useCallback:适用于在组件渲染过程中需要传递函数作为 props 的场景,如事件处理函数、回调函数等。
主要区别是 React.useMemo 将调用 fn 函数并返回其结果,而 React.useCallback 将返回 fn 函数而不调用它。
避免重复计算 :useMemo 缓存计算结果,只要依赖项不变,就复用结果
减少内存开销 :重复计算不仅浪费 CPU 资源,还会增加内存开销。useMemo 减少不必要计算,避免频繁创建和销毁大量中间数据,优化内存使用。
自定义hooks 写法?
需要明确它的功能和用途:例如,是用于数据获取、状态管理、表单处理、副作用处理还是其他特定功能。这将决定 hooks 的内部逻辑和返回值。
1.遵循 hooks 规则 : 只能在 React 函数组件或其他自定义 hooks 的顶层调用,不能在循环、条件语句或嵌套函数中调用。
2. 要注意分离关注点 : 将与组件渲染无关的业务逻辑从组件中分离出来,自定义 hooks 负责处理数据获取、状态管理。
3. 命名规范: 使用 use,方便react识别它是一个 hooks。返回值可以是状态值、操作函数、计算结果等。
import { useState, useEffect } from 'react';
const useWindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return windowSize;
};
export default useWindowSize;
useWindowSize是一个自定义 hooks,它在组件挂载时添加了一个resize事件监听器,用于获取窗口大小,并在组件卸载时移除监听器。通过使用这个自定义 hooks,任何组件都可以方便地获取当前窗口的大小,而不需要在每个组件中都重复编写获取窗口大小的逻辑。
受控组件和非受控组件 以及类组件和函数组件的关系?
在 React 中,受控组件和非受控组件是从组件数据处理方式 角度进行的分类,受控组件:由 prop 驱动,非受控组件:由自定义组件内部的 state 驱动。而类组件和函数组件则是从组件的定义形式 角度进行的划分。
- 受控组件
定义:在 React 中,表单元素的值由 React 组件的状态控制,并且在用户输入时,通过事件处理函数更新组件状态,从而实现表单元素与组件状态的双向绑定,这样的组件称为受控组件。数据的流向是明确的,从用户输入到组件状态,再到渲染表单元素,便于理解和调试。
- 非受控组件
非受控组件是指表单元素的值不受 React 组件状态控制,而是通过引用 DOM 节点的方式获取表单元素的值。过ref.current.value来访问。在提交表单时,可以直接从ref获取表单元素的值,而不需要通过组件状态。对于一些简单的表单场景,代码可能更简洁
对比与选择
- 数据一致性 :如果需要确保表单数据与组件状态的一致性,以及方便地进行数据验证和处理,受控组件是更好的选择。如果表单数据的一致性要求不高,或者表单比较简单,非受控组件可能更合适。
- 交互复杂度 :对于需要实时交互和动态更新的表单,如实时搜索、表单联动等,受控组件能够更好地处理复杂的交互逻辑。而对于一些简单的表单提交场景,非受控组件可能已经足够。
- 性能考量 :在大多数情况下,受控组件和非受控组件的性能差异并不明显。但在处理大量表单数据或频繁更新的场景中,非受控组件可能会有一定的性能优势,因为它不需要频繁更新组件状态。
React事件机制
事件处理流程
- 事件触发:当用户在浏览器中执行某个操作,如点击按钮、输入文本等,浏览器会触发相应的原生事件。
- 事件委托:React 采用了事件委托的机制,将所有的事件都绑定在组件的根元素上。当事件发生时,通过事件冒泡机制,事件会从触发的目标元素向上冒泡到组件的根元素。
- 合成事件生成:在事件冒泡到根元素的过程中,React 会根据原生事件创建相应的合成事件对象,并将其传递给事件处理函数。
- 事件处理函数执行:React 会调用绑定在元素上的事件处理函数,并将合成事件对象作为参数传递给它。在事件处理函数中,开发者可以根据需要进行相应的操作,如更新状态、调用其他函数等。
react所有事件都挂在documnet对象上,先触发真实dom事件,再触发react事件,最后执行document上挂载的事件。
setstate是异步的吗还是同步
setState的执行在大多数情况下是异步的,但在某些特定场景下会表现出同步的特性;
异步情况: :当在 React 组件的事件处理函数(如onClick、onChange等中调用setState时,它是异步执行的。 React 在组件内部维护了一个更新队列,当在事件处理函数中调用setState时,React 不会立即执行状态更新并触发渲染,而是将这些更新操作放入队列中。当事件处理函数执行完毕后,React 会对队列中的所有更新操作进行合并和处理,最终只进行一次渲染操作更新组件。
同步情况: 原生事件处理函数中例如给一个原生 DOM 元素添加addEventListener监听事件,在该事件处理函数中调用setState,则会同步更新状态。这是因为原生事件处理函数不属于 React 的控制范围,React 无法对其进行批量更新优化,所以会立即执行状态更新并触发组件重新渲染。**setTimeout/setInterval 回调函数中:**在setTimeout或setInterval的回调函数中调用setState也是同步的。因为这些回调函数不在 React 的事件系统和更新队列机制内。**在 React 生命周期函数之外:**如果在组件的生命周期函数(如componentDidMount、componentDidUpdate等)之外调用setState
Webpack
核心概念:module、chunk 和 bundle,module 是任何通过 import 或者 require 导入的代码,包括 js、css、图片资源等;多个 module 可以组成一个 chunk,每个 chunk 经过处理后会输出一个 bundle 文件。
webpack解决了什么?
背景: 更高效地管理和维护项目中的每一个资源,通过文件划分的形式实现模块化,但是模块都是在全局中工作,大量模块成员污染了环境,模块与模块之间并没有依赖关系、维护困难、没有私有空间等问题。就出现了命名空间方式,规定每个模块只暴露一个全局对象,window.moduleA这种方式也并没有解决第一种方式的依赖等问题。再后来,我们使用立即执行函数为模块提供私有空间,通过参数的形式作为依赖声明。但是仍然存在一些没有解决的问题。我们是用过script标签在页面引入这些模块的,这些模块的加载并不受代码的控制,时间一久维护起来也十分的麻烦。
理想的解决方式是,在页面中引入一个JS入口文件,其余用到的模块可以通过代码控制,按需加载进来。除了模块加载的问题以外,还需要规定模块化的规范,如今流行的则是CommonJS、ES Modules。
需求:
1.监听文件的变化来并且反映到浏览器上,提高开发的效率
2.使用一些高级的特性来加快我们的开发效率或者安全性,比如通过ES6+、TypeScript开发脚本逻辑,通过sass、less等方式来编写css样式代码
3.开发完成后我们还需要将代码进行压缩、合并以及其他相关的优化
解决了解决模块依赖复杂的问题,资源整合与优化问题(代码压缩、文件分割),开发流程优化问题(热更新、自动化构建过程)、跨平台环境兼容性的问题(适配不同浏览器、支持多种环境)
webpack配置用过哪些?
一般在webpack.config.js的文件里面配置;
entry和output
- 单入口单出口,一般用于单页应用,最终只会产生一个 chunk。
- 多入口单出口。
entry: ['./src/index1.js','./src/index2.js'],
entry作为数组。 - 多入口多出口。 entry改为对象,key表示打包后输出文件的名字。
output: filename:xxxx,path:reslove(__dirname,'build')
配置模式mode
默认production | development
JS语法检查
eslint-loader: 在eslintConfig配置项中设置 ,
- “rules”: { // eslint检查的规则 0 忽略 1 警告 2 错误
“no-console”: 0, // 不检查console
“eqeqeq”: 2, // 用而不用=就报错
“no-alert”: 2 // 不能使用alert
}, - “env”: { // 设置环境
“browser”: true, // 支持浏览器环境: 能够使用window上的全局变量
“node”: true // 支持服务器环境: 能够使用node上global的全局变量
},
JS语法转换
babel-loader 将浏览器不能识别的新语法转换成原来识别的旧语法,做浏览器兼容性处理
js兼容性处理
- 使用经典的polyfill @babel/polyfill
优点: 解决babel只能转换部分低级语法的问题(如:let/const/解构赋值…),引入polyfill可以转换高级语法(如:Promise…)
2.core-js
打包样式文件中的图片资源
图片文件webpack不能解析,需要借助loader编译解。析添加2张图片:• 小图, 小于8kb • 大图, 大于8kb • 在less文件中通过背景图的方式引入图片
file-loader url-loader
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 8192, // 8kb --> 8kb以下的图片会base64处理
outputPath: 'images', // 决定文件本地输出路径
publicPath: '../build/images', // 决定图片的url路径
name: '[hash:8].[ext]' // 修改文件名称 [hash:8] hash值取8位 [ext] 文件扩展名
}
}
},
打包HTML文件
html文件webpack不能解析,需要借助插件编译解析 html-webpack-plugin
打包html中图片资源
html中的图片url-loader没法处理,它只能处理js中引入的图片 / 样式中图片,不能处理html中img标签,需要引入其他html-loader处理。
webpack构建流程?
初始化-编译构建-输出
1.初始化参数。获取我们在webpack.config.js文件配置的参数
2.然后开始编译,初始化一个叫compiler的对象,还有各种plugins插件,这个时候插件会开始监听webpack构建过程中的事件。
3.然后webpack要确定入口,一般是从entry开始,开始解析文件构建ast语法树,找抽依赖,递归下去。
4.这个递归的过程,根据文件类型和loader配置,调用相应的loader对不同的文件做转换处理,在找出该模块依赖的模块,递归本操作。
5.递归结束,编译后的 Module 组合成 Chunk,根据entry以及output等配置生成代码块chunk,输出到文件系统
Loader和Plugin的区别?
loader,它是一个转换器,将A文件进行编译成B文件,比如:将A.less转换为A.css,单纯的文件转换过程。
plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。插件的范围包括,打包优化,代码压缩,甚至可以重新定义环境中的变量。
loader针对代码或资源,plugins针对工程。
官方文档说:相对于loader转换指定类型的模块功能,plugins能够被用于执行更广泛的任务比如打包优化、文件管理、环境注入等……
手写一个loader?
假设我们要写一个 loader,它的功能是将输入的文本内容里的所有单词首字母都转换为大写形式。
- 创建 loader 文件:uppercase-loader.js
module.exports = function (source) {
const words = source.split(' ');
const transformedWords = words.map(word => {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
});
return transformedWords.join(' ') + '\n';
};
- 使用 loader:要在 Webpack 项目中使用这个 loader,需要在 Webpack 配置文件(通常是 webpack.config.js)中进行配置
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.txt$/, // 假设我们要处理的是.txt文件内容
use: [
path.resolve(__dirname, 'uppercase-loader.js') // 使用我们自定义的loader
]
}
]
}
};
通过 module.rules 数组来定义模块的处理规则。test 属性指定了要应用该规则的文件类型,use 配置中指定了要使用的 loader这样,当 Webpack 打包项目时,如果遇到 .txt 文件,就会使用我们自定义的 uppercase-loader 来处理文件内容,将其中的单词首字母都转换为大写形式。
UMD Universal Module Definition
- 兼容性: 它融合了 CommonJS(主要用于 Node.js)和 AMD(常用于浏览器端异步模块定义,如 RequireJS)的语法,让模块既能在 Node.js 环境通过require加载,也能在浏览器中使用script标签引入或通过 AMD 加载器加载。这极大地提高了模块的复用性和可移植性。
- 比如开发一个日期处理库,既希望 Node.js 开发者能通过npm install安装并使用require引入,又希望前端开发者能直接在 HTML 页面中通过
JS
DOM标准事件的三个阶段
分为三个阶段,分别是捕获阶段、目标阶段和冒泡阶段。
- capture: 从最外层的祖先元素开始,沿着 DOM 树向下传播,直到到达触发事件的目标元素。主要用于在事件到达目标元素之前进行一些预处理操作,或者对事件进行拦截和控制。如记录点击事件的发生次数等。
- Target: 当事件传播到触发事件的目标元素时,事件在目标元素上触发,目标元素上绑定的相应事件监听器将被执行。执行的操作可能是提交表单、显示隐藏元素等。
- Bubble: 与捕获阶段相反,事件从触发事件的目标元素开始,沿着 DOM 树向上传播,依次触发父元素上绑定的相应事件监听器,直到到达最外层的祖先元素。冒泡阶段常用于事件的委托。
- addEventListener 如果为true,则在捕获阶段触发;如果为false或省略,则在冒泡阶段触发。
- 在 JavaScript 中,可以使用event.stopPropagation()方法来阻止事件冒泡。
null和undefined区别
let null = 1; // 语法错误
function undefined() {} // 语法错误
console.log(null == undefined); // true
console.log(null === undefined); // false
- 在 JavaScript 中,null表示一个空对象指针,它原来指代一个对象类型的值,但该对象不存在或被置空,它是一个 JavaScript 的原始值,在内存中占据一定的空间,有明确的类型为object。typeof null 是历史问题, 对象类型的类型标记位为 000,而 null 在底层表示上全是 0。 当一个对象被销毁或不再需要时,可以将其赋值为null,以便垃圾回收器回收其占用的内存。
- undefined:表示变量声明但未初始化,未定义。或者用于对象的属性不存在,函数没有返回值等情况。类型为undefined。
- Object.prototype.toString.call() 方法更准确地判断变量的类型:
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
Object.prototype.toString.call()的原理
- 牵扯到原型链,每个JS对象都有一个prototype属性,这个Object.prototype的一个方法是toString,返回:“[object Object]”,其中第一个Object表示这是一个对象,第二个Object表示对象的具体类型是普通对象。
- 当使用call()方法调用时,可以改变this指针的指向,使其作用于不同的对象,从而返回该对象的准确类型字符串。
- Object.prototype.toString.call()方法可以准确判断各种数据类型,包括基本数据类型和引用数据类型。
JS编写的规范
- 代码中出现地址、时间等字符串时需要使用常量代替。
- 在进行比较的时候吧,尽量使用’=', '!‘代替’==', ‘!=’
- 不要在内置对象的原型上添加方法,如 Array, Date。
- switch 语句必须带有 default 分支。
- for 循环必须使用大括号。
- if 语句必须使用大括号。
原型链
原型对象:每个函数在创建的时候,都会生成一个属性prototype,这个prototype指向一个对象,这个对象就是函数的原型对象。比如说 function Person() {} Person.prototype = person.__proto; 你把这个person new成实例对象之后,每个实例对象都有一个 proto 属性(又叫隐式原型属性),指向它构造函数的原型对象。原型链又叫隐式原型链,原型链的作用是用来查找对象的属性的,如果一个属性A.name在当前的对象上没有找到就会沿着原型链向上查找,直到找到这个属性或者到达原型链的顶端。Object.prototype.__proto__的值是null;
Object的hasOwnProperty()方法返回一个布尔值,判断对象是否包含特定的自身(非继承)属性。
Object.prototype.__proto__设置为null,就表示原型链在这里结束了,再往上就没有其他对象可查找了,这符合原型链的设计逻辑。如果Object.prototype的__proto__指向了其他非null对象,那么在进行原型链查找时,就会继续沿着这个指向去查找,可能会导致无限循环或者不必要的查找开销。
for…in和for…of的区别
for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值
- for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
- for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
- 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;
总结: for…in 循环主要是为了遍历对象而生,不适用于遍历数组;for…of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
闭包
闭包是指有权访问另一个函数作用域中变量的函数。函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。循环中使用闭包解决 var 定义函数的问题。模拟私有成员:在 JavaScript 中没有像其他面向对象语言那样严格的私有成员概念,但通过闭包可以模拟实现。
this绑定的优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
Promise
如果Promise.all有一个异常了,其他Promise还会继续执行么?
promise.all中所有的请求成功了,走.then(),在.then()中能得到一个数组,数组中是每个请求resolve抛出的结果。
promise.all中只要有一个失败了,走.catch(),在.catch()中获取第一个失败请求rejected抛出的结果。
具体地说,一旦一个 Promise 失败,Promise.all 就会中止并返回一个新的拒绝状态的 Promise。然后其他未完成的 Promise 将会继续运行,但是它们的结果将会被忽略,不会进一步处理。
浏览器和网络
跨域?
浏览器同源策略引起: 协议、域名、端口号,重用的有 jsonp、iframe、cors、img、HTML5 postMessage等等。其中用到 html 标签进行跨域的原理就是 html 不受同源策略影响。但只是接受 Get 的请求方式。
CSS
讲一下flex弹性布局
当你设置display: flex或display: inline-flex给容器,那么它的子元素都变成了弹性项目。
容器上 flex-direction 、 flex-wrap 、 justify-content定义了项目在主轴上的对齐方式
项目上:
- order属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
- flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
- flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
- flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
- flex属性是flex-grow,flex-shrink和flex-basis的简写,默认值为0 1 auto。
- align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。
flex:1 : flex-grow: 1; flex-shrink: 1; flex-basis: 0%;