1.props和state相同点和不同点?render方法在哪些情况下会执行?
Props(属性)和 State(状态)是 React 组件中用于管理数据的两个概念。
相同点:
-
它们都是用于存储和管理组件数据的。
-
当它们的值发生变化时,都可以触发组件重新渲染。
不同点:
-
Source(数据来源):Props 是从父组件传递给子组件的,而 State 则是在组件内部定义和管理的。
-
Mutability(可变性):Props 是只读的,无法在子组件内部直接修改,只能通过父组件重新传递新的 Props。而 State 可以在组件内部进行修改和更新。
-
更新方式:当 Props 发生变化时,React 会自动进行组件的重新渲染。而 State 的更新需要调用组件的
setState
方法来触发重新渲染。
关于 render
方法的执行情况:
-
初始渲染:组件首次被渲染时,
render
方法会被调用,并生成对应的虚拟 DOM 树。 -
数据变化:当组件的 Props 或 State 发生变化时,React 会重新调用
render
方法来更新虚拟 DOM,并与之前的虚拟 DOM 进行对比。 -
强制更新:通过调用组件的
forceUpdate
方法可以强制触发render
方法的执行,即使 Props 和 State 没有发生变化。 -
父组件更新:如果父组件进行重新渲染,子组件的
render
方法也会被调用。
需要注意的是,只有在 render
方法中返回的虚拟 DOM 与之前的虚拟 DOM 不同,React 才会重新渲染真实 DOM,并更新到页面上。
2. shouldComponentUpdate有什么作用?
shouldComponentUpdate 是 React 组件的生命周期函数之一,用于控制组件是否需要重新渲染。
当组件的 Props 或 State 发生变化时,React 会自动触发组件的重新渲染,但是在渲染之前,React 会调用 shouldComponentUpdate 方法来判断是否需要重新渲染。如果 shouldComponentUpdate 返回 true,则组件将继续进行重新渲染;如果返回 false,则组件将停止重新渲染,直接使用之前的结果。
shouldComponentUpdate 的作用主要有以下两个方面:
-
性能优化:在某些情况下,组件可能会因为不必要的重新渲染而浪费大量性能。比如,在组件的某个 Prop 值没有发生变化时,我们可以通过重写 shouldComponentUpdate 方法来告诉 React 直接复用上次的渲染结果,从而避免不必要的计算和渲染,提升应用性能。
-
控制组件渲染的粒度:有些组件可能包含多个子组件,当 Props 或 State 发生变化时,所有子组件都会被重新渲染,即使对于某些子组件来说,数据并没有发生变化。此时,我们可以通过在父组件中重写 shouldComponentUpdate 来手动控制子组件的重新渲染,从而优化渲染性能和用户体验。
需要注意的是,shouldComponentUpdate 每次在重新渲染之前都会被调用,因此返回 false 可以有效地防止组件重复渲染。
3. 说说React中的虚拟dom?在虚拟dom计算的时候diff和key之间有什么关系?
虚拟 DOM(Virtual DOM)是 React 中的一种机制,通过在内存中构建一棵轻量级的虚拟 DOM 树来代替操作浏览器 DOM,从而提高组件的渲染性能和用户体验。
在 React 中,当组件的 Props 或 State 发生变化时,React 会根据最新的数据重新生成一棵虚拟 DOM 树,并与之前的虚拟 DOM 树进行对比。在对比的过程中,React 会找到两棵树中不同的节点,并将它们对应的真实 DOM 节点进行修改、删除或添加,最终将更新后的 DOM 渲染到页面上。
虚拟 DOM 的 diff 算法是 React 优化渲染性能的核心。在 diff 算法中,每个节点都有一个唯一的标识符,称为 key。当新旧虚拟 DOM 树进行对比时,React 会通过 key 来判断两个节点是否表示相同的内容。在判断过程中,React 会先以新节点为基准,在旧节点中查找对应的节点。如果找到了相同的节点,则进行更新;否则,将新节点插入到旧节点中或从旧节点中删除。
在使用 React 进行开发时,我们应该尽量避免使用索引作为 key,因为索引本身并没有表示唯一性,容易造成错误的判断结果和性能问题。相反,我们应该在数据中为每个元素提供一个唯一的标识符,例如数据库中的 ID 或者全局唯一的 UUID。
需要注意的是,虽然虚拟 DOM 可以有效地降低浏览器对真实 DOM 的操作次数,但也会带来一些额外的开销。例如,在生成和比较虚拟 DOM 树时,需要进行大量的计算和判断,可能会影响应用的整体性能。因此,在实际开发中,我们需要根据具体情况,权衡使用虚拟 DOM 的益处和代价,选择最适合自己应用的方案。
4. react新出来两个钩子函数是什么?和删掉的will系列有什么区别?(2)
React 新增的两个钩子函数是 getDerivedStateFromProps和getSnapshotBeforeUpdate
1、getDerivedStateFromProps 方法是一个静态方法,只能通过类名访问到,而之前的 componentWillReceiveProps 方法是实例方法。 2、getDerivedStateFromProps 方法不能访问组件实例,因此不能在该方法中使用 this 关键字,也不能调用实例方法,只能根据传入的 props 和 state 返回一个新的 state 对象。而 componentWillReceiveProps 方法可以访问组件实例和上一个 props 和 state,可以在该方法中修改实例数据。 3、getSnapshotBeforeUpdate 方法是在 render 方法执行之后、更新之前调用的,它的返回值会作为参数传递给 componentDidUpdate 方法,可以在该方法中获取更新前的 DOM 快照,用于处理一些复杂的动画和交互场景。而之前的 componentWillUpdate 方法只能在更新之前做一些数据准备工作,不能访问到真实的 DOM。
相比于被删除的 will
系列生命周期函数,getDerivedStateFromProps和getSnapshotBeforeUpdate 是基于钩子函数的,提供更好的性能和更好的交互体验。与之前的生命周期方法相比,它们更加灵活和强大,但也需要开发者在使用时注意一些细节和约束。
5. React的props.children使用map函数来遍历会收到异常显示,为什么?应该 如何遍历?(2)
使用 map
函数遍历 React 组件的 props.children
时,可能会遇到异常显示的问题。这是因为 props.children
在不同情况下的类 型不同,导致 map
函数操作的方式也不同。
解决方法:
1.首先检查 props.children
的类型。
2.如果它是一个数组,那么可以直接使用 map
函数进行遍历。
3.如果它只有一个子元素,需要先将它转换为数组,然后再进行遍历
6. React组件之间如何通信?(3)
1.props:通过向子组件传递 props,可以实现父子组件之间的通信。这是 React 中最基本的通信方式。父组件可以将需要传递给子组件的数据作为 props 传递给子组件,子组件可以通过 this.props
访问这些数据。
2.Context:Context 是 React 中的一种全局数据管理方式。它可以让子组件在不通过 props 传递的情况下,直接访问父组件或者祖先组件中的数据。使用 Context 需要先创建一个 Context 对象,然后在祖先组件中通过 Provider 提供数据,在子孙组件中通过 Consumer 访问数据。
3.Refs:Refs 允许我们访问在组件中创建的 DOM 或者其他组件实例。通过 Refs,组件可以在不通过 props 或者 context 的情况下,直接修改子组件或者 DOM 元素的属性。
4.Event Bus:Event Bus 是一种跨组件通信方式,它可以让任何两个组件之间都可以进行通信。
7. Vue组件之间通信的方式有哪些,能够说出七种及其以上的得满分?(2)
当在Vue中的组件之间需要进行通信时,有多种方式可以实现。以下是七种及以上常用的组件通信方式:
-
父子组件通信:父组件通过props向子组件传递数据,子组件通过事件触发向父组件发送消息。
-
子父组件通信:子组件通过$emit方法触发自定义事件,父组件通过监听这些事件来接收子组件发送的消息。
-
兄弟组件通信:可以通过共享一个父组件来实现兄弟组件之间的通信,即将需要共享的数据放在父组件中,然后通过props传递给各个子组件。
-
跨级组件通信:可以使用Vue的provide/inject特性,在父组件中使用provide提供数据,然后在子孙组件中使用inject注入数据。
-
EventBus/事件总线:创建一个空的Vue实例作为事件总线,通过��监听事件,o**n监听事件,emit触发事件,各个组件通过事件总线来进行通信。
-
Vuex:Vuex是Vue的官方状态管理库,可以用于管理全局的状态和实现组件之间的通信。
-
$refs:通过在组件上使用ref属性,可以在父组件中直接访问子组件的实例,从而进行直接的方法调用和数据访问。
-
localStorage或sessionStorage:使用浏览器的本地存储来实现组件之间的通信,将数据存储在localStorage或sessionStorage中,在不同的组件中读取和修改这些数据。
-
和parent和children:通过访问组件实例的parent和children属性,可以在父组件和子组件之间进行直接的方法调用和数据访问。
以上是常用的七种及以上的Vue组件之间通信方式。根据具体的场景和需求,选择合适的通信方式可以更好地组织和管理组件之间的交互。
8. redux本来是同步的,为什么它能执行异步代码?实现原理是什么?中间件的 实现原理是什么?(2)
Redux 是一个使用单一数据源和不可变数据的状态管理库,它本身是同步的,但通过中间件可以实现异步操作。Redux 中最常用的异步中间件是 Redux Thunk、Redux Saga 和 Redux Observable。
具体来说,当我们需要在 Redux 中执行异步代码时,通常会使用如下流程:
-
在 View 中触发一个异步 Action。
-
Action 发送到 Reducer,Reducer 更新 Store 中的状态。
-
如果需要执行异步操作,Middleware 捕获到这个 Action,并执行异步代码。
-
异步代码完成后, Middleware 发送新的 Action 到 Reducer,Reducer 再次更新 Store 中的状态。
实现原理是中间件利用了 Redux 提供的 action 和 reducer 的单向数据流机制,使得 action 可以被拦截并且异步处理后再次派发出去。中间件本质上是对 dispatch 函数的重写,并且它可以执行某些操作,例如异步处理、日志记录、错误报告等。
中间件的原理是 Redux 通过在 dispatch 中间注入中间件的执行代码,在 action 到达 reducer 之前对 action 进行了修改或者是对应的 side effect 操作。具体来说,每个中间件都是一个函数,它接收 store 的 dispatch 和 getState 函数作为参数,返回一个新的函数,这个函数被用来包装 action creator,在 dispatch 前后进行操作。这种方式支持链式调用多个中间件,以便进行不同操作,例如数据处理、异步调用、日志记录、错误报告等。
9. redux中同步action与异步action最大的区别是什么?(2)
1.同步 Action: 同步 action 是指在触发后立即执行并完成的 action。具体表现为,在 Redux 中通过 dispatch 触发同步 action 后,它 会立即被发送到 reducer 进行状态更新
2.异步 Action: 异步 action 是指在触发后需要一定时间进行处理的 action,通常用于处理异步操作,比如网络请求、定时器等。异步 action 通常会触发多个相关的同步 action,以表示异步操作的不同阶段
10. redux-saga和redux-thunk的区别与使用场景?
Redux Saga 和 Redux Thunk 都是用于处理异步操作的 Redux 中间件,它们在实现和使用上有一些区别,适用于不同的场景。
-
区别:
-
实现方式:Redux Thunk 是一个函数,允许我们在 Action Creator 中返回一个函数,这个函数可以进行异步操作并手动调用 dispatch。而 Redux Saga 则基于 ES6 的 Generator 函数,通过使用特定的语法来处理异步操作。
-
控制流程:Redux Thunk 使用简单的回调函数来处理异步操作,通常是通过链式调用多个 Action,从而实现异步流程控制。而 Redux Saga 则使用生成器来定义和控制异步操作的流程,通过监听和响应不同的 Action 来执行相应的操作。
-
异常处理:Redux Thunk 需要手动处理错误,通过 try-catch 捕获异常,并在回调函数中 dispatch 错误信息。而 Redux Saga 具备异常捕获和处理的能力,在 Generator 函数内部可以使用 try-catch 捕获异常,并且可以派发不同的 Action 处理异常情况。
-
使用场景:
-
Redux Thunk 适用于简单的异步场景,例如发起一个 AJAX 请求并在请求成功后更新状态。它的学习曲线比较低,容易上手,适合于对异步处理需求不复杂的项目。
-
Redux Saga 适用于复杂的异步场景,例如需要处理多个连续的异步操作、任务取消、并发请求等。它提供了更强大和灵活的异步处理能力,并且通过 Generator 函数的形式使得异步流程易于阅读和维护。但是相对复杂性也较高,需要掌握 Generator 函数和 Saga 相关的代码结构。
总而言之,Redux Thunk 适用于简单的异步操作,学习曲线较低;而 Redux Saga 适用于复杂的异步场景,提供了更强大和灵活的异步处理能力。选择哪个中间件取决于项目的具体需求和开发团队的技术背景。
11. 在使用redux过程中,如何防止定义的action-type的常量重复?
在 Redux 中,为了避免定义的 action type 常量重复,可以采用以下几种方式:
-
使用字符串常量:定义 action type 时使用字符串常量,在不同的模块或文件中使用不同的命名空间来确保唯一性。
-
使用枚举类型:使用 TypeScript 的枚举类型来定义 action type,枚举成员的名称是唯一的
-
使用工具库:可以使用一些辅助工具库来帮助管理和生成唯一的 action type,例如
redux-act
、redux-actions
,uuid 等。
12. Vuex的实现原理是什么,写出其实现的核心代码?
Vuex是Vue.js的官方状态管理库,它的实现原理主要包括以下几个核心概念和代码:
-
State(状态):用于存储应用程序的状态数据。
javascriptCopy Code// 示例状态 const state = { count: 0 };
-
Mutations(变更):用于修改状态的方法,只能进行同步操作。
javascriptCopy Code// 示例变更 const mutations = { increment(state) { state.count++; }, decrement(state) { state.count--; } };
-
Actions(动作):用于触发变更的方法,可以进行异步操作。
javascriptCopy Code// 示例动作 const actions = { incrementAsync(context) { setTimeout(() => { context.commit('increment'); }, 1000); } };
-
Getters(获取器):用于从状态中派生出新的状态。
javascriptCopy Code// 示例获取器 const getters = { doubleCount(state) { return state.count * 2; } };
-
Store(仓库):将上述概念组合在一起,形成一个完整的状态管理仓库。
javascriptCopy Codeimport Vuex from 'vuex'; const store = new Vuex.Store({ state, mutations, actions, getters });
以上是Vuex的基本实现原理,通过定义状态、变更、动作和获取器,然后创建一个仓库来集中管理应用程序的状态。在组件中可以使用this.$store
来访问仓库中的状态和方法。
需要注意的是,上述代码只是简化的示例,实际的Vuex实现还包括更多的功能和细节,例如模块化组织、插件扩展等。在实际开发中,可以根据具体需求来使用和配置Vuex。
13. 为什么for循环比forEach性能高?
在 JavaScript 中,常用的循环有 for 循环和 forEach 循环。虽然两者都可以遍历数组,但它们的实现方式不同,因此性能也有所不同。
for 循环是一种基于索引值(或下标)的循环方式,通过数组的下标索引来访问数组元素。而 forEach 循环则是一种迭代器,对数组中的每个元素都执行一次回调函数。
因此,for 循环相对于 forEach 循环具有以下优势:
-
for 循环不需要编写额外的函数,可以直接对数组进行操作,因此其执行过程相对更加高效。
-
在 for 循环中,可以通过定义一个本地变量(如
var len = arr.length
)将数组长度缓存起来,避免多次访问arr.length
属性导致的性能损失。 -
在需要对数组进行修改时,for 循环比 forEach 循环更为方便且高效。因为在 for 循环中,可以通过获取数组的下标索引来修改数组元素,而在 forEach 循环中无法直接修改数组元素。
总体而言,在大多数情况下,for 循环比 forEach 循环更具有优势。但是,对于需要进行异步操作的情况,forEach 循环可能更为适用,因为它可以支持 async/await
操作,而 for 循环不支持。
需要注意的是,虽然 for 循环比 forEach 循环更快,但在实际应用中,在性能方面的差别通常不会太大。因此,选择使用哪种循环方式应该根据实际情况而定,以符合代码的可读性、可维护性和执行效率等方面的要求。
14. React的路由的原理是什么,写出其实现的核心代码?(2)
React的路由原理是基于浏览器的history
API和组件化的思想。在React中,通常使用第三方库(如React Router)来实现路由功能。
路由的核心代码实现包括以下几个方面:
-
路由配置:定义路由与组件之间的映射关系。可以通过一个配置文件或直接在代码中进行配置。
javascriptCopy Codeconst routes = [ { path: '/', component: Home }, { path: '/about', component: About }, { path: '/contact', component: Contact } ];
-
路由容器:负责监听URL的变化,并根据路由配置渲染相应的组件。
javascriptCopy Codeclass Router extends React.Component { constructor(props) { 继承父组件方法 super(props); 设置路由名称为当前路由名称 this.state = { currentPath: window.location.pathname }; 修改函数指向 this.handlePopState = this.handlePopState.bind(this); } 挂载完成后设置事件监听,监听函数的变化 componentDidMount() { window.addEventListener('popstate', this.handlePopState); } 销毁前删除事件监听 componentWillUnmount() { window.removeEventListener('popstate', this.handlePopState); } 设置函数,将当前路径名称赋值给变量 handlePopState() { this.setState({ currentPath: window.location.pathname }); } render() { 设置变量为this.state,类组件继承时已经设置,故,current为当前路径名车呢个 const { currentPath } = this.state; 在路由中查找路径名称为当前路径的变量,并赋值给变量 const route = routes.find(r => r.path === currentPath); 若路由存在,将当前组件赋值给变量 const Component = route ? route.component : NotFound; return <Component />; } }
在上述代码中,我们定义了一个Router
组件,它监听浏览器的popstate
事件,当URL发生变化时更新当前路径currentPath
的状态。根据当前路径,找到对应的路由配置,并渲染相应的组件。
-
路由链接:用于在页面中生成链接,点击链接时改变URL并触发路由容器的更新。
javascriptCopy Codeclass Link extends React.Component { handleClick(event) { 阻止默认事件 event.preventDefault(); 获取to的参数(路径) const { to } = this.props; 利用原生事件的history跳转至指定路径 window.history.pushState(null, '', to); } render() { 获取要跳转的当前路由和子路由 const { to, children } = this.props; 返回一个a标签,利用a标签跳转,单击后会触发函数从而阻止跳转并转用原生事件进行跳转 return <a href={to} onClick={this.handleClick}>{children}</a>; } }
在上述代码中,我们定义了一个Link
组件,它通过调用window.history.pushState()
方法改变URL,并阻止默认的链接跳转行为。
使用React Router时,可以使用以上的核心代码作为参考来实现路由功能。React Router还提供了更多高级的特性,如嵌套路由、路由参数等,可以根据具体需求进行使用和配置。
16. 说说reduce方法的作用?自己手动封装一个reduce,写出其核心代码?
reduce()
是JavaScript中的一个高阶函数,用于对数组中的元素进行累加操作,最终返回一个单一的值。它接受两个参数:一个回调函数和一个可选的初始值。回调函数接受四个参数:累加器、当前值、当前索引和原数组。
reduce()
方法的作用是将数组中的每个元素依次传入回调函数中进行处理,并将处理结果累加到累加器中,最终返回累加器的值。
下面是一个手动封装的reduce()
函数的核心代码:
javascriptCopy Codefunction myReduce(arr, callback, initialValue) { let accumulator = initialValue !== undefined ? initialValue : arr[0]; for (let i = initialValue !== undefined ? 0 : 1; i < arr.length; i++) { accumulator = callback(accumulator, arr[i], i, arr); } return accumulator; }
在上述代码中,我们定义了一个名为myReduce
的函数,它接受三个参数:要操作的数组、回调函数和可选的初始值。我们首先通过判断initialValue
是否存在来确定累加器的初始值,如果不存在则默认使用数组的第一个元素作为初始值。然后使用for
循环遍历数组中的每个元素,将累加器、当前值、当前索引和原数组传入回调函数中进行处理,并将处理结果赋值给累加器。最后返回累加器的值。
使用手动封装的myReduce()
函数和原生的reduce()
方法的方式是相同的,例如:
javascriptCopy Codeconst arr = [1, 2, 3, 4, 5]; const sum = myReduce(arr, (accumulator, currentValue) => accumulator + currentValue); console.log(sum); // 15
在上述代码中,我们使用myReduce()
函数对数组arr
中的元素进行累加操作,并将结果打印到控制台上。
17. 什么是发布订阅模式,写出其核心实现代码?
发布订阅模式(Publish-Subscribe Pattern),也称为观察者模式(Observer Pattern),是一种常见的设计模式,用于解耦事件的生产者和消费者之间的关系。在该模式中,事件的生产者(发布者)不直接通知事件的消费者(订阅者),而是通过一个中间件(消息队列、事件总线等)来进行消息传递,从而实现了事件的异步处理和解耦。
下面是一个简单的发布订阅模式的核心实现代码:
javascriptCopy Code// 定义一个消息中心对象 const messageCenter = { events: {}, subscribe: function(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); }, unsubscribe: function(event, callback) { if (!this.events[event]) { return; } const index = this.events[event].indexOf(callback); if (index > -1) { this.events[event].splice(index, 1); } }, publish: function(event, data) { if (!this.events[event]) { return; } this.events[event].forEach(callback => { callback(data); }); } }; // 订阅事件 messageCenter.subscribe('click', function(data) { console.log('click event:', data); }); // 发布事件 messageCenter.publish('click', 'hello world!'); // 取消订阅事件 messageCenter.unsubscribe('click');
在上述代码中,我们定义了一个messageCenter
对象作为消息中心,它有三个核心方法:
-
subscribe(event, callback)
:订阅事件,将事件名和回调函数添加到events
对象中。 -
unsubscribe(event, callback)
:取消订阅事件,从events
对象中删除指定事件名和回调函数。 -
publish(event, data)
:发布事件,根据事件名在events
对象中查找对应的回调函数,并且传递数据给这些回调函数。
我们可以通过调用subscribe
方法来订阅事件,通过调用publish
方法来发布事件,通过调用unsubscribe
方法来取消订阅事件。这样,我们就可以实现事件的异步处理和解耦。
18. 大文件的断点续传如何实现,写出其核心思路代码,前后端都要写?
大文件的断点续传可以通过前端和后端的协作来实现,其核心思路如下:
-
前端上传文件时,将文件切割成若干个块,并记录每个块的起始位置和大小信息,同时生成一个唯一的文件标识符(fileId)。
-
前端通过Ajax请求向后端发送文件块数据和文件标识符,后端接收到数据后,将文件块保存在临时文件中,同时记录已经接收的文件块信息。
-
当前端上传完所有文件块后,向后端发送一个合并请求,后端读取所有临时文件中的数据,按照文件块的顺序合并成一个完整的文件,并删除临时文件。
-
如果上传过程中出现网络中断或其他异常情况,前端可以再次发送上传请求,携带上次上传的进度信息(已经上传的文件块信息),后端根据进度信息进行断点续传。
以下是前端和后端的代码示例:
前端代码(使用jQuery库):
javascriptCopy Code// 文件上传函数 function uploadFile(file) { var chunkSize = 1024 * 1024; // 块大小为1MB var chunks = Math.ceil(file.size / chunkSize); // 计算块数 var fileId = generateFileId(); // 生成文件标识符 // 分割文件 for (var i = 0; i < chunks; i++) { var start = i * chunkSize; var end = Math.min(start + chunkSize, file.size); var chunk = file.slice(start, end); // 发送块数据 $.ajax({ url: '/upload', type: 'POST', data: { fileId: fileId, chunkIndex: i, chunkData: chunk }, success: function(data) { console.log('上传成功:', data); }, error: function(xhr, status, error) { console.log('上传失败:', error); } }); } // 合并文件 $.ajax({ url: '/merge', type: 'POST', data: { fileId: fileId }, success: function(data) { console.log('合并成功:', data); }, error: function(xhr, status, error) { console.log('合并失败:', error); } }); } // 生成文件标识符 function generateFileId() { var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; var fileId = ''; for (var i = 0; i < 32; i++) { fileId += chars.charAt(Math.floor(Math.random() * chars.length)); } return fileId; }
后端代码(使用Node.js和Express框架):
javascriptCopy Codeconst express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); // 临时文件目录 const tempDir = path.join(__dirname, 'temp'); // 创建临时文件目录 if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir); } // 处理块上传请求 app.post('/upload', (req, res) => { var fileId = req.body.fileId; var chunkIndex = parseInt(req.body.chunkIndex); var chunkData = req.body.chunkData; // 将块数据保存到临时文件中 var tempFilePath = path.join(tempDir, fileId + '_' + chunkIndex); fs.writeFile(tempFilePath, chunkData, (err) => { if (err) { console.error(err); res.status(500).send('上传失败'); } else { console.log('上传成功:', fileId, chunkIndex); res.send('上传成功'); } }); }); // 处理文件合并请求 app.post('/merge', (req, res) => { var fileId = req.body.fileId; var filePath = path.join(__dirname, fileId); // 读取所有块数据,按照顺序合并成一个完整的文件 var chunks = fs.readdirSync(tempDir) .filter(file => file.startsWith(fileId + '_')) .sort((a, b) => parseInt(a.split('_')[1]) - parseInt(b.split('_')[1])) .map(file => path.join(tempDir, file)); var writer = fs.createWriteStream(filePath); chunks.forEach(chunk => { var reader = fs.createReadStream(chunk); reader.pipe(writer, { end: false }); reader.on('end', () => { fs.unlink(chunk, err => { if (err) { console.error(err); } else { console.log('删除临时文件:', chunk); } }); }); }); writer.on('finish', () => { console.log('合并完成:', fileId); res.send('合并成功'); }); });
以上代码仅为示例代码,实际应用中需要根据具体需求进行修改和优化。
19. React中”栈调和”Stack Reconciler过程是怎样的?
在React中,"栈调和"(Stack Reconciler)是指React的协调器(Reconciler)在处理组件更新时使用的一种算法。下面是大致的栈调和过程:
-
构建组件树:React首先通过组件的render方法构建整个组件树。这个过程是递归的,从根组件开始,逐级构建子组件,直到构建完整个组件树。
-
Diff算法:在组件树构建完成后,React会将前后两次渲染的组件树进行比较,找出需要更新的部分。这个比较的过程就是Diff算法。
-
深度优先遍历:React使用深度优先遍历算法遍历组件树,从根节点开始,递归地访问每个组件的子组件。这样可以确保先处理子组件,然后再处理父组件。
-
Diff过程:
-
对比组件类型:React首先对比当前节点的组件类型,判断是否需要更新。如果组件类型不同,React会销毁旧组件并创建新组件。
-
对比属性:React会对比当前节点的属性,判断是否需要更新。如果属性有变化,React会更新组件的属性。
-
对比子节点:React会对比当前节点的子节点,判断是否需要更新。如果子节点有变化,React会递归地对比子节点的差异。
-
对比顺序:React会对比当前节点的兄弟节点,判断是否需要更新。如果兄弟节点有变化,React会重新排序组件。
-
-
更新过程:
-
更新组件:如果组件需要更新,React会调用组件的生命周期方法(如shouldComponentUpdate、componentWillUpdate等)进行预处理。
-
渲染组件:React会重新渲染组件,并生成新的虚拟DOM。
-
应用更新:React会将新的虚拟DOM与旧的虚拟DOM进行比较,找出差异并应用到实际的DOM上,完成组件的更新。
-
-
递归处理:重复以上步骤,直到遍历完整个组件树。
通过这种栈调和的方式,React能够高效地处理组件的更新,只更新必要的部分,提高了性能和用户体验。
需要注意的是,上述描述是一个简化的概述,实际的栈调和算法还包含一些优化手段,例如使用Fiber架构、异步渲染等。这些优化措施可以进一步提升React的性能和响应能力。
20.shouldComponentUpdate如何做性能优化的?
shouldComponentUpdate是React组件生命周期中的一个方法,用于控制组件是否需要重新渲染。默认情况下,每当组件的props或state发生变化时,React会重新渲染组件并更新DOM,但有时候我们希望能够避免不必要的渲染,以提高性能。
通过在shouldComponentUpdate中实现一些逻辑来判断是否需要重新渲染组件,可以有效地进行性能优化。以下是一些常见的优化技巧:
-
比较props和state:在shouldComponentUpdate中比较当前props和state与下一次更新的props和state,只有当它们发生变化时才返回true,否则返回false。这样可以避免不必要的渲染。
-
使用浅比较:如果props或state是复杂对象或数组,可以使用浅比较来判断它们是否发生变化。可以使用浅比较工具函数(如lodash的isEqual)或直接比较引用是否相同,避免深度比较导致的性能损耗。
-
排除无关的props或state:如果组件只依赖于部分props或state,可以在shouldComponentUpdate中排除无关的属性,只比较关键的属性。这样可以减少比较的复杂性和性能开销。
-
使用PureComponent或React.memo:React提供了PureComponent和React.memo这两个优化组件的方法。PureComponent会自动进行浅比较,而React.memo可以对函数组件进行记忆化,只有在props发生变化时才重新渲染。
-
使用Immutable数据结构:使用Immutable.js或其他类似的库可以确保props和state是不可变的,这样可以更容易地进行比较和优化。
需要注意的是,shouldComponentUpdate的优化需要谨慎使用,因为过度的优化可能会导致代码复杂性增加,甚至引入隐藏的bug。在进行优化时,建议先进行性能测试和分析,确保优化带来的性能提升是值得的。
21.setState 是同步,还是异步的?(2)
在React中,setState方法既可以是同步的,也可以是异步的,具体取决于调用setState的情况。
-
同步情况:
-
在React的生命周期函数(如componentDidMount、componentDidUpdate、componentWillUnmount等)中调用setState,通常会触发同步更新。这是因为在这些生命周期函数中,React已经完成了对组件的渲染和更新,并且没有其他异步操作需要等待。
-
在原生事件处理函数中(如onClick、onKeyDown等),调用setState也会触发同步更新。这是因为React无法知道原生事件的触发时间,所以默认将其视为同步操作。
-
-
异步情况:
-
在React的合成事件处理函数(如通过onClick绑定的事件处理函数)中调用setState通常会触发异步更新。这是因为React会对多个setState进行批量处理,以提高性能和减少不必要的渲染。在批量处理过程中,多次调用的setState会被合并成一次更新操作,只有最后一次的状态会被应用。
-
在使用React Hooks时,调用useState或useReducer返回的更新函数也会触发异步更新。
-
22.React 和 Vue 在技术层面有哪些区别?(2)
React 和 Vue 是两个非常流行的前端框架,它们在技术层面有以下几点区别:
-
数据驱动方式不同:React 的数据驱动是单向的,即从父组件向子组件传递数据,子组件不能直接修改父组件的数据。Vue 的数据驱动则是双向的,即可以从父组件向子组件传递数据,也可以从子组件向父组件传递数据。
-
组件化实现方式不同:React 使用 JSX 语法编写组件,将 HTML 和 JavaScript 在代码层面融合在一起,组件之间的通信通过 props 和回调函数实现。Vue 则使用模板语法编写组件,将 HTML 和 JavaScript 分离开来,组件之间的通信通过 prop 和自定义事件实现。
-
运行时机制不同:React 使用虚拟 DOM 技术,在数据变化时生成新的虚拟 DOM 树与旧的虚拟 DOM 树进行比较,找出发生变化的节点进行更新。Vue 使用响应式系统,在数据变化时自动触发视图更新。
-
扩展性不同:React 提供了强大的扩展能力,可以结合其他库和框架进行使用,如 Redux、MobX 等。Vue 相对而言扩展性不太好,需要对整个框架进行定制化开发。
-
社区和生态环境不同:React 的社区比较大,拥有相对完善的生态环境,可以很容易地找到开源组件和工具。Vue 的社区虽然不如 React 大,但是也在不断发展壮大,并且 Vue 本身提供了许多常用的功能和组件。
这些是 React 和 Vue 在技术层面上的一些主要区别,但两者都是优秀的前端框架,都有自己的优点和适用场景。选择框架时,应根据实际需求和团队技术栈进行选择。
23.为什么 useState 返回的是数组而不是对象?
在 React 中,useState
是一个用于在函数组件中声明状态的钩子函数。它的返回值是一个长度固定为 2 的数组,而不是一个对象,这是由设计选择所决定的。
使用数组来表示状态,是因为它具有以下优势:
-
简单直观:将状态以数组的形式进行管理,可以更容易地理解和使用。
-
顺序保持一致:使用
useState
声明多个状态时,它们的顺序和声明的顺序是完全一致的。你可以根据索引访问和更新每个状态,而不需要命名或记住状态的特定名称。 -
无需每次指定键:当更新状态时,不需要像使用对象那样指定键。只要调用
useState
返回的第二个元素(通常命名为setXXX
)即可。
24.TypeScript支持的访问修饰符有哪些?(3次)
TypeScript 支持以下几种访问修饰符:
-
public
(默认):公共访问修饰符,表示成员可以在任何地方被访问。 -
private
:私有访问修饰符,表示成员只能在类内部被访问。派生类也无法访问。 -
protected
:受保护的访问修饰符,表示成员可以在类内部以及派生类中被访问,但不能在类外部被访问。 -
readonly
:只读访问修饰符,表示成员只能在声明时或构造函数内赋值,赋值后不可修改。
这些访问修饰符可以应用于类的属性、方法和构造函数参数上。默认情况下,类的属性和方法是公共的(public
),但如果显式指定了其他访问修饰符,属性或方法将具有相应的访问权限。
25.实现一个方法,传递一个数据,可以验证数据类型?返回结果可能是object,array,string...?
可以使用JavaScript中的typeof运算符来判断数据类型,结合switch语句返回不同的结果。具体实现如下:
javascriptCopy Codefunction getType(data) { const type = typeof data; switch (type) { case 'object': if (Array.isArray(data)) { return 'array'; } else if (data === null) { return 'null'; } else { return 'object'; } default: return type; } }
这个方法接收一个参数data,使用typeof运算符获取其数据类型,并根据类型返回不同的结果。当data的类型为object时,需要进一步判断是否为数组或null,如果不是则返回object类型。
使用该方法可以方便地验证数据类型,例如:
javascriptCopy Codeconsole.log(getType('hello')); // string console.log(getType(123)); // number console.log(getType(true)); // boolean console.log(getType(null)); // null console.log(getType([])); // array console.log(getType({})); // object
注意,typeof运算符对于null值会返回'object',这是JavaScript的一个历史遗留问题。因此在getType方法中需要单独判断null值的情况。
26.git代码合并、git解决冲突的方法?
Git是一种分布式版本控制系统,它提供了多种方法来合并代码和解决冲突。下面是两个常见的场景以及对应的操作步骤:
-
合并分支
当我们在一个项目中有多个分支时,需要将不同分支中的代码合并到一起。假设我们有一个主分支master和一个开发分支dev,现在需要将dev分支合并到master分支中。
-
首先切换到master分支:
git checkout master
-
然后执行合并命令:
git merge dev
-
如果没有冲突,则会自动完成合并;如果有冲突,则需要手动解决冲突并提交。
-
解决冲突
当我们在合并分支或者拉取远程代码时,可能会遇到代码冲突的情况。这时需要手动解决冲突并提交。假设我们在合并dev分支到master分支时遇到了冲突:
-
执行合并命令:
git merge dev
-
如果出现冲突,则使用
git status
命令查看冲突文件,并打开编辑器手动解决冲突。 -
解决冲突后,使用
git add
命令将修改后的文件添加到暂存区。 -
最后使用
git commit
命令提交修改,并添加合适的提交信息。
除了手动解决冲突外,还可以使用一些工具来辅助解决冲突,例如Visual Studio Code中的GitLens插件、SourceTree等。
需要注意的是,在解决冲突时要仔细检查修改的代码,确保没有引入新的问题。同时,合并和解决冲突时也需要遵循团队的协作规范和流程,以避免不必要的错误和麻烦
27.说说你对自定义hook的理解?
自定义Hook是React中的一种编程模式,它可以让我们将组件中的一些状态逻辑提取到可复用的函数中。通过自定义Hook,我们可以将组件的业务逻辑与UI逻辑分离,提高代码的可读性和可维护性。
自定义Hook是一个函数,它以use开头,并且可以使用其他的Hook。自定义Hook可以接收任意数量的参数,返回一个数组或者对象,其中包含了需要共享的状态值、方法等。在组件中使用自定义Hook时,只需要调用该函数并将返回值解构即可。
例如,下面是一个自定义Hook,它用于处理计数器的逻辑 :
javascriptCopy Codeimport { useState } from 'react'; function useCounter(initialValue, step) { const [count, setCount] = useState(initialValue); function increment() { setCount(count + step); } return [count, increment]; } export default useCounter;
这个自定义Hook接收两个参数:初始值initialValue和步长step。它使用useState Hook来创建一个计数器的状态count,并定义了一个increment方法来增加count的值。最后,它将count和increment方法以数组的形式返回。
在组件中使用这个自定义Hook时,只需要调用useCounter函数并解构返回值即可:
javascriptCopy Codeimport useCounter from './useCounter'; function Counter() { const [count, increment] = useCounter(0, 1); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); }
通过自定义Hook,我们可以将计数器的逻辑从组件中抽离出来,使得组件更加简洁,易于理解和维护。同时,由于自定义Hook是一个普通的JavaScript函数,因此它可以被多个组件共享,从而实现代码的复用。
28.Typescript中 interface 和 type 的区别是什么?(2)
在 TypeScript 中,interface
和 type
都用于定义类型,但它们有一些区别。
1. 语法差异:
-
interface
关键字用于声明接口,使用interface
可以定义对象的形状、函数的签名等。 -
type
关键字用于声明类型别名,可以给一个类型起一个新的名字。
2. 合并能力:
-
interface
具有合并能力,即同名的接口会自动合并为一个接口,合并后的接口会继承所有同名接口的成员。 -
type
不具有合并能力,同名的类型别名会报错。
3. 实现能力:
-
interface
可以被类实现(使用implements
),用于类与接口的约束关系。 -
type
不能被类实现,它只是给类型起别名,无法用于类与类型的约束关系。
4. 扩展能力:
-
interface
可以通过extends
关键字扩展其他接口或类,实现接口的继承。 -
type
可以使用交叉类型(&
)或联合类型(|
)组合多个类型来创建新的类型。
总结来说,interface
用于定义对象的形状 [Something went wrong, please try again later.]
29.说说你对 useReducer 的理解?
理解:
1.useReducer
是 React 提供的一个 Hook,用于实现状态管理和状态更新,它的作用类似于类组件中的 this.setState
方法。通过 useReducer
,可以将组件的状态逻辑抽离出来,以更清晰、可维护的方式管理组件的状态。
2.使用 useReducer
需要提供两个参数:reducer 函数和初始状态。其中,reducer 函数接收当前状态和动作(action),并返回新的 状态。
3.useReducer
返回的数组包含当前状态和 dispatch 方法,通过解构赋值可以分别获取它们
useReducer的好处:
1.当状态逻辑较复杂时,可以将其拆分为多个 case,每个 case 只关注自己的逻辑,使代码更易于理解和维护。
2.useReducer
还可以结合 Context
API 实现全局状态管理
30.移动端1像素的解决方案?
在移动端开发中,由于不同设备的像素密度差异,1像素问题成为了一个常见的难题。如果我们不对这个问题进行针对性的解决,那么会导致页面显示效果不美观,甚至影响用户体验。
以下是一些解决方案:
-
使用css3的scale属性:将要渲染的元素放大一倍,然后通过scale缩小回去。例如,将一个1像素的边框放大到2像素,再通过scale(0.5)恢复原来大小。这种方法可以使边框看起来更加清晰,但是可能会影响元素的布局和性能。
-
通过伪元素实现:使用伪元素before或after,并设置其content属性为空,然后通过border设置为1像素粗细的边框。例如:
cssCopy Code.box::before { content: ""; position: absolute; left: 0; top: 0; width: 100%; height: 100%; border: 1px solid #ddd; -webkit-box-sizing: border-box; box-sizing: border-box; z-index: -1; }
这种方法可以避免影响元素布局,但是可能会增加HTML代码量和CSS复杂度。
-
通过JavaScript动态设置viewport缩放比例: 使用JavaScript获取设备物理像素和设备独立像素的比例,然后动态设置viewport的缩放比例,从而实现1像素问题的解决。例如:
javascriptCopy Codevar scale = 1 / window.devicePixelRatio; document.querySelector('meta[name="viewport"]').setAttribute('content', 'width=device-width,initial-scale=' + scale + ',maximum-scale=' + scale + ',minimum-scale=' + scale + ',user-scalable=no');
这种方法可以根据设备分辨率进行动态适配,但是可能会对页面布局和性能产生影响。
-
使用第三方库:有一些开源的第三方库可以帮助我们解决1像素问题,例如border.css、postcss-1px等。这些库可以通过CSS预处理器或者PostCSS等工具使用。
31.常用的hooks都有哪些,说出他们的作用,最少列出来5个?
以下是常用的 React Hooks,并列出了它们的作用:
-
useState:用于在函数组件中添加状态管理。它返回一个包含当前状态和更新状态的数组,可以通过解构赋值来使用。
-
useEffect:用于在函数组件中执行副作用操作,比如订阅外部数据、操作 DOM 等。它接收一个回调函数和依赖数组,并在每次渲染后执行回调函数。
-
useContext:用于从 React 的 Context 中获取当前的上下文值。它接收一个 Context 对象,并返回该 Context 的当前值。
-
useReducer:类似于 Redux 中的 reducer,用于管理复杂的状态逻辑。它返回当前状态和一个 dispatch 函数,用于派发状态更新的动作。
-
useCallback:用于性能优化,避免不必要的函数重新创建。它接收一个回调函数和依赖数组,并返回一个记忆化的版本,只在依赖数组变化时才会重新创建。
-
useMemo:用于性能优化,缓存计算结果。它接收一个回调函数和依赖数组,并返回计算结果。只有当依赖数组发生变化时,才会重新计算结果。
-
useRef:用于在函数组件中创建可变的 ref 对象。它返回一个包含
current
属性的对象,该属性可以保存任意可变值,并在组件重新渲染时保持不变。 -
useLayoutEffect:类似于
useEffect
,但它在 DOM 更新之后同步执行。在大多数情况下,可以使用useEffect
替代,但在需要获取真实 DOM 属性时,可能需要使用useLayoutEffect
。 -
useImperativeHandle:用于自定义暴露给父组件的实例值。它接收一个 ref 对象和一个工厂函数,在父组件中可以通过 ref 访问工厂函数返回的值。
-
useDebugValue:用于在 React 开发者工具中显示自定义的钩子值,方便调试和跟踪。它接收一个值和一个格式化函数,用于显示在工具中的自定义标签。
这些是常用的 React Hooks,每个 Hook 都有特定的作用,能够方便地处理组件的状态管理、副作用操作、上下文等功能。使用 Hooks 可以使函数组件更易于编写、理解和维护。
32.怎么预防用户快速连续点击,造成数据多次提交,给出三种方案?
当用户快速连续点击时,可能会导致数据的多次提交,为了预防这种情况发生,可以考虑以下三种方案:
-
禁用按钮:在用户点击按钮后,立即禁用按钮,直到数据提交完成后再启用。这样可以防止用户再次点击按钮。可以通过添加一个
disabled
属性或者通过 JavaScript 动态设置按钮的状态来实现。 -
添加延迟:在用户点击按钮后,可以在数据提交之前添加一个短暂的延迟。这样,如果用户快速连续点击多次,只有第一次点击会触发数据提交,后续的点击会被忽略。可以使用
setTimeout
函数来实现延迟。javascriptCopy Codevar button = document.getElementById('submit-button'); var submitting = false; button.addEventListener('click', function() { if (!submitting) { submitting = true; // 执行数据提交操作 setTimeout(function() { submitting = false; }, 1000); // 设置延迟时间,例如1秒 } });
-
防抖函数:使用防抖函数可以限制用户连续点击的频率。防抖函数会在用户最后一次点击后的一段时间内等待,如果在等待时间内没有再次触发点击事件,则执行数据提交操作。如果在等待时间内又触发了点击事件,则重新计时。可以使用 Lodash 等类似的库提供的防抖函数,或者自己实现一个简单的防抖函数。
javascriptCopy Codefunction debounce(func, wait) { var timeout; return function() { clearTimeout(timeout); timeout = setTimeout(function() { func.apply(this, arguments); }, wait); }; } var button = document.getElementById('submit-button'); var submitData = debounce(function() { // 执行数据提交操作 }, 1000); // 设置等待时间,例如1秒 button.addEventListener('click', submitData);
这些方案可以根据具体的需求和场景选择使用。禁用按钮和添加延迟是比较简单的实现方式,而防抖函数则可以更灵活地控制用户点击的频率。
33.shouldComponentUpdate如何做组件不必要的更新?
在React中,你可以使用shouldComponentUpdate
方法来优化组件的性能,避免不必要的渲染。
1)shouldComponentUpdate
是一个生命周期方法,它会在组件更新之前被调用。默认情况下,React会在每次props
或state
发生变化时重新渲染组件,但有时候组件的状态没有发生实质性的变化,此时就可以通过shouldComponentUpdate
来告诉React该组件不需更新。
2)在shouldComponentUpdate
方法中,你可以根据新的props
和state
与当前的值进行比较,如果它们是相等的,你可以返回false
来阻止组件的更新。通过这种方式,你可以避免不必要的渲染,提高组件的性能。
34.如何理解事件循环?
事件循环(Event Loop)是一种用于处理异步任务的机制,它是现代 JavaScript 运行时环境中的一部分,如浏览器环境或 Node.js。理解事件循环是理解 JavaScript 异步编程的关键。
事件循环的核心思想是,JavaScript 运行时环境维护一个任务队列(Task Queue)来存储需要执行的任务。任务可以是同步任务(即立即执行的任务)或异步任务(需要等待某个事件或条件触发的任务)。
事件循环的过程如下:
\1. 执行同步任务,直到遇到异步任务。
\2. 将异步任务添加到任务队列中,等待执行。
\3. 继续执行后续的同步任务,直到所有同步任务执行完毕。
\4. 检查任务队列,如果有任务需要执行,则从队列中取出一个任务并执行。
\5. 直到任务队列为空。
异步任务完成后,会被添加到任务队列中,等待下一次事件循环执行。具体说,异步任务可以是定时器回调、网络请求结果、用户交互事件等。事件循环会不断地循环执行,保证所有任务都得到适时地执行。
35.Umi中dva的工作流程是什么?
\1. 创建model:首先,你需要创建一个或多个model来定义应用程序的数据模型。每个model都包含了状态(state)、处理逻辑的reducers、处理副作用的effects等。
\2. 注册model:在应用程序的入口文件(通常是src/app.js
)中,通过app.model
方法注册你的model。
\3. 初始化dva:在应用程序的入口文件中,通过app.start()
来初始化dva。这会创建一个Redux store,并配置Redux DevTools等。
\4. 组件中使用数据和触发action:在React组件中,你可以通过connect
函数来连接dva的状态和操作到组件上。然后,你可以通过props来使用数据,并且通过dispatch
方法来触发action。
\5. Reducers和Effects的处理:当你的组件通过dispatch
方法触发action时,对应的reducers会被执行来更新状态。reducers是纯函数,接收旧的状态和action作为参数,返回新的状态。Effects用于处理副作用,如异步请求或非纯函数逻辑。Effects可以是一个生成器函数,通过yield关键字来发起异步操作,并通过put来触发action。
\6. 数据更新和视图更新:当状态更新时,与状态相关的组件会自动重新渲染。你可以通过connect
函数和mapStateToProps
来简化组件和状态之间的连接,并且通过mapDispatchToProps
来简化action和组件之间的连接。
\7. 卸载组件时自动注销:当组件被卸载时,dva会自动取消监听状态的更新,防止内存泄漏。
36.闭包的特点,优缺点,应用场景?
闭包是指在函数内部创建的函数,并且该函数能够访问到其外部函数的作用域。
闭包有以下特点:
\1. 内部函数可以访问外部函数中的变量和参数。
\2. 外部函数的执行上下文被保留在内存中,即使外部函数执行完成后,内部函数仍然可以访问外部函数的作用域。
\3. 多个内部函数可以共享同一个父级作用域,形成一个闭包函数族。
闭包的应用场景包括但不限于:
\1. 保护变量:通过使用闭包,可以隐藏变量,只提供对外部函数公开的接口。这样,可以确保变量不会被外部直接修改,增加了数据的安全性。
\2. 计数器和累加器:通过闭包,可以在函数外部保存一个内部状态,并在每次调用内部函数时修改该状态。这一特性可用于实现计数器、累加器等功能。
\3. 延迟执行和回调函数:将函数作为返回值,可以实现延迟执行或者在特定条件满足时回调执行。
37.扩展运算符都有哪些作用,详细介绍一下?
\1. 数组展开:扩展运算符可以将一个数组展开为多个独立的元素。这使得我们可以方便地将一个数组的元素插入到另一个数组中,或者在函数调用时将数组的元素作为参数传递。
\2. 对象展开:扩展运算符还可以将对象展开为多个独立的属性。这样可以轻松地创建新的对象,或者将多个对象的属性合并到一个新对象中。
\3. 函数调用:扩展运算符可以将一个数组或类数组对象作为参数传递给函数,方便地将数组的元素作为独立的参数传递给函数。
\4. 数组和对象的浅拷贝:通过使用扩展运算符,可以轻松地进行数组和对象的浅拷贝操作。
以上扩展运算符提供了一种简洁、方便的语法来处理数组和对象,减少了代码的重复和冗余,提高了代码的可读性和可维护性。
38.Jsx语法规范是什么?
\1. 标签:JSX 中的标签类似于 HTML 中的标签,但是标签名称可以是任何有效的 JavaScript 标识符。标签可以被自闭合,也可以有开始和结束标签。
\2. 表达式:在 JSX 中,使用花括号 {} 包裹 JavaScript 表达式,可以在标签中插入动态的值。
\3. 属性:在 JSX 中,可以给标签添加属性,属性的值可以是字符串字面量,也可以是表达式。
\4. 样式:在 JSX 中,可以使用对象语法来设置元素的内联样式,属性名使用驼峰命名。
\5. 注释:在 JSX 中,以花括号包裹的注释 {/* */} 会被视为 JavaScript 代码的一部分。
\6. 类名:为了避免与 JavaScript 中的关键字冲突,使用 JSX 的 class 属性应该写成 className。
39.实现一个方法,传递一个数据,可以验证数据类型?返回结果可能是object,array,string...?
方法:
function getDataType(data) {
const dataType = typeof data;
if (Array.isArray(data)) {
return 'array';
} else if (dataType === 'object' && data !== null) {
return 'object';
} else {
return dataType;
}
}
40.封装一个使用递归方式的深拷贝方法?
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
// 递归结束条件:如果是 null 或不是对象或数组,则直接返回原值
return obj;
}
// 创建一个空的目标对象或数组
const clone = Array.isArray(obj) ? [] : {};
// 遍历原对象或数组的属性/元素
for (let key in obj) {
// 递归调用深拷贝函数来拷贝每个属性/元素的值
clone[key] = deepClone(obj[key]);
}
return clone;
}
上述方法会递归地遍历原对象或数组的属性/元素,并使用深拷贝方式来创建新的对象或数组。递归结束条件是如果属性/元素是 null 或不是对象或数组,直接返回原值。
41.setState 是同步,还是异步的
在React 18版本之前,setState
方法原生事件和计时器是同步的。
在React18版本之后i,setState
方法合成事件和生命周期是异步的。
当你调用setState
方法时,React会将更新请求添加到一个队列中,并在适当的时机进行批处理和执行更新。这意味着在调用setState
之后,React不会立即重新渲染组件,而是在更新队列中等待,并在合适的时机进行批处理更新。
React之所以采用异步更新的机制,是为了优化性能。当连续多次调用setState
时,React会将这些更新合并,并在一次性更新时进行最优化的渲染。这样可以避免不必要的重复渲染,提高性能和用户体验。
然而,需要注意的是,虽然setState
是异步的,但React提供了一些机制来处理异步更新。例如,可以在setState
方法中使用回调函数来处理更新后的操作,或者使用componentDidUpdate
生命周期方法来响应更新完成后的操作。
总结起来,setState
方法是异步的,但React采用了一些优化策略来延迟更新和批处理更新,以提高性能。
42.React 和 Vue 在技术应用层面有哪些区别
React和Vue是目前最流行的前端框架之一,它们在技术应用层面有以下几个区别:
-
编程风格:React采用的是JSX (JavaScript XML)的编程风格,它将HTML结构与JavaScript逻辑代码紧密结合在一起,使用类似于JavaScript的语法进行组件开发。而Vue采用的是模板语法,使用HTML标记和特定的Vue指令来描述组件的结构和行为。
-
社区生态和生态系统:React和Vue都有庞大的社区和丰富的生态系统,但在某些方面有所不同。React生态系统更加灵活和庞大,有更多的第三方库和工具可供选择,适用于复杂的大型应用程序。Vue生态系统更加一体化和一致性,配套的工具和插件更多直接集成在核心框架中,适用于快速构建中小型应用。
-
学习曲线:React相对来说比较灵活和直接,但对于初学者可能需要更多的学习和理解,尤其是对于JSX和一些React特有的概念。Vue则相对更简单易懂,学习曲线较为平缓,遵循着更传统的开发模式。
-
组件化开发:React和Vue都支持组件化开发。在React中,组件是使用JavaScript类或函数定义的,可以使用props和state进行数据传递和管理。在Vue中,组件可以使用Vue的组件选项进行定义,也可以使用单文件组件(.vue文件)的形式编写,其中包含模板、JavaScript代码和样式。
-
状态管理:在React中,状态管理常常使用第三方库(如Redux、MobX)进行处理,这些库提供了一些高级的状态管理能力。而Vue则内置了Vuex作为官方的状态管理方案,可以更好地与Vue配合使用,提供了更简单和一致的状态管理解决方案。
总体而言,React注重灵活性、扩展性和大型应用的架构设计,适合拥有一定前端开发经验和对JavaScript语言更熟悉的开发者。Vue注重简洁性、易用性和快速开发,适合初学者和更轻量级的应用场景。选择React还是Vue需要根据具体的项目需求、团队技术背景和偏好来进行权衡和选择。
43.为什么 useState 返回的是数组而不是对象?
在 React 中,useState
是一个用于在函数组件中声明状态的钩子函数。它的返回值是一个长度固定为 2 的数组,而不是一个对象,这是由设计选择所决定的。
使用数组来表示状态,是因为它具有以下优势:
-
简单直观:将状态以数组的形式进行管理,可以更容易地理解和使用。
-
顺序保持一致:使用
useState
声明多个状态时,它们的顺序和声明的顺序是完全一致的。你可以根据索引访问和更新每个状态,而不需要命名或记住状态的特定名称。 -
无需每次指定键:当更新状态时,不需要像使用对象那样指定键。只要调用
useState
返回的第二个元素(通常命名为setXXX
)即可。
44.TypeScript支持的访问修饰符有哪些
TypeScript 支持以下几种访问修饰符:
-
public
(默认):公共访问修饰符,表示成员可以在任何地方被访问。 -
private
:私有访问修饰符,表示成员只能在类内部被访问。派生类也无法访问。 -
protected
:受保护的访问修饰符,表示成员可以在类内部以及派生类中被访问,但不能在类外部被访问。 -
readonly
:只读访问修饰符,表示成员只能在声明时或构造函数内赋值,赋值后不可修改。
这些访问修饰符可以应用于类的属性、方法和构造函数参数上。默认情况下,类的属性和方法是公共的(public
),但如果显式指定了其他访问修饰符,属性或方法将具有相应的访问权限。
45.TS里面的函数重载
在TypeScript中,函数重载(Function Overloads)是指在函数声明中编写多个签名(签名是指函数的参数类型和返回类型),以允许函数接受不同类型和数量的参数,并根据参数的不同类型或数量执行不同的处理逻辑。 每个重载签名包含了参数列表和返回类型的定义,以及可能的重载数量是没有限制的。 重载签名定义的函数类型仅用于类型检查,实际的函数实现部分应写在函数的最后一部分。 使用函数重载可以提高代码的可读性和类型安全性,使得我们能够更清晰地描述函数的用法和预期的参数类型。当我们调用这样一个重载函数时,TypeScript编译器会根据传入的参数类型自动选择正确的函数重载进行类型检查和执行。如果传入的参数类型与重载签名不匹配,则会引发编译时错误。
46. Typescript中 interface 和 type 的区别是什么?(2次)
在 TypeScript 中,interface
和 type
都用于定义类型,但它们有一些区别。
1. 语法差异:
-
interface
关键字用于声明接口,使用interface
可以定义对象的形状、函数的签名等。 -
type
关键字用于声明类型别名,可以给一个类型起一个新的名字。
2. 合并能力:
-
interface
具有合并能力,即同名的接口会自动合并为一个接口,合并后的接口会继承所有同名接口的成员。 -
type
不具有合并能力,同名的类型别名会报错。
3. 实现能力:
-
interface
可以被类实现(使用implements
),用于类与接口的约束关系。 -
type
不能被类实现,它只是给类型起别名,无法用于类与类型的约束关系。
4. 扩展能力:
-
interface
可以通过extends
关键字扩展其他接口或类,实现接口的继承。 -
type
可以使用交叉类型(&
)或联合类型(|
)组合多个类型来创建新的类型。
总结来说,interface
用于定义对象的形状 [Something went wrong, please try again later.]
47.React的路由模式,优缺点写出三种,路由跳转传参如何传递如何接收
React中常用的路由模式有三种:HashRouter、BrowserRouter和MemoryRouter。下面是它们的优缺点:
-
HashRouter:
-
优点:兼容性好,可以在不同的服务器上正常工作。通过在URL的哈希部分(#)进行路由,可以避免与服务器进行实际的通信。
-
缺点:URL中包含哈希,不太美观。冗长的URL可能导致搜索引擎排名下降。
-
-
BrowserRouter:
-
优点:URL更美观,不存在哈希,可以与传统域名和路径一样。使用HTML5的history API来处理路由,提供了更好的用户体验。
-
缺点:要求服务器配置来支持,因为路由信息是通过URL路径显示的,需要服务器配置从不同的路径返回同一个HTML文件。
-
-
MemoryRouter:
-
优点:适用于无需使用URL的场景,如移动应用程序或Electron桌面应用程序。路由信息保存在内存中,不会在URL中显示,同时也不需要服务器配置。
-
缺点:无法在浏览器地址栏中显示路由信息,不适用于需要直接访问某个特定页面的情况。
-
在React中,路由跳转传参可以通过以下方式进行传递和接收:
-
通过URL参数传递:使用
<Link>
组件或编程式导航(如history.push
)时,在目标URL的路径中添加参数。接收时,可以通过react-router
库提供的useParams()
或withRouter
组件来获取参数信息。 -
使用查询字符串传递:通过URL查询字符串的形式传递参数(例如:
/path?param1=value1¶m2=value2
)。接收时,可以使用react-router
库中的useLocation()
来获取查询字符串,并解析为参数对象。 -
使用状态对象传递:通过
history
对象的state
属性,在跳转时传递一个状态对象。接收时,可以通过react-router
库提供的useLocation()
来获取状态对象。
需要根据具体的需求和场景选择最合适的参数传递方式。无论哪种方式,接收参数都可以使用useParams()
、useLocation()
或withRouter
来获取路由参数。
48.React闭包陷阱产生的原因是什么,如何解决
react闭包陷阱产生的原因是由于在React组件中使用了异步操作(如定时器、事件监听等)时,闭包会保留对旧状态的引用,导致更新后的状态无法正确地被获取或使用。
这个问题的核心在于JavaScript的闭包特性。当在组件内部定义一个函数,并在该函数中引用了组件作用域中的变量时,闭包会创建一个对该变量的引用,而不是复制变量的值。当变量发生改变时,闭包中存储的引用依然指向旧值,从而产生问题。
为了解决React闭包陷阱,你可以采取以下方法:
使用函数式更新:React提供了函数式更新的方式,使用这种方式可以避免闭包陷阱。例如,使用setState((prevState) => ...)而不是setState({...})来更新状态,确保获取到的是最新的状态值。
使用useRef钩子:通过使用useRef钩子创建一个可变的引用对象,可以绕过闭包陷阱。将需要访问的变量保存在ref对象中,而不是直接引用组件作用域中的变量。
清除副作用:在组件卸载时,确保清除所有可能引起闭包陷阱的副作用。比如清除定时器、取消事件监听等。可以使用React的useEffect钩子来处理副作用,返回一个清理函数。
通过以上方法,可以避免React闭包陷阱产生的问题,确保正确地获取和使用最新的状态值。