美团一面-2
1. 请尽可能详细地说明,React中diff的过程。你的回答中不要写出示例代码。
React中的diff过程是React高效更新DOM的关键部分,它通过比较前后两个虚拟DOM树的差异,最小化实际DOM的更新。以下是React diff过程的详细说明:
1. 组件级别的比较(Component-level diffing)
React首先会比较新旧虚拟DOM树的根组件。如果根组件的类型不同,React会认为整个子树都发生了变化,并且会销毁旧树,然后创建并挂载一个全新的树。如果根组件类型相同,则会继续进行更深层次的比较。
2. 元素级别的比较(Element-level diffing)
当组件类型相同时,React会逐个比较它们的子元素。这个过程遵循以下规则:
a. Key的使用
React使用key
属性来识别列表中的每个元素。在列表渲染时,为每个元素提供一个唯一的key
可以帮助React识别哪些元素被添加、删除或移动了。如果没有提供key
,React可能会错误地复用元素,导致状态错乱。
b. 列表对比算法
对于子元素的列表,React使用一种优化的算法来比较它们:
- 如果两个元素的
key
相同,React会认为它们是同一个元素,并进行进一步的属性和子元素的比较。 - 如果
key
不同,React会认为这是一个全新的元素,并创建一个新的DOM节点。
c. 属性对比(Props diffing)
对于每个元素,React会比较其属性(props)。如果属性发生了变化,React会更新相应的DOM属性。有些属性的变化(如className
)会导致DOM节点的重绘,而有些(如style
)可能会导致重排。
d. 子元素递归对比
React会递归地对每个元素的子元素进行相同的比较过程。这个过程会一直持续到所有的子元素都被比较完毕。
3. 组件实例的复用
如果组件的类型相同,React会尝试复用现有的组件实例。这意味着组件的this.props
和this.state
会被保留,组件的生命周期方法(如componentWillReceiveProps
和shouldComponentUpdate
)会被调用,允许组件决定是否需要更新。
4. 更新DOM
一旦确定了所有必要的DOM更改,React会将这些更改批量应用到实际的DOM上。这个过程被称为“提交”(commit)。在这个阶段,React还会调用生命周期方法componentDidUpdate
。
5. 性能优化
React提供了一些工具和方法来优化diff过程:
PureComponent
和React.memo
可以帮助减少不必要的渲染。shouldComponentUpdate
生命周期方法允许开发者手动控制组件是否应该更新。- 使用不可变数据结构可以帮助React更高效地检测状态的变化。
总的来说,React的diff算法通过智能地比较新旧虚拟DOM树的差异,并且只更新必要的部分,从而实现了高效的DOM更新。这个过程是React能够快速响应用户交互和数据变化的关键。
2. 请尽可能详细地说明,React中父组件获取子组件的DOM元素,有哪些方法。你的回答中不要写出示例代码。
在React中,父组件可以通过几种不同的方法获取子组件的DOM元素。以下是详细的说明:
1. 使用 Refs
Refs提供了一种访问DOM节点或在render方法中创建的React元素的方式。这是最直接的方法来获取子组件的DOM元素。
a. 创建 Refs
在父组件中,你可以使用React.createRef()
或者函数组件中的useRef
钩子来创建一个ref对象。
b. 将 Refs 分配给子组件
将创建的ref对象通过props传递给子组件,并在子组件的DOM元素上使用这个ref。
c. 访问 DOM 元素
在父组件中,你可以通过ref对象的current
属性来访问子组件的DOM元素。
2. 使用回调 Refs
回调Refs是一种更灵活的方式来访问DOM元素,它允许你在组件挂载或卸载时执行自定义逻辑。
a. 定义回调函数
在父组件中定义一个回调函数,该函数会在子组件的DOM元素被挂载或卸载时被调用。
b. 将回调函数传递给子组件
通过props将回调函数传递给子组件,并在子组件的DOM元素上使用这个回调函数。
c. 在回调函数中保存 DOM 元素的引用
在回调函数中,你可以保存对子组件DOM元素的引用,以便在父组件中使用。
3. 使用 Forwarding Refs
如果你想让子组件直接将ref转发到它的一个子元素上,你可以使用forwarding refs。
a. 在子组件中转发 Refs
在子组件中,你可以使用React.forwardRef
来转发接收到的ref到内部的DOM元素或其他组件上。
b. 在父组件中使用 Forwarded Refs
在父组件中,你可以像使用普通refs一样使用forwarded refs来访问子组件的DOM元素。
4. 使用 Context API
虽然Context API主要用于跨组件层级传递数据,但它也可以用来传递refs,尤其是在需要绕过中间组件层级时。
a. 创建 Context
创建一个Context来存储ref对象。
b. 提供 Context
在父组件中提供Context,并将ref对象作为value传递给Context。
c. 消费 Context
在子组件中消费Context,并从中获取ref对象,然后将其应用到DOM元素上。
注意事项
- 直接操作DOM元素可能会违反React的数据驱动原则,因此应该谨慎使用。
- 在函数组件中使用refs时,应该使用
useRef
钩子。 - 使用forwarding refs时,要注意不要破坏组件的封装性。
以上就是在React中父组件获取子组件DOM元素的几种方法。每种方法都有其适用场景,开发者应根据具体情况选择最合适的方法。
3. 请尽可能详细地说明,React的事件合成的原理。你的回答中不要写出示例代码。
React的事件合成机制是其内部实现的一部分,旨在提供一种跨浏览器一致的事件处理方式。以下是对React事件合成原理的详细说明:
1. 事件池(Event Pooling)
React维护了一个事件池,用于存储所有事件对象。这个池子的目的是重用事件对象,以减少内存消耗和提高性能。当一个事件被触发时,React会从事件池中取出一个事件对象,处理完后再将其放回池中。
2. 合成事件(SyntheticEvent)
React不直接使用浏览器的原生事件对象,而是创建了一个轻量级的合成事件对象(SyntheticEvent)。这个对象封装了原生事件对象,并提供了跨浏览器的一致性接口。SyntheticEvent具有以下特点:
a. 跨浏览器兼容性
SyntheticEvent隐藏了不同浏览器之间的事件处理差异,开发者无需担心兼容性问题。
b. 批量更新
React会将多个事件处理函数的调用合并成一个批量更新,以提高性能。这意味着在事件处理函数中对状态的修改会被延迟到事件处理结束后统一处理。
c. 自动绑定
SyntheticEvent对象会自动绑定到当前组件实例,开发者无需手动绑定this
。
3. 事件委托(Event Delegation)
React使用事件委托机制来优化事件处理。具体来说,React在顶层DOM节点(通常是document
)上监听所有事件,而不是在每个子节点上分别监听。当事件触发时,React会根据事件的target和currentTarget属性来确定具体哪个组件应该处理该事件。
4. 事件捕获和冒泡
React支持事件捕获和冒泡阶段。通过在事件处理器中指定capture
属性,开发者可以选择在捕获阶段处理事件。默认情况下,事件处理器在冒泡阶段被调用。
5. 事件取消和阻止默认行为
SyntheticEvent提供了preventDefault
和stopPropagation
方法,用于取消事件的默认行为和阻止事件冒泡。这些方法与原生事件对象中的对应方法具有相同的功能。
6. 事件清理
当组件卸载时,React会自动清理与之相关的所有事件监听器,以防止内存泄漏。
7. 异步事件处理
React的事件处理是异步的。这意味着在事件处理函数中对状态的修改不会立即反映到DOM上,而是会在下一次渲染时统一更新。
总结
React的事件合成机制通过创建统一的SyntheticEvent对象、使用事件委托、批量更新和跨浏览器兼容性处理,提供了一种高效且一致的事件处理方式。这种机制不仅简化了开发者的操作,还提高了应用的性能和稳定性。
4. 请尽可能详细地说明,React组件中如果想实现props变化,相应的state就改变,应该怎么做。你的回答中不要写出示例代码。
在React组件中,如果想要实现props变化时相应的state也跟着改变,可以通过以下几种方法来实现:
1. 使用生命周期方法(适用于类组件)
a. componentDidUpdate
在类组件中,可以使用componentDidUpdate
生命周期方法来检测props的变化,并在变化时更新state。这个方法会在组件更新后被调用,可以在这里比较当前的props和之前的props,如果发现变化,则调用setState
来更新state。
b. static getDerivedStateFromProps
这是一个静态方法,它在组件实例化后以及重新渲染之前被调用。它可以用来根据新的props来更新state。这个方法返回一个对象来更新state,或者返回null表示不需要更新。
2. 使用Hooks(适用于函数组件)
a. useEffect
在函数组件中,可以使用useEffect
钩子来监听props的变化。useEffect
接受两个参数:一个副作用函数和一个依赖数组。当依赖数组中的值发生变化时,副作用函数会被执行。可以将props作为依赖项,当props变化时,副作用函数内部可以调用useState
的更新函数来改变state。
b. useRef
虽然useRef
主要用于引用DOM元素或保存可变的值,但它也可以用来存储前一次渲染时的props。通过比较当前的props和useRef
中存储的props,可以决定是否需要更新state。
注意事项
- 在使用这些方法时,应该小心避免无限循环。确保在更新state时不会再次触发相同的props变化。
- 应该合理使用这些方法,只有在确实需要根据props变化来更新state时才使用,以保持组件的清晰和可维护性。
- 在
componentDidUpdate
和useEffect
中进行状态更新时,最好使用函数形式的setState
,这样可以确保总是基于最新的state来进行更新。
通过上述方法,可以实现React组件中props变化时相应state的更新,从而使得组件能够响应外部传入的数据变化,保持UI和数据的一致性。
5. 请尽可能详细地说明,JavaScript中的类,执行super发生了哪些事情。你的回答中不要写出示例代码。
在JavaScript中,super
关键字用于调用父类(超类)的方法。当在一个子类的构造函数或方法中使用super
时,会发生以下一系列事情:
1. 确定父类
首先,JavaScript引擎会确定super
关键字所引用的父类。这通常是在子类定义时通过extends
关键字指定的。
2. 调用父类构造函数
如果super
出现在子类的构造函数中,那么它会调用父类的构造函数。这是为了确保父类的初始化逻辑得到执行,包括设置原型链和初始化任何必要的状态。
- 设置原型链:
super
调用会确保子类的原型正确地链接到父类的原型,这样子类就能继承父类的方法和属性。 - 执行父类构造函数:父类的构造函数会被调用,允许父类设置其实例的初始状态。
3. 上下文绑定
在使用super
调用父类方法时,JavaScript会自动将当前子类实例的上下文(即this
)绑定到该方法。这意味着在父类方法内部,this
将引用调用该方法的子类实例。
4. 返回值处理
如果super
调用的是一个返回值的函数(比如构造函数),那么这个返回值会被用作super
表达式的结果。对于构造函数来说,通常期望返回的是一个对象,这个对象会成为子类实例的基础。
5. 方法解析顺序(MRO)
在多重继承的情况下(尽管JavaScript的原型链继承并不直接支持多重类继承,但可以通过其他方式模拟),super
的调用会遵循方法解析顺序(Method Resolution Order)。这决定了在多个父类中查找方法的顺序。
6. 限制与注意事项
super
只能在子类的构造函数或方法中使用,不能在静态方法之外的普通函数中使用。- 在子类的构造函数中,必须在使用
this
关键字之前调用super()
,否则会抛出错误,因为this
在调用super
之前是不可用的。 - 如果子类没有显式定义构造函数,JavaScript会自动添加一个调用
super()
的默认构造函数。
综上所述,super
关键字在JavaScript中扮演着连接子类与父类的重要桥梁角色,它不仅负责初始化继承链,还确保了正确的上下文绑定和方法解析顺序。
6. 请尽可能详细地说明,JavaScript中new 一个函数之后发生了哪些事情。你的回答中不要写出示例代码。
在JavaScript中,使用new
关键字来调用一个函数会创建一个新的对象实例,并且这个函数会被当作构造函数来执行。以下是使用new
操作符调用函数时发生的一系列步骤:
1. 创建新对象
一个新的空对象被创建。这个对象是最终返回的实例对象。
2. 设置原型链
新创建的对象的内部[[Prototype]]
属性(通常通过__proto__
访问器属性访问)被设置为构造函数的prototype
属性所指向的对象。这样,新对象就能够继承构造函数原型上的属性和方法。
3. 绑定this
构造函数内部的this
被绑定到新创建的对象。这意味着在构造函数中,this
将引用新创建的实例对象。
4. 执行构造函数
构造函数的代码被执行。这通常包括初始化新对象的属性和方法。如果在构造函数中显式返回一个对象,那么这个对象将覆盖默认创建的对象并成为new
表达式的结果。
5. 返回结果
如果构造函数没有显式返回一个对象,那么新创建的对象将被返回。如果构造函数返回了一个基本类型的值或者null
,那么这个返回值将被忽略,仍然返回新创建的对象。
注意事项
- 使用
new
调用函数时,这个函数被称为构造函数,即使它可能并没有使用constructor
关键字定义。 - 如果构造函数抛出了异常,那么
new
表达式的执行会被中断,并且新创建的对象(如果有的话)不会被返回。 - 构造函数通常不会包含
return
语句,因为它们隐式地返回新创建的对象。但是,如果确实需要返回一个对象,那么应该确保这个对象是通过return
语句显式返回的。
总的来说,new
操作符在JavaScript中用于创建对象实例,并且负责设置原型链、绑定this
上下文、执行构造函数逻辑以及返回最终的实例对象。
7. 请尽可能详细地说明,Vue中的nextTick的原理,和应用场景。你的回答中不要写出示例代码。
Vue中的nextTick
是一个非常重要的API,它允许开发者在下次DOM更新循环结束之后执行延迟回调。以下是对nextTick
原理和应用场景的详细说明:
原理
1. 异步更新队列
Vue在检测到数据变化时,并不会立即更新DOM,而是将这些变化放入一个异步更新队列中。这样做的好处是可以避免不必要的DOM操作,提高性能。
2. 批量处理
Vue会将同一事件循环中发生的所有数据变化合并在一起,然后在下一个事件循环“tick”中统一处理这些更新。这样可以确保在一次更新中只进行一次DOM操作,减少重绘和回流。
3. nextTick的实现
nextTick
的实现基于JavaScript的事件循环机制。它利用了Promise
、MutationObserver
、setImmediate
(在IE中)或setTimeout
等API来确保回调函数在下一个DOM更新周期之后执行。
- Promise:在支持原生Promise的环境中,Vue会优先使用Promise来实现
nextTick
。 - MutationObserver:这是一个用于监听DOM变化的API,Vue会在内部使用它来检测DOM更新完成。
- setTimeout:作为后备方案,Vue会在不支持上述API的环境中使用
setTimeout
来延迟执行回调。
4. 回调队列
Vue维护了一个回调队列,所有通过nextTick
注册的回调都会被放入这个队列中。在下一个DOM更新周期结束时,Vue会依次执行这些回调。
应用场景
1. 数据更新后的DOM操作
当需要在数据变化后立即获取更新后的DOM状态时,可以使用nextTick
。例如,在修改数据后,可能需要立即读取某个DOM元素的尺寸或内容。
2. 异步组件加载
在处理异步组件或动态组件的加载时,可以使用nextTick
来确保组件已经完全渲染并挂载到DOM上。
3. 生命周期钩子中的同步操作
在某些生命周期钩子(如mounted
或updated
)中,如果需要进行一些依赖于DOM更新的操作,应该将这些操作放在nextTick
的回调函数中。
4. 批量更新优化
当需要手动触发多次数据更新,并且希望这些更新合并为一次DOM操作时,可以在每次更新后调用nextTick
,然后在最后一次更新的nextTick
回调中执行后续逻辑。
5. 避免竞态条件
在复杂的交互逻辑中,可能会出现多个数据变化相互影响的情况。使用nextTick
可以确保在处理这些变化时,DOM已经反映了最新的状态,从而避免竞态条件。
总结
nextTick
是Vue提供的一个强大的工具,它利用JavaScript的事件循环机制,允许开发者在DOM更新完成后执行特定的逻辑。正确使用nextTick
可以帮助开发者编写更高效、更可靠的代码,特别是在处理与DOM相关的操作时。
8. 请尽可能详细地说明,Vue中的v-if对比v-show区别,以及应用场景。你的回答中不要写出示例代码。
Vue中的v-if
和v-show
都是用于条件性地渲染DOM元素的指令,但它们在实现方式、性能影响以及适用场景上存在显著的区别。
v-if
实现方式
v-if
是一个“真正的”条件渲染指令,因为它会确保条件块内的组件在条件为真时才会被渲染到DOM中。如果条件为假,那么该元素及其内部的事件监听器和子组件都不会被渲染。
性能影响
由于v-if
涉及到组件的销毁和重建,因此它的切换开销相对较大。在条件频繁切换的场景下,这种开销可能会变得尤为明显。
应用场景
- 条件不经常改变:当条件很少改变时,使用
v-if
是合适的,因为它可以减少不必要的DOM元素渲染。 - 初始渲染开销较大:如果某个组件的初始渲染成本很高,且不一定总是需要显示,那么使用
v-if
可以在不需要时避免渲染该组件。 - 路由守卫:在路由切换时,可以使用
v-if
来根据当前路由条件性地渲染不同的组件。
v-show
实现方式
v-show
则是通过CSS的display
属性来控制元素的显示和隐藏。无论条件是真是假,元素始终会被渲染并保持在DOM中,只是简单地切换CSS属性。
性能影响
由于v-show
只是简单地切换CSS属性,因此它的初始渲染开销较小。然而,在频繁切换显示状态时,由于每次切换都会触发重排(reflow),所以性能开销可能会逐渐累积。
应用场景
- 条件频繁切换:当需要频繁切换元素的显示状态时,使用
v-show
更为合适,因为它避免了组件的销毁和重建过程。 - 动画效果:
v-show
适合与CSS动画结合使用,因为它可以在显示和隐藏之间平滑过渡。 - 占位符:在某些情况下,可能需要一个始终存在于DOM中的元素作为占位符,这时可以使用
v-show
来控制其可见性。
总结
v-if
和v-show
各有优劣,选择哪个指令取决于具体的应用场景和需求。如果条件很少改变或者初始渲染成本较高,那么v-if
可能是更好的选择。而如果需要频繁切换元素的显示状态或者希望与CSS动画结合使用,那么v-show
可能更适合。在实际开发中,应根据具体情况灵活运用这两个指令。
9. 请尽可能详细地说明,React中类组件和函数组件对比。你的回答中不要写出示例代码。
在React中,组件主要分为两种类型:类组件(Class Components)和函数组件(Functional Components)。以下是对这两种组件类型的详细对比:
类组件(Class Components)
定义方式
- 类组件是通过ES6的
class
语法来定义的,它们继承自React.Component
。
状态管理
- 类组件可以使用
this.state
来声明组件的内部状态,并通过this.setState()
方法来更新状态。 - 状态更新是异步的,并且可以触发组件的重新渲染。
生命周期方法
- 类组件拥有一系列的生命周期方法,如
componentDidMount
、componentDidUpdate
和componentWillUnmount
等,这些方法允许在组件的不同阶段执行特定的逻辑。
性能优化
- 可以通过
PureComponent
或React.memo
(用于函数组件)来优化类组件的性能,避免不必要的渲染。
使用场景
- 当组件需要维护自己的状态,或者需要使用生命周期方法时,通常会选择使用类组件。
函数组件(Functional Components)
定义方式
- 函数组件是通过普通的JavaScript函数来定义的,它们接收
props
作为参数并返回JSX。
状态管理
- 在React Hooks出现之前,函数组件无法拥有自己的状态。但现在,通过
useState
和useReducer
等Hooks,函数组件也可以轻松地管理状态。
生命周期方法
- 函数组件本身没有生命周期方法,但可以通过
useEffect
Hook来模拟类组件中的生命周期行为。
性能优化
- 函数组件默认就是纯函数,只要它们的输入(props)不变,输出(渲染的JSX)就不会变。这使得函数组件在很多情况下比类组件更加高效。
使用场景
- 对于简单的展示性组件,或者不需要复杂状态管理的组件,函数组件是一个很好的选择。
- 使用Hooks后,函数组件几乎可以替代类组件,成为编写React组件的首选方式。
对比总结
- 语法简洁性:函数组件通常比类组件更加简洁,因为它们不需要处理
this
上下文。 - 学习曲线:对于初学者来说,函数组件可能更容易上手,因为它们更接近传统的编程模式。
- 状态与生命周期:虽然函数组件现在可以通过Hooks来管理状态和模拟生命周期,但在某些复杂场景下,类组件可能仍然更具优势。
- 社区趋势:随着React Hooks的普及,越来越多的开发者倾向于使用函数组件,因为它们提供了更大的灵活性和更简洁的语法。
总的来说,类组件和函数组件各有千秋,选择哪种类型取决于具体的项目需求和个人偏好。不过,随着React生态的发展,函数组件正逐渐成为主流。
10. 代码口述结果。请尽可能详细地说明,变量提升是什么。请从作用域的角度解释下面代码的运行结果。你的回答中不要写出示例代码。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0)
async1();
new Promise(function (resolve) {
console.log('promise3');
resolve();
}).then(function () {
console.log('promise4');
});
console.log('script end');
function fn(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
console.log(d)
}
console.log(b)
console.log(c)
}
fn()
变量提升(Hoisting)是JavaScript中的一种行为,它允许变量和函数声明在它们被声明的代码块中的任何位置被“提升”到顶部。这意味着无论变量和函数在哪里声明,它们都会被移动到它们所在的作用域的顶部。然而,只有声明本身会被提升,赋值操作会留在原地。
在ES6之前,JavaScript只有函数作用域和全局作用域,使用var
关键字声明的变量具有函数作用域。ES6引入了块级作用域的概念,使用let
和const
关键字声明的变量具有块级作用域。
现在,让我们从作用域的角度解释提供的代码的运行结果:
全局作用域
fn
函数被声明在全局作用域中。
函数作用域(fn
内部)
var a = 1
:变量a
使用var
声明,因此它具有函数作用域。它的声明会被提升到函数作用域的顶部,但赋值操作a = 1
留在原地。let b = 2
:变量b
使用let
声明,因此它具有块级作用域。它的声明也会被提升,但由于暂时性死区(Temporal Dead Zone, TDZ),在声明之前访问b
会导致引用错误。
块级作用域(内部的{}
)
let b = 3
:在这个块级作用域内,b
被重新声明并赋值为3。这个b
与外部的b
是不同的变量,因为它们在不同的块级作用域中。var c = 4
:变量c
使用var
声明,因此它具有函数作用域,而不是块级作用域。它的声明会被提升到最近的函数作用域(即fn
函数作用域)的顶部,但赋值操作c = 4
留在原地。let d = 5
:变量d
使用let
声明,具有块级作用域。它的声明会被提升到块级作用域的顶部,但由于TDZ,在声明之前访问d
会导致引用错误。
运行结果分析
- 在内部的块级作用域中,
console.log(a)
会输出1,因为a
是在函数作用域中声明的,它的声明被提升到了函数作用域的顶部。 console.log(b)
在这个块级作用域中会输出3,因为这里的b
是这个块级作用域中声明的新变量。console.log(d)
会输出5,因为d
在这个块级作用域中被声明并赋值。- 在块级作用域外部,
console.log(b)
会输出2,因为这里的b
是指函数作用域中的b
,而不是块级作用域中的b
。 console.log(c)
会输出4,因为c
虽然是块级作用域中声明的,但由于var
的特性,它的声明被提升到了函数作用域的顶部。
总结来说,变量提升和作用域规则共同决定了变量的可见性和生命周期,而var
、let
和const
关键字的不同行为是理解这些规则的关键。
11. LeetCode 5
https://leetcode.cn/problems/longest-palindromic-substring/description/
我的解法:
const isLoopStr=(str:string)=>{
const len=str.length;
for(let i=0;i<len/2;i++){
if(str[i]!==str[len-i-1]){
return false;
}
}
return true;
}
const findLoopSubString=(str:string)=>{
if(isLoopStr(str)){
return str;
}
const left=str.slice(0,str.length-1);
const right=str.slice(1,str.length);
const leftAns=findLoopSubString(left);
const rightAns=findLoopSubString(right);
return leftAns.length>rightAns.length?leftAns:rightAns;
}
console.log(findLoopSubString('aafddfa'));
console.log(findLoopSubString('aafddfa'));
原文地址:https://blog.csdn.net/qq_43858368/article/details/142491611
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!