1.计算属性、watch侦听、方法的区别; Watch \ WatchEffect
计算属性: 是Vue.js中的一种数据属性,它能根据其他数据属性的值计算出一个新的值。计算属性是基于它们的依赖进行缓存的。只有当计算属性的依赖发生变化时,计算属性才会重新计算。使用计算属性可以避免在模板中写复杂的逻辑,提高代码的可读性和可维护性。 应用场景:当需要对数据进行复杂的计算或格式化时,可以使用计算属性;比如,计算商品的总价、格式化日期等。(总结:依赖其他数据,只有当所依赖的数据发生变化时才会重新计算,否则一直读取的是缓存的值)
watch侦听:是Vue.js中的一个观察者,它可以监听数据属性的变化,并在变化时执行相应的操作。watch可以监听单个数据属性或一组数据属性的变化,也可以监听对象或数组的变化。使用watch可以在数据发生变化时,及时地更新页面或执行其他操作。 应用场景:当需要在数据发生变化时执行某些操作,比如异步请求数据、提交表单等,可以使用watch。另外,当需要监听对象或数组的变化时,只能使用watch,不能使用计算属性。(总结:更多的是观察作用,无缓存性,当侦听的数据发生变化时才执行其回调函数,可以执行异步操作)
计算属性 VS watch侦听:watch是监听数据属性的变化,当数据发生变化时执行相应的操作;而计算属性是根据其他数据属性计算出新的值,不需要手动触发。计算属性中不建议执行异步函数,因为计算属性是基于它的依赖缓存的,当计算属性所依赖的数据发生变化时,计算属性会重新计算并缓存结果。如果计算属性中执行了异步函数,那么每次计算属性被调用时都会触发异步函数执行,这样会大大降低应用的性能。如果需要执行异步操作,可以使用watch监听数据的变化,并在回调函数中执行异步操作。或者可以使用Vue提供的异步组件加载机制,将异步组件的加载放在created或mounted钩子函数中,避免在计算属性中执行异步操作。
计算属性 VS 方法:计算属性是基于它们的依赖进行缓存的。只有当计算属性的依赖发生变化时,计算属性才会重新计算。而方法在每次调用时都会重新执行。因此,如果一个值需要在多个地方使用,使用计算属性可以减少重复计算,提高性能;计算属性可以像数据属性一样在模板中直接使用。而方法必须在模板中调用才能使用。
2.Vue的生命周期钩子
Vue的生命周期可以分为8个阶段:创建阶段、挂载阶段、更新阶段和销毁阶段,每个阶段都有对应的钩子函数。具体如下:
1.创建实例阶段
beforeCreate:实例刚被创建,数据观测和事件机制都未初始化,不能访问props、data、computed、methods和watch等数据和方法。created:实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成。但是此时虚拟DOM还没有被渲染成真正的DOM,因此无法访问到DOM元素。(模板编译:模板编译是在Vue实例的创建过程中,即在beforeCreate和created钩子函数之间进行的,具体来说是在beforeCreate钩子函数之后、created钩子函数之前的阶段。在这个阶段,Vue会将模板解析成抽象语法树(AST),然后通过AST生成渲染函数,即render函数或者template渲染函数。)2.挂载阶段
beforeMount:相关的render函数首次被调用,把data里面的数据和模板生成html。Vue将模板编译成虚拟DOM,然后再将虚拟DOM渲染成真正的DOM,将其插入到页面中。beforeMount钩子函数在模板编译完成之后,但是在将虚拟DOM渲染成真正的DOM之前触发,因此此时可以访问到虚拟DOM,但是真正的DOM还没有被渲染出来,因此无法访问到真正的DOM元素。mounted:模板已经渲染成HTML并插入页面,此时可以访问到真实的DOM元素进行DOM操作,进行数据初始化、异步请求等操作。3.更新阶段
beforeUpdate:数据更新时触发,但尚未重新渲染虚拟DOM。可以访问到最新的数据和旧的数据。该阶段建议不应该对数据进行修改,否则会导致无限循环的更新。如果需要对数据进行修改,应该在watch或computed等响应式API中进行。updated:此时DOM已经完成了更新,可以访问新的DOM和更新后的数据。updated钩子函数中可以访问到更新后的DOM和数据,可以进行一些DOM操作或者更新数据的操作。4.销毁阶段
beforeDestroy(beforeUnmount):实例销毁之前触发,此时实例仍然完全可用,可以进行一些清理工作,比如清除定时器、取消事件监听等。destroyed(unmounted):实例销毁后触发,此时实例所有的数据和方法都已经被清除,不能再进行访问。
3.Vue中组件之间的传值方式
- 父组件向子组件传值:通过
props向子组件传递数据。- 子组件向父组件传值:通过事件向父组件传递数据。
- 兄弟组件之间传值:通过一个共同的父组件进行数据传递,即通过父组件作为中转站来传递数据(eventbus)。
- 跨级组件传值:通过
provide/inject实现跨级组件传递数据。- 使用Vuex/Pinia:将需要共享的数据存储在Vuex/Pinia的store中,各个组件通过store来访问和修改数据。
- 使用listeners:listeners可以获取父组件绑定在子组件上的所有事件,这两个属性可以在子组件中实现非prop属性和事件的传递。
- ref和$refs可以用来在同一组件内部访问子组件或者DOM元素,但不是用来进行组件间传值的。
4.localStorage sessionStorage cookies 有什么区别?
localStorage、sessionStorage和cookies都是在客户端存储数据的方式,但是它们有以下几点区别:
- 存储容量不同:localStorage和sessionStorage的存储容量一般是5MB,而cookies的存储容量一般只有4KB。
- 数据有效期不同:localStorage存储的数据永久保存,除非手动清除,而sessionStorage存储的数据只在当前会话中有效,在关闭浏览器窗口或者标签页之后失效。而cookies可以设置一个过期时间,在过期之后失效。
- 数据共享方式不同:localStorage和sessionStorage只能在同源(同协议、同域名、同端口)的页面之间共享数据,不同源的页面访问不到;而cookies可以在不同源之间共享。
- 与服务器交互方式不同:localStorage和sessionStorage是在客户端进行存储和获取,不需要与服务器进行交互;而cookies可以设置httpOnly属性,防止客户端通过JavaScript获取cookie,提高了安全性,但是需要与服务器进行交互。
总的来说,localStorage和sessionStorage适合存储一些较大的、不需要与服务器进行交互的数据,比如用户的浏览记录、表单数据等;而cookies适合存储一些需要与服务器进行交互的数据,比如用户的登录信息、购物车信息等。
5.什么是重排和重绘,它们之间有什么联系?
重排(
reflow)和重绘(repaint)是浏览器渲染网页时发生的两个关键过程。重排:指的是当网页的布局和几何属性发生改变时,浏览器需要重新计算元素的位置和大小,并重新布局整个页面。例如当添加或删除元素、改变元素的大小或位置都会触发重排(reflow 的本质就是重新计算 layout 树)。
重绘:指的是当网页的样式(例如背景颜色、字体大小等)发生改变时,浏览器需要重新绘制受影响的元素。重新绘制并不需要重新计算元素的位置和大小,因为元素的几何属性没有改变。例如改变元素的颜色、字体、背景等样式都会触发重绘。
它们之间的联系是,重排一定会触发重绘。因为当元素的布局和位置发生改变时,它的外观也会发生变化,需要重新绘制。但是重绘不一定会触发重排,因为元素的外观变化不一定会影响布局和位置(总结:只要变化引起元素大小和页面布局发生变化的就会触发重排,其余变化触发重绘)。
为了提高页面性能,应该尽量减少 DOM 操作和样式修改从而减少重排和重绘的次数。可以通过以下方法来实现:
- 合并样式修改:将多个样式修改合并成一次修改,减少触发重排和重绘的次数。
- 使用CSS3动画代替JavaScript动画:CSS3动画会触发GPU加速,性能更好。
- 将元素脱离文档流后进行修改:脱离文档流后,元素的修改不会影响其他元素的布局和位置,可以减少重排和重绘的次数。
6.HTML5、CSS3、ES6新增特性
HTML5、CSS3、ES6是Web开发中的三种重要技术,下面分别介绍它们的新增特性:
- HTML5新增特性:
(1)语义化标签:header、nav、section、article、aside、footer等。
(2)多媒体:
Canvas:可以使用 JavaScript 绘制 2D 和 3D 图形。SVG:支持矢量图形,可以通过 JavaScript 来操作和修改 SVG。audio(音频)和video(视频):支持在网页中嵌入音频和视频。(3)表单:增加了新的表单元素和属性,如date、time、email、url、number等。
(4)Web本地存储:localStorage和sessionStorage。
(5)Web Workers:在后台运行JavaScript程序,提高网页的响应速度。
(6)Geolocation API:获取用户地理位置信息。
- CSS3新增特性:
(1)盒子模型:增加了box-sizing(怪异盒模型)属性,可以改变盒子模型的计算方式。
(2)选择器:新增伪类、伪元素和属性选择器。
(3)渐变:新增linear-gradient和radial-gradient属性,可以实现渐变效果。
(4)阴影和边框:新增box-shadow、text-shadow和border-radius属性,可以实现阴影和圆角边框效果。
(5)动画和过渡:新增animation和transition属性,可以实现动画和过渡效果。
- ES6新增特性:
(1)变量声明:新增let和const关键字,可以用来声明块级作用域变量。
(2)箭头函数:简化函数的写法。
(3)模板字符串:可以使用反引号来定义多行字符串和插值表达式。
(4)解构赋值:可以从数组或对象中提取值,并赋值给变量。
(5)类和继承:新增class和extends关键字,可以使用面向对象的方式编程。
(6)Promise:用于处理异步操作,可以避免回调地狱的问题。
以上是HTML5、CSS3和ES6的一些主要新增特性,它们都可以大大提高Web开发的效率和体验。
7.常见的检测数据类型的几种方式?
- typeof :
我们可以使用
typeof来判断number, string, object, boolean, function, undefined, symbol 这七种数据类型,可以使用typeof用于判断基本类型,点击查看MDN文档。
但是
object类型不能具体细分其包含的数据类型(对象,数组,Set类型,Map类型),并且null类型也会被判断为"object"。在 JavaScript 中,可以使用 typeof 运算符来判断值的数据类型。但是,
typeof运算符并不是完全准确的,它只能判断出基本数据类型和函数类型,无法区分引用数据类型(对象、数组)和基本数据类型null,结果都为object
- instanceof:
主要的作用就是判断一个实例是否属于某种类型,
instanceof主要的实现原理就是只要右边变量的prototype在左边变量的原型链上即可。因此,instanceof在查找的过程中会遍历左边变量的原型链,直到找到右边变量的prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。参考文章。function Foo() {} Object instanceof Object // true Function instanceof Function // true Function instanceof Object // true Foo instanceof Foo // false Foo instanceof Object // true Foo instanceof Function // true
- Object.prototype.toString:
因此,如果要准确无误地判断 JavaScript 中的所有数据类型,可以使用
Object.prototype.toString方法。这个方法返回一个表示对象类型的字符串,可以准确判断出所有的数据类型,包括基本数据类型、复杂数据类型和函数类型。例如,以下是一些数据类型的判断:
console.log(Object.prototype.toString.call(1)); // [object Number] console.log(Object.prototype.toString.call("")); // [object String] console.log(Object.prototype.toString.call(true)); // [object Boolean] console.log(Object.prototype.toString.call(Symbol())); // [object Symbol] console.log(Object.prototype.toString.call(undefined)); // [object Undefined] console.log(Object.prototype.toString.call(null)); // [object Null] console.log(Object.prototype.toString.call({})); // [object Object] console.log(Object.prototype.toString.call([])); // [object Array] console.log(Object.prototype.toString.call(function(){})); // [object Function] console.log(Object.prototype.toString.call(new Date())); // [object Date] console.log(Object.prototype.toString.call(/test/)); // [object RegExp]
- 封装一个判断类型的方法
function getType(value) { let type = typeof value if (type !== 'object') { // 如果是原始数据类型,直接返回 return type } // 如果是引用数据类型,再进一步判断,正则返回结果 let typeValue = Object.prototype.toString.call(value).replace(/^\[object (\S+)\]$/, '$1') let firstChar = typeValue.charAt(0).toLowerCase() let newStr = firstChar + typeValue.slice(1) return newStr } getType(123); // number getType('xxx'); // string getType(() => {}); // function getType([]); // array getType({}); // object getType(null); // null
8.Vue中定义data里面数据为啥要有return,没有return行不行?
在Vue中定义data选项时,我们需要使用一个函数来返回一个对象,这个对象包含了组件内部需要的一些数据。这个函数是一个工厂函数,每次调用时都会返回一个新的对象,这样可以避免不同实例之间共享数据,造成数据混乱的问题。
这个函数的返回值是必须的,因为Vue会把这个返回值作为组件的数据来使用。如果没有返回值,Vue就无法获取到组件的数据,就会导致组件数据为空,从而引发一些难以排查的问题。
虽然在定义data选项时,可以不显式地写return语句,但是Vue会默认返回一个空对象。这样做会让代码不够明确,也容易引发潜在的错误。而且,如果需要在data选项中动态计算一些数据,就必须使用return语句来返回一个包含这些数据的对象。
因此,为了保证代码的清晰明了,避免潜在的错误,以及支持动态数据计算,我们建议在Vue中定义data选项时始终使用return语句来返回一个包含组件数据的对象。
同时data必须是一个函数而不能是一个对象:这是为了防止多个组件的数据产生污染。 如果都是对象的话,会在合并的时候,指向同一个地址,会导致数据混乱。 而如果是函数的时候,合并的时候调用,会产生多个空间。保证各个组件数据的独立性和可维护性。(Vue根组件的data可以为一个对象)
9.深浅拷贝
深浅拷贝是针对引用数据类型而言。基本数据类型在拷贝时会直接复制其值。 常用的深拷贝和浅拷贝方法:
浅拷贝:
- 直接赋值
- Object.assign()
let user1 = { name : "蓝月亮" } let user2 = Object.assign(user1, {age : 18}, {sex : 'male'}); console.log(user2); // { name: '蓝月亮', age: 18, sex: 'male' }深拷贝:
- JSON.parse(JSON.stringify())
let obj1 = {a: 1, b: {c: 2}, d: [3, 4]}; let obj2 = JSON.parse(JSON.stringify(obj1)); // {a: 1, b: {c: 2}, d: [3, 4]}这种方法有一些缺点如下:
- 无法处理循环引用的情况,会报错;
- 无法序列化函数、RegExp、Error等特殊对象,会被忽略掉;
- 对于NaN、Infinity和null的处理不一致;
- 无法处理Date对象,会被转换成字符串;
- 无法处理undefined;
- 无法处理Symbol类型的数据;
- 对于一些复杂的数据类型,比如Map、Set、WeakMap、WeakSet等,也无法正确地实现深拷贝。
- 手写递归的方式来实现深拷贝
基本实现:
function isObject(value) { const valueType = typeof value return (value != null) && (valueType === 'object' || valueType == 'function') } function deepClone(originValue) { if(!isObject(originValue)){ return originValue } const newObject = Array.isArray(originValue) ? [] : {} for (const key in originValue) { newObject[key] = deepClone(originValue[key]) } return newObject }完整实现:
function isObject(value) { const valueType = typeof value return (value !== null) && (valueType === "object" || valueType === "function") } function deepClone(originValue) { // 判断是否是一个Set类型 if (originValue instanceof Set) return new Set([...originValue])} // 判断是否是一个Map类型 if (originValue instanceof Map) return new Map([...originValue])} // 判断如果是Symbol的value, 那么创建一个新的Symbol if (typeof originValue === "symbol") return Symbol(originValue.description) // 判断如果是函数类型, 那么直接使用同一个函数 if (typeof originValue === "function") return originValue // 对时间Date处理直接返回一个新的Date if (originValue instanceof Date) return new Date(originValue) // 判断传入的originValue是否是一个对象类型 if (!isObject(originValue)) return originValue // 判断传入的对象是数组, 还是对象 const newObject = Array.isArray(originValue) ? []: {} for (const key in originValue) { newObject[key] = deepClone(originValue[key]) } // 对Symbol的key进行特殊的处理 // Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有 Symbol 属性的数组。 const symbolKeys = Object.getOwnPropertySymbols(originValue) for (const sKey of symbolKeys) { // const newSKey = Symbol(sKey.description) newObject[sKey] = deepClone(originValue[sKey]) } return newObject }
10.for...in 和 for...of的区别?
for...in和for...of都是循环语句,但是它们的使用场景和循环方式有所不同。
- for...in循环
for...in循环主要用于遍历对象的可枚举属性,也可以遍历数组的索引(index),但是不推荐使用for...in遍历数组,因为它会把数组的原型链上的属性也遍历出来。
const obj = { a: 1, b: 2, c: 3 }; for (const key in obj) { console.log(key, obj[key]); } // 输出:a 1, b 2, c 3
- for...of循环
for...of循环主要用于遍历可迭代对象,如数组、字符串、Set、Map等,它可以遍历元素的值,而不是索引或属性名。for...of是ES6(ECMAScript 2015)中新增的特性。
const arr = [1, 2, 3]; for (const item of arr) { console.log(item); } // 输出:1, 2, 3
11.Event Loop浏览器的事件循环
Event Loop是JavaScript实现异步编程的一种机制,它是JavaScript的核心部分,负责管理调用栈、消息队列和微任务队列。在JavaScript中,所有的任务都被分为同步任务和异步任务。同步任务会立即执行,而异步任务则会被放入消息队列中,等待Event Loop的处理。
当执行栈为空时,Event Loop会从消息队列中取出一个任务,压入执行栈中执行。异步任务分为宏任务和微任务两种。宏任务包括setTimeout、setInterval、setImmediate、requestAnimationFrame、I/O操作等,而微任务则包括Promise、process.nextTick等。
在每次执行宏任务前,Event Loop会清空所有的微任务队列。微任务的执行优先级高于宏任务,因此微任务会优先执行,直到所有微任务执行完毕,才会执行下一个宏任务。
总的来说,Event Loop是JavaScript实现异步编程的核心机制,它通过调度和管理任务队列的方式,实现了JavaScript在单线程模型下的异步执行。
12.强大的Promise
Promise是JavaScript中的一种异步编程解决方案,用于处理异步操作。它可以将异步操作封装成一个Promise对象,该对象代表了一个异步操作的最终完成状态(成功或失败)和其返回的值。Promise对象有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),一旦Promise对象的状态发生变化,就会触发相应的回调函数。Promise可以链式调用,实现逻辑清晰、代码简洁的异步操作。
13.异步处理async-await
async-await是一种异步处理方式,它是ES2017引入的新特性。
在异步编程中,我们通常会使用回调函数、Promise等方式来管理异步任务,但这些方式都存在一些问题,比如回调地狱、Promise链过长等。async-await则是为了解决这些问题而出现的。
async-await的基本思想是通过async声明一个异步函数,然后在函数内部使用await等待异步操作的完成,从而实现代码的同步化书写。使用async-await可以让异步代码更加易读、易维护。
14.v-model语法糖
v-model是Vue中的一个语法糖,它可以用于在表单元素和组件上创建双向数据绑定。使用v-model可以将表单元素(如input、textarea、select等)的值与Vue实例中的数据进行双向绑定,当表单元素的值发生变化时,Vue实例中的数据会自动更新,反之亦然。
15.箭头函数和普通函数的区别
(1) 箭头函数比普通函数更加简洁
- 如果没有参数,就直接写一个空括号即可
let fn = () => true- 如果只有一个参数,可以省去参数括号
let fn = a => true- 如果有多个参数,用逗号分割
let fn = (a,b,c) => true- 如果函数体的返回值只有一句,可以省略大括号和
let fn = () => true- 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常用的就是调用一个函数:
let fn = () => void doesNotReturn()(2) 箭头函数没有自己的
this,它的this始终指向定义时所在的上下文环境,而不是执行时的上下文环境。在普通函数中,this的值取决于函数的调用方式。(3) 箭头函数没有自己的
arguments对象,它的arguments对象始终指向定义时所在的上下文环境的arguments对象。在普通函数中,arguments对象表示当前函数调用时的参数列表。(4) 由于箭头函数的
this在函数定义时就已经确定好了,所以call()、apply()、bind()等方法不能改变箭头函数中的this指向(5) 箭头函数不能用作构造函数,不能通过
new关键字创建对象。普通函数可以用作构造函数,可以通过new关键字创建对象。(6) 箭头函数没有
prototype属性,也不能通过Object.getPrototypeOf方法获取原型。普通函数有prototype属性,可以通过Object.getPrototypeOf方法获取原型。
16.JavaScript中 New操作符做了什么事情?
JavaScript 中的
new操作符用于创建一个新的对象,它的具体执行过程包括以下几个步骤:(1)创建一个空对象
{ },作为将要返回的对象实例。(创建对象)(2)将这个空对象的原型
__proto__指向构造函数的prototype属性,这样新对象就可以访问构造函数原型上定义的属性和方法。(设置原型)(3)将构造函数的
this指向新对象。(修改this指向)(4)执行构造函数内部的代码,给新对象添加属性和方法。判断返回值类型:如果构造函数返回了一个对象
return { },那么这个对象会取代new创建的新对象成为返回的对象实例。 如果构造函数没有返回任何值或者返回了一个非对象类型的值,则返回new创建的新对象实例。(执行代码,确定返回值)function Person(name, age) { this.name = name; this.age = age; } // 在构造函数Person原型上添加sayName方法 Person.prototype.sayName = function () { console.log(this.name); } const person = new Person('Tom', 18); // person就是new操作时返回的那个对象 console.log(person); // Person { name: 'Tom', age: 18 } console.log(person.name); // 输出 Tom console.log(person.age); // 输出 18 person.sayName(); // 输出 Tom // 在这个例子中,使用 new 操作符创建了一个 Person 对象实例,该对象实例可以访问 // Person.prototype 上定义的 sayName 方法,同时也拥有 name 和 age 属性。
17.JavaScript 中的this关键字
在 JavaScript 中,
this关键字用于引用当前函数执行时的上下文对象。具体来说,this的值取决于函数的调用方式,它可能是:
- 全局对象:如果函数是在全局作用域中调用的,
this就是全局对象(浏览器环境中是window对象,Node.js 环境中是global对象)。(默认绑定)- 调用者对象:如果函数是作为对象的方法调用的,
this就是调用该方法的对象。(隐式绑定)- 显示绑定的对象:如果函数是通过
call、apply或bind方法绑定到指定对象上的,this就是该指定对象。(显示绑定)、- 构造函数实例对象:如果函数是作为构造函数使用的,
this就是新创建的对象实例。(new绑定)this的绑定优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
需注意:
- (1) 箭头函数的
this始终指向定义时所在的上下文环境,而不是执行时的上下文环境。- (2) call和apply的作用是一样的,都是立即执行函数并改变函数的this指向,区别在于传参方式不同。call的参数是一个一个传入的,apply的参数是以数组形式传入的。bind不会立即执行函数,而是返回一个新的函数,这个新函数的this指向被绑定的对象,调用这个新函数时,新函数会执行原函数,并且原函数的this指向被绑定的对象。
18.Vue中 v-show 和 v-if 的区别?
v-show和v-if都是 Vue 中用来控制元素显示和隐藏的指令,但它们之间有一些不同之处:
- 编译时差异:
v-show在编译时会直接被渲染到模板中,而v-if会根据表达式的值在运行时进行条件渲染,当表达式的值为false时,对应的元素不会被渲染到页面中。- 显示方式差异:
v-show通过 CSS 的display属性控制元素的显示和隐藏,当指令的值为false时,元素会被隐藏但不会从 DOM 中移除(但是元素并不会占据页面空间)。而v-if则是通过动态添加和删除 DOM 元素实现的条件渲染,当指令的值为false时,对应的元素会从 DOM 中移除。- 切换性能差异:
v-show切换元素的显示和隐藏时,只需要修改元素的display属性,不会引发 DOM 的重新构建和渲染,因此在频繁切换元素的显示和隐藏时,v-show的性能优于v-if。而v-if切换元素的显示和隐藏时,需要不断地添加和删除 DOM 元素,会引发 DOM 的重新构建和渲染,因此在频繁切换元素的显示和隐藏时,v-if的性能较低。综上所述,如果需要频繁切换元素的显示和隐藏,应该使用
v-show,否则使用v-if即可。二者都可能引起浏览器的重排,可以尝试通过CSS的opacity或visibility属性来实现,这些属性的变化通常只会引发重绘,而不会引起重排,对页面的性能影响较小。
19.Vue中的 v-for 和 v-if
- Vue官方并不建议将v-for和v-if同时作用于同一个元素上。因为二者的优先级并不明确,在Vue2版本中v-for 的优先级是高于v-if的,但是在Vue3版本中v-if的优先级是高于v-for的。
场景一:每次重新渲染的时候,都要重新遍历整个列表,其实我们只需要列表的一部分,这样做浪费性能。推荐的做法是,通过计算属性先过滤出我们需要的部分,再去渲染,更高效。(当我们需要通过v-for遍历得到里面的数据来判断显示和隐藏的条件时就应该使用计算属性过滤得到判断条件,然后只使用v-if)
`<li v-for="user in users" v-if="user.show">`场景二:当需要先判断globalShow是否为真时再遍历就可以将v-if上移至上层容器或template。
`<li v-for="user in users" v-if="globalShow">`
20.Vue的响应式原理(若想掌握更好建议阅读源码)
一:Vue2的响应式原理:
Vue2的响应式原理主要分为以下几个步骤:
- 对象劫持
在Vue2中,当一个对象被Vue实例化后,Vue2会通过递归地遍历其所有属性,对其属性进行劫持,即使用
Object.defineProperty()方法将属性转化为getter(依赖搜集)和setter(派发更新)方法,从而使得当属性被读取或修改时,Vue2能够自动地检测到变化并进行响应式更新。
- 依赖收集
(在Vue2中为对象中每个属性添加getter方法时会搜集所有依赖的操作,并存储在属于该属性的依赖存储器中) 在Vue2中,当一个组件访问一个响应式对象的属性时,Vue2会将该组件与该属性建立一个依赖关系,即将组件实例作为观察者,将该属性的变化作为目标,建立一个订阅者与发布者之间的关系。这个过程就是依赖收集,Vue2会将组件实例与属性之间的依赖关系管理起来,从而使得当属性发生变化时,能够自动地通知与之相关的组件进行更新。
- 派发更新
(当修改该属性时就会被setter方法所监听到,并派发指令更新该属性依赖存储器中的所有依赖) 当响应式对象的某个属性发生变化时,Vue2就会自动地通知与之相关的组件进行更新。这个过程称为派发更新。
总体来说,Vue2的响应式原理是通过对对象的劫持、依赖收集和派发更新等过程来实现的,这个过程是在Vue2实例化时自动进行的,使得开发者可以方便地进行数据绑定和更新视图。
- 对象中新增属性和删除属性的监听
Vue.set()方法可以向一个对象中添加一个新属性,并且能够触发响应式更新。它接受三个参数:要添加属性的对象、属性名和属性值。
Vue.delete()方法可以删除对象中的一个属性,并且能够触发响应式更新。它接受两个参数:要删除属性的对象和属性名。
- 监听数组的变化具体实现如下:
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(method => { // 缓存原始方法 const original = arrayProto[method] // 重写方法 def(arrayMethods, method, function mutator(...args) { // 调用原始方法 const result = original.apply(this, args) // 获取数组对象的观察者实例 const ob = this.__ob__ // 对于 push、unshift、splice 方法,需要对新增元素进行观察 let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // 发送通知 ob.dep.notify() return result }) }) function def(obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }上述代码中,我们定义了一个
arrayMethods对象,它继承自Array.prototype,并重写了数组的七个方法:push、pop、shift、unshift、splice、sort 和 reverse。在重写这些方法时,我们先缓存了原始方法,然后在重写后的方法中调用了原始方法并获取了返回值。接着,通过
this.__ob__获取数组对象的观察者实例,并对新增元素进行观察(对于 push、unshift 和 splice 方法),最后发送通知(通过ob.dep.notify())。重写数组方法的核心代码是
def(arrayMethods, method, function mutator (...args) {...}),它使用了一个名为def的工具方法,用于在对象上定义属性或方法。这个方法的具体实现可以参考 Vue 源码的util/lang.js文件。二、Vue3的响应式原理:
待完善Vue3使用
Proxy实现响应式。具体实现原理如下:
- 在初始化时,Vue3会对data进行遍历,将data中的每个属性都转换为响应式数据。对于数组,Vue3会对数组的方法进行劫持,以便能够自动更新视图。
- 在实现响应式数据时,Vue3使用Proxy代理对象,将data中的每个属性转换为代理对象的属性。当访问代理对象的属性时,会调用get方法,该方法会将当前的Watcher对象添加到依赖列表中。
- 当data中的属性值发生变化时,会触发代理对象的set方法。set方法会遍历依赖列表,通知每个Watcher对象更新视图。
- 在组件销毁时,Vue3会将所有的依赖列表清空,以便能够释放内存。
总的来说,Vue3使用Proxy代理对象实现响应式,能够更好地支持动态添加和删除属性,而且性能也比Vue2更好。
简单实现Vue2-Vue3的响应式原理:
class Dep { constructor() { this.subscribers = new Set() } depend() { if(activeEffect) { this.subscribers.add(activeEffect) } } notify() { this.subscribers.forEach(effect => effect()) } } let activeEffect = null function watchEffect(effect) { activeEffect = effect effect() activeEffect = null } const targetMap = new WeakMap() function getDep(target, key) { // 创建Map let depsMap = targetMap.get(target) if(!depsMap) { depsMap = new Map() targetMap.set(target, depsMap) } // 取出dep let dep = depsMap.get(key) if (!dep) { dep = new Dep() depsMap.set(key, dep) } return dep } // Vue2数据劫持 // function reactive(raw) { // Object.keys(raw).forEach(key => { // const dep = getDep(raw, key) // let value = raw[key] // Object.defineProperty(raw, key, { // get() { // // 依赖搜集 // dep.depend() // return value // }, // set(newValue) { // // 派发更新 // if (value !== newValue) { // value = newValue // dep.notify() // } // } // }) // }) // return raw // } // Vue3对raw进行数据劫持 function reactive(raw) { return new Proxy(raw, { get(raw, key) { const dep = getDep(raw, key); dep.depend(); return Reflect.get(raw, key) }, set(raw, key, newValue) { const dep = getDep(raw, key); Reflect.set(raw, key, newValue) dep.notify(); } }) } // 测试代码 const info = reactive({ counter: 100, age: '18岁了' }) const foo = reactive({ address: '重庆' }) watchEffect(function () { console.log("effect1:", info.counter * 2, info.age) }) watchEffect(function () { console.log("effect2:", info.counter * info.counter) }) watchEffect(function () { console.log("effect3:", info.counter + 10, info.age) }) watchEffect(function () { console.log("effect4:", foo.address) }) watchEffect(function () { console.log("effect5:", foo.name) }) // info.counter = 200 foo.address = '北京' foo.name = '蓝月亮'
21.Vue3 相比于 Vue2 有以下优点:
Vue3相比于Vue2有以下优点:
- 更快的渲染性能:Vue3使用了新的响应式系统,可以减少对于响应式对象的依赖追踪次数,提高了渲染性能。(
详情见22)- 更小的体积:Vue3采用了Tree-shaking的方式,去除了一些不必要的代码,使得整个库的体积更小。
- 更好的TypeScript支持:Vue3内置了对TypeScript的支持,并且对于TypeScript的类型检查更加友好。
- 更好的开发体验:Vue3加入了Composition API,可以更灵活地组织代码,让代码更易于维护和复用。(
详情见23)- 更好的性能优化支持:Vue3提供了更多的性能优化的API和工具,例如Teleport和Suspense等。
总之,Vue3 相比于 Vue2 在性能、体积、开发体验、可维护性等方面都有了明显的提升,是一个更加优秀的前端框架。
22.Vue中Proxy 相比 Object.defineProperty 有如下优点:
- 监听新增属性: Proxy可以监听到对象新增属性,而Object.defineProperty只能监听已有属性值的变化。
- 监听删除属性: Proxy也可以监听到对象属性的删除,而Object.defineProperty不能监听到。
- 监听数组: Proxy可以监听数组的变化,包括数组元素的增删改,而Object.defineProperty只能监听到数组长度的变化。
- 性能更优: Proxy具有更好的性能和更低的内存占用,尤其是在观察大型对象时,Proxy比Object.defineProperty更快。(
详情看下面附1)综上所述,Vue3中采用Proxy替代Object.defineProperty,使得Vue3的响应式系统更加灵活、高效、不仅仅能够响应对象属性的变化,同时也能够监控数组,并且有更好的性能表现,同时也提供了更好的类型推断和类型检查。
附1: Proxy相对于Object.defineProperty性能更优,主要有以下两个方面:
- 赋值性能更好:当通过Proxy赋值时,它内部使用了高效的修改机制,只有在需要更新时才会将更新的内容写入代理对象,不需要像Object.defineProperty那样修改原始对象,从而消耗更少的性能。而当通过Object.defineProperty赋值时,会产生频繁的垃圾回收,并且会产生高强度的对象访问,导致性能下降。
- 内存占用更小:Object.defineProperty在对象上创建了一个不可枚举的属性,而Proxy只在对象之外创建了一个代理对象。因此,当一个对象有大量的监听器时,使用Proxy的内存占用比Object.defineProperty更小,以及减少了在原始对象上创建不可枚举属性的空间占用。
23.说一下Vue3中 Composition API 和 Vue2中 Options API 的区别?
Vue3 中新增了一个 Composition API,它是一种不同于 Vue2 中 Options API 的编写组件的方式。Composition API 带来了以下几个方面的改进:
- 更好的逻辑组织: Options API 将代码功能划分为不同的属性和方法,而 Composition API 则是将相关逻辑封装在一个逻辑单元中,使逻辑相关的代码更容易组织和管理。
- 更好的代码复用: Composition API 使代码能够更好地复用和组合。可以将同一逻辑的代码封装在一个组合函数中,然后在多个组件中复用。
- 更好的类型推导和检测: Options API 不支持类型推导,而 Composition API 提供了更好的类型支持,可以更方便地进行类型推导和检测。
- 更好的测试: Composition API 将逻辑封装在函数中,方便进行单元测试。可以更好地测试组合的函数,而 Options API 则通常需要在组件实例化后才能测试。
上述改进使得 Vue3 中的 Composition API 更具有可维护性、可测试性和可复用性,使 Vue3 更适合较大规模的项目开发,也更适合于团队协作开发。
24.CommonJS 和 ES Module 二者的区别
CommonJS和ES Module是JavaScript中两种不同的模块系统。
CommonJS是Node.js中使用的模块系统。它是一种同步模块系统,这意味着当需要时同步加载模块。CommonJS模块使用
require()函数来加载模块,使用module.exports对象来导出模块的值。ES Module(ESM)是ES6中引入的模块系统。它是JavaScript的标准化模块系统,可在浏览器和Node.js中使用。ESM是一种异步模块系统,这意味着需要时异步加载模块。ES模块使用
import语句来加载模块,使用export关键字来导出模块的值。CommonJS和ES Module之间的一些关键区别是:
- CommonJS是同步的,而ES Module是异步的。CommonJS 模块是运行时加载,ES Module模块是编译时输出接口。
- CommonJS使用
require()函数来加载模块,而ES Module使用import语句。- CommonJS使用
module.exports对象来导出模块的值,而ES Module使用export关键字。- CommonJS模块只加载一次并缓存,而ES Module模块每次加载时都会重新加载。
- CommonJS模块输出的是一个值的拷贝,ES Module模块输出的是值的引用。
总的来说,CommonJS和ES Module在功能上有很多相似之处,但是它们的语法和一些细节有所不同。选择使用哪种模块系统应该取决于开发者的个人喜好和项目的需求。
25.什么是 MVVM 以及它优点
MVVM是一种前端架构模式,它将前端应用程序分为三个部分:模型(Model)、视图(View)和视图模型(ViewModel)。
- 模型(Model):负责数据存储和管理,通常是指数据模型、数据访问层等。
- 视图(View):是用户界面,通常是指HTML、CSS等。
- 视图模型(ViewModel):是模型和视图之间的中介,负责将模型数据转换为视图模型数据,同时将用户的交互操作转换为对模型的操作,通常是指JavaScript代码。
MVVM的优点如下:
- 易于维护和修改。MVVM将应用程序分成了三个部分,使得每个部分的职责更加清晰,易于维护和修改。
- 提高开发效率。MVVM采用双向数据绑定和模板引擎等技术,使得开发人员可以更快速地开发应用程序。
- 降低代码耦合度。MVVM通过ViewModel中介,将模型和视图分离,降低了代码的耦合度,使得代码更加清晰和易于维护。
- 提高代码可测试性。MVVM将业务逻辑和视图分离,使得业务逻辑更容易进行单元测试,提高了代码的可测试性。
总的来说,MVVM是一种优秀的前端架构模式,它可以使应用程序更加易于维护、开发效率更高、代码耦合度更低、代码可测试性更强等。
26.Vue中使用了以下几种设计模式:
- 观察者模式(Observer Pattern):Vue使用了观察者模式来实现数据绑定和依赖追踪。当数据发生变化时,Vue会通过观察者模式通知相关的组件进行更新。(computed、watch、dep)
- 发布-订阅模式(Pub/Sub Pattern):Vue使用了发布-订阅模式来实现组件之间的通信。当一个组件发布一个事件时,其他订阅了该事件的组件会收到通知并执行相应的操作。(eventBus)
- 工厂模式(Factory Pattern):Vue使用了工厂模式来创建组件实例。Vue的组件实例都是通过工厂函数来创建的,这样可以避免重复的代码,提高代码的复用性和可维护性。
- 代理模式(Proxy Pattern):Vue使用了ES6的Proxy对象来实现数据劫持,从而实现数据绑定和依赖追踪。当访问数据属性时,Proxy会拦截该操作,并触发响应式更新。
27.JavaScript中操作数组的常见方法:
push:向数组末尾添加一个或多个元素,并返回新数组的长度。
pop:从数组末尾移除一个元素,并返回该元素的值。
shift:从数组开头移除一个元素,并返回该元素的值。
unshift:向数组开头添加一个或多个元素,并返回新数组的长度。
splice:从数组中添加或移除元素,并返回被移除的元素。
// splice()方法可以在数组中添加或删除元素,并返回被删除元素组成的新数组(如果没有删除元素,则返回一个空数组)。 // splice() 方法可以接受三个参数:(第三个参数及后面参数都是第三个参数): // 起始位置(必需):要删除或添加元素的位置。 // 删除个数(可选):要删除的元素个数,如果不指定,则删除从起始位置到数组结尾的所有元素。 // 添加元素(可选):要添加到数组中的元素。 // 例如,以下代码将从数组中删除第 2 个元素,并将新元素 "a" 和 "b" 添加到数组中: let arr = [1, 2, 3, 4, 5]; let removed = arr.splice(1, 1, 'a', 'b'); console.log(arr); // [1, "a", "b", 3, 4, 5] console.log(removed); // [2]
sort:对数组元素进行排序
reverse:反转数组元素的顺序
slice:截取一段数组返回一个新的数组,由 begin 和 end 决定的原数组的浅拷贝(左闭右开 “[)”)。原始数组不会被改变。
let arr = [1, 2, 3, 4, 5]; let newArr = arr.slice(1, 4) console.log(newArr) // [2, 3, 4]
concat:将两个或多个数组合并成一个新的数组,并返回该新数组。
join:将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串。
// 如果数组只有一个项目,那么将返回该项目而不使用分隔符。 // 数组元素默认用逗号(,)分隔。如果是空字符串 (""),则所有元素之间都没有任何字符。 { const elements = ['Fire', 'Air', 'Water']; console.log(elements.join()); // "Fire,Air,Water" console.log(elements.join('')); // "FireAirWater" console.log(elements.join('-')); // "Fire-Air-Water" // 如果一个元素为 undefined 或 null,它会被转换为空字符串。 const array = [99, 22, 33, null, undefined] console.log(array.join('+')); // "99+22+33++" console.log('-------------------------------------------------23'); }
- indexOf:返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。第一个参数代表要查找的元素;第二个参数代表开始查找的位置。
- map:对数组中的每个元素执行一个函数,并返回执行结果组成的新数组。
- filter:对数组中的每个元素执行一个函数,返回满足条件的元素组成的新数组。
- reduce:对数组中的每个元素执行一个函数,返回最终结果。
- forEach:对数组中的每个元素执行一个函数,不返回任何结果。
这些方法都是 JavaScript Array 对象自带的方法,可以直接通过数组对象调用。使用这些方法可以方便地对数组进行增删改查等操作。(push、pop、shift、unshift、splice、sort、reverse 这些方法会直接修改原数组)。
28.JavaScript 中常用遍历数组的方式
JavaScript 中遍历数组的方式有以下几种:
- for 循环:使用最为广泛的一种遍历方式,通过循环变量来遍历数组中的每一个元素。例如:
const arr = [1, 2, 3]; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); }2. forEach 方法:数组的 forEach 方法可以对数组进行遍历,接收一个回调函数作为参数,回调函数的参数分别为数组中的元素、元素的索引和数组本身。例如:
const arr = [1, 2, 3]; arr.forEach((item, index, arr) => { console.log(item); });3. map 方法:数组的 map 方法可以对数组进行遍历,并返回一个新的数组,新数组中的元素是回调函数的返回值。例如:
const arr = [1, 2, 3]; const newArr = arr.map((item, index, arr) => { return item * 2; }); console.log(newArr); // [2, 4, 6]4. filter 方法:数组的 filter 方法可以对数组进行遍历,并返回一个新的数组,新数组中只包含满足条件的元素。例如:
const arr = [1, 2, 3]; const newArr = arr.filter((item, index, arr) => { return item > 1; }); console.log(newArr); // [2, 3]5. reduce 方法:数组的 reduce 方法可以对数组进行遍历,并返回一个累加值。该方法接收两个参数,第一个参数是回调函数,第二个参数是累加值的初始值。例如:
const arr = [1, 2, 3]; const sum = arr.reduce((prev, cur, index, arr) => { return prev + cur; }, 0); console.log(sum); // 66. for...of 循环:ES6 新增的一种遍历方式,可以直接遍历数组中的元素,不必通过索引来访问数组。例如:
const arr = [1, 2, 3]; for (let item of arr) { console.log(item); }需要注意的是,使用 for...of 循环时不能使用 break 和 continue 语句,只能使用 return 语句。