1、vue3的响应式系统,是怎么跟他的父作用做关联的,非对象类型怎么处理的等等
Vue 3 的响应式系统利用了 ES6 中的 Proxy 对象来实现。在 Vue 3 中,每个组件实例都有一个对应的响应式对象,它通过 Proxy 对象来捕获对属性的访问和修改,并触发更新机制来实现响应式。
当在一个组件中定义一个响应式对象时,该对象会与组件实例建立联系,并且会自动和父子作用域建立关联。具体来说,响应式对象会在组件实例创建时进行初始化,并在父子组件关系建立时进行关联。
当一个响应式对象被创建时,它会被转换成一个 Proxy 对象,并使用一个隐藏的属性 __v_raw 来存储原始对象。这个隐藏属性是不能被外部访问的,它只能在内部用于判断一个对象是否已经被转换为 Proxy 对象。当我们访问响应式对象的属性时,Proxy 对象就会拦截这个访问,并触发依赖收集过程。
对于非对象类型,Vue 3 采用了一种叫做“wrapper”的技术来处理。例如,当我们在模板中使用一个基本类型值(如字符串或数值)时,Vue 3 会将其封装在一个简单的对象中,以便能够进行响应式处理。
在 Vue 3 中,除了对象和数组,所有其他原始类型值都会被封装为一个“wrapper”对象。这个对象包含一个内部属性 _value,用于存储原始的值。当我们在模板中使用一个基本类型值时,Vue 3 会使用这个“wrapper”对象来进行响应式处理。当我们修改这个值时,Vue 3 会更新“wrapper”对象的 _value 属性,并通知所有依赖该值的地方进行更新。
2、vue2,vue3的diff算法具体说说
diff 算法是用于在虚拟 DOM 中寻找差异并更新视图的
Vue 2 的 diff 算法采用了双端比较算法,它会对新旧节点进行同层级的比较。在比较过程中,如果发现新旧节点类型不同,则直接删除旧节点,并创建新节点;如果发现新旧节点类型相同,则继续进行同层级的子节点比较,依次递归下去。当所有子节点都比较完之后,如果新节点没有子节点了,但旧节点有子节点,则直接删除旧节点的子节点;如果新节点有子节点,但旧节点没有,则直接添加新节点的子节点。而对于相同节点,则采用“就地复用”的方式进行更新,只更新节点属性和子节点(这里的子节点指的是文本节点)。这样做的好处是,在保证更新速度的同时,还能减少不必要的 DOM 操作,提高性能。
Vue 3 的 diff 算法采用了更为高效的“静态标记”和“动态标记”技术。在首次渲染时,Vue 3 会根据模板生成一颗静态树,同样是采用双端比较算法进行 Diff 操作。但在更新过程中,Vue 3 会在静态树的基础上,再生成一棵动态树,用来表示那些可能发生改变的节点。因为静态节点在更新过程中不会发生变化,因此 Vue 3 可以跳过对静态节点的比较和更新。
在动态节点比较方面,Vue 3 的 diff 算法采用了“递归遍历+双端比较”的策略,这样可以最大程度地减少无效的 DOM 操作。具体来说,在比较两个动态节点时,Vue 3 会先比较它们的 key 值是否相同,如果不同,则直接删除旧节点,并创建新节点;如果相同,则继续进行同层级的子节点比较,依次递归下去。
3、如何理解前端模块化
是指将前端代码划分为相互独立、可复用的模块,以便于开发和维护。它的目标是提高代码的可维护性、可重用性和可扩展性,使前端开发更加高效和可靠。
可以从以下几个方面来考虑:
- 封装:模块化将代码封装在独立的模块中,每个模块负责特定的功能。便于后期的理解和维护。
- 依赖管理:通过模块化,可以明确地定义模块之间的依赖关系。模块可以通过导入其他模块来使用其功能,可以避免命名冲突和全局污染问题。同时,模块的依赖关系也使得代码复用更加方便,可以提高开发效率。
- 可复用性:模块化使得代码可以被多个项目或团队共享和复用。一个好的模块应该具有独立性和可组合性,可以在不同的项目中使用,提高开发效率和代码质量。
- 打包和构建:模块化对于打包和构建工具的支持非常重要。通过打包工具,可以将各个模块打包成一个或多个文件,减少网络请求次数,提高加载速度。同时,构建工具还可以对模块进行压缩、合并和优化,减小文件大小,提升性能。
4、amd、cmd、cjs、esm了解吗
- AMD(Asynchronous Module Definition):AMD 是 RequireJS 提出的模块化规范,主要用于浏览器端异步加载模块。使用
define函数来定义模块,并通过回调函数的方式来获取模块的依赖项。但由于其语法冗长,逐渐被其他更简洁的模块化方案所取代。 - CMD(Common Module Definition):CMD 是 SeaJS 提出的一种模块化规范,和 AMD 相似,也是用于浏览器端异步加载模块。不同的是,CMD 模块的加载方式是按需加载。使用
define函数来定义模块,但在获取模块的依赖项时采用延迟执行(按需加载)的方式。 - CommonJS(CJS):CommonJS 是一种服务器端的模块化规范,主要用于 Node.js 环境下的模块管理。它的特点是模块同步加载,使用
require来导入模块,使用exports或module.exports来导出模块。主要用于服务器端(如 Node.js) - ESM(ECMAScript Modules):ESM 是 JavaScript 的官方模块化规范,自 ES6 开始推广。使用
import来导入模块,使用export来导出模块。既可以用于浏览器端又可以用于服务器端,支持静态引用和编译优化,可以进行静态分析、摇树优化等。
5、esm被导出后,引用的地方去修改这个值,会影响原值吗?
默认导出
对于default这种默认导出方式来说,导出的是变量的值,而不是变量变量本身。相当于模块把变量赋值给一个特殊的内部变量,此后始终导出这个特殊变量。
导出的地方
let numA = 3
setTimeout(() => {
numA = 99
})
export default {
objA: {
nameA: 'esmA',
idsA: [1, 2, 3, 4, 5]
},
numA
}
引入的地方
// 默认导出
import * as esmA from './esmA.mjs'
console.log(esmA, '初始化的值=====');
setTimeout(() => {
console.log(esmA, '异步打印的esmA')
})
我们会发现打印出的numA的值都是为 3。
接下来我们尝试在引用的地方修改导入的值。
import * as esmA from './esmA.mjs'
console.log(esmA, '初始化的值=====');
esmA.default.numA++
esmA.default.objA.nameA = '修改objA.nameA'
console.log(esmA, '修改后的值=====');
结论:会影响原值
命名导出
导出的地方
let numA = 3
setTimeout(() => {
numA = 99
})
let objA = {
nameA: 'esmA',
idsA: [1,2,3,4,5]
}
export {
objA,
numA
}
引入的地方
// 命名导出
import { objA, numA } from './esmA.mjs'
console.log(objA, numA, '初始化的值=====');
setTimeout(() => {
console.log(numA) // 99
})
我们会发现先打印 3,再打印 99。因为setTimout中的console.log执行时,numA已经被赋值为 99,而且命名导出的是变量本身,类似于 C 语言中的指针。
接下来我们尝试在引用的地方修改导入的值。
import { objA, numA } from './esmA.mjs'
console.log(objA, numA, '初始化的值=====');
numA++ //这一行会报错 TypeError: Assignment to constant variable.
objA.nameA = '修改objA.nameA'
console.log(objA, numA, '修改后的值=====');
命名导出的变量,修改后报错的信息是与修改常量的报错信息是一致的,也就是说导入的变量的类似用了const声明。
把报错的
numA++这一行注释之后,只更改对象中的属性:
结论:会影响原值,但只能修改引用值中的属性。
在 ESM 中,当我们导入一个变量时,实际上是导入了该变量的引用。这意味着,如果导出的变量在导入模块中发生了改变,导入的变量也会随之改变。
而在 CommonJS 中,导入的是导出模块的值的拷贝,而不是引用。这意味着,即使导出模块中的值发生了改变,导入模块中导入的变量不会受到影响。
简而言之,ESM 导入的是值的引用,而 CJS 导入的是值的拷贝。
6、esm中,a模块中导入b模块,b模块中导入a模块,会出现死循环吗?
结论:不管是esm还是cjs都不会死循环
ES Module导出的是一份值的引用,CommonJS则是一份值的拷贝。也就是说,CommonJS是把暴露的对象拷贝一份,放在新的一块内存中,每次直接在新的内存中取值,所以对变量修改没有办法同步;而ES Module则是指向同一块内存,模块实际导出的是这块内存的地址,每当用到时根据地址找到对应的内存空间,这样就实现了所谓的“动态绑定”。
- CommonJS借助模块缓存,遇到require函数会先检查是否有缓存,已经有的则不会进入执行,在模块缓存中还记录着导出的变量的拷贝值;
- ES Module借助模块地图,已经进入过的模块标注为获取中,遇到import语句会去检查这个地图,已经标注为获取中的则不会进入,地图中的每一个节点是一个模块记录,上面有导出变量的内存地址,导入时会做一个连接——即指向同一块内存。
7、自己实现一个深拷贝,如何处理循环引用、window情况。
weakMap
let obj = {
a: 1,
b: {
c: 2
}
}
let foo = {
aa: window,
bb: obj,
cc: obj,
dd: foo
}
问:怎么手写一个函数深拷贝foo这个对象。
8、weakMap为什么不会内存泄漏
WeakMap 是 JavaScript 中的一种数据结构,它和普通的 Map 类似,也是用来存储键值对的。但是与普通的 Map 不同的是,WeakMap 中的键必须是对象,而且是弱引用,即当键不再被引用时,它所对应的值也会被自动释放,这样可以避免内存泄漏。
具体来讲,当一个对象作为 WeakMap 的键时,这个键对应的内存地址会被记录下来,但并不会增加这个对象的引用计数,也就是说,这个对象仍然可以被垃圾回收器回收。如果在程序运行过程中,这个对象不再被其他部分引用,那么垃圾回收器就会回收它所占用的内存,同时也会自动删除 WeakMap 中对应的键值对。
相比之下,如果使用普通的 Map,当一个对象作为键时,这个对象的引用计数会增加,即使这个对象已经不再需要了,也无法被垃圾回收器回收,直到显式地从 Map 中删除这个键值对。如果这个操作被忘记了,就会导致内存泄漏。
因此,WeakMap 能够避免内存泄漏,是因为它的键是弱引用的,不会增加对象的引用计数,也不会阻止垃圾回收器对这个对象进行回收。但是需要注意的是,由于 WeakMap 中的键是弱引用的,因此无法像普通的 Map 那样遍历所有的键,也无法判断 WeakMap 中是否存在某个键。同时,WeakMap 也不能使用 for...of 或 forEach 等迭代方法来遍历键值对。
9、大文件缓存:如何缓存一个几十M、几百M的数据
借助ServiceWorker(具体查阅其他资料)
10、借助ServiceWorker进行大文件缓存,具体用worker干了什么
(具体查阅其他资料)
11、async await 相比promise的链式调用的方式有什么优势呢。
- 更直观和易读:
async/await语法使异步代码看起来更像是同步代码,更直观和易于理解。它使用了类似于同步代码的结构,通过async声明异步函数,使用await关键字等待异步操作完成。 - 错误处理更简洁:在传统的 Promise 链式调用中,错误处理通常通过
.catch()方法或多个.then()方法中的回调函数来处理。而在async/await中,你可以使用try/catch块来捕获和处理异常,使错误处理更加简洁和集中。 - 更好的代码可读性和维护性:
async/await使得异步代码的流程更加线性和自然,减少了嵌套和回调地狱的问题。这样可以提高代码的可读性和维护性,降低理解和调试异步代码的难度。 - 更灵活的错误处理:
async/await允许你在每个异步操作之间执行自定义的错误处理逻辑,而不需要将错误处理逻辑集中在一个地方。你可以根据不同的情况对错误进行不同的处理,提供更灵活的错误处理能力。 - 支持同步风格的编程:使用
async/await可以方便地处理需要按照顺序执行的异步操作,而无需使用回调函数或者 Promise 链式调用。这使得编写复杂的同步风格代码变得更加容易。
12、vue3的组合式api相比vue2的选项式api,解决了什么问题
- 代码复用性:Vue 2 的选项式 API 在处理逻辑复用时存在一些限制。通常情况下,我们需要使用 mixins 或高阶组件来实现逻辑的复用,但这往往会导致代码的维护和理解变得困难。而组合式 API 提供了
setup函数,允许我们将逻辑组织为可复用的函数,从而更好地实现代码的复用。 - 组件逻辑的组织:Vue 2 的选项式 API 中,组件的逻辑往往是根据不同的选项(如
data、methods、computed等)进行分割的,这可能导致相关的逻辑在不同的选项中分散。而组合式 API 允许我们将相关的逻辑放在同一个函数中,使得组件的逻辑更加集中和清晰。 - 更好的类型推导和编辑器支持:由于 Vue 2 的选项式 API 是基于对象的,对于类型系统和编辑器支持并不友好。而组合式 API 利用 TypeScript 的类型推导和编辑器支持,提供了更好的类型检查和自动补全,使我们能够更早地发现错误,并提供更好的开发体验。
- 解决了命名冲突和命名空间污染的问题:在 Vue 2 中,不同的选项(如
data、methods、computed等)中可能存在命名冲突或命名空间污染的问题。而组合式 API 允许我们使用函数来定义逻辑,避免了命名冲突和命名空间污染的问题。