基础
var、let、const 之间的区别
(1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:
- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
(2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
(3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
(4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
(5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
(7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
针对cosnt声明的变量:
cosnt 声明时,必须赋值,否则会报错,这是因为const声明的变量在初始化之后就不能再次赋值,因此必须在声明时赋值。
cosnt声明的是常量,不能更改, 指的是基本数据类型不能更改值,而引用数据类型不能更改他的引用地址,这意味着无法通过赋值语句来更改常量的值,但是如果const声明的变量是一个对象或数组,则仍然可以更改其属性或元素的值。
Axios 的理解及项目中的二次封装(主要用到什么?)
Axios 是一种基于Promise封装的HTTP客户端,其特点如下:
- 浏览器端发起XMLHttpRequests请求
- node端发起http请求
- 支持Promise API
- 监听请求和返回
- 对请求和返回进行转化
- 取消请求
- 自动转换json数据
- 客户端支持抵御XSRF攻击
@description 请求拦截器
客户端发送请求 -> [请求拦截器] -> 服务器
token校验(JWT) : 接受服务器返回的 token,存储到 vuex/pinia/本地储存当中
this.service.interceptors.request.use();
@description 响应拦截器 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
this.service.interceptors.response.use();
对跨域的理解及常见的解决方案
1、 通过jsonp跨域 2、 document.domain + iframe跨域 3、 location.hash + iframe 4、 window.name + iframe跨域 5、 postMessage跨域 6、跨域资源共享(CORS) 7、nginx代理跨域 8、nodejs中间件代理跨域 9、WebSocket协议跨域
前端浏览器缓存了解吗,讲讲对其的理解
Cookie:
- 用来存储客户端的 HTTP 状态信息。
- 同一个域名下的 cookie 是共享的。不利于 http 性能的提升,而且不同域名间会产生跨域。
- 数量受限,大小受限。不同的浏览器对 cookie 都有各自的数量限制,且每个 cookie 只能存储 4KB 大小的数据。
- 可以设置有效期。关闭浏览器后,没有设置有效期的 cookie 会被清掉,设置了有效期的 cookie 会继续生效,直到过期时自动清掉。
sessionStorage(会话存储):
- 用于临时保存同一窗口(或标签页)的数据。在关闭窗口后这些数据会自动删除。
- 只能存储字符串。
- 不会产生跨域。
- 同步存储。
- 大小受限,最多只能存储 5M 。
localStorage(本地存储):
- 用来持久保存客户端的数据。只要不人为删除数据会一直在。
- 只能存储字符串。
- 不会产生跨域。
- 同步存储。
- 大小受限,最多只能存储 5M 。
深拷贝浅拷贝的区别和方法
- 浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)。浅拷贝只复制指向某个对象的指针(引用地址),而不复制对象本身,新旧对象还是共享同一块内存。
- 深拷贝: 在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。
- 简单来讲:拷贝的层级不同,深拷贝是指每一层数据的改动都不会影响原对象和新对象;浅拷贝只有第一层的属性变动不互相影响,深层的数据变动还会互相影响。
方式:
浅拷贝:数组可以用拓展运算符[...arr],或者slice();浅拷贝对象可以用Object.assign({},obj);
深拷贝:JSON.parse(JSON.stringify(obj)),或封装递归方法,或使用第三方库的方法,比如 JQuery的$.extend({},obj),或者lodash 的cloneDeep。
深入:
JSON.parse(JSON.stringify(obj))处理的缺点?
- 如果对象中有属性是function或者undefined,处理后会被过滤掉;
- 如果属性值是对象,且由构造函数生成的实例对象,会丢弃对象的
constructor;
vue2
Vue2的响应式原理
原理:是使用Object.defineProperty来劫持数据的getter、setter,获取属性值会触发getter方法,设置属性值会触发setter方法,在setter方法中取调用修改dom的方法来实现试图更新。
-
defineProperty的缺点
- 只能劫持对象的属性,所以vue里面对data里进行遍历,如果属性有多层嵌套,就会进行递归,大量的递归会会导致调用栈溢出
- 不能监听到对象的新增属性和删除属性
- 无法监听到数组的方法,所以vue里面封装了六七个数组的方法(push,pop,shift,unshift,reverse,sort.,splice,slice)
- 直接使用下标的方法去修改数组的时候是无法响应的
nextTick 原理及作用
原理:将传入的回调函数包装成宏(setImmediate, setTimeout)微(Promise,MutationObserver)任务加入到Vue异步队列
使用:
- 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构时,这个操作就需要方法在
nextTick()的回调函数中。 - 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在
nextTick()的回调函数中。
v-model的实现原理
原理:
- v-bind绑定响应数据
:value - 触发input事件并传递数据
@input
vuex状态管理
Vuex中action和mutation的区别
- Mutation专注于修改State,理论上是修改State的唯一途径;Action业务代码、异步请求。
- Mutation:必须同步执行;Action:可以异步,但不能直接操作State。
- 在视图更新时,先触发actions,actions再触发mutation
- mutation的参数是state,它包含store中的数据;store的参数是context,它是 state 的父级,包含 state、getters
slot插槽的理解
- 默认插槽:又名匿名插槽,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
- 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
- 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
默认插槽,具名插槽数据在父组件上;作用域插槽数据在子组件上。
路由传参方式,以及注意点
query参数,params参数
备注1:传递params参数时,若使用to的对象写法,必须使用name配置项,不能用path。
备注2:传递params参数时,需要提前在规则中占位。
vue3
Vue 3的响应式原理
原理:Proxy是ES6中的一个新特性,它允许你拦截对对象的访问、修改和删除等操作,并在拦截函数中执行自定义的逻辑。
vue3-生命周期(和vue2的区别)
-
概念:
Vue组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子 -
规律:
生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。
-
Vue2的生命周期创建阶段:
beforeCreate、created挂载阶段:
beforeMount、mounted更新阶段:
beforeUpdate、updated销毁阶段:
beforeDestroy、destroyed -
Vue3的生命周期创建阶段:
setup挂载阶段:
onBeforeMount、onMounted更新阶段:
onBeforeUpdate、onUpdated卸载阶段:
onBeforeUnmount、onUnmounted -
常用的钩子:
onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)
vue3-组件通信
watch
- 作用:监视数据的变化(和
Vue2中的watch作用一致) - 特点:
Vue3中的watch只能监视以下四种数据:
ref定义的数据。reactive定义的数据。- 函数返回一个值(
getter函数)。- 一个包含上述内容的数组。
pinia状态管理
websocket和sse
websocket双端推送
sse单像推送
整体
请描述你在前端项目中进行性能优化的经验。
- 对静态资源进行压缩合并,减少页面请求次数和资源大小。
- 使用懒加载和按需加载技术,优化页面渲染速度。
- 优化图片加载,如使用雪碧图、图片压缩等方式。
- 缓存优化,利用浏览器缓存、CDN缓存等机制提高页面加载速度。
- 代码优化,避免DOM操作、减少HTTP请求等,提升页面性能。
如何用webpack来优化前端性能?
⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。
- 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css
- 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径
- Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现
- Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
- 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码
描述一个你在前端项目中遇到的挑战,并说明你是如何解决。
箭头函数
1、箭头函数中没有arguments
2、箭头函数中没有自己的this,箭头函数中的this总是外层作用域的this
3、箭头函数的this无法通过call()、apply()、bind()修改
4、箭头函数无法作为构造函数使用
UI框架组件使用
使用过那些UI组件库?
项目中有使用过ElementUI吗?有遇到过哪些问题?它的使用场景主要是哪些?
ElementUI是怎么做表单验证的?在循环里对每个input验证怎么做呢?
ElementUI怎么修改组件的默认样式
是否二次封装过ElementUI组件?
React
说一下 useMemo 和 useCallback 有什么区别
useMemo 和 useCallback 都是 React 中的 Hook,用于优化性能,避免不必要的重新计算或重新创建函数。虽然它们有类似的用法,但它们的用途和行为有所不同。
1. useMemo 的用途和用法
用途: useMemo 用于优化计算开销较大的操作。当你有一些计算开销较大的逻辑(例如大数据的计算、复杂的处理逻辑等),并且不希望在每次渲染时都重新计算,你可以使用 useMemo。它会缓存计算结果,只有当依赖项改变时才会重新计算。
用法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
解释:
useMemo会返回一个缓存的值,只有当a或b改变时,computeExpensiveValue才会重新执行。- 如果依赖项
a和b没有变化,memoizedValue就会直接返回之前缓存的值,从而避免不必要的计算。
2. useCallback 的用途和用法
用途: useCallback 用于优化函数的创建。特别是在子组件依赖父组件传递的回调函数时,如果回调函数在每次渲染时都重新创建,可能会导致子组件不必要的重新渲染。使用 useCallback 可以将这个函数缓存起来,只有在依赖项变化时才重新创建。
用法:
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
解释:
useCallback会返回一个缓存的函数,只有当a或b改变时,doSomething这个函数才会重新创建。- 如果依赖项
a和b没有变化,memoizedCallback就会返回之前缓存的那个函数实例,从而避免不必要的子组件重新渲染。
主要区别
-
返回值类型:
useMemo返回的是一个值(通常是计算结果)。useCallback返回的是一个函数。
-
适用场景:
useMemo用于缓存计算结果,避免重复计算。useCallback用于缓存函数,避免不必要的函数重新创建。
-
触发条件:
- 两者都需要传入依赖项数组,依赖项变化时会重新计算/创建。
useMemo的依赖项变化时会重新计算值。useCallback的依赖项变化时会重新创建函数。
总结:
- 如果你想缓存一个计算结果以提高性能,使用
useMemo。 - 如果你想缓存一个函数,避免因为函数重新创建导致子组件不必要的重新渲染,使用
useCallback。
说一下 useEffect 和 useLayoutEffect 有什么区别
useEffect 和 useLayoutEffect 都是 React 中的 Hook,用于在函数组件中执行副作用(side effects),比如数据获取、订阅、手动更改 DOM 等。虽然它们的使用方法类似,但它们的执行时机和影响有所不同。
1. useEffect
执行时机:
useEffect在浏览器完成渲染之后执行。这意味着它不会阻塞浏览器绘制 UI。- 通常用于异步操作(如数据获取)、订阅或事件监听等操作,这些操作不需要在 DOM 渲染之前完成。
用法:
useEffect(() => {
// 副作用代码,比如数据获取或事件监听
return () => {
// 清理代码,比如取消订阅或清除定时器
};
}, [dependencies]);
应用场景:
- 数据获取
- 订阅事件(如 WebSocket 或 DOM 事件)
- 修改状态以触发重新渲染
性能考虑:
- 因为
useEffect是在浏览器完成渲染之后执行的,所以它不会影响用户体验或阻塞页面的初次渲染。
2. useLayoutEffect
执行时机:
useLayoutEffect在浏览器执行绘制之前同步执行。这意味着它会在浏览器完成布局和绘制之前运行,阻塞浏览器的绘制。- 它的执行时机与
componentDidMount和componentDidUpdate类似,适合在 DOM 更新后立即执行的一些同步操作。
用法:
useLayoutEffect(() => {
// 同步副作用代码
return () => {
// 清理代码
};
}, [dependencies]);
应用场景:
- 需要同步读取 DOM 布局信息并重新计算 DOM 布局。
- 需要立即在页面绘制之前进行 DOM 操作,以避免页面闪烁或布局抖动。
性能考虑:
useLayoutEffect会阻塞浏览器的绘制,因此使用不当可能会导致性能问题,尤其是在影响用户体验的操作上。
3. 总结
-
执行时机:
useEffect:在浏览器绘制完成后执行,不阻塞浏览器渲染。useLayoutEffect:在浏览器绘制前同步执行,可能阻塞浏览器渲染。
-
使用场景:
useEffect:适用于不影响 UI 的副作用操作,如数据获取、设置订阅、更新状态等。useLayoutEffect:适用于需要立即执行并可能影响 UI 的操作,如同步测量 DOM、大量更新 DOM 等。
-
性能考虑:
useEffect对用户体验影响较小,推荐使用。useLayoutEffect使用时要谨慎,只有在必要时才使用,以避免阻塞浏览器的绘制流程。
React 代码层的优化可以说一下么?
React 代码层的优化主要围绕着提升组件的性能、减少不必要的渲染和提高代码的可维护性展开。以下是一些常见的 React 代码优化策略:
1. 避免不必要的重新渲染
-
使用
React.memo:-
React.memo是一种高阶组件,用于缓存函数组件的渲染结果,只有当组件的 props 发生变化时才重新渲染。它类似于类组件中的shouldComponentUpdate。 -
示例:
const MyComponent = React.memo((props) => { // 组件代码 });
-
-
使用
useCallback和useMemo:-
使用
useCallback来缓存函数,避免因函数引用变化导致的子组件重新渲染。 -
使用
useMemo来缓存计算结果,避免每次渲染时都进行开销较大的计算。 -
示例:
const memoizedCallback = useCallback(() => { // 缓存的函数 }, [dependencies]); const memoizedValue = useMemo(() => { return computeExpensiveValue(a, b); }, [a, b]);
-
-
避免在 render 方法中定义函数和对象:
- 在
render方法或函数组件中定义函数和对象会导致它们在每次渲染时都被重新创建,从而可能触发不必要的子组件重新渲染。可以将这些函数和对象定义在组件外部或使用useCallback和useMemo进行缓存。
- 在
2. 优化组件的渲染
-
适当拆分组件:
- 将大型组件拆分为多个小型组件,使每个组件只关心自己的状态和 props,从而减少渲染的复杂性和不必要的渲染。
-
使用
key属性优化列表渲染:-
在渲染列表时,为每个列表项提供唯一的
key,以帮助 React 识别哪些元素发生了变化、被添加或删除。这可以提高列表渲染的性能。 -
示例:
{items.map(item => ( <ListItem key={item.id} item={item} /> ))}
-
-
避免不必要的状态更新:
- 通过合理组织状态和状态更新逻辑,避免触发与当前渲染无关的状态更新。可以使用
shouldComponentUpdate或React.memo来控制组件的重新渲染。
- 通过合理组织状态和状态更新逻辑,避免触发与当前渲染无关的状态更新。可以使用
3. 减少 Reconciliation 过程的开销
-
避免深层次嵌套的组件树:
- 深层次嵌套的组件树会增加 React 的调和(Reconciliation)过程的复杂性,可以通过减少嵌套深度来提高性能。
-
使用
key正确标识元素:- 确保在列表中使用唯一的
key,避免使用索引作为key,因为这可能导致 React 误判元素的顺序变化,从而引发不必要的重绘。
- 确保在列表中使用唯一的
4. 异步和惰性加载
-
代码分割和懒加载:
-
使用
React.lazy和Suspense进行代码分割和懒加载,将不需要立即加载的组件或代码拆分到单独的文件中,从而减少初始加载时间。 -
示例:
const LazyComponent = React.lazy(() => import('./LazyComponent')); function MyComponent() { return ( <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> ); }
-
-
减少不必要的依赖:
- 对于大型应用,按需引入库而不是一次性加载所有依赖,可以显著减少打包后的文件大小。
5. 使用 React 开发工具进行性能分析
-
React DevTools:
- 使用 React DevTools 进行性能分析,查找可能导致性能问题的组件(例如渲染时间过长、频繁重新渲染的组件等)。
- 使用 Profiler 来测量组件渲染时间,找出性能瓶颈。
6. 使用适合的数据结构
-
避免不必要的数据拷贝:
- 当状态是一个复杂的数据结构时,尽量避免深拷贝,考虑使用不可变数据结构(如
Immutable.js)来管理状态。 - 对于频繁更新的数据结构,可以考虑使用
Map或Set来提高操作效率。
- 当状态是一个复杂的数据结构时,尽量避免深拷贝,考虑使用不可变数据结构(如
7. 利用批量更新
-
React 的批量更新机制:
- React 会在事件处理程序中自动进行批量状态更新,避免多次渲染。
- 如果需要在异步操作中进行批量更新,可以使用
ReactDOM.flushSync强制批量更新。
8. 服务器端渲染 (SSR) 与静态生成 (SSG)
-
服务器端渲染 (SSR) :
- 使用 Next.js 等框架进行服务器端渲染,可以在服务器生成 HTML,提升首屏渲染速度和 SEO。
-
静态生成 (SSG) :
- 对于不频繁更新的页面,使用静态生成(SSG)可以显著减少服务器压力和用户等待时间。
通过这些优化策略,你可以显著提高 React 应用的性能和用户体验,同时保持代码的可维护性。
useEffect 对应在 class 中都生命周期怎么写?
在 React 类组件中,生命周期方法可以替代 useEffect 钩子来处理副作用。根据 useEffect 的不同使用场景,对应的类组件生命周期方法如下:
1. useEffect(无依赖数组)
useEffect(() => {
// 副作用代码
});
对应的生命周期方法:
componentDidMount:组件首次渲染后执行。componentDidUpdate:组件更新后执行。
class MyComponent extends React.Component {
componentDidMount() {
// 副作用代码(组件挂载后执行)
}
componentDidUpdate(prevProps, prevState) {
// 副作用代码(组件更新后执行)
}
render() {
return <div>...</div>;
}
}
2. useEffect(带依赖数组)
useEffect(() => {
// 副作用代码
}, [someValue]);
对应的生命周期方法:
componentDidMount:组件首次渲染后执行。componentDidUpdate:当依赖项发生变化时执行。
class MyComponent extends React.Component {
componentDidMount() {
// 副作用代码(组件挂载后执行)
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.someValue !== this.props.someValue) {
// 副作用代码(依赖项变化时执行)
}
}
render() {
return <div>...</div>;
}
}
3. useEffect(带清理函数)
useEffect(() => {
// 副作用代码
return () => {
// 清理代码
};
}, [someValue]);
对应的生命周期方法:
componentDidMount:组件首次渲染后执行。componentDidUpdate:当依赖项发生变化时执行。componentWillUnmount:组件卸载前执行清理操作。
class MyComponent extends React.Component {
componentDidMount() {
// 副作用代码(组件挂载后执行)
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.someValue !== this.props.someValue) {
// 副作用代码(依赖项变化时执行)
}
}
componentWillUnmount() {
// 清理代码(组件卸载前执行)
}
render() {
return <div>...</div>;
}
}
4. useEffect(只在组件挂载和卸载时执行)
useEffect(() => {
// 副作用代码
return () => {
// 清理代码
};
}, []); // 空依赖数组
对应的生命周期方法:
componentDidMount:组件首次渲染后执行。componentWillUnmount:组件卸载前执行清理操作。
class MyComponent extends React.Component {
componentDidMount() {
// 副作用代码(组件挂载后执行)
}
componentWillUnmount() {
// 清理代码(组件卸载前执行)
}
render() {
return <div>...</div>;
}
}
总结
componentDidMount对应useEffect第一次渲染时执行的部分。componentDidUpdate对应useEffect依赖项变化时执行的部分。componentWillUnmount对应useEffect返回的清理函数。
通过这些生命周期方法,你可以在类组件中实现与 useEffect 相同的副作用管理逻辑。
如果在 if 里面写 useEffect 会有什么表现?
在 React 中,useEffect 是一个 Hook,它必须在函数组件的顶层调用,不能在条件语句(如 if)或循环等块级作用域中调用。React 的 Hooks(包括 useEffect)有一些严格的使用规则,最重要的之一就是 Hooks 必须在 React 函数组件的顶层调用。违反这一规则会导致意外的行为或错误。
1. 行为或表现
如果你在 if 语句中使用 useEffect,React 会出现以下两种情况之一:
1.1. 直接违反 Hook 规则
React 会抛出一个错误,类似于:
Error: Hooks can only be called inside the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
这个错误是因为 React 依赖于 Hooks 调用的顺序来管理内部的状态和副作用。如果 Hooks 在条件语句中调用,它们的调用顺序可能会在不同的渲染中发生变化,从而破坏 React 的执行顺序。
1.2. 可能的意外行为
如果 React 没有立即抛出错误,可能会出现一些不易察觉的错误或意外行为。例如,某些副作用可能会在预期之外的时刻执行,或者某些清理逻辑可能无法按预期运行,导致内存泄漏或状态不一致。
2. 正确的做法
如果你需要在某些条件下运行副作用,应该在 useEffect 内部处理逻辑,而不是将 useEffect 放在 if 语句中。
useEffect(() => {
if (someCondition) {
// 执行副作用
}
}, [someCondition]);
这种方式确保了 useEffect 始终在每次渲染时被调用,但副作用的逻辑只有在满足条件时才会执行。
3. 总结
- Hooks(包括
useEffect)必须在 React 函数组件的顶层调用,不能在条件语句、循环或其他块级作用域中调用。 - 如果在
if语句中使用useEffect,React 会抛出错误,或者导致不易察觉的错误和意外行为。 - 处理条件逻辑时,应该在
useEffect内部进行,而不是将useEffect放在条件语句中。
说一下 React 的 Fiber 架构是什么
React Fiber 是 React 内部的一种新的协调引擎(Reconciliation Engine),它是对 React 核心算法的一次重构和优化,引入了更细粒度的更新调度机制,从而使 React 能够更好地处理高优先级任务,并提供更加流畅的用户体验。
1. 背景与动机
React 的最初版本采用了同步递归的方式来协调(reconcile)组件树的更新,这意味着一旦开始渲染,就会一直渲染到底,直到整个组件树更新完成。这种方式在处理简单的应用时是高效的,但当应用变得复杂时,长时间的渲染会导致浏览器掉帧,用户界面卡顿,无法响应用户交互。
Fiber 架构的引入是为了解决这些问题,使 React 能够更好地处理复杂应用的渲染,并确保界面始终保持响应。
2. 什么是 Fiber?
Fiber 是 React 中用于表示组件节点的一种新的数据结构。每个 Fiber 对象对应于组件树中的一个节点,保存着该节点的状态、props、DOM 引用以及与该节点相关的其他信息。
3. 核心特性
- 增量渲染(Interruptible Rendering) : Fiber 引入了一种增量渲染的机制,可以将渲染工作分成多个小的任务块,允许在任务块之间中断渲染,以处理更高优先级的任务(如用户输入或动画)。
- 优先级调度(Priority Scheduling) : Fiber 允许为不同类型的更新分配不同的优先级。例如,用户交互通常具有较高的优先级,而数据同步等后台任务则可以被分配较低的优先级。这使得 React 能够优先处理高优先级的任务,避免界面卡顿。
- 双缓存机制(Double Buffering) : Fiber 允许 React 在一块内存区域中构建新的 Fiber 树,同时保留当前正在显示的树。在新树准备好后,可以快速地将其切换为当前树,从而使 UI 更新更加流畅。
- 可恢复的更新(Reconciler Reentrance) : Fiber 允许中断和恢复更新操作,这意味着即使渲染被中断,React 也可以在稍后恢复并继续未完成的工作。
4. 工作原理
Fiber 将渲染过程分为两个阶段:
- 调和阶段(Reconciliation Phase,也叫“Diffing Phase”) : 在这个阶段,React 会遍历 Fiber 树,找出需要更新的节点。这个过程是可以中断的,因此即使组件树很大,React 也可以在中间暂停,以响应用户输入或其他高优先级任务。
- 提交阶段(Commit Phase) : 一旦调和阶段完成,React 会将这些变化应用到实际的 DOM 中。提交阶段是不可中断的,因为它涉及到具体的 DOM 操作,必须一气呵成。
5. 优点与改进
- 提高用户体验:通过增量渲染和优先级调度,Fiber 减少了长时间的阻塞更新,确保了用户界面更加流畅、响应更快。
- 更灵活的调度:Fiber 支持复杂的更新场景,可以根据任务的紧急程度灵活地调度更新任务,避免阻塞高优先级的交互。
6. 应用场景
- 动画和流畅的用户交互:对于动画和用户交互密集的应用,Fiber 架构可以显著提高流畅性。
- 复杂的组件树更新:在大型应用中,组件树的更新可能非常复杂,Fiber 可以分阶段处理这些更新,避免性能瓶颈。
7. 与传统架构的区别
- 同步 vs. 异步:传统 React 使用同步递归方式渲染,Fiber 通过异步调度机制,使得 React 可以在必要时中断渲染,进行任务调度。
- 单一任务 vs. 分块任务:传统渲染将所有工作一气呵成,Fiber 将渲染任务分块,允许浏览器在任务间隙执行其他高优先级任务。
8. 总结
React Fiber 是对 React 渲染引擎的一次全面重构,使 React 具备了更强的性能调度能力,能够更好地处理复杂的用户界面需求。通过 Fiber,React 能够实现增量渲染、优先级调度等特性,从而提升用户体验,特别是在处理动画、复杂的组件树更新和高交互性的应用时表现得更加出色。
前面提到,在 if 语句里面写 hook 会报错,你可以用 fiber 架构来解释一下吗?
用 React Fiber 架构来解释为什么在 if 语句中写 Hook 会报错,关键在于 Fiber 架构对于 React 更新机制的依赖。Fiber 架构重构了 React 的调和过程(Reconciliation),使得它能够以更加灵活、增量式的方式处理组件的渲染和更新。这种灵活性依赖于 React 能够精确控制 Hook 的调用顺序和执行过程。
1. Fiber 的基本概念
Fiber 是一种链式数据结构,每个 Fiber 对象对应组件树中的一个节点。React 使用这些 Fiber 对象来跟踪每个组件的状态、props、子元素等信息。为了确保组件的正确更新,React 必须在每次渲染时以相同的顺序调用相同的 Hook。
2. Hook 调用顺序的重要性
React 依赖于 Hook 的调用顺序来管理内部状态。每次渲染时,React 都会通过 Fiber 树遍历组件,并按照固定顺序调用每个 Hook。这个顺序在初次渲染时建立,在后续渲染时保持不变。
-
固定顺序的原因:
- React 使用单链表来管理 Hook 的状态。每次组件渲染时,React 都会根据上次渲染的顺序遍历 Hook 并更新状态。如果 Hook 的调用顺序在渲染过程中发生变化,React 无法正确地将 Hook 与其对应的状态匹配,导致状态错乱或其他意外行为。
3. 为什么 if 语句中的 Hook 会报错?
当你在 if 语句中使用 Hook 时,会导致 Hook 的调用顺序在不同渲染之间可能发生变化。例如,在一次渲染中,if 条件为 true,导致某个 Hook 被调用;而在下一次渲染中,if 条件为 false,该 Hook 就不会被调用。这种变化会使得 React 无法正确地匹配 Hook 与其状态。
-
影响:
- 状态错乱:因为 Hook 的顺序变化,React 会尝试将错误的状态分配给下一个 Hook,导致状态错乱。
- 不可预测的行为:由于 Fiber 调度是基于 Hook 的顺序进行的,顺序变化会导致 React 不能正确管理任务优先级、调度和中断。
- 错误报告:为了避免这种问题,React 会在开发模式下抛出错误,指出 Hook 不能在条件语句中调用。
4. Fiber 如何保障 Hook 的正确执行?
Fiber 依赖于固定的 Hook 调用顺序来确保调和过程的正确性:
- 一致性:通过确保每次渲染中的 Hook 调用顺序一致,React 能够在不同的渲染中正确恢复状态。
- 增量更新:在 Fiber 架构中,React 可以中断并恢复渲染进程,固定的 Hook 顺序有助于 Fiber 在恢复时正确地找到上一次中断的位置,继续执行。
5. 总结
React 的 Fiber 架构依赖于固定的 Hook 调用顺序来确保增量更新和调和过程的正确性。if 语句中的 Hook 可能导致 Hook 调用顺序的变化,进而破坏 Fiber 架构的调度和状态管理机制。为了防止状态错乱和不可预测的行为,React 强制要求 Hook 在组件的顶层调用,而不是在条件语句或循环中使用。
你知道 useEffect 第二个参数内部是怎么比较的吗?
内部是浅比较,源码中用 for 循环配合 Object.is 实现。
对未来的技术上有什么规划呢?
主要从业务转型工程化,比如做一些工具链什么的,构建、打包、部署、监控几个大的方向,node 相关的,这些都是我感兴趣的方向,未来都可以去探索,当然了现在也慢慢的在做这些事情。
在 js 中原型链是一个很重要的概念,你能介绍一下它吗?
原型链是 JavaScript 中用于实现对象继承和属性查找的核心机制。它使得对象可以从其他对象继承属性和方法,形成一个链式结构。理解原型链对于掌握 JavaScript 的继承、对象创建和方法共享非常重要。以下是关于原型链的详细介绍:
1. 原型链的基本概念
- 对象的原型(Prototype) :每个对象都有一个内部属性
[[Prototype]],这个属性指向另一个对象,这个对象称为该对象的原型。原型对象本身也可能有自己的原型,形成一个链式结构,这就是原型链。 - 原型链:对象的原型链是从该对象的
[[Prototype]]开始,沿着每个原型的[[Prototype]]属性一直追溯到原型链的顶端。原型链的顶端是Object.prototype,它的[[Prototype]]是null。
2. 原型链的工作原理
当你访问一个对象的属性时,JavaScript 引擎会首先在对象本身查找这个属性。如果属性不存在于对象本身,JavaScript 会沿着原型链向上查找,即查找对象的原型及其原型的原型,直到找到属性或到达原型链的顶端。
-
查找属性:
- 首先在对象本身查找。
- 如果未找到,则在对象的原型中查找。
- 如果在原型中也未找到,则继续在原型的原型中查找。
- 一直查找到
Object.prototype为止。 - 如果
Object.prototype也没有该属性,最终返回undefined。
3. 创建和修改原型链
-
创建对象:
// 创建一个普通对象 const obj = {}; // obj 的原型是 Object.prototype -
使用构造函数:
function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log(`Hello, my name is ${this.name}`); }; const person = new Person('Alice'); person.sayHello(); // 输出:Hello, my name is Alice在这个例子中,
person的原型链包括Person.prototype和Object.prototype。 -
设置原型:
// 使用 Object.create 设置原型 const proto = { greet() { console.log('Hello!'); } }; const obj = Object.create(proto); obj.greet(); // 输出:Hello! -
修改原型:
// 修改构造函数的原型 function Animal() {} Animal.prototype.eat = function() { console.log('Eating...'); }; function Dog() {} Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.bark = function() { console.log('Woof!'); }; const dog = new Dog(); dog.eat(); // 输出:Eating... dog.bark(); // 输出:Woof!
4. 原型链的实际应用
- 继承: 原型链用于实现对象的继承。通过构造函数和原型链,可以创建具有共享方法的多个实例。
- 方法共享: 使用原型链可以在多个实例之间共享方法和属性,减少内存使用。
5. 原型链的顶端
-
Object.prototype: 所有普通对象的原型链最终会到达
Object.prototype。Object.prototype是原型链的顶端,其[[Prototype]]是null。console.log(Object.getPrototypeOf(Object.prototype)); // null
6. 总结
原型链是 JavaScript 对象模型的核心特性,使得对象可以通过继承机制共享属性和方法。它允许对象在查找属性时递归地向上查找,直到找到属性或到达原型链的顶端。理解原型链对于掌握 JavaScript 的继承、对象创建和性能优化非常重要。
object 的原型指向谁?
在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]],这个属性指向另一个对象,即该对象的原型。对于大多数通过对象字面量 {} 或 new Object() 创建的对象来说,它们的原型指向 Object.prototype。
1. Object.prototype
- 当你创建一个普通的对象(例如
{}或new Object()),这个对象的原型默认指向Object.prototype。 Object.prototype本身是一个对象,包含了许多通用的方法和属性,例如toString()、hasOwnProperty()等。这些方法和属性可以在所有对象上被访问到,因为它们位于原型链的顶端。
2. 原型链
- 如果你试图访问一个对象的属性,而这个属性不存在于该对象本身,JavaScript 会沿着原型链向上查找,直到找到该属性或者到达原型链的顶端(即
Object.prototype)。 - 如果原型链的顶端仍然没有找到这个属性,JavaScript 返回
undefined。
3. 特殊情况
- 如果一个对象是通过构造函数创建的,例如
new Array(),它的原型指向Array.prototype。 - 如果对象是通过
Object.create(null)创建的,那么这个对象没有原型,它的[[Prototype]]是null。
4. 验证对象的原型
你可以通过 Object.getPrototypeOf(obj) 方法或通过 __proto__ 属性(不推荐的方式)来查看一个对象的原型。
const obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
const arr = [];
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
const objWithNullProto = Object.create(null);
console.log(Object.getPrototypeOf(objWithNullProto)); // null
5. Object.prototype 的原型
Object.prototype是原型链的顶端,它的[[Prototype]]属性指向null,这意味着原型链在此终止。
console.log(Object.getPrototypeOf(Object.prototype)); // null
总结:
- 大多数对象的原型指向
Object.prototype。 - 原型链的顶端是
Object.prototype,它的原型是null。 - 特殊的对象(如通过构造函数创建的对象)可能有不同的原型,但最终都会在原型链上追溯到
Object.prototype或null。
能说一下原型链的查找过程吗?
原型链的查找过程是 JavaScript 对象属性访问机制的核心,了解这一过程对于深入理解 JavaScript 的继承和对象模型至关重要。以下是详细的原型链查找过程:
1. 对象属性访问过程
当你访问一个对象的属性时,JavaScript 引擎会按照以下步骤进行查找:
-
在对象本身查找:
- JavaScript 首先检查对象自身是否具有该属性。
- 如果属性存在于对象本身,直接返回该属性的值。
-
在原型链中查找:
- 如果对象本身没有该属性,JavaScript 会查找对象的原型。
- 对象的原型是通过对象的
[[Prototype]]属性指向的另一个对象(通常是通过构造函数创建的prototype对象)。 - 如果原型对象存在该属性,则返回这个属性的值。
-
继续向上查找:
- 如果原型对象中也没有找到属性,则继续查找原型对象的原型。
- 这个过程会持续进行,直到找到属性或到达原型链的顶端。
-
到达原型链的顶端:
- 原型链的顶端是
Object.prototype。如果Object.prototype也没有该属性,则返回undefined。 Object.prototype的[[Prototype]]是null,表示原型链的终点。
- 原型链的顶端是
2. 示例
// 创建一个对象
const obj = {
name: 'Alice',
greet() {
return 'Hello!';
}
};
// 在 obj 上查找属性
console.log(obj.name); // 输出: Alice (在 obj 本身查找到了 name 属性)
console.log(obj.greet()); // 输出: Hello! (在 obj 本身查找到了 greet 方法)
// 创建一个构造函数
function Person(name) {
this.name = name;
}
// 在构造函数的原型上添加方法
Person.prototype.sayHello = function() {
return `Hello, my name is ${this.name}`;
};
// 创建一个 Person 实例
const person = new Person('Bob');
// 在 person 上查找属性
console.log(person.name); // 输出: Bob (在 person 本身查找到了 name 属性)
console.log(person.sayHello()); // 输出: Hello, my name is Bob (在原型链中查找到了 sayHello 方法)
// 确认原型链
console.log(Object.getPrototypeOf(person) === Person.prototype); // 输出: true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // 输出: true
console.log(Object.getPrototypeOf(Object.prototype)); // 输出: null
3. 原型链的实际效果
-
属性查找:
- 当访问
person.name时,JavaScript 在person对象本身查找name属性,找到后返回。 - 当访问
person.sayHello时,person对象本身没有sayHello属性,JavaScript 会查找Person.prototype,找到该方法后返回。
- 当访问
-
方法共享:
- 通过原型链,
Person构造函数的所有实例都能共享sayHello方法,而不是每个实例都独立拥有一份sayHello方法,从而节省内存。
- 通过原型链,
4. 总结
原型链的查找过程允许 JavaScript 对象通过继承机制共享属性和方法。在属性访问时,JavaScript 引擎会从对象本身开始查找,若未找到则沿着原型链向上查找,直到找到属性或到达原型链的顶端(Object.prototype),最终返回 undefined。了解这一过程有助于掌握 JavaScript 的继承机制和对象属性管理。
js 中的基础类型和对象类型有什么不一样?
在 JavaScript 中,数据类型分为基础类型(Primitive Types)和对象类型(Object Types)。这两种类型有不同的特性和行为,它们在内存存储、操作方式和应用场景上各有不同。以下是基础类型和对象类型的主要区别:
1. 基础类型(Primitive Types)
基础类型包括以下几种:
- Number:表示数字,包括整数和浮点数。
- String:表示字符序列(字符串)。
- Boolean:表示逻辑值
true或false。 - Undefined:表示变量未定义时的状态。
- Null:表示空值或无效对象的占位符。
- Symbol(ES6 引入):表示唯一的标识符。
- BigInt(ES11 引入):表示任意精度的整数。
特性:
-
不可变性:
- 基础类型的值是不可变的。这意味着一旦创建了一个基础类型的值,它的内容不能被修改。如果你对一个基础类型的值进行修改,实际上是创建了一个新的值。
let str = 'hello'; str = 'world'; // 这里创建了一个新的字符串 'world',原来的 'hello' 不变 -
按值传递:
- 基础类型的值是按值传递的。当你将基础类型的值赋给一个新变量时,实际上是创建了这个值的副本。
let a = 10; let b = a; // b 现在是 10,a 和 b 是两个不同的值 -
存储方式:
- 基础类型的值通常存储在栈(stack)中。
2. 对象类型(Object Types)
对象类型包括:
- Object:最基本的对象类型,包含键值对。
- Array:数组对象,用于存储有序的值。
- Function:函数对象,可调用的代码块。
- Date:表示日期和时间。
- RegExp:表示正则表达式。
- Map、Set、WeakMap、WeakSet(ES6 引入):集合数据结构。
- Buffer(在 Node.js 中):用于处理二进制数据。
特性:
-
可变性:
- 对象类型的值是可变的。你可以修改对象的属性或方法的值,而不需要创建新的对象。
let obj = { name: 'Alice' }; obj.name = 'Bob'; // 修改了对象的属性 -
按引用传递:
- 对象类型的值是按引用传递的。当你将一个对象赋给一个新变量时,实际上是传递了这个对象的引用,而不是对象的副本。
let obj1 = { name: 'Alice' }; let obj2 = obj1; // obj2 和 obj1 引用的是同一个对象 obj2.name = 'Bob'; console.log(obj1.name); // 输出: Bob,因为 obj1 和 obj2 引用的是同一个对象 -
存储方式:
- 对象类型的值通常存储在堆(heap)中,栈中存储的是对这些对象的引用。
3. 总结
-
基础类型:
- 不可变
- 按值传递
- 存储在栈中
-
对象类型:
- 可变
- 按引用传递
- 存储在堆中
理解基础类型和对象类型的区别对于正确地处理数据、避免错误和优化性能非常重要。
http 是在哪一层实现的?
应用层
说一下 get 跟 post 有什么区别?
GET 和 POST 是 HTTP 协议中最常用的请求方法,它们用于向服务器发送请求并获取或提交数据。以下是它们之间的主要区别:
1. 功能和目的
-
GET:
- 用于从服务器请求数据。
- 主要用于获取资源或查询数据。
- 请求参数(查询字符串)附加在 URL 中,因此适合传输较少的数据。
- 请求是幂等的,即多次请求不会对服务器状态产生副作用。
-
POST:
- 用于向服务器提交数据。
- 主要用于创建资源或提交表单数据。
- 请求数据包含在请求体中,而不是附加在 URL 中,这使得可以传输较大的数据量。
- 请求不是幂等的,即多次请求可能会对服务器状态产生副作用,例如创建多个资源。
2. 请求数据的方式
-
GET:
- 数据作为 URL 的查询参数附加在请求 URL 后面,格式为
?key1=value1&key2=value2。 - 数据长度受限(通常由浏览器或服务器的最大 URL 长度限制),一般适合传输少量数据。
GET /search?q=javascript&lang=en HTTP/1.1 Host: example.com - 数据作为 URL 的查询参数附加在请求 URL 后面,格式为
-
POST:
- 数据放在请求体中,而不是附加在 URL 后面。适合传输较大或复杂的数据。
- 不受 URL 长度限制,可以传输更多数据,如表单数据、文件等。
POST /submit-form HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded name=John+Doe&email=john.doe%40example.com
3. 缓存和书签
-
GET:
- 请求可以被缓存,因此可以提高效率和减少服务器负担。
- URL 可以被书签保存,因为所有数据都在 URL 中。
-
POST:
- 请求通常不会被缓存,因此每次请求都会发送到服务器。
- URL 中不包含数据,不能直接书签保存请求的结果。
4. 安全性
-
GET:
- 请求数据以明文形式附加在 URL 中,可能会被浏览器历史记录或服务器日志记录,因此不适合传输敏感信息。
-
POST:
- 数据在请求体中传输,相对较难被直接看到。尽管如此,仍然需要通过 HTTPS 来加密数据以确保安全。
5. 使用场景
-
GET:
- 适用于获取资源,如访问网页、读取数据、搜索查询等。
- 示例:获取用户信息、查询产品列表等。
-
POST:
- 适用于提交数据或创建资源,如提交表单、上传文件、发送数据等。
- 示例:用户注册、提交评论、创建新文章等。
6. 例子
-
GET 请求:
GET /api/users?id=123 HTTP/1.1 Host: example.com该请求用于获取 ID 为 123 的用户数据,查询参数
id=123附加在 URL 中。 -
POST 请求:
POST /api/users HTTP/1.1 Host: example.com Content-Type: application/json { "name": "John Doe", "email": "john.doe@example.com" }该请求用于创建一个新的用户,用户数据在请求体中以 JSON 格式提交。
总结
- GET 用于获取数据,数据附加在 URL 中,适用于无副作用的请求(如读取资源)。
- POST 用于提交数据或创建资源,数据在请求体中,适用于可能有副作用的请求(如提交表单、上传文件)。
说一下浏览器输入 url 到页面加载的过程:
浏览器从输入 URL 到页面加载的过程涉及多个阶段,每个阶段都包括一系列复杂的操作。以下是这个过程的详细步骤:
1. 输入 URL
- 用户在浏览器地址栏中输入 URL 并按下 Enter。
2. 解析 URL
- 浏览器解析输入的 URL,将其分解为协议(如
http或https)、主机(如www.example.com)、路径(如/index.html)、查询参数(如?id=123)和锚点(如#section)等部分。
3. DNS 解析(域名解析)
-
浏览器需要将域名(如
www.example.com)解析为 IP 地址。这一过程涉及 DNS 查询:- 本地缓存:浏览器首先检查本地缓存中是否已存在该域名的 IP 地址。
- 操作系统缓存:如果本地缓存中没有,浏览器会查询操作系统的 DNS 缓存。
- DNS 服务器:如果操作系统缓存中也没有,浏览器会向 DNS 服务器发起查询,获取 IP 地址。
- 递归查询:DNS 服务器可能会进行递归查询,通过一系列的 DNS 服务器最终得到 IP 地址。
4. 建立连接
-
TCP 连接:
-
浏览器通过 IP 地址与目标服务器建立 TCP 连接。这一过程包括三次握手:
- SYN:客户端发送 SYN 包请求建立连接。
- SYN-ACK:服务器回应 SYN-ACK 包确认请求。
- ACK:客户端发送 ACK 包确认连接建立完成。
-
-
TLS/SSL 握手(如果使用 HTTPS):
- 如果 URL 使用 HTTPS,浏览器与服务器之间会进行 TLS/SSL 握手,以建立加密连接。这个过程包括证书验证和加密密钥交换。
6. 发送 HTTP 请求
- 浏览器构建一个 HTTP 请求报文,包括请求方法(如 GET 或 POST)、请求头、请求体等,发送到服务器。
7. 服务器处理请求
- 服务器接收请求,处理请求内容(如查询数据库、执行程序逻辑),并生成 HTTP 响应报文。
8. 接收 HTTP 响应
- 浏览器接收服务器返回的 HTTP 响应报文,包括状态码、响应头和响应体(HTML 内容、CSS 文件、JavaScript 文件等)。
9. 渲染页面
-
解析 HTML:浏览器解析 HTML 内容,构建 DOM(文档对象模型)树。
-
解析 CSS:浏览器解析 CSS 文件,构建 CSSOM(CSS 对象模型)树。
-
构建渲染树:结合 DOM 和 CSSOM,构建渲染树,确定页面的布局和样式。
-
布局和绘制:
- 布局:计算每个元素的位置和大小,确定页面的布局。
- 绘制:将页面内容绘制到屏幕上,包括文本、图像、背景色等。
-
执行 JavaScript:浏览器执行 JavaScript 脚本,可能会修改 DOM 和 CSSOM,从而影响页面的内容和样式。
10. 完成加载
- 当页面的 DOM 和样式表都被处理完毕,JavaScript 脚本执行完毕,页面渲染完成,浏览器会触发
load事件,表示页面已完全加载。
11. 资源加载和异步操作
- 页面加载后,可能会有异步资源(如图像、外部脚本)继续加载。浏览器会处理这些异步请求,确保所有资源正确加载和渲染。
总结
浏览器从输入 URL 到页面加载的过程包括 URL 解析、DNS 解析、TCP 连接、HTTP 请求和响应、页面渲染、资源加载等多个步骤。每个步骤都有其复杂性和重要性,确保网页能够快速、准确地呈现给用户。
答案另一种表述:
输入网址发生以下步骤:
- 通过
DNS解析域名的实际IP 地址 - 检查浏览器是否有
缓存,命中则直接取本地磁盘的html,如果没有命中强缓存,则会向服务器发起请求(先进行下一步的 TCP 连接) - 若
强缓存和协商缓存都没有命中,则返回请求结果 - 然后与 WEB 服务器通过
三次握手建立 TCP 连接。期间会判断一下,若协议是https则会做加密,如果不是,则会跳过这一步 - 加密完成之后,浏览器发送请求获取页面 html,服务器响应 html,这里的服务器可能是
server、也可能是cdn - 接下来是浏览器解析
HTML,开始渲染页面
顺便说了渲染页面的过程:
- 浏览器会将 HTML 解析成一个
DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。 - 将 CSS 解析成
CSS Rule Tree(css 规则树)。 - 解析完成后,浏览器引擎会根据
DOM 树和CSS 规则树来构造Render Tree。(注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像Header或display:none的东西就没必要放在渲染树中了。) - 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的
从属关系。下一步进行layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。 - 最后一个步骤就是
绘制,即遍历 RenderTree,层绘制每个节点。根据计算好的信息绘制整个页面。
渲染完成之后,开始执行其它任务:
- dom 操作
- ajax 发起的 http 网络请求等等……
- 浏览器处理事件循环等异步逻辑等等……
tree-shaking的作用,如何才能生效
Tree-shaking 是一种优化技术,用于在打包 JavaScript 代码时移除未使用的代码,从而减小最终构建文件的大小。它的主要作用是减少代码体积,提高应用的加载性能和运行效率。下面是对 Tree-shaking 的详细解释以及如何使其生效的方法。
Tree-Shaking 的作用
-
减少代码体积:
- 移除未使用的模块、函数和变量,减少生成的 JavaScript 文件的大小。
-
提高性能:
- 减少下载和解析的代码量,加快页面加载速度。
-
优化运行时:
- 减少不必要的代码执行,提升运行时性能。
如何使 Tree-Shaking 生效
1. 使用 ES6 模块
Tree-shaking 主要依赖于 ES6 模块(import 和 export)。与 CommonJS 模块不同,ES6 模块的静态结构允许打包工具在构建时分析和移除未使用的代码。
示例:
// utils.js
export function usedFunction() {
// Function code
}
export function unusedFunction() {
// Function code
}
// main.js
import { usedFunction } from './utils';
// Only `usedFunction` will be included in the final bundle
usedFunction();
2. 使用现代打包工具
现代打包工具如 Webpack 和 Rollup 支持 Tree-shaking。确保使用正确的配置来启用该特性。
Webpack 配置:
-
确保使用 ES6 模块:
- Webpack 默认支持 Tree-shaking,只要使用 ES6 模块。
-
配置
mode为production:- 在生产模式下,Webpack 会自动启用许多优化,包括 Tree-shaking。
// webpack.config.js module.exports = { mode: 'production', // Other configuration options }; -
确保使用
sideEffects:- 在
package.json文件中,使用sideEffects字段来声明哪些文件具有副作用,未使用的文件可以被移除。
// package.json { "sideEffects": false }- 如果只部分文件有副作用,可以指定一个数组:
// package.json { "sideEffects": [ "./src/some-file-with-side-effects.js" ] } - 在
Rollup 配置:
-
Rollup 天生支持 Tree-shaking:
- Rollup 通过静态分析和 ES6 模块支持 Tree-shaking,通常不需要额外配置。
// rollup.config.js export default { input: 'src/main.js', output: { file: 'dist/bundle.js', format: 'es', }, // Other configuration options };
3. 确保代码的可树摇
为了确保代码能够被 Tree-shaking 正确处理,遵循以下实践:
-
避免动态导入:
- 动态导入(如
require)会使 Tree-shaking 难以确定哪些模块是实际使用的。尽量使用静态导入。
- 动态导入(如
-
避免副作用:
- 确保模块不具有副作用,即导入一个模块不应影响其他模块或全局环境。副作用的模块可能无法被移除。
-
避免使用
export *:export *可能会导出所有内容,包括未使用的部分,影响 Tree-shaking 的效果。优先使用明确的命名导出。
总结
-
Tree-shaking:优化技术,用于移除未使用的代码,减小构建文件的体积。
-
生效条件:
- 使用 ES6 模块。
- 配置现代打包工具(如 Webpack 或 Rollup)。
- 确保代码的可树摇,避免副作用和动态导入。
Tree-shaking 是现代前端构建工具中的重要特性,可以显著优化代码的体积和性能。通过正确使用和配置,可以确保 Tree-shaking 的有效性。
手写防抖和节流
防抖(Debouncing)和节流(Throttling)是 JavaScript 中两种常用的技术,用于优化频繁触发的事件处理函数,如窗口调整大小、滚动、输入等。它们的目的是减少事件处理的频率,提高性能。
防抖(Debouncing)
防抖是一种优化技术,用于限制在短时间内重复触发某个函数。只有在事件触发停止后,函数才会执行。适用于需要等待事件停止后再执行的情况,如输入框的自动完成。
防抖实现:
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
使用示例:
// 输入框变化时进行防抖处理
const handleInput = debounce(function (event) {
console.log('Input value:', event.target.value);
}, 300);
document.getElementById('myInput').addEventListener('input', handleInput);
解释:
debounce函数接收两个参数:需要防抖的函数fn和延迟时间delay。- 返回一个新的函数,该函数在每次调用时会清除之前的定时器,并重新设置一个新的定时器。
- 只有在指定的延迟时间内没有再次调用新函数,
fn才会被执行。
节流(Throttling)
节流是一种优化技术,用于限制函数在一定时间内只能执行一次。适用于需要限制事件处理频率的情况,如滚动监听、窗口调整大小等。
节流实现:
function throttle(fn, limit) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
fn.apply(this, args);
}
};
}
使用示例:
// 滚动事件时进行节流处理
const handleScroll = throttle(function () {
console.log('Scroll event');
}, 200);
window.addEventListener('scroll', handleScroll);
解释:
throttle函数接收两个参数:需要节流的函数fn和限制时间limit。- 返回一个新的函数,该函数会记录上次调用的时间。
- 每次调用时,如果当前时间距离上次调用时间超过限制时间
limit,则执行fn并更新上次调用时间。
总结
-
防抖(Debouncing) :
- 适用于等待事件停止后再执行,如输入框自动完成。
- 实现方式:在事件触发后延迟执行,只有在指定时间内没有再次触发时才执行。
-
节流(Throttling) :
- 适用于限制事件处理频率,如滚动监听。
- 实现方式:限制函数在指定时间内只能执行一次。
这两种技术可以根据具体的需求来选择使用,有助于提高性能和用户体验。
手写深拷贝
深拷贝(Deep Copy)是指创建一个对象的完全独立副本,包括所有嵌套的对象和数组。与浅拷贝不同,深拷贝确保原始对象和新对象在内存中完全分开,修改一个对象不会影响另一个对象。
以下是实现深拷贝的几种方法,包括手动实现和使用 JavaScript 内置方法。
1. 使用递归实现深拷贝
这种方法通过递归遍历对象的所有属性来实现深拷贝。
function deepClone(obj, map = new WeakMap()) {
// 基本数据类型和函数直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 防止循环引用
if (map.has(obj)) {
return map.get(obj);
}
// 创建新对象或数组
const clone = Array.isArray(obj) ? [] : {};
// 记录循环引用
map.set(obj, clone);
// 遍历对象的所有属性
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], map);
}
}
return clone;
}
使用示例:
const original = {
name: 'Alice',
age: 30,
address: {
city: 'Wonderland',
zip: '12345'
},
hobbies: ['reading', 'hiking']
};
const copy = deepClone(original);
console.log(copy);
2. 使用 JSON 方法实现深拷贝
JSON.parse 和 JSON.stringify 是一种常用的深拷贝方法,适用于没有函数、undefined 和循环引用的对象。
function deepCloneUsingJSON(obj) {
return JSON.parse(JSON.stringify(obj));
}
使用示例:
const original = {
name: 'Alice',
age: 30,
address: {
city: 'Wonderland',
zip: '12345'
},
hobbies: ['reading', 'hiking']
};
const copy = deepCloneUsingJSON(original);
console.log(copy);
注意:
- 不支持:
JSON方法不能处理函数、undefined、Symbol、Date对象、正则表达式等特殊对象类型。 - 性能问题:对于大对象,
JSON方法可能会带来性能问题。
3. 使用 structuredClone 方法(在现代浏览器中)
structuredClone 是一个内置方法,用于深拷贝对象,支持更多的数据类型,包括 Date、Map、Set 等。
const copy = structuredClone(original);
使用示例:
const original = {
name: 'Alice',
age: 30,
address: {
city: 'Wonderland',
zip: '12345'
},
hobbies: ['reading', 'hiking'],
date: new Date()
};
const copy = structuredClone(original);
console.log(copy);
注意:
structuredClone方法在一些较老的浏览器中可能不支持。
总结
- 递归实现:适用于需要处理复杂对象和循环引用,能够自定义深拷贝逻辑。
- JSON 方法:简单易用,但不能处理某些特殊对象类型。
structuredClone:现代浏览器的内置方法,支持广泛的数据类型,但可能不在所有浏览器中可用。
实现promise
实现一个基本的 Promise 对象可以帮助理解 JavaScript 中异步编程的核心概念。Promise 是一个用于表示异步操作最终完成(或失败)及其结果的对象。一个基本的 Promise 实现需要处理以下几方面:
- Promise 状态管理:
Promise有三种状态:pending(等待中)、fulfilled(已完成)和rejected(已拒绝)。 - 异步处理:
Promise应该允许用户提供then和catch方法来处理异步操作的结果。 - 链式调用:
Promise应该支持链式调用,以便将多个异步操作串联起来。
下面是一个简单的 Promise 实现:
class MyPromise {
constructor(executor) {
this.state = 'pending'; // 状态: pending, fulfilled, rejected
this.value = undefined; // 成功的值
this.reason = undefined; // 失败的原因
this.onFulfilledCallbacks = []; // 存储成功回调
this.onRejectedCallbacks = []; // 存储失败回调
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(callback => callback(value));
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(callback => callback(reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
// then 方法
then(onFulfilled, onRejected) {
// 如果没有传入 onFulfilled 则使用默认的回调
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
// 如果没有传入 onRejected 则使用默认的回调
onRejected = typeof onRejected === 'function' ? onRejected : (reason) => { throw reason; };
return new MyPromise((resolve, reject) => {
const handleFulfilled = () => {
try {
const result = onFulfilled(this.value);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
};
const handleRejected = () => {
try {
const result = onRejected(this.reason);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
};
if (this.state === 'fulfilled') {
handleFulfilled();
} else if (this.state === 'rejected') {
handleRejected();
} else {
this.onFulfilledCallbacks.push(handleFulfilled);
this.onRejectedCallbacks.push(handleRejected);
}
});
}
// catch 方法
catch(onRejected) {
return this.then(null, onRejected);
}
}
解释
-
构造函数 (
constructor) :- 接收一个
executor函数,该函数包含两个参数:resolve和reject,用于改变Promise的状态。
- 接收一个
-
状态管理:
state存储当前Promise的状态。value存储成功时的值。reason存储失败的原因。onFulfilledCallbacks和onRejectedCallbacks用于存储成功和失败时的回调函数。
-
resolve和reject函数:- 改变
Promise的状态,并执行相应的回调函数。
- 改变
-
then方法:- 允许用户注册成功和失败的回调。
- 支持链式调用,返回一个新的
Promise。
-
catch方法:- 用于捕获
Promise的错误,等价于then(null, onRejected)。
- 用于捕获
使用示例
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => resolve('Success!'), 1000);
});
promise
.then(result => {
console.log(result); // "Success!"
return 'Another success!';
})
.then(result => {
console.log(result); // "Another success!"
})
.catch(error => {
console.error(error);
});
总结
这个 Promise 实现覆盖了基本的功能,包括状态管理、链式调用、异步处理等。但请注意,实际的 Promise 实现(如 ES6 的内建 Promise)包含更多的细节和优化,如对微任务队列的处理、错误传播等。这个简单实现旨在帮助理解 Promise 的核心概念。
实现promise.all
Promise.all 是一个用于处理多个 Promise 对象的静态方法。当所有传入的 Promise 对象都成功时,Promise.all 返回一个新的 Promise,该 Promise 的 resolve 回调会接收一个包含所有 Promise 结果的数组。如果其中一个 Promise 失败,Promise.all 返回的 Promise 将会立即拒绝,并返回第一个失败的 Promise 的错误信息。
实现 Promise.all
下面是一个简单的 Promise.all 实现:
function promiseAll(promises) {
return new Promise((resolve, reject) => {
// 存储每个 Promise 的结果
const results = [];
// 已完成 Promise 的计数器
let completed = 0;
// 如果传入的不是一个数组,直接拒绝
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'));
}
// 如果数组为空,直接 resolve 空数组
if (promises.length === 0) {
return resolve([]);
}
// 遍历所有传入的 Promise
promises.forEach((promise, index) => {
// 确保每个元素都是 Promise 对象
Promise.resolve(promise)
.then(value => {
// 存储结果
results[index] = value;
// 增加已完成的计数器
completed += 1;
// 如果所有 Promise 都已完成,resolve 结果数组
if (completed === promises.length) {
resolve(results);
}
})
.catch(err => {
// 其中一个 Promise 失败,立即 reject
reject(err);
});
});
});
}
解释
-
输入验证:
- 如果
promises不是数组,直接reject一个TypeError。 - 如果
promises数组为空,直接resolve一个空数组。
- 如果
-
处理每个 Promise:
- 使用
Promise.resolve(promise)确保每个元素都是 Promise 对象。 - 通过
then方法获取每个 Promise 的结果,并存储在results数组中。 - 使用
completed计数器跟踪已完成的 Promise 数量。
- 使用
-
完成处理:
- 当所有 Promise 都完成时,
resolve结果数组。 - 如果任何 Promise 失败,
reject返回第一个失败的 Promise 的错误信息。
- 当所有 Promise 都完成时,
使用示例
const p1 = Promise.resolve(1);
const p2 = new Promise((resolve) => setTimeout(() => resolve(2), 1000));
const p3 = new Promise((resolve, reject) => setTimeout(() => reject('Error'), 500));
promiseAll([p1, p2])
.then(result => {
console.log(result); // [1, 2]
})
.catch(error => {
console.error(error);
});
promiseAll([p1, p2, p3])
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error); // 'Error'
});
总结
promiseAll函数接收一个 Promise 数组,并返回一个新的 Promise,该 Promise 解决为包含所有 Promise 结果的数组,或在任意一个 Promise 失败时拒绝。- 实现包括输入验证、结果存储、计数器跟踪以及错误处理。
这个实现提供了对 Promise.all 行为的基本理解,但实际的 Promise.all 可能包含更多的优化和细节。
实现promise.retry
Promise.retry 是一个扩展功能,用于在 Promise 失败时自动重试。实现 Promise.retry 需要处理以下几个要点:
- 重试逻辑:当 Promise 失败时,指定重试次数和重试间隔时间。
- 成功处理:当 Promise 成功时,返回成功结果。
- 失败处理:当所有重试次数用尽仍然失败时,返回最后的失败错误。
下面是一个简单的 Promise.retry 实现:
function promiseRetry(fn, retries = 3, interval = 1000) {
return new Promise((resolve, reject) => {
function attempt(n) {
fn()
.then(resolve)
.catch(error => {
if (n <= 0) {
reject(error);
} else {
setTimeout(() => attempt(n - 1), interval);
}
});
}
attempt(retries);
});
}
解释
-
函数参数:
fn:一个返回 Promise 的函数。retries:最大重试次数,默认为 3。interval:重试间隔时间(毫秒),默认为 1000 毫秒(1 秒)。
-
内部
attempt函数:-
调用
fn并处理其返回的 Promise。 -
如果 Promise 解决(成功),调用
resolve返回成功结果。 -
如果 Promise 被拒绝(失败),检查重试次数:
- 如果重试次数用尽,调用
reject返回最后的错误。 - 否则,使用
setTimeout在指定的间隔后重新尝试调用attempt函数。
- 如果重试次数用尽,调用
-
使用示例
function unreliableOperation() {
return new Promise((resolve, reject) => {
const succeed = Math.random() > 0.5; // 50% 成功率
setTimeout(() => {
if (succeed) {
resolve('Success!');
} else {
reject('Failed!');
}
}, 500);
});
}
promiseRetry(unreliableOperation, 5, 2000)
.then(result => {
console.log(result); // 如果成功,输出 'Success!'
})
.catch(error => {
console.error(error); // 如果失败,输出 'Failed!'(在所有重试失败时)
});
解释
unreliableOperation:一个示例函数,模拟可能失败的操作。promiseRetry:尝试调用unreliableOperation函数,最多重试 5 次,每次间隔 2 秒。- 如果操作成功,
then输出成功结果。 - 如果所有重试均失败,
catch输出最终的失败错误。
总结
Promise.retry:用于在 Promise 失败时进行自动重试,直到成功或重试次数用尽。- 实现包括重试逻辑、成功和失败处理、以及延时处理。
这个实现提供了对 Promise.retry 行为的基本理解,可以根据具体需求进行优化和扩展。