跳到主要内容

聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的

Vue.js 采用了双向数据绑定的机制,这意味着当数据发生变化时,模型(Model)会更新视图(View),同时当用户与视图进行交互时,视图也可以更新模型。下面我将详细说明 Vue 中的双向数据绑定过程:

  1. Model 如何改变 View:

    • 在 Vue 中,你可以使用v-model指令将表单元素与数据模型进行绑定。当数据模型发生变化时,绑定的表单元素会自动更新。
    • 当用户修改与数据模型绑定的表单元素时,Vue 会自动监听输入事件(如inputchange),并将变化的值更新到数据模型中。
    • 当数据模型的值发生变化时,Vue 会使用虚拟 DOM 技术进行高效的比对,并更新相应的视图,只更新需要更新的部分,以提高性能。
  2. View 如何改变 Model:

    • 用户与视图进行交互时,例如在输入框中输入文本,Vue 会监听输入事件(如inputchange)。
    • 当用户输入或修改视图中绑定的表单元素时,Vue 会自动捕获输入的值,并将其更新到数据模型中,实现了视图对模型的反向绑定。
    • Vue 会检测到模型的变化,并执行相应的响应式更新,以确保模型和视图的同步。

需要注意的是,双向绑定在 Vue 中是默认开启的,但也可以使用 v-bind 指令将数据单向绑定到视图,或使用 v-on 指令监听事件来手动更新模型。此外,Vue 还提供了计算属性(computed properties)和观察者(watchers)等功能,以便更灵活地处理模型和视图之间的数据变化。

总结来说,Vue 中的双向数据绑定通过监听模型的变化来更新视图,同时也通过监听视图的交互事件来更新模型,实现了模型和视图之间的同步更新。这种机制简化了开发过程,使得开发人员可以更方便地管理和操作数据和视图的变化。

在 Vue 中,子组件为何不可以修改父组件传递的 Prop

如果修改了,Vue 是如何监控到属性的修改并给出警告的。

在 Vue 中,子组件不应该直接修改父组件传递的 Prop 的原因是为了保持单向数据流的特性和数据的可追踪性。Vue 的设计哲学是将组件之间的数据流保持清晰和可预测,使得组件之间的通信更加可控和可维护。

当子组件修改了父组件传递的 Prop 值时,可能会导致以下问题:

  1. 数据流混乱:如果允许子组件直接修改父组件的 Prop,那么在复杂的组件树中会变得难以追踪和理解数据的来源和变化。这使得代码的维护和调试变得困难。

  2. 难以追踪数据变化:Vue 通过使用响应式系统来追踪数据的变化,并在数据发生改变时更新相关的视图。如果子组件可以直接修改父组件的 Prop,那么父组件将无法准确监测到数据的变化,导致视图无法正确更新。

为了避免这些问题,Vue 在开发模式下会给出警告,提醒开发者不要修改父组件传递的 Prop。Vue 在内部实现了一些机制来监测数据的变化和追踪组件之间的关系。

当子组件试图修改父组件传递的 Prop 时,Vue 会在控制台中发出一个警告。Vue 使用了一种叫做“浅检测”的机制,它会比较新值和旧值之间的引用是否相同。如果发现引用发生了变化,说明子组件试图修改了 Prop 值,Vue 将发出警告。

这个警告的目的是为了帮助开发者遵循 Vue 的最佳实践,确保数据流的清晰和可预测性。通过单向数据流,我们可以更好地追踪数据的变化,并更容易理解和维护组件间的关系。

双向绑定和 vuex 是否冲突

在严格模式中使用 Vuex,当用户输入时,v-model 会试图直接修改属性值,但这个修改不是在 mutation 中修改的,所以会抛出一个错误。当需要在组件中使用 vuex 中的 state 时,有 2 种解决方案:

1、在 input 中绑定 value(vuex 中的 state),然后监听 input 的 change 或者 input 事件,在事件回调中调用 mutation 修改 state 的值

2、使用带有 setter 的双向绑定计算属性。见以下例子(来自官方文档):

<input v-model="message">
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}

答案来自:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/81#issuecomment-495092242

Vue 的响应式原理中 Object.defineProperty 有什么缺陷?

在 Vue 的响应式原理中,使用了 Object.defineProperty 方法来实现属性的劫持和监听。虽然这种方法为 Vue 提供了响应式的能力,但也存在一些缺陷和限制。

  1. 无法监听新增属性:Object.defineProperty 只能劫持已经存在的属性,无法自动监听新增属性的变化。这意味着在 Vue 实例创建之后,如果给对象添加新属性,那么该属性将不会被自动响应式地追踪。

  2. 无法监听数组索引和 length 属性的变化:Object.defineProperty 无法直接监听数组索引的变化,也无法监听数组的 length 属性的变化。这是因为 Vue 在对数组进行响应式处理时,使用了重写数组的原型方法来实现监听,而不是通过 Object.defineProperty

  3. 递归遍历的性能问题:在 Vue 的响应式系统中,为了对对象的所有属性进行劫持,需要递归遍历对象的所有属性,并对每个属性使用 Object.defineProperty。当对象的层级很深或对象中包含大量属性时,这种遍历的性能开销可能会比较大,影响初始化和响应式追踪的性能。

  4. 无法监听属性的删除:使用 Object.defineProperty 无法直接监听属性的删除操作。一旦通过 delete 操作符删除了对象的属性,那么对应的响应式能力也会丧失。

由于上述缺陷,Vue 在某些情况下可能无法精确地追踪对象的变化,需要使用特定的方法来处理新增属性、数组变化等情况。为了解决这些问题,Vue 3 采用了 Proxy 对象来替代 Object.defineProperty,以提供更强大和灵活的响应式能力,并解决了上述缺陷。Proxy 对象可以监听对象的新增属性、删除属性、数组索引的变化等,同时也具有更好的性能。

Vue 的父组件和子组件生命周期钩子执行顺序是什么

1.首次加载过程

父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> (子 activated) -> 父 mounted

2.父组件更新过程

父 beforeUpdate -> (子 deactivated) -> 父 updated

3.子组件更新过程

父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

4.销毁过程

父 beforeDestroy-> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

vue 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?

在 Vue 的 v-for 指令中,给每项元素绑定事件时通常不需要使用事件代理。

事件代理是一种将事件处理程序绑定到父元素上,通过事件冒泡机制来处理子元素的事件。它在原生 JavaScript 中常用于处理大量动态生成的元素的事件,以减少事件处理程序的数量。

然而,在 Vue 中,使用 v-for 指令时,每个生成的元素都会被 Vue 实例所管理,而且每个元素都会有自己的事件处理程序。Vue 会为每个元素创建独立的监听器,并在元素销毁时自动清除这些监听器,以确保正确的事件处理和内存管理。

因此,当使用 v-for 生成元素列表时,不需要使用事件代理,可以直接在每个元素上绑定事件处理程序。

例如,在 Vue 模板中使用 v-for 绑定点击事件的示例:

<ul>
<li v-for="item in items" @click="handleItemClick(item)">{{ item }}</li>
</ul>

在上述示例中,每个生成的 <li> 元素都会绑定 click 事件处理程序 handleItemClick(item),其中的 item 是当前遍历项的数据对象。Vue 会为每个 <li> 元素创建独立的事件监听器,确保事件处理的正确性和隔离性。

总结起来,Vue 在 v-for 生成元素列表时会为每个元素创建独立的事件监听器,因此不需要使用事件代理,可以直接在每个元素上绑定事件处理程序。这样可以简化代码,并保证事件处理的正确性和可维护性。

vue2 和 vue3 构建实例区别

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import plugins from "./components/common/index";

// 注意必须在构建 Vue 实例之前就将需要的组件注册进去
Vue.use(plugins);

Vue.component("button-counter", {
data: () => ({
count: 0,
}),
template: '<button @click="count++">Clicked {{ count }} times.</button>',
});

Vue.config.productionTip = false;

new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");

在 vue2 中注册全局组件、挂载原型、全局插件必须在构建 Vue 实例之前,其实这样非常污染 Vue 实例

const app1 = new Vue({ el: "#app-1" });
const app2 = new Vue({ el: "#app-2" });

此时如果创建两个 Vue 实例,会导致每个实例都挂载了相同的插件、全局组件,因为插件的注册是在 new Vue 之前的,即挂载在 Vue 原型上

除了 component 还有以下全局 API 都会影响到 Vue 实例:

  • Vue.directive()
  • Vue.mixin()
  • Vue.use
  • Vue.config
  • Vue.prototype

其原因其实是因为 Vue2 版本是没有考虑到多个应用程序的,这使得创建 Vue 的副本非常困难。

构造函数的形式不利于隔离不同的 Vue 实例应用。调用构造函数的静态方法会对所有 Vue 实例应用生效

构造函数的形式不利于 Tree Shaking,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是 Vue 实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到

import Vue from "vue";
Vue.mixin();
Vue.use();
Vue.nextTick(() => {});

Vue3 引入 Tree Shaking 特性,将全局 API 进行分块。如果你不使用其某些功能,它们将不会包含在你的基础包中

import { nextTick, observable } from "vue";

nextTick(() => {});

通过 Tree ShakingVue3 给我们带来的好处是:

  • 减少程序体积(更小)
  • 减少程序执行时间(更快)
  • 便于将来对程序架构进行优化(更友好)

其次 Vue3 采用 createApp 工厂函数来返回一个 Vue 实例,所有全局 API 修改都通过这个 实例 来挂载处理,这样,多个 createApp 创建的实例,它们之间互相不干扰

const { createApp } = Vue;
const app = createApp({});

app.component("my-component", {
/* ... */
});

vue2 和 vue3 响应式的变化

vue2 和 vue3 都是在相同的生命周期(beforeCreate 之后、created 之前)完成数据的响应式。

vue2 的响应式原理是怎么样的?

vue2 的响应式对象是通过 Object.defineProperty 对每个属性进行监听,当对属性进行读取的时候,就会触发 getter,对属性进行设置的时候,就会触发 setter。

由于 Object.defineProperty 无法监听对象的变化,所以 Vue2 中设置了一个 Observer 类来管理对象的响应式依赖,同时也会递归侦测对象中子数据的变化。

Observer 类的作用就是把一个对象全部转换成响应式对象,包括子属性数据,当对象新增或删除属性的时候负责通知对应的

vue3 相比于 Vue2 的响应式原理有什么变化?

Vue 3 在响应式系统方面进行了一些重大变化,以提供更好的性能和更丰富的功能。下面是 Vue 2 和 Vue 3 在响应式方面的主要变化:

  1. Proxy 替代 Object.defineProperty:Vue 2 使用 Object.defineProperty 来实现响应式数据,而 Vue 3 使用了新的 JavaScript 特性 Proxy 来实现响应式。Proxy 提供了更强大和灵活的拦截器,能够捕获更多操作,从而实现更高效的响应式追踪。

  2. 更好的数组响应式:Vue 3 对数组的响应式处理进行了改进。在 Vue 3 中,通过改写数组的原型链,使得数组的方法(如 push、pop、splice 等)能够触发响应式更新。这意味着在 Vue 3 中,可以直接修改数组,而无需使用特定的 $set 方法。

  3. ref 和 reactive:Vue 3 引入了 refreactive 两个新的响应式 API。ref 用于将基本类型数据包装为响应式对象,而 reactive 用于将对象包装为响应式对象。这两个 API 提供了更明确的语义和更好的类型推导。

  4. Setup 函数:Vue 3 中,组件的逻辑被提取到 setup 函数中。setup 函数是一个在组件创建之前执行的函数,它接收组件的 props 和上下文作为参数,并且可以返回响应式数据和其他需要在模板中使用的内容。这种方式使得组件的逻辑更加清晰和灵活。

  5. Composition API:Vue 3 引入了 Composition API,这是一个基于函数的 API,使得在组件中编写逻辑更加灵活和可组合。Composition API 可以让开发者更好地组织和重用代码,避免了 Vue 2 中组件逻辑散落在不同的选项中的问题。

  6. Watch 选项的替代:Vue 3 中的 watch 选项被替换为 watchEffect 函数。watchEffect 函数接收一个响应式的数据源,并在数据源发生变化时自动运行。这种方式更加直观和简洁。

需要注意的是,Vue 3 的响应式系统与 Vue 2 的相比,具有更好的性能和更多的功能,但也意味着在迁移到 Vue 3 时,需要注意一些语法和用法上的变化,并且确保相关插件和库已经升级以适应 Vue 3 的新特性。

VUE3.X 为何弃用 Object.defineProperty?

首先探讨下 VUE2.0 中 Object.defineProperty 存在的问题

为了让读者有更直观的一个印象,我这里先将 VUE2.0 中 Object.defineProperty 存在的问题罗列出来

  1. 不能监听数组索引和长度的变更
  2. 无法监听 属性的添加和删除
  3. 必须遍历对象的每个属性, 必须深层遍历嵌套的对象。Object.defineProperty是对对象属性的操作,所以需要对对象进行深度遍历去对属性进行操作。

注:很多小伙伴可能有疑问,不是说 VUE2.0 中 Object.defineProperty 不能监听数组索引和长度的变更吗,为什么我们在使用 vue2 的时候,一样可以监听数组索引和长度的变更? 我们得明白主体,我们只是说 Object.defineProperty 不能,并没说 vue2.0 不能。并不能说因为 vue2.0 使用了 Object.defineProperty,vue2.0 就不能监听数组内部属性的变化了。而 vue2 之所以能监听,是 vue2.0 对数组相关的方法或其他进行了重写。当然 vue2.0 中还是存在无法监听直接修改数组中某一项值和数组长度,如 ar[0]=1, arr.length=12 是无法监听的,针对这个 vue2.0 有其他解决方案,请看下文。

问题 1、不能监听数组索引和长度的变更

例:

const obj = {};
let initValue = 1;

Object.defineProperty(obj, "name", {
set: function (value) {
console.log("set方法被执行了");
initValue = value;
},
get: function () {
return initValue;
},
});
console.log(obj.name); // 1

obj.name = []; // 会执行set方法,会打印信息

// 给 obj 中的name属性 设置为 数组 [1, 2, 3], 会执行set方法,会打印信息
obj.name = [1, 2, 3];

// 然后对 obj.name 中的某一项进行改变值,不会执行set方法,不会打印信息
obj.name[0] = 11;

// 然后我们打印下 obj.name 的值
console.log(obj.name);

// 然后我们使用数组中push方法对 obj.name数组添加属性 不会执行set方法,不会打印信息
obj.name.push(4);

//使用数组中unshift方法对 obj.name数组添加属性 不会执行set方法,不会打印信息
obj.name.unshift(5);

console.log(obj.name, "删除前"); //删除前
obj.name.pop(); //删除obj.name数组最后一个元素
console.log(obj.name, "删除后"); //删除后,最后一个元素被删除了,但是set方法并没有执行,

obj.name.length = 5; // 也不会执行set方法
console.log(obj.name.length, obj.name[3]);

下图是运行结果

从运行结果可以看到,当我们改变数组某一项的值,给数组添加或删除属性、或改变数组长度,都没有执行 set 方法。也就是如果我们对数组中的内部属性值直接更改的话,都不会触发 set 方法。

因此如果我们想实现数据双向绑定的话,我们就不能简单地使用 obj.name[1] = newValue 这样的来进行赋值了。那么对于 vue 这样的框架,那么一般会重写 Array.property.push 方法,并且生成一个新的数组赋值给数据,这样数据双向绑定就触发了。

问题 2、 无法监听 属性的添加和删除

在 vue2 中使用中你可能遇到过这样的问题


#template
<div @click="add(obj.a)">{{ obj.a }}</div>
<div @click="addb(obj.b)">{{ obj.b }}</div>

#srcript
data () {
return {
obj:{
a:1
}
}
},
mounted () {
this.obj.b = 1; //给对象新增属性b
},
methods: {
addb(item){
item += 1;
console.log(this.obj)
}
}

当点击 obj.a 是响应式, 页面也会更新

而点击新增的 obj.b 则不会更新。

所以 Object.defineProperty 无法监听到新增的对象属性,删除也一样

针对这个问题 vue2 的解决方案:

新增对象属性$set

Vue.set(object, "key", value); //第一个参数是添加属性的对象,第二个参数是要添加的属性,第二个参数是要添加的属性的值
this.$set(object, "key", value);
//上面两种写法都一样

this.obj = Object.assign({}, this.obj, { b: 1, e: 2 });
this.obj = { ...this.obj, ...{ b: 3, e: 2 } };

数组解决方案类似 关于 vue 无法侦听数组及对象属性的变化的解决方案

删除对象属性 $delete

Vue.delete(target, "object"); //第二个参数是字符串[也就是我们要删除的属性]
this.$delete(target, "object");
问题 3、 必须遍历对象的每个属性, 必须深层遍历嵌套的对象。

Object.defineProperty是对对象属性的操作,所以需要对对象进行深度遍历去对属性进行操作。 使用 Object.defineProperty() 多数要配合 Object.keys() 和遍历,,于是多了一层嵌套.

Object.keys(obj).forEach((key) => {
Object.defineProperty(obj, key, {
// ...
});
});

当一个对象为深层嵌套的时候,必须进行逐层遍历,直到把每个对象的每个属性都调用 Object.defineProperty() 为止。

https://juejin.cn/post/6988156161425932296

vue3 的响应式原理是怎么样的?

  • vue3 中提供了 reactive()ref() 两个方法用来将 目标数据 变成 响应式数据,通过 Proxy 来实现 数据劫持(或代理)
  • 普通对象类型直接配合 Proxy 提供的捕获器实现响应式
  • 数组类型 也可以直接复用大部分和 普通对象类型 的捕获器,但其对应的查找方法和隐式修改 length 的方法仍然需要被 重写 / 增强
  • 原始值数据类型 主要通过 ref() 函数来进行响应式处理,不过内容不会对 原始值类型 使用 reactive()(或 Proxy) 函数来处理,而是在内部自定义 get value(){}set value(){} 的方式实现响应式,毕竟原始值类型的操作无非就是 读取设置,核心还是将 原始值类型 转变为了 普通对象类型
    • ref() 函数可实现原始值类型转换为 响应式数据,但 ref() 接收的值类型并没只限定为原始值类型,若接收到的是引用类型,还是会将其通过 reactive() 函数的方式转换为响应式数据
// vue3的响应式原理
const person = {
name: "李四",
age: 20,
};
const p = new Proxy(person, {
get(target, propName) {
// 读取属性调用
// target:源对象 propName:属性名
return Reflect.get(target, propName); // Reflect ES6的语法
},
set(target, propName, value) {
// 修改、追加属性调用
// target:源对象 propName:属性名 value:追加/修改的值
return Reflect.set(target, propName, value); // Reflect ES6的语法
},
deleteProperty(target, propName) {
// 删除属性调用
return Reflect.deleteProperty(target, propName); // Reflect ES6的语法
},
});

proxy 内部使用 Reflect 静态方法来实现对数据的操作

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法,这些方法与 Proxy handler 提供的的方法是一一对应的,且 Reflect 不是一个函数对象,即不能进行实例化,其所有属性和方法都是静态的。

说说 vue3 中 Tree Shaking 特性?举例说明一下?

在 Vue 3 中,Tree Shaking 是一项重要的特性,它利用 ES2015 模块系统的静态结构特性,允许构建工具在打包过程中只包含应用程序中实际使用的代码,从而减少最终的包大小。

Tree Shaking 的原理是通过静态分析代码,识别出哪些代码没有被使用,然后在打包过程中将这些未使用的代码剔除掉,以减少最终的输出文件大小。

下面是一个示例,说明如何在 Vue 3 中使用 Tree Shaking 特性:

import { createApp } from "vue";
import { Button, Input } from "ant-design-vue";

const app = createApp();

app.component("Button", Button);
app.component("Input", Input);

app.mount("#app");

在上述示例中,我们使用了 Ant Design Vue 库中的 ButtonInput 组件,并通过 createApp 函数创建了一个 Vue 应用实例。在这个例子中,我们只引入了 ButtonInput 两个组件,但实际上 Ant Design Vue 库中还包含其他许多组件。

由于 Vue 3 支持 Tree Shaking,构建工具在打包过程中会分析代码,并只引入实际使用到的 ButtonInput 组件,剔除其他未使用的组件。这样就可以最小化最终打包后的文件大小,只包含应用程序中所需的代码。

需要注意的是,Tree Shaking 的有效性取决于代码的结构和引入的模块是否按照 ES2015 模块的静态结构规范编写。如果模块的导入和导出不符合静态结构规范,或者代码中存在动态导入的情况,那么 Tree Shaking 的效果可能会受到限制。

总结起来,Vue 3 中的 Tree Shaking 特性能够通过静态分析代码,剔除未使用的代码,从而减少最终打包后的文件大小。这可以帮助优化应用程序的性能和加载速度。

v-for 和 v-if 的优先级

vue2 中 v-for 优先级高于 v-if,虽然 vue2 规范中不建议 v-for 和 v-if 同写一行,因为在 循环中+判断 这样会带来性能问题。

vue3 中 v-if 优先级高于 v-for,因为 vue3 觉得 vue2 既然不推荐 v-for 和 v-if 同行,那设置优先级本身没有什么意义。

vue 中 nextTick 的作用

在 Vue 中,nextTick 是一个异步方法,它的作用是在 DOM 更新之后执行回调函数或者获取更新后的 DOM。

由于 Vue 的更新是异步的,当你修改数据后,Vue 并不会立即更新 DOM。相反,它会将更新操作放入一个队列中,然后进行批量更新。这意味着在更新数据后,你不能立即获取到更新后的 DOM 或者执行与 DOM 相关的操作。

nextTick 方法提供了一个方式来在 DOM 更新之后执行回调函数或者获取更新后的 DOM。它会在下一个 DOM 更新周期之前执行指定的回调函数。

下面是一些 nextTick 的常用场景和用法:

  1. 执行回调函数:你可以在 nextTick 的回调函数中进行一些操作,例如访问更新后的 DOM、触发浏览器重绘等。
Vue.nextTick(function () {
// 在 DOM 更新之后执行的回调函数
// 可以访问更新后的 DOM 或者执行其他相关操作
});
  1. 使用 Promise:
Vue.nextTick().then(function () {
// 在 DOM 更新之后执行的回调函数
// 可以访问更新后的 DOM 或者执行其他相关操作
});
  1. 使用 async/await:
async function asyncUpdate() {
await Vue.nextTick();
// 在 DOM 更新之后执行的回调函数
// 可以访问更新后的 DOM 或者执行其他相关操作
}

需要注意的是,nextTick 方法是异步的,所以回调函数的执行顺序是不确定的。如果需要确保回调函数在 DOM 更新之后执行,可以使用 nextTick 方法来控制。

总而言之,nextTick 方法提供了一种在 DOM 更新之后执行回调函数或者获取更新后的 DOM 的方式,它对于处理异步更新和执行与 DOM 相关的操作非常有用。

vue 中 watchEffect 与 watch 有什么不同

  1. watchEffect 不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了响应式的属性, 那么当这些属性变更的时候,这个回调都会执行
  2. watch 只能监听指定的属性
  3. watch 是惰性的,如需组件初始化就执行请携带 immediate: true 参数
  4. watch 可以获取到新值与旧值,而 watchEffect 不行
  5. watchEffect 在组件初始化的时候就会执行一次用以收集依赖(与computed同理),后续收集的依赖发生变化,这个回调才会再次执行

https://www.xiangshu233.cn/%E5%89%8D%E7%AB%AF%203-5%20%E5%B9%B4%E7%BB%8F%E9%AA%8C%E9%9D%A2%E8%AF%95%E9%A2%98/#vue2-%E5%92%8C-vue3-%E5%93%8D%E5%BA%94%E5%BC%8F%E7%9A%84%E5%8F%98%E5%8C%96

vue3 setup 有什么用

在 Vue 3 中,setup 函数是组件选项之一,它是用来配置组件的函数。setup 函数在组件创建过程中被调用,并且在其他组件选项(如 datacomputedmethods 等)之前执行。

setup 函数的作用是:

  1. 设置组件的响应式状态:setup 函数中可以使用 Vue 提供的响应式 API(如 refreactive 等)来创建响应式的状态。这些响应式状态可以在模板中使用,并且会自动追踪其依赖关系。

  2. 处理组件的逻辑和副作用: setup 函数是用来处理组件的逻辑和副作用的地方。您可以在 setup 函数中编写普通的 JavaScript 代码,来处理数据逻辑、调用 API、订阅事件等。这样可以使组件的逻辑更加清晰和可测试。

  3. 暴露数据给模板和父组件: setup 函数可以返回一个对象,该对象中的属性和方法将会被暴露给组件的模板和父组件。这样,您可以在模板中直接使用返回的属性和方法。

  4. 访问组件实例:setup 函数中,可以通过 this 或者 context 参数来访问组件实例。这使得您可以访问组件的生命周期钩子、实例方法等。

总而言之,setup 函数是 Vue 3 中用来配置组件的函数,它用于设置组件的响应式状态、处理逻辑和副作用、暴露数据给模板和父组件,以及访问组件实例。它提供了更灵活和强大的方式来编写组件,并且能够更好地组织和重用代码。

Composition API 和 Option API 的区别

Vue 提供了两种不同的 API 风格:Composition API(组合式 API)和 Option API(选项式 API)。它们在编写 Vue 组件时有一些区别:

Option API(选项式 API): Option API 是 Vue 2.x 中主要使用的 API 风格。它基于组件选项对象,您可以在选项对象中定义各种属性和方法来配置组件。

  1. 分散定义逻辑:使用 Option API,您需要在选项对象中分散定义组件的不同部分,如 datacomputedmethodscreatedmounted 等。

  2. 较大的选项对象:随着组件逻辑的增长,选项对象可能会变得很大,导致代码可读性和维护性下降。

  3. 难以重用逻辑:在 Option API 中,逻辑的复用需要通过混入(mixin)或高阶组件(Higher Order Component)来实现,这可能导致命名冲突和代码复杂性。

Composition API(组合式 API): Composition API 是 Vue 3 中引入的新的 API 风格。它基于函数,并提供了一组函数和钩子,允许您按照逻辑关注点组织代码。

  1. 按逻辑组织代码:使用 Composition API,您可以按照逻辑关注点将代码组织在一起,将相关的函数和响应式状态放在同一个地方。

  2. 逻辑复用和组合:Composition API 鼓励逻辑的复用,可以将相关逻辑封装在自定义函数中,并在多个组件中重复使用。

  3. 更好的类型推断:Composition API 基于函数,使得 TypeScript 有更好的类型推断支持,提高了代码的可靠性和可维护性。

  4. 更小的选项对象:使用 Composition API,您只需要一个选项对象,即 setup 函数,它更加简洁和集中。

总而言之,Composition API 提供了更灵活、可组合和可维护的方式来编写 Vue 组件,更好地组织代码逻辑,并提供了更好的类型推断。相比之下,Option API 更适合简单的组件或者在 Vue 2.x 中迁移现有代码。

刷新后 vuex 状态丢失怎么办

当刷新页面后,Vuex 状态丢失的问题通常是由于状态没有持久化到持久性存储(如浏览器的本地存储)导致的。您可以采取以下几种方法来解决这个问题:

  1. 使用插件进行状态持久化: 可以使用一些专门为 Vuex 设计的插件,如 vuex-persistedstate,它可以将 Vuex 状态自动持久化到浏览器的本地存储(如 localStorage 或 sessionStorage)。您可以在项目中引入该插件,并按照其文档进行配置和使用。

  2. 手动实现状态持久化: 如果您不想使用第三方插件,也可以手动实现状态持久化。在 Vuex 的 store 对象中,可以使用 localStoragesessionStorage API 将状态存储在浏览器的本地存储中。在应用初始化时,可以从本地存储中读取状态并还原到 Vuex 中。

    // 在应用初始化时从本地存储中读取状态
    const savedState = JSON.parse(localStorage.getItem("vuex-state"));
    const store = new Vuex.Store({
    state: savedState || initialState,
    // ...
    });

    // 在每次状态变更时将状态保存到本地存储
    store.subscribe((mutation, state) => {
    localStorage.setItem("vuex-state", JSON.stringify(state));
    });

    注意,手动实现状态持久化需要注意安全性和性能方面的考虑,例如对敏感信息进行加密,并避免在状态过大时频繁地写入本地存储。

  3. 使用后端存储: 如果您的应用需要跨多个设备或保持用户登录状态,可以将状态保存在后端存储中,例如数据库或服务器会话中。在用户登录或访问特定页面时,从后端获取状态并将其同步到 Vuex。

    这种方法需要与后端进行数据交互,但可以提供更可靠和适用于多设备的状态持久化解决方案。

无论您选择哪种方法,都应该注意在存储敏感信息时确保安全性,并根据您的具体需求选择最适合的持久化方案。

vuex 有什么缺点

模块化这一块做的过于复杂,用的时候容易出错。比如访问 store 时要带上模块 key,内嵌模块的话会很长,不得不配合 mapState,对 ts 支持不友好,使用模块时没有代码提示,pinia 出现之后使用体验好了很多,vue3 + pinia 会是更好的组合

vue-router history 和 hash 模式有什么区别

Vue Router 提供了两种路由模式:History 模式和 Hash 模式。它们在 URL 的表现形式和与服务器的交互方式上有所区别。

History 模式:

  • 在 History 模式下,Vue Router 通过使用 HTML5 History API,将路由信息保存在浏览器的 History 栈中,而不是在 URL 的哈希部分。
  • URL 中不会出现 # 符号,而是直接使用常规的 URL 路径。例如:https://example.com/user/profile
  • History 模式需要服务器的支持,以确保在直接访问路由时能够返回正确的页面,而不是 404 错误。
  • 当用户在应用程序中导航时,浏览器不会发送额外的请求,因为路由信息仅存在于浏览器的 History 栈中。
  • 当用户直接访问一个路由时,服务器需要配置以返回应用程序的主页(index.html),然后由 Vue Router 接管路由并显示正确的页面。

Hash 模式:

  • 在 Hash 模式下,Vue Router 通过修改 URL 的哈希部分(即 # 符号后面的部分)来表示不同的路由。
  • URL 中的哈希部分用于客户端路由,并不会发送到服务器。例如:https://example.com/#/user/profile
  • Hash 模式不需要服务器的特殊配置,因为 URL 的哈希部分不会被发送到服务器。
  • 当用户在应用程序中导航时,浏览器不会向服务器发送额外的请求,因为哈希部分的变化不会触发页面的刷新。
  • 当用户直接访问一个带有哈希部分的 URL 时,浏览器会请求完整的 URL,然后由客户端的 Vue Router 解析哈希部分并显示正确的页面。

选择使用 History 模式还是 Hash 模式取决于您的具体需求和服务器环境。如果您的服务器能够正确处理直接访问的 URL,并返回正确的页面,那么可以使用 History 模式。如果您的服务器只能返回主页,并且您不想进行额外的服务器配置,那么可以使用 Hash 模式。

$route$router 的区别

在 Vue.js 中,$route$router是 Vue Router 提供的两个对象,它们在功能和使用上有所区别。

  • $route对象表示当前路由的相关信息,它是一个只读对象,提供了对当前路由的访问和查询。通过$route对象,您可以获取当前路由的路径、参数、查询参数、路由元信息等。

    例如,您可以通过$route.path获取当前路由的路径,通过$route.params获取路由的动态参数,通过$route.query获取查询参数等。$route对象通常在组件内部使用,用于根据当前路由的信息进行渲染或业务逻辑处理。

  • $router对象是 Vue Router 的路由实例,它提供了一些导航方法和路由配置的访问。通过$router对象,您可以进行导航、跳转到其他路由、监听路由事件等。

    例如,您可以使用$router.push()方法进行编程式导航到其他路由,使用$router.replace()方法进行替换当前路由,使用$router.go()方法进行前进或后退导航,还可以使用$router.beforeEach()方法监听全局的路由导航守卫等。$router对象通常在组件内部或路由守卫中使用,用于进行路由的导航控制和处理。

总结:

  • $route是表示当前路由信息的对象,用于获取当前路由的相关信息,是一个只读对象。
  • $router是 Vue Router 的路由实例,用于进行路由的导航和配置,提供了导航方法和路由事件的监听。

在组件中,可以通过this.$routethis.$router访问$route$router对象。

router 路径传值

query 是显式传值(直接显式在 http://localhost:8080/about?a=1

params 是隐式传值

说一说 scoped 样式隔离

Vue 在创建组件的时候,会给组件生成唯一的 id 值,当 style 标签给 scoped 属性时,会给组件的 html 节点都加上这个 id 值标识,如 data-v4d5aa038,然后样式表会根据这 id 值标识去匹配样式,从而实现样式隔离

什么是 MVVM?

MVVM 是一种软件架构模式,它代表 Model-View-ViewModel(模型-视图-视图模型)。MVVM 模式被广泛应用于前端开发,特别是在使用框架如 Vue.js、Angular 和 Knockout.js 等进行开发时。

下面是 MVVM 架构中的三个主要组成部分:

  1. Model(模型): Model 表示应用程序的数据模型,它负责处理数据的获取、存储、验证和业务逻辑等。Model 通常是应用程序的数据源,可以从后端服务器获取数据,或者直接在客户端创建和操作数据。

  2. View(视图): View 是用户界面的可视化部分,它负责展示数据和与用户进行交互。View 通常是由 HTML、CSS 和其他 UI 组件组成,可以通过绑定数据和事件来显示和更新用户界面。

  3. ViewModel(视图模型): ViewModel 是 View 和 Model 之间的桥梁,它负责处理 View 和 Model 之间的通信和数据同步。ViewModel 通过将数据从 Model 获取并将其转换为 View 可用的格式,然后将用户的输入转发给 Model 进行处理。它还可以包含额外的逻辑和状态,以支持 View 的特定行为和交互。

MVVM 的核心思想是数据绑定(Data Binding),通过建立 View 和 ViewModel 之间的双向绑定关系,实现数据的自动同步和更新。当 Model 的数据发生变化时,ViewModel 会通知 View 更新相应的界面;当用户在 View 上进行操作时,ViewModel 会更新 Model 的数据。

MVVM 模式的优点包括:

  • 解耦和可测试性: MVVM 将界面逻辑和数据逻辑分离,使得它们可以独立开发和测试,提高了代码的可维护性和可测试性。
  • 代码复用: 通过将界面逻辑封装在 ViewModel 中,可以实现多个 View 共享同一个 ViewModel 的情况,减少了重复编写代码的工作量。
  • 可维护性: MVVM 的结构清晰,使代码更易于理解和维护。ViewModel 的存在使得开发人员可以更容易地修改和扩展界面逻辑,而无需修改底层的数据模型。

总结来说,MVVM 是一种将界面逻辑、数据模型和用户界面分离的架构模式,通过数据绑定实现数据的自动同步和更新。它提供了一种结构化和可维护的方式来开发前端应用程序。

v-for 和 v-if 的优先级

vue2 中 v-for 优先级高于 v-if,虽然 vue2 规范中不建议 v-for 和 v-if 同写一行,因为在 循环中+判断 这样会带来性能问题。

vue3 中 v-if 优先级高于 v-for,因为 vue3 觉得 vue2 既然不推荐 v-for 和 v-if 同行,那设置优先级本身没有什么意义。

Vuex 和 Pinia 的区别

vuex 变更状态必须显示的提交执行 commit mutations 里的方法

import { useStore } from "vuex";
const vuexStore = useStore();
vuexStore.commit("setVuexMsg", "hello juejin");

或者在 actions 中进行 mutations 修改 state

import { createStore } from "vuex";
export default createStore({
strict: true,
// 全局state,类似于vue种的data
state() {
return {
vuexmsg: "hello vuex",
};
},
// 修改state函数
mutations: {
setVuexMsg(state, data) {
state.vuexmsg = data;
},
},
// 提交的mutation可以包含任意异步操作
actions: {
async getState({ commit }) {
// const result = await xxxx 假设这里进行了请求并拿到了返回值
commit("setVuexMsg", "hello juejin");
},
},
});

组件中使用 dispatch 进行分发 actions。

<template>
<div>{{ vuexStore.state.vuexmsg }}</div>
</template>

<script setup>
import { useStore } from 'vuex'
const vuexStore = useStore()
vuexStore.dispatch('getState')
</script>

Pinia 则可以直接修改状态,且调试工具 能够记录到每一次的变化

Pinia 可以调用 $patch 方法修改多个 state 中的值,当然也可以修改一个

import { storeA } from "@/piniaStore/storeA";
const piniaStoreA = storeA();
console.log(piniaStoreA.name); // xiaoyue
piniaStoreA.$patch({
piniaMsg: "hello juejin",
name: "daming",
});
// 也可以使用函数的方式进行修改状态
// cartStore.$patch((state) => {
// state.name = 'daming'
// state.piniaMsg = 'hello juejin'
// })
console.log(piniaStoreA.name); // daming

也可以在 actions 中修改状态

Vuex 不同,Pinia 移除了 mutations,所以在 actions 中修改 state 就和 Vuexmutations 修改 state 一样。 实这也是我比较推荐的一种修改状态的方式,就像上面说的,这样可以实现整个数据流程都在状态管理器内部,便于管理。

import { defineStore } from "pinia";
export const storeA = defineStore("storeA", {
state: () => {
return {
piniaMsg: "hello pinia",
name: "xiao yue",
};
},
actions: {
setName(data) {
this.name = data;
},
},
});

在组件中调用也不在需要 dispatch 函数,直接调用 store 的方法即可。

import { storeA } from "@/piniaStore/storeA";
const piniaStoreA = storeA();
piniaStoreA.setName("daming");

Pinia 可以使用 $reset 将状态重置为初始值。

import { storeA } from "@/piniaStore/storeA";
const piniaStoreA = storeA();
piniaStoreA.$reset();

getters

其实 Vuex 中的 getters 和 pinia 中的 getters 用法是一致的,用于自动监听对应 state 的变化,从而动态计算返回值(和 vue 中的计算属性差不多),并且 getters 的值也具有缓存特性。

modules

如果项目比较大,使用单一状态库,项目的状态库就会集中到一个大对象上,显得十分臃肿难以维护。所以 Vuex 就允许我们将其分割成模块(modules),每个模块都拥有自己 state、mutations、actions…。而 Pinia 每个状态库本身就是一个模块。

Pinia 没有 modules,如果想使用多个 store,直接定义多个 store 传入不同的 id 即可,如:

import { defineStore } from "pinia";
export const storeA = defineStore("storeA", {...});
export const storeB = defineStore("storeB", {...});
export const storeC = defineStore("storeB", {...});

总结:

Vuex 变更 state 状态必须显示的提交执行 commit mutations 里的方法,或者可以在 actions 中进行 commit mutations 修改 state,组件里则调用 dispatch('xxx') 分发 actions

Pinia 变更状态直接修改,引入 store 直接点对应属性(但是不建议这么搞,最好对应的数据流程变更都在状态管理器内部,这样更好管理)

另外 Pinia 移除了 mutations,修改状态放到了 actions 里,外部组件调用引入 store 后直接点就可以了

getters 上两者都一样,都是自动监听 state 的变化,从而动态计算返回值,和计算属性差不多

Pinia 同时也没有 modules 属性,如果想使用多个 store,直接定义多个 store 传入不同的 id 即可

Vuex 的 modules 属性一般写在总的入口 index.js 内,里面为 modules 文件里的各个 module

组件使用中 Vuex 需要 vuexStore.state.moduleA.count

import { useStore } from 'vuex'
let vuexStore = useStore()
console.log(vuexStore.state.moduleA.count) // 1

而 Pinia 则直接引用具体的 module ,然后调用 module 里面属性

import { useUserStore } from "@/store/modules/user";
const userStore = useUserStore();
userStore.xxx;

什么是 vue 中的插槽 slot

在 Vue 中,插槽(slot)是一种用于在组件之间传递内容的机制。它允许你在组件的模板中定义带有特殊标记的插槽,然后在使用该组件时,将内容插入到这些插槽中。

使用插槽可以使组件更加灵活和可重用,因为它允许父组件向子组件传递任意的内容,而不仅限于简单的数据属性。

在 Vue 中,有两种类型的插槽:具名插槽和默认插槽。

具名插槽(Named Slots): 具名插槽允许你在组件中定义多个具有不同名字的插槽,以便在使用组件时将内容插入到相应的插槽中。定义具名插槽时,需要使用 <slot> 元素,并通过 name 属性指定插槽的名字。

示例:

<!-- 子组件 MyComponent.vue -->
<template>
<div>
<h2>我是子组件的标题</h2>
<slot name="content"></slot>
</div>
</template>

<!-- 父组件中使用子组件 -->
<template>
<div>
<my-component>
<template v-slot:content>
<p>我是插入到具名插槽中的内容</p>
</template>
</my-component>
</div>
</template>

在上面的示例中,父组件使用 <my-component> 组件,并在其中通过具名插槽 <template v-slot:content> 插入了一段 <p> 标签的内容。

默认插槽(Default Slot): 默认插槽是在组件模板中没有被具名的插槽,可以通过不带属性的 <slot> 元素来定义。默认插槽可以用于插入组件的任意内容,当组件没有具名插槽时,会将内容插入到默认插槽中。

示例:

<!-- 子组件 MyComponent.vue -->
<template>
<div>
<h2>我是子组件的标题</h2>
<slot></slot>
</div>
</template>

<!-- 父组件中使用子组件 -->
<template>
<div>
<my-component>
<p>我是插入到默认插槽中的内容</p>
</my-component>
</div>
</template>

在上述示例中,父组件使用 <my-component> 组件,并在其中插入了一个 <p> 标签的内容,该内容会被插入到子组件的默认插槽中。

通过插槽,你可以更方便地将内容嵌入到组件中,使组件更加灵活和可配置。插槽是 Vue 中非常有用的特性,特别适用于构建可复用的组件和布局。

vue 中的 data 为什么是一个函数

在 Vue 组件中,data 选项通常被定义为一个函数而不是一个对象。这是因为 Vue 组件可以被复用多次,每个实例都应该有自己独立的数据对象,而不是共享同一个对象实例。

data 定义为一个函数的好处在于,每个组件实例在创建时都会调用该函数生成一个独立的数据对象。这样可以确保每个组件实例都有自己的数据副本,而不会相互影响。

示例:

// 错误的写法,data 是一个对象
data: {
message: 'Hello'
}

// 正确的写法,data 是一个函数
data() {
return {
message: 'Hello'
}
}

当组件被创建时,Vue 会调用 data 函数并返回一个新的数据对象。这样做的结果是,每个组件实例都可以独立地修改和管理自己的数据,而不会干扰其他组件实例。

如果将 data 定义为对象,那么该对象将被所有组件实例共享。这意味着当一个组件实例修改了 data 中的值时,其他组件实例也会受到影响,因为它们引用的是同一个对象。

通过将 data 定义为函数,Vue 可以确保每个组件实例都有自己的数据副本,避免了数据共享和相互干扰的问题,保证了组件的独立性和可复用性。

watch 和 computed 的区别是

watchcomputed 是 Vue 中用于响应式数据处理的两个重要特性,它们之间有一些区别。

区别 1:用途

  • watch:用于监听数据的变化,并在数据变化时执行相应的操作。通常用于监听单个或多个数据的变化,并执行异步操作、网络请求、数据处理等。
  • computed:用于基于已有的数据计算出新的衍生数据。它会缓存计算结果,只有依赖的数据变化时才会重新计算。通常用于计算属性、过滤器、数据转换等。

区别 2:语法

  • watch:通过在组件选项中定义一个 watch 对象来创建监视器。watch 对象中的每个属性都是一个监视器,可以监听一个或多个数据的变化,并指定相应的处理函数。
  • computed:通过在组件选项中定义一个或多个计算属性来创建衍生数据。计算属性的定义类似于普通的数据属性,但是使用 get 函数来计算属性的值。

区别 3:触发时机

  • watch:在被监听的数据变化后立即触发相应的处理函数。可以通过配置 immediate: true 来在初始渲染时立即执行一次处理函数。
  • computed:只有在计算属性所依赖的数据发生变化时才会重新计算计算属性的值。计算属性的值会被缓存,只有当依赖的数据发生改变时,才会重新计算。

区别 4:使用场景

  • watch:适用于监听数据的变化,并执行异步操作、复杂的数据处理、网络请求等。当需要在数据变化时执行一些具有副作用的操作时,通常使用 watch
  • computed:适用于基于已有的数据计算出新的衍生数据。当需要根据现有数据派生出一些衍生数据时,通常使用 computed。它具有缓存特性,避免不必要的重复计算。

综上所述,watch 用于监听数据的变化,并执行相应的操作,而 computed 用于计算衍生数据。它们在用途、语法、触发时机和使用场景上存在一些区别,根据具体的需求和场景选择合适的特性来处理数据。

什么是 keep-alive,有什么作用?如何使用

<keep-alive> 是 Vue 提供的一个内置组件,用于缓存和管理其他组件的实例。它可以将动态组件进行缓存,以便在组件切换时保留其状态和避免重复创建和销毁。

<keep-alive> 组件的作用如下:

  1. 组件缓存: <keep-alive> 可以缓存已经渲染过的组件实例,而不是销毁它们。这样可以避免每次切换到缓存的组件时都重新创建实例和重新渲染,提高了性能和响应速度。

  2. 状态保持: 缓存的组件实例会保持其状态,包括数据、DOM 状态和用户输入等。当再次切换到缓存的组件时,它们可以恢复到之前的状态,提供更好的用户体验。

使用 <keep-alive> 非常简单,只需要将需要缓存的组件包裹在 <keep-alive> 标签内即可。例如:

<template>
<div>
<keep-alive>
<component-a v-if="showComponentA" />
</keep-alive>

<button @click="showComponentA = !showComponentA">
Toggle Component A
</button>
</div>
</template>

<script>
import ComponentA from "./ComponentA.vue";

export default {
components: {
ComponentA,
},
data() {
return {
showComponentA: false,
};
},
};
</script>

在上述示例中,<component-a> 组件被包裹在 <keep-alive> 内。当 showComponentAtrue 时,<component-a> 会被渲染,并被缓存。每次切换 showComponentA 的值时,<component-a> 不会被销毁,而是保持其状态。这样,在切换回 <component-a> 时,它会恢复到之前的状态,而不是重新创建和渲染。

需要注意的是,被 <keep-alive> 缓存的组件在激活和停用时,会触发其特定的生命周期钩子函数(例如 activateddeactivated),以及 <keep-alive> 组件自身的生命周期钩子函数(例如 beforeRouteEnterbeforeRouteLeave)。这些钩子函数可以用于执行一些特定的操作,例如重置数据、处理动画效果等。

总结起来,<keep-alive> 是 Vue 提供的用于缓存和管理组件实例的组件,它可以提高性能、保持组件状态,并提供更好的用户体验。通过简单地将需要缓存的组件包裹在 <keep-alive> 内,即可享受其带来的好处。

keep-alive 的声明周期执行

当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活

一个持续存在的组件可以通过 onActivated()onDeactivated() 注册相应的两个状态的生命周期钩子:

vue

<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})

onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>

请注意:

  • onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。
  • 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。

谈谈对 vue 生命周期的理解

Vue.js 是一款流行的 JavaScript 前端框架,它提供了一套完整的生命周期钩子函数,用于在组件的不同阶段执行特定的操作。理解 Vue 的生命周期对于正确使用和管理组件非常重要。

Vue 的生命周期可以分为以下阶段:

  1. 创建阶段(Creation):

    • beforeCreate:在实例初始化之后,数据观测(data observer)和事件配置之前调用。
    • created:在实例已经完成数据观测(data observer),属性和方法的运算,watchers 和事件回调的设置之后调用。此时,实例已经完成了初始化,但尚未挂载到 DOM 中。
  2. 挂载阶段(Mounting):

    • beforeMount:在挂载开始之前被调用,此时模板编译已完成,但尚未将组件挂载到 DOM 中。
    • mounted:在实例挂载到 DOM 后调用,此时组件已经被渲染到页面上并且可以进行 DOM 操作。
  3. 更新阶段(Updating):

    • beforeUpdate:在响应式数据发生改变,但在更新 DOM 之前调用。
    • updated:在数据变化导致虚拟 DOM 重新渲染和打补丁之后调用。此时组件已经更新到最新状态。
  4. 销毁阶段(Destruction):

    • beforeDestroy:在实例销毁之前调用,此时实例仍然完全可用。
    • destroyed:在实例销毁之后调用,此时实例中的所有指令和事件监听器都已经被移除,组件被彻底销毁。

生命周期钩子函数允许我们在组件不同的阶段执行自定义的逻辑。例如,在 created 钩子函数中可以进行数据初始化或异步操作的请求,而在 mounted 钩子函数中可以执行 DOM 操作或与第三方库进行交互。

理解生命周期还有助于解决组件间的通信和资源管理问题。例如,在 beforeDestroy 钩子函数中可以清理定时器、取消订阅或释放其他资源,以避免内存泄漏。

需要注意的是,Vue 3 中的生命周期钩子函数发生了一些变化,具体的钩子函数名称和用法可能会有所不同。在使用 Vue 3 时,建议参考官方文档以获取最新的生命周期信息。

总之,Vue 的生命周期提供了一种组件生命周期管理的机制,通过合理地使用生命周期钩子函数,我们可以在不同的阶段执行相应的操作,并且更好地控制和管理组件的行为。

v-for 中 key 的作用

在 Vue 的 v-for 指令中,key 属性用于帮助 Vue 跟踪和管理被渲染的元素列表。key 的作用是为每个列表项提供一个唯一的标识符,以便 Vue 可以识别每个项的身份,从而高效地更新和重用 DOM 元素。

以下是 key 属性的几个重要作用:

1. 识别每个列表项的身份: 通过给每个列表项设置唯一的 key 值,Vue 可以确定每个项的身份。这样,在列表数据发生变化时,Vue 可以更准确地识别出新旧列表中哪些项发生了变化、被添加或被删除。

2. 提高 DOM 元素的重用性: Vue 使用 key 来判断列表中的元素是否被复用。当列表数据变化时,Vue 会尽可能地复用已存在的 DOM 元素,而不是重新创建和销毁元素。使用相同的 key 值,Vue 会将新的列表项映射到已存在的元素上,这样可以减少 DOM 操作,提高渲染性能。

3. 保持组件状态和用户输入: 如果在列表中的项包含表单输入、状态或组件实例,使用 key 可以确保在重新渲染时,每个项能够保持自己的状态和用户输入。如果没有设置 key,Vue 可能会将旧的状态应用到新的项上,导致意料之外的行为。

需要注意的是,key 的值应该是唯一且稳定的。最好使用列表项的唯一标识符,如数据库中的 ID 或具有唯一性的属性。避免使用索引作为 key,因为索引在列表项顺序变化时可能会导致性能问题和不良行为。

以下是一个使用 key 的示例:

<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>

在上述示例中,每个列表项都有一个唯一的 id 属性,通过 :key="item.id"item.id 作为 key 值,以确保每个项的唯一性和正确的更新。

vue 组件的通信方式

在 Vue 中,组件之间可以通过多种方式进行通信。以下是常用的几种通信方式:

1. 父子组件通信: 父组件可以通过 props 将数据传递给子组件,并在子组件中使用。子组件可以通过事件($emit)将数据发送给父组件。这种方式适用于父子组件之间的简单通信。

2. 子组件访问父组件: 子组件可以通过 $parent 属性访问其父组件实例,并直接访问父组件的数据和方法。但是这种方式在组件层级较深时可能不太方便,且耦合度较高,不推荐在大型应用中广泛使用。

3. 兄弟组件通信: 兄弟组件之间的通信可以通过共享状态(如父组件中的数据或 Vuex 状态管理)来实现。兄弟组件可以通过共同的父组件或 Vuex 作为中央事件总线来进行通信。

4. 使用事件总线(Event Bus): 可以创建一个独立的 Vue 实例作为事件总线,用于组件之间的通信。通过在事件总线实例上触发和监听自定义事件,组件可以进行跨层级的通信。这种方式适用于中小型应用,但在大型应用中易于混乱,因此需要慎重使用。

5. 使用 Vuex 状态管理: Vuex 是 Vue.js 官方的状态管理库,用于在多个组件之间共享和管理状态。通过定义和修改 Vuex 中的状态,组件可以实现高效的通信和状态共享。Vuex 适用于大型复杂应用,提供了强大的状态管理能力。

6. 使用事件触发器(Vue 3): Vue 3 中引入了 Composition API,其中包括 provideinject 方法,用于父组件向子孙组件提供数据和方法。通过 provide 在父组件中提供数据,在子孙组件中使用 inject 获取提供的数据。这种方式适用于跨层级的组件通信。

以上是一些常见的 Vue 组件通信方式。选择合适的通信方式取决于应用程序的规模、复杂性和组件之间的关系。对于简单的父子组件通信,使用 props 和事件机制通常足够。对于更复杂的应用程序,可以考虑使用 Vuex 或其他状态管理方案来管理共享状态和实现组件之间的通信。

双向绑定实现原理

双向绑定是 Vue 中一项重要的特性,它可以将模型(数据)和视图(DOM)之间的变化自动同步,使开发者无需显式地操作 DOM 元素。

在 Vue 中,双向绑定的实现原理可以概括如下:

  1. 数据劫持(Data Observation): Vue 使用了一种称为“响应式的数据劫持”机制,通过使用 Object.defineProperty() 方法来劫持(拦截)对象的属性访问,从而实现对数据的观测。当访问或修改数据时,Vue 可以捕获到这些操作,并触发相应的更新。

  2. 监听器(Watcher): 在 Vue 中,每个被观测的数据属性都会关联一个 Watcher 对象,它负责订阅数据的变化并执行相应的回调函数。当数据发生变化时,Watcher 会接收到通知,并触发视图的更新。

  3. 模板编译(Template Compilation): Vue 使用基于模板的语法来描述视图,即使用类似 HTML 的模板来定义组件的结构和内容。在编译过程中,Vue 会将模板转换为渲染函数,并在渲染函数中插入数据绑定和更新逻辑。这样,当数据发生变化时,渲染函数会重新执行,更新视图。

  4. 视图更新(View Update): 当数据发生变化时,相应的 Watcher 会被触发,它会调用渲染函数来重新生成虚拟 DOM(Virtual DOM)。然后,Vue 会通过比较新旧虚拟 DOM,找出需要更新的部分,并将这些变化应用到实际的 DOM 中,实现视图的更新。

总结起来,Vue 的双向绑定通过数据劫持、监听器、模板编译和视图更新等机制实现。当数据发生变化时,Vue 可以自动感知并更新相关的视图,而当用户与视图进行交互时,也能自动将变化的值同步到数据中。这种自动的数据流动使得开发者可以专注于业务逻辑,而不必关注手动操作 DOM 的细节。

v-model 的实现以及它的实现原理吗?

v-model 是 Vue 中用于实现表单元素双向数据绑定的指令,它可以将表单元素的值与 Vue 实例的数据属性进行绑定,实现数据的自动同步。

v-model 的实现原理可以概括如下:

  1. 对于输入元素(如 <input><textarea><select>): 当使用 v-model 指令绑定一个输入元素时,Vue 首先会监听输入元素的 inputchange 事件。当用户输入或选择内容时,会触发该事件,Vue 会捕获到输入元素的值,并将其更新到绑定的数据属性上。

  2. 对于组件: 当使用 v-model 指令绑定一个自定义组件时,Vue 会查找组件中是否定义了名为 value 的 prop 属性和名为 input 的事件。value prop 用于接收数据属性的值,input 事件用于在值发生变化时通知 Vue 更新数据属性。当用户与组件进行交互时,组件会触发 input 事件并传递新的值,Vue 会捕获到该事件,并将新的值更新到数据属性上。

综上所述,v-model 的实现原理主要是通过事件监听和数据更新来实现双向数据绑定。当用户与表单元素或自定义组件进行交互时,Vue 会根据元素类型或组件定义的接口来获取新的值,并将其更新到绑定的数据属性上。同时,当数据属性的值发生变化时,Vue 会将新的值同步到表单元素或组件上,保持数据的一致性。

需要注意的是,v-model 并不是一个单独的指令,它实际上是 Vue 内置指令的语法糖。在不同类型的表单元素上,v-model 的实际转化形式会略有不同,但原理是相似的。通过 v-model,我们可以简化表单元素与数据属性之间的绑定,提高开发效率。

nextTick 的实现

nextTick 是 Vue 提供的一个异步方法,它用于在下次 DOM 更新循环结束之后执行回调函数。它的实现原理可以简述如下:

  1. 首先,Vue 会使用浏览器原生提供的 PromiseMutationObserver(如果可用)来异步执行回调函数。这些原生方法能够在下一次事件循环中执行回调,确保在 DOM 更新之后执行。

  2. 如果浏览器不支持上述方法,Vue 会降级为使用 setTimeout 进行异步延迟执行。通过设置一个延迟时间为 0 的定时器,将回调函数放入宏任务队列中,在下一次事件循环中执行。

总的来说,无论使用哪种异步机制,nextTick 的实现都是通过将回调函数推入一个任务队列,在下一次事件循环中执行。这样可以确保在当前代码执行完毕、DOM 更新完成后,才会执行 nextTick 的回调函数。

使用 nextTick 的主要目的是在 Vue 更新 DOM 后,立即执行一些需要基于更新后的 DOM 进行操作的代码。这样可以确保在更新后的 DOM 上进行操作,避免出现同步代码中无法获取到最新 DOM 的问题。

在代码中使用 nextTick 的示例:

Vue.nextTick(() => {
// 在 DOM 更新后执行的代码
});

需要注意的是,由于 nextTick 会异步执行回调函数,所以在回调函数内部访问的数据可能已经发生了变化。如果需要确保访问的数据是最新的,可以在 nextTick 回调函数内使用闭包或者通过参数传递需要的数据。

另外,Vue 3 中的 Composition API 中提供了类似的 nextTick 函数,可以使用 import { nextTick } from 'vue' 来引入并使用。它的使用方式和原理与 Vue 2 中的 nextTick 类似。

vnode 的理解,compiler 和 patch 的过程

vnode(虚拟节点)是 Vue 中用于描述 DOM 结构的对象。它是 Vue 在进行渲染过程中的一个重要概念,用于表示组件的结构和内容,以及组件之间的关系。

vnode 的结构一般包含以下几个关键属性:

  • tag:表示节点的标签名,如 "div", "span", "p" 等。
  • props:表示节点的属性,如 "class", "style", "id" 等。
  • children:表示节点的子节点,可以是一个 vnode 对象或一个包含多个 vnode 对象的数组。
  • text:表示节点的文本内容,用于表示纯文本节点的内容。

在 Vue 的编译和渲染过程中,vnode 扮演着重要的角色。下面是简要的编译和渲染过程:

  1. 编译过程(Compiler): 在编译过程中,Vue 的模板会被转换为渲染函数。编译器会对模板进行解析,生成对应的 vnode 树,该树结构包含了模板中的各个节点信息。编译器会遍历模板的 AST(抽象语法树),将每个节点转换为对应的 vnode 对象。

  2. 渲染过程(Patch): 渲染过程是将 vnode 对象转换为实际的 DOM 元素,并将其插入到文档中的过程。在渲染过程中,Vue 会将旧的 vnode 与新的 vnode 进行比较,并找出需要更新的部分。这个过程称为补丁(patch)。

    • 首次渲染:如果是首次渲染,Vue 会将整个 vnode 树转换为实际的 DOM 元素,并插入到文档中。
    • 更新节点:如果是更新节点,Vue 会根据新的 vnode 与旧的 vnode 进行比较,并找出需要更新的部分。然后,Vue 会将更新的部分应用到实际的 DOM 元素上,实现局部的 DOM 更新。

    在比较和更新过程中,Vue 会使用一些优化策略,如使用 key 属性来提高节点的比较效率,只更新发生变化的节点等。

总结起来,vnode 是 Vue 中用于描述 DOM 结构的对象,它在编译和渲染过程中起到了关键的作用。编译器会将模板转换为 vnode 树,而补丁过程则将 vnode 转换为实际的 DOM 元素,并实现局部的 DOM 更新。这样,Vue 实现了高效的虚拟 DOM 更新,减少了直接操作实际 DOM 的成本,提高了性能。

new Vue 后整个的流程

好的,我理解您的问题。让我来详细介绍一下在创建一个新的 Vue 实例后整个的流程:

  1. 初始化 Vue 实例:

    • 当使用 new Vue() 创建一个新的 Vue 实例时,Vue 会进行初始化工作,如设置一些选项属性、观察数据变化、编译模板等。
  2. 数据响应式系统:

    • Vue 会通过使用 Object.defineProperty() 方法将数据属性转换为 getter 和 setter,从而实现数据的响应式。当数据发生变化时,Vue 可以检测到并更新对应的视图。
  3. 模板编译:

    • Vue 会将模板编译成渲染函数。这个编译过程会将模板解析成 AST(抽象语法树),然后生成优化的 JavaScript 渲染函数。
  4. 首次渲染:

    • 当数据和模板都准备就绪后,Vue 会调用渲染函数,生成虚拟 DOM 树(vnode)。
    • Vue 使用 diff 算法比较新旧 vnode,找出需要更新的部分,然后更新到实际的 DOM 中。
  5. 事件监听和数据更新:

    • Vue 会自动为模板中的事件添加事件监听器,当用户交互时触发对应的事件处理函数。
    • 当数据发生变化时,Vue 的响应式系统会自动检测到变化,然后再次调用渲染函数,生成新的 vnode,进行 DOM 更新。
  6. 生命周期钩子:

    • 在 Vue 实例的各个阶段,如创建、挂载、更新、销毁等,都会触发相应的生命周期钩子函数。开发者可以在这些钩子函数中执行自定义的逻辑。
  7. 组件嵌套与通信:

    • 复杂的 Vue 应用通常由多个组件组成,组件之间可以通过 props、事件、 Provide/Inject 等方式进行通信。
  8. 其他功能:

    • Vue 还提供了丰富的功能,如指令系统、过滤器、插件机制等,供开发者进一步扩展和定制 Vue 应用。

总之,从创建 Vue 实例到最终渲染和更新 DOM,Vue 内部经历了一系列的初始化、编译、渲染、响应式等过程。这些过程保证了 Vue 可以高效地管理组件的状态和视图,为开发者提供了便利。

你怎么理解 Vue 中的 diff 算法?

Vue 中的 diff 算法是用来比较新旧虚拟 DOM 树,找出需要更新的部分并更新到实际 DOM 树上的算法。它是一个高效的算法,可以最大限度地减少 DOM 操作,提高性能。

diff 算法的核心思想是:

  1. 首先,比较两个虚拟 DOM 树的根节点。
  2. 如果根节点相同,则继续比较其子节点。
  3. 如果子节点不同,则根据以下规则进行更新:
    • 如果新节点是文本节点,则用新节点替换旧节点。
    • 如果新节点是元素节点,则比较其属性和子节点。
    • 如果新节点的属性和子节点都不同,则用新节点替换旧节点。
    • 如果新节点的属性和子节点都相同,则不进行任何操作。

diff 算法的优化

  1. 使用双指针算法来比较两个虚拟 DOM 树的子节点,可以减少不必要的比较。
  2. 使用 key 属性来标识虚拟 DOM 节点,可以提高比较效率。
  3. 使用一些额外的优化策略,例如缓存和跳过一些不必要的比较。

diff 算法的优点

  1. 高效:diff 算法可以最大限度地减少 DOM 操作,提高性能。
  2. 准确:diff 算法可以准确地找出需要更新的部分,并更新到实际 DOM 树上。
  3. 易于理解:diff 算法的思想简单易懂,易于理解和实现。

diff 算法的缺点

  1. 复杂度高:diff 算法的复杂度较高,在大型应用中可能会影响性能。
  2. 难以调试:diff 算法的代码比较复杂,难以调试。

总而言之,diff 算法是 Vue 中一个重要的算法,它可以提高 Vue 的性能和效率。了解 diff 算法的原理和实现可以帮助我们更好地理解 Vue 的内部机制,并编写更高效的 Vue 代码。

你都做过哪些 Vue 的性能优化?

在 Vue 中,性能优化是一个持续的过程,需要根据实际情况进行调整。以下是一些常见且有效的优化方法:

1. 虚拟 DOM 优化:

  • 减少数据层级: 尽量减少组件嵌套层级,避免数据层级过深导致虚拟 DOM 更新频繁。
  • 使用 v-for 替代 v-if: 当需要循环渲染多个元素时,使用 v-for 比 v-if 更高效。
  • 使用 computed 属性: 将复杂计算逻辑封装在 computed 属性中,可以减少重复计算,提高性能。
  • 使用缓存: 对于经常使用的数据,可以使用缓存机制,避免重复获取和处理数据。

2. 组件优化:

  • 拆分大型组件: 将大型组件拆分成多个更小的组件,可以提高代码的可维护性和性能。
  • 使用函数式组件: 函数式组件比普通组件更轻量级,可以提高性能。
  • 使用 keep-alive 缓存组件: 对于经常使用的组件,可以使用 keep-alive 缓存组件,避免重复渲染。

3. 数据绑定优化:

  • 避免使用 v-bind 动态绑定大量数据: 动态绑定大量数据会降低性能,可以使用 computed 属性或方法进行处理。
  • 使用 v-model 替代 v-on: 在表单元素中,使用 v-model 比 v-on 更高效。
  • 使用事件代理: 对于需要监听大量事件的元素,可以使用事件代理机制,减少事件监听器数量。

4. 其他优化:

  • 使用 CDN 加载静态资源: 将静态资源如 JavaScript、CSS 文件放在 CDN 上,可以提高页面加载速度。
  • 使用代码压缩工具: 使用代码压缩工具可以减小代码体积,提高性能。
  • 使用性能分析工具: 使用性能分析工具可以帮助你分析页面性能瓶颈,并进行针对性优化。

你知道 Vue3 有哪些新特性吗?它们会带来什么影响?

Vue 3 的新特性

Vue 3 带来了许多新特性,它们将对 Vue 的开发体验和性能产生重大影响。以下是一些主要的特性:

1. Composition API: Composition API 是一种新的 API,它允许你以更灵活的方式组织你的代码。它可以让你将代码分解成更小的、可重用的函数,并使用它们来创建组件。

2. 虚拟 DOM 优化: Vue 3 对虚拟 DOM 进行了重构,使其性能更高效。新的虚拟 DOM 算法可以减少更新 DOM 的次数,从而提高页面渲染速度。

3. 响应式系统改进: Vue 3 对响应式系统进行了改进,使其更易于使用和理解。新的响应式系统可以更好地处理复杂的数据结构,并提供更好的错误提示。

4. TypeScript 支持: Vue 3 提供了对 TypeScript 的官方支持。这使得开发人员可以更容易地编写类型安全的代码,并获得更好的代码完成和错误提示。

5. 其他特性: 除了以上主要特性,Vue 3 还引入了许多其他新特性,例如:

  • 全局 API 改进
  • 新的指令和组件
  • 改进的调试工具
  • 改进的国际化支持

新特性的影响

Vue 3 的新特性将对 Vue 的开发体验和性能产生重大影响。以下是其中一些影响:

  • 更灵活的代码组织: Composition API 允许开发人员以更灵活的方式组织代码,这将使代码更容易维护和重用。
  • 更快的页面渲染: 虚拟 DOM 优化和响应式系统改进将使页面渲染速度更快,从而改善用户体验。
  • 更易于编写类型安全的代码: TypeScript 支持将使开发人员更容易编写类型安全的代码,这将有助于提高代码质量和减少错误。
  • 更好的开发体验: 新的特性和改进将使 Vue 的开发体验更好,这将吸引更多的开发人员使用 Vue。

总而言之,Vue 3 的新特性将使 Vue 成为一个更强大、更易于使用和更受欢迎的框架。

实现双向绑定 Proxy 与 Object.defineProperty 相比优劣如何?

Proxy

优点:

  • 简洁易用: Proxy 提供了一种更简洁易用的方式来拦截和处理对象的属性访问和修改。
  • 灵活性高: Proxy 可以拦截所有类型的属性访问,包括原型链上的属性。
  • 功能强大: Proxy 可以实现更复杂的双向绑定逻辑,例如级联更新和数据校验。

缺点:

  • 浏览器兼容性: Proxy 是 ES6 的新特性,在一些旧浏览器中可能无法使用。
  • 性能开销: Proxy 在拦截属性访问时会带来一些性能开销。

Object.defineProperty

优点:

  • 浏览器兼容性: Object.defineProperty 是 ES5 的特性,在所有现代浏览器中都支持。
  • 性能更高: Object.defineProperty 的性能开销比 Proxy 更低。

缺点:

  • 使用复杂: Object.defineProperty 需要手动为每个属性定义拦截器,这比较繁琐。
  • 灵活性低: Object.defineProperty 只能拦截直接定义在对象上的属性,无法拦截原型链上的属性。
  • 功能有限: Object.defineProperty 只能实现简单的双向绑定逻辑,例如数据更新。

总结

总的来说,Proxy 在实现双向绑定方面比 Object.defineProperty 更简洁易用、灵活和强大,但性能开销也更高。Object.defineProperty 则更轻量级,但使用起来更复杂,功能也更有限。

在实际应用中,可以选择根据项目的具体需求和浏览器兼容性来选择合适的方案。如果需要实现更复杂的双向绑定逻辑,或者需要更好的浏览器兼容性,可以选择 Proxy。如果需要更轻量级的方案,可以选择 Object.defineProperty。

nextTick 的实现原理是什么?

Vue.js 的 nextTick 方法是一个非常重要的工具,它允许你延迟一次更新周期后再执行某个操作。这在你需要等待 Vue 完成所有的 DOM 更新后再进行某些操作时非常有用。

nextTick 的实现原理是基于 JavaScript 的事件循环和微任务(microtask)队列的。在 JavaScript 中,微任务的优先级高于宏任务(macrotask)。Vue.js 利用了这个特性,通过创建一个 Promise、MutationObserver 或 setImmediate 的微任务,来延迟执行你传给 nextTick 的函数。

当你在 Vue 组件中修改数据后,Vue 会异步地把这些改动添加到一个队列中,然后在下一个事件循环的微任务阶段执行这个队列,完成 DOM 的更新。如果你在这个过程中再次修改了数据,Vue 会把这些改动合并到同一个队列中,避免了不必要的 DOM 更新和重渲染。

因此,当你调用 nextTick 时,你传给它的函数会被添加到一个不同的队列中,这个队列会在上述的 DOM 更新队列执行完毕后再执行。这就保证了你在 nextTick 的回调函数中看到的 DOM 是最新的状态。

以下是一个简单的例子:

new Vue({
el: "#app",
data: {
message: "Hello Vue!",
},
methods: {
updateMessage: function () {
this.message = "Hello nextTick!";
this.$nextTick(function () {
console.log(this.message); // 输出 "Hello nextTick!"
});
},
},
});

在这个例子中,updateMessage 方法首先修改了 message 数据,然后调用了 nextTick。在 nextTick 的回调函数中,我们可以确保 console.log 输出的是最新的 message 数据。这就是 nextTick 的实现原理和用法。希望这个解释对你有所帮助!

Vue 2.x 模板中的指令是如何解析实现的?

指令本质上就是一个 JavaScript 对象,对象上挂着一些钩子函数,无论是官方提供的指令,还是自定义指令,一个指令从第一次被绑定到元素上到最终与被绑定的元素解绑,它会经过以下几种状态:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

了每个状态的钩子函数,这样我们就可以让指令在不同状态下做不同的事情。当虚拟 DOM 渲染更新的时候会触发 create、update、destory 这三个钩子函数,从而就会执行 updateDirectives 函数来处理指令的相关逻辑,执行指令函数,让指令生效。

链接:https://juejin.cn/post/6987070062490288165

简要说明 Vue 2.x 的全链路运作机制?

Vue 2.x 的全链路运作机制可以分为以下几个关键部分:模板解析、渲染函数生成、虚拟 DOM、响应式系统和组件实例。

  1. 模板解析:Vue 2.x 使用编译器将模板解析为抽象语法树(AST),识别其中的指令、表达式和静态内容。

  2. 渲染函数生成:基于解析得到的 AST,编译器生成渲染函数。渲染函数是一个 JavaScript 函数,用于生成组件的虚拟节点树。

  3. 虚拟 DOM:渲染函数执行时,会生成组件的虚拟节点树。虚拟节点是一个描述组件结构和数据的 JavaScript 对象,它们可以用于渲染和更新视图。

  4. 响应式系统:Vue 2.x 的响应式系统通过 Object.defineProperty 实现对组件数据的劫持。当数据发生变化时,系统会追踪依赖并触发相应的更新,从而实现响应式的视图更新。

  5. 组件实例:每个组件都是一个 Vue 实例,包含了数据、计算属性、方法以及生命周期钩子等。组件实例与虚拟节点树建立联系,通过组件实例维护状态和处理用户交互。

整个链路的运作机制如下:

  1. 初始化阶段:Vue 实例化时,会进行组件的初始化。初始化过程包括对数据进行响应式处理、编译模板生成渲染函数等。

  2. 挂载阶段:将渲染函数执行得到的虚拟节点树挂载到真实 DOM 上,完成组件的初次渲染。

  3. 响应式和依赖追踪:当组件数据发生变化时,响应式系统会追踪数据的依赖关系,并触发相应的更新。这包括重新执行渲染函数生成新的虚拟节点树,并通过虚拟 DOM 的 diff 算法与旧的虚拟节点树进行比较,更新真实 DOM。

  4. 用户交互和事件处理:当用户与组件进行交互时,Vue 的事件系统会捕获和处理事件,并调用组件实例中定义的方法或触发相应的事件钩子。

  5. 生命周期钩子:在组件的生命周期中,Vue 提供了一系列的生命周期钩子函数,允许开发者在组件不同的阶段执行自定义逻辑。

  6. 卸载和销毁:当组件不再需要时,会进行卸载和销毁。这时会清理相关的事件监听、定时器等资源,并将组件从 DOM 中移除。

通过以上的机制,Vue 2.x 实现了组件化的开发方式,提供了高效的视图更新和响应式数据管理,使开发者能够构建灵活、可维护的 Web 应用。

如何理解 Vue 是一个渐进式框架?

Vue 是一个渐进式框架,这意味着它被设计成可以逐步采用和应用的框架。这种渐进式的设计思想使得开发者可以根据项目的需求和复杂度选择性地引入和使用 Vue 的不同特性和功能。

以下是对 Vue 渐进式框架的理解:

  1. 核心库:Vue 的核心库(Vue.js)专注于视图层的渲染和响应式,它提供了基本的数据绑定、组件化和虚拟 DOM 等功能。你可以将 Vue.js 仅用于处理视图层,而保留现有的前端技术栈(如 jQuery 或 AngularJS)来处理其他方面的功能。

  2. 组件化开发:Vue 提供了组件化的开发方式,允许将应用程序拆分为小而可复用的组件。你可以逐步将现有的应用程序迁移到 Vue,只需将其中一个部分或页面替换为 Vue 组件,而不必重写整个应用程序。

  3. 插件系统:Vue 具有强大的插件系统,使得开发者可以选择性地引入第三方插件来增强 Vue 的功能。例如,Vue Router 用于处理路由,Vuex 用于状态管理,Vue CLI 用于项目脚手架等。你可以根据项目的需要选择性地使用这些插件。

  4. 工具链和生态系统:Vue 提供了一整套的工具链和生态系统,包括 Vue Router、Vuex、Vue CLI、Vue Devtools 等。这些工具和生态系统相互配合,提供了开发、调试和部署 Vue 应用的全面支持。

总而言之,Vue 的渐进式设计使得开发者可以根据项目的需求和现有的技术栈,选择性地引入和使用 Vue 的不同功能和工具。这种灵活性使得 Vue 可以适应各种规模和复杂度的项目,并促进了代码的重用性、可维护性和扩展性。

Vue 里实现跨组件通信的方式有哪些?

在 Vue 中,有几种方式可以实现跨组件通信:

  1. Props 和事件:通过使用 props 属性和自定义事件,可以在父组件和子组件之间进行通信。父组件通过 props 将数据传递给子组件,子组件通过触发事件将数据传递回父组件。这种方式适用于父子组件之间的通信。

  2. 事件总线:可以创建一个空的 Vue 实例作为事件总线,用于在非父子关系的组件之间进行通信。通过在事件总线上触发和监听事件,组件可以进行跨层级的通信。需要注意的是,事件总线可以成为全局事件中心,因此在大型应用中要小心管理事件名称,以避免冲突。

  3. Vuex(状态管理):Vuex 是 Vue 的官方状态管理库,用于管理应用程序的状态。通过在 Vuex 的 store 中定义和修改状态,组件可以通过订阅和派发 actions 和 mutations 来进行跨组件通信。Vuex 适用于大型应用程序或需要共享状态的多个组件之间的通信。

  4. $refs:Vue 组件实例上的 $refs 属性可以用于在父组件中访问子组件的实例。通过 $refs,父组件可以直接调用子组件的方法或访问子组件的数据,实现跨组件通信。

  5. provide / inject:通过 provide 和 inject,可以在父组件中提供数据,并在子孙组件中进行注入。这种方式可以实现跨层级组件之间的通信,但需要注意使用时的依赖关系和耦合性。

需要根据具体的场景和需求选择适合的方式来实现跨组件通信。对于简单的父子组件通信,可以使用 props 和事件;对于跨层级或大型应用程序,可以考虑使用事件总线、Vuex 或 $refs。

Vue 中响应式数据是如何做到对某个对象的深层次属性的监听的?

Vue 中响应式数据通过使用 JavaScript 的 Object.defineProperty 方法对对象进行劫持来实现对深层次属性的监听。

当 Vue 初始化一个组件实例时,它会遍历组件实例的数据对象,并使用 Object.defineProperty 将每个属性转换为 getter 和 setter。这样,当访问或修改这些属性时,Vue 能够捕获并响应变化。

对于对象的深层次属性,当 Vue 遇到一个对象属性是对象或数组时,它会递归地对该属性进行深层次监听。具体的实现逻辑如下:

  1. 对象属性劫持:当 Vue 遇到一个对象属性时,它会为该属性创建一个 Dep(依赖)对象,并为该属性的值递归调用 observe 方法,实现对对象属性的深层次监听。

  2. 数组劫持:对于数组,Vue 重写了数组的一些方法(如 push、pop、splice 等),以便在调用这些方法时能够触发响应式更新。通过拦截这些数组方法,在执行数组操作后,Vue 可以通知相关的 Watcher(观察者)进行更新。

  3. 依赖追踪:每个属性的 getter 在被访问时会收集依赖,将当前的 Watcher 添加到属性的 Dep 对象中。当属性的 setter 被调用时,它会通知 Dep 中的所有 Watcher 进行更新。

  4. Watcher:Watcher 是 Vue 响应式系统的核心,它负责依赖的管理和更新。每个组件实例都会有一个 Watcher,它会订阅组件中使用的响应式数据,并在数据变化时触发组件的重新渲染。

通过这种方式,Vue 实现了对对象的深层次属性的监听。无论对象的属性层级多深,只要访问或修改这些属性,Vue 都能够捕获变化并触发相应的更新,保证了视图与数据的同步。

MVVM、MVC 和 MVP 的区别是什么?各自有什么应用场景?

MVVM、MVC 和 MVP 是三种常见的软件架构模式,它们在组织和管理应用程序的代码和逻辑方面有一些区别和特点。

  1. MVC(Model-View-Controller)模式:

    • 概念:MVC 将应用程序分为三个主要部分:模型(Model)、视图(View)和控制器(Controller)。
    • 职责:
      • 模型(Model):负责处理应用程序的数据和业务逻辑。
      • 视图(View):负责呈现用户界面,将数据显示给用户。
      • 控制器(Controller):负责处理用户交互、更新模型和通知视图更新。
    • 应用场景:MVC 适用于需要将应用程序的逻辑和界面分离的场景。例如,Web 应用程序中的后端服务和前端界面可以使用 MVC 架构进行开发。
  2. MVP(Model-View-Presenter)模式:

    • 概念:MVP 是一种演化自 MVC 的模式,将应用程序分为三个主要部分:模型(Model)、视图(View)和呈现器(Presenter)。
    • 职责:
      • 模型(Model):负责处理应用程序的数据和业务逻辑。
      • 视图(View):负责呈现用户界面,将数据显示给用户,但不处理用户交互。
      • 呈现器(Presenter):充当视图和模型之间的中介,处理用户交互并更新模型和视图。
    • 应用场景:MVP 适用于需要将视图与模型分离,并且需要更好的测试性和可维护性的场景。例如,桌面应用程序和某些 Android 应用程序可以使用 MVP 架构。
  3. MVVM(Model-View-ViewModel)模式:

    • 概念:MVVM 是一种将应用程序分为三个主要部分:模型(Model)、视图(View)和视图模型(ViewModel)的模式。
    • 职责:
      • 模型(Model):负责处理应用程序的数据和业务逻辑。
      • 视图(View):负责呈现用户界面,将数据显示给用户。
      • 视图模型(ViewModel):充当视图和模型之间的中介,将模型数据转换为视图所需的数据,并处理用户交互。
    • 应用场景:MVVM 适用于需要实现数据绑定和前端开发的场景。例如,基于 Web 的应用程序和移动应用程序可以使用 MVVM 架构。

总结:

  • MVC 主要关注于应用程序的分层和代码组织,适用于传统的后端服务和前端界面分离的场景。
  • MVP 强调视图与模型的分离,适用于需要更好可测试性和可维护性的桌面应用程序和某些 Android 应用程序。
  • MVVM 强调数据绑定和前端开发,适用于基于 Web 的应用程序和移动应用程序。

选择适合的架构模式取决于具体的应用需求、开发团队的熟悉度以及项目的规模和复杂性。

Vue CLI 3.x 有哪些功能?Vue CLI 3.x 的插件系统了解?

Vue CLI 3.x 是一个基于 Vue.js 的官方脚手架工具,用于快速搭建 Vue.js 项目。它提供了一系列功能和工具,使得项目的开发、构建和部署更加便捷和高效。

Vue CLI 3.x 的主要功能包括:

  1. 项目搭建:Vue CLI 3.x 提供了快速创建 Vue.js 项目的能力。它支持创建基于现有预设模板的项目,如默认的 Vue 2、Vue 3、TypeScript、PWA(渐进式 Web 应用)等,并且可以自定义配置选项。

  2. 开发服务器:Vue CLI 3.x 集成了开发服务器,支持热重载(Hot Reload)功能,能够在开发过程中实时预览和调试项目。

  3. 插件系统:Vue CLI 3.x 引入了插件系统,使得开发人员可以通过插件来扩展和定制项目的功能。插件可以用于添加新的命令、配置、依赖项等,从而满足特定项目的需求。

  4. 配置管理:Vue CLI 3.x 使用了基于项目的配置方式,通过一个名为 vue.config.js 的配置文件来管理项目的构建配置。可以在该文件中配置构建选项、Webpack 相关的配置、开发服务器等。

  5. 构建和部署:Vue CLI 3.x 提供了用于构建生产环境代码的命令,包括代码压缩、文件打包、资源优化等。同时,它还支持一键部署到静态文件托管服务(如 GitHub Pages、Netlify 等)。

关于 Vue CLI 3.x 的插件系统,它允许开发人员创建和使用插件来扩展 Vue CLI 的功能。插件可以包含一系列的功能、配置和命令,以满足特定的项目需求。Vue CLI 3.x 的插件系统允许插件作者在项目创建阶段自动执行任务、修改配置、添加依赖等。开发人员可以使用 Vue CLI 3.x 提供的命令来安装和管理插件,也可以自己开发和发布插件供他人使用。

通过插件系统,Vue CLI 3.x 提供了灵活的扩展机制,使得开发人员可以根据项目需求定制和拓展构建工具的功能,提高开发效率和项目的可维护性。

Vue CLI 3.x 中的 Webpack 是如何组装处理的?

对比 vue-cli2,cli3 最主要的就是生成的项目中,进行 webpack 配置的文件没有了。cli3 的脚手架封装了 webpack 绝大部分配置,使得生成的项目更加清晰,但是在开发中免不了会有自己的个性需求,来添加一些自己的项目配置,此时只需在项目的根目录下新建一个 vue.config.js 文件即可。而 webpack 中是通过 resolve.alias 来实现此功能的。在 vue.config.js 中修改 webpack 的配置,可以通过 configureWebpack 方法。

链接:https://juejin.cn/post/6987070062490288165

Vue SSR 的工作原理?Vuex 的数据如何同构渲染?

Vue SSR,即 Vue 的服务器端渲染,其工作原理主要包括以下步骤:

  1. 服务器接收请求:当服务器接收到一个请求时,会将请求交给 Vue SSR。
  2. 创建 Vue 实例:Vue SSR 会创建一个新的 Vue 实例,这个实例会根据请求的 URL 渲染对应的组件。
  3. 渲染为 HTML:Vue SSR 会将 Vue 实例渲染为 HTML 字符串,并将其发送回客户端。
  4. 客户端接收 HTML:客户端接收到 HTML 后,会将其解析并显示在浏览器中。
  5. 挂载 Vue 实例:最后,客户端会创建一个新的 Vue 实例,并将其挂载到从服务器接收到的 HTML 上。

至于 Vuex 的数据同构渲染,其主要步骤如下:

  1. 服务器预取数据:在服务器端,我们可以在渲染之前预取和解析数据。这是通过在路由信息对象上暴露出一个静态的 asyncData 方法来完成的,此方法可以在组件被渲染之前调用。
  2. 填充 Store:预取的数据会被用来填充 Vuex Store。
  3. 序列化和内联在 HTML 中:然后,服务器会将状态序列化并内联在页面 HTML 中。
  4. 客户端挂载到状态:最后,在客户端,Store 会从内联的状态中进行初始化。

这样,服务器和客户端共享了同一份 Store 状态,实现了数据的同构渲染。希望这个解释对你有所帮助!

SSR 技术和 SPA 技术的各自的优缺点是什么?

服务器端渲染 (SSR) 和单页应用 (SPA) 都有各自的优点和缺点。下面是一些主要的考虑因素:

服务器端渲染 (SSR):

优点

  1. 更好的 SEO:由于搜索引擎爬虫可以直接抓取到完整渲染的页面,因此 SSR 可以提供更好的搜索引擎优化。
  2. 更快的首屏加载时间:SSR 可以在服务器端直接生成 HTML,所以用户可以更快地看到首屏页面。

缺点

  1. 服务器压力大:每次请求都需要服务器端渲染,这会增加服务器的 CPU 和内存消耗。
  2. 总体体验较差:由于每次页面跳转都需要从服务器获取新的 HTML,所以可能会出现页面闪烁的情况。

单页应用 (SPA):

优点

  1. 流畅的用户体验:SPA 只需要加载一次 HTML、CSS 和 JavaScript,之后所有的操作都可以在客户端完成,用户体验更加流畅。
  2. 减轻服务器压力:由于大部分工作都在客户端完成,所以可以减轻服务器的压力。

缺点

  1. SEO 不友好:由于 SPA 的内容是通过 JavaScript 动态生成的,一些搜索引擎可能无法正确地抓取到页面内容。
  2. 首屏加载时间长:由于需要先加载所有的 JavaScript 和 CSS,所以 SPA 的首屏加载时间可能会比 SSR 长。

这些只是一些基本的考虑因素,具体的选择还需要根据你的应用需求来决定。希望这个解释对你有所帮助!

什么是数据同构

数据同构,通常在前端开发中,是指服务器端和客户端共享同一份数据的状态。这种技术主要用于解决单页应用(SPA)首屏渲染慢和不利于 SEO 的问题。

在同构应用中,服务器会先执行一部分 JavaScript 代码,预先获取数据,然后用这些数据渲染出 HTML 页面,最后将这些数据和 HTML 一起发送给客户端。客户端在接收到数据和 HTML 后,会用这些数据去初始化应用的状态,然后挂载到已经渲染好的 HTML 上。

这样,服务器端和客户端就实现了数据的同构,也就是说,它们共享了同一份数据状态。这种方式既解决了 SPA 首屏渲染慢的问题,又使得页面内容能够被搜索引擎正确抓取,从而提高了 SEO 效果。希望这个解释对你有所帮助!

vue-router 中路由方法 pushState 和 replaceState 能否触发 popSate 事件

pushStatereplaceState方法是 HTML5 History API 的一部分,它们可以改变浏览器的历史记录,但是不会触发popstate事件

  • pushState方法会将一个新的记录添加到浏览器的历史堆栈中,而replaceState方法则会替换当前的历史记录。这两个方法都不会重新加载页面。

  • popstate事件则是在浏览器的历史记录发生变化,如用户点击了前进或后退按钮,或者使用了history.back()history.forward()history.go()方法时触发。

所以,当你使用pushStatereplaceState方法时,不会触发popstate事件。你需要通过其他方式(如用户点击浏览器的前进或后退按钮)来触发popstate事件。希望这个解释对你有所帮助!

如何理解 Vue 中的模板编译原理?

Vue.js 的模板编译原理主要包括以下几个步骤:

  1. 解析模板:首先,Vue.js 会使用正则等方式解析 template 模板中的 Vue 语法,将模板中的变量替换成 JavaScript 表达式,形成一个 JavaScript 字符串。这个过程称为 "解析器(parse)"。

  2. 优化静态内容:解析完成后,Vue.js 会对模板进行优化,主要是标记静态根节点,这样在后续的 diff 算法执行时就不需要再关注这些节点。这个过程称为 "优化器(optimize)"。

  3. 生成代码:最后,Vue.js 将上一步中生成的字符串转换成 JavaScript 代码,主要是通过创建 "虚拟 DOM" 来实现,因为直接操作 DOM 是非常耗费性能的,所以 Vue.js 采用 "虚拟 DOM" 来降低这部分的性能损耗。这个过程称为 "代码生成器(codegen)"。

以上就是 Vue.js 的模板编译原理的简单解释。希望对你有所帮助!

vue.mixin 的使用场景和原理?

Vue.js 中的 mixin 是一种代码复用的技术,它可以让你在多个组件之间共享 Vue 组件的选项,如 methods、computed、mounted 等。当一个组件使用了 mixin,所有的 mixin 对象的选项将被混入到该组件本身的选项。

使用场景

  • 当你在多个组件中使用相同的方法或选项时,可以使用 mixin 进行抽象和复用,避免代码重复。
  • 当你需要在全局范围内应用一些行为时,如监听全局事件或添加全局的 Vue 实例方法等,可以使用全局 mixin。

原理: Vue.js 在创建组件实例时,会合并(混入)mixin 中的选项和组件自身的选项。如果两者存在相同的选项,会按照一定的合并策略进行处理。例如,data 对象在内部会进行递归合并,并在发生冲突时以组件数据优先。

需要注意的是,使用 mixin 需要谨慎,因为它可能会影响到所有使用了该 mixin 的 Vue 组件,如果使用不当,可能会导致命名冲突或其他问题。

以下是一个简单的 mixin 使用示例:

// 定义一个 mixin
var myMixin = {
created: function () {
this.hello();
},
methods: {
hello: function () {
console.log("Hello from mixin!");
},
},
};

// 定义一个使用了这个 mixin 的组件
var Component = Vue.extend({
mixins: [myMixin],
});

var component = new Component(); // => "Hello from mixin!"

在这个例子中,Component 组件使用了 myMixin,所以它具有了 myMixincreated 钩子函数和 hello 方法。当 component 实例创建时,hello 方法会被调用,打印出 "Hello from mixin!"。

希望这个答案对你有所帮助!

Vue 的组件 data 为什么必须是一个函数?

在 Vue.js 中,组件的data必须是一个函数,这是因为每个组件实例需要维护一份被返回对象的独立的拷贝。如果data直接是一个对象,则所有的组件实例将共享这个对象,一旦一个组件实例改变了这个对象的属性,其他的组件实例的相同属性也会被改变,这显然不是我们想要的。

data是一个函数时,每个组件实例在被创建时,都会调用这个函数,因此每个组件实例都会维护一份独立的数据拷贝,互不影响。这就是为什么 Vue 的组件data必须是一个函数的原因。

以下是一个例子:

Vue.component("my-component", {
data: function () {
return {
message: "hello",
count: 0,
};
},
methods: {
increment: function () {
this.count++;
},
},
});

在这个例子中,每个my-component组件实例都有自己的messagecount数据,互不影响。当我们在一个组件实例中调用increment方法时,只有这个组件实例的count数据会被改变,其他的组件实例的count数据不会受到影响。这就是为什么data必须是一个函数的原因。希望这个解释对你有所帮助!

Vue.set 方法是如何实现的?

Vue.set 方法是 Vue.js 提供的一个全局方法,用于向响应式对象中添加一个属性,并确保新属性同样是响应式的,触发视图更新。这个方法通常用于向已经创建的对象添加新属性,特别是添加到一个已经被 Vue 观察的对象上。

Vue.set 的实现原理主要涉及到 Vue 的响应式系统。在 Vue 中,当你创建一个 Vue 实例或组件时,Vue 会遍历你在 data 中定义的所有属性,并使用 Object.defineProperty 把它们转化为 getter/setter,使得当这些属性被访问或修改时,Vue 能够追踪到这些变化并触发视图更新。

然而,由于 JavaScript 的限制,Vue 无法检测到对象属性的添加或删除。当你直接使用 obj.newProp = value 的方式添加一个新属性时,新属性不会被转化为 getter/setter,因此它不是响应式的,不会触发视图更新。

这就是 Vue.set 的作用所在。当你调用 Vue.set(obj, 'newProp', value) 时,Vue.set 会先检查 obj.newProp 是否已经存在,如果不存在,它会使用 Object.defineProperty 为 obj.newProp 创建一个新的 getter/setter,使得 newProp 成为响应式的,然后设置 newProp 的值为 value,并触发视图更新。

以下是一个简单的例子:

new Vue({
el: "#app",
data: {
message: "Hello Vue!",
},
methods: {
addNewProp: function () {
this.$set(this, "newProp", "Hello Vue.set!");
console.log(this.newProp); // 输出 "Hello Vue.set!"
},
},
});

在这个例子中,addNewProp 方法使用 this.$set(这是 Vue.set 的一个别名)添加了一个新的响应式属性 newProp,并设置了它的值。这就是 Vue.set 的实现原理和用法。希望这个解释对你有所帮助!

Vue 的 diff 算法原理是什么?

Vue.js 的 diff 算法是其虚拟 DOM 技术的核心部分。这个算法用于比较新旧两个虚拟 DOM 树,计算出最小的一系列修改操作,然后应用到实际的 DOM 树上,从而尽可能地减少对实际 DOM 的操作,提高性能。

Vue.js 的 diff 算法主要基于两个假设:

  1. 两个相同的组件会产生类似的 DOM 结构,不同的组件会产生不同的 DOM 结构。
  2. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

基于这两个假设,Vue.js 的 diff 算法使用了一种双端比较的策略。在比较新旧两个节点列表时,首先会比较两个列表的头部和尾部的节点,然后根据比较结果进行相应的节点添加、删除或移动操作。

以下是算法的大致步骤:

  1. 创建两个指针,分别指向新旧节点列表的头部。
  2. 比较两个指针指向的节点,如果相同,则移动指针。
  3. 如果不同,再创建两个指针,分别指向新旧节点列表的尾部,进行同样的比较和移动操作。
  4. 如果头部和尾部的节点都不同,那么会通过唯一 id 查找新节点在旧节点列表中的位置,然后进行相应的移动操作。

这个算法的时间复杂度为 O(n),是一种高效的 diff 算法。希望这个解释对你有所帮助!

既然 vue 通过数据劫持可以精准的探测数据变化,为什么还要进行 diff 检测差异?

Vue.js 的数据劫持和 diff 算法是两个解决不同问题的技术。

数据劫持(通过 Object.defineProperty 或 Proxy)是 Vue.js 实现响应式系统的基础,它使得 Vue 能够追踪到数据的变化。当你修改一个响应式数据时,Vue 会立即知道,并将这个改动添加到一个队列中,等待异步执行。

然而,数据劫持并不能告诉 Vue 这个改动如何影响到 DOM。例如,当你修改了一个数组的元素,Vue 知道这个数组已经被修改,但是它并不知道这个修改如何映射到 DOM 的改动上。这就需要 diff 算法来解决。

diff 算法的任务是比较新旧两个虚拟 DOM 树,计算出最小的一系列修改操作,然后应用到实际的 DOM 树上。这个过程也被称为重新渲染或更新视图。

因此,数据劫持和 diff 算法是相辅相成的。数据劫持使得 Vue 能够知道数据何时被修改,而 diff 算法使得 Vue 能够知道如何根据数据的改动来更新视图。这就是为什么 Vue 需要同时使用数据劫持和 diff 算法的原因。希望这个解释对你有所帮助!

请说明 key 的作用和原理

在 Vue.js 中,key 是一个特殊的属性,主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会按照索引顺序对子节点进行复用,这样可以提高性能。

然而,这种做法的副作用是 Vue 无法追踪节点的身份,在某些情况下可能会产生不正确的行为。例如,如果你有一个包含多个可编辑项的列表,且你想在它们之间进行切换,那么使用 key 是非常必要的,否则每次切换时,Vue 会复用节点,导致你看到的可能是旧的状态。

key 的工作原理是这样的:当 Vue 更新组件时,它会根据 key 的值来决定是否复用节点。如果两个节点的 key 值相同,Vue 会认为它们是同一节点,然后复用它们,只更新它们的属性。如果 key 值不同,Vue 会销毁旧节点,创建并挂载新节点。

以下是一个简单的例子:

<div v-for="(item, index) in items" :key="item.id">
{{ item.text }}
</div>

在这个例子中,每个循环项都有一个唯一的 keyitem.id。这样,无论 items 如何变化,每个项都会正确地保持自己的状态。

总的来说,key 的作用是帮助 Vue 跟踪每个节点的身份,从而复用和重新排序现有元素,你在使用 Vue 时,应当尽量提供唯一的 key 值。希望这个解释对你有所帮助!

image.png

谈谈对组件的理解

在 Vue.js 中,组件(Component)是一种自定义元素,它是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以表现为用 is 特性进行了扩展的原生 HTML 元素。

组件系统让我们可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用的界面都可以抽象为一个组件树:

<my-component></my-component>

Vue 组件的核心是一个 Vue 实例,所以它们非常相似,都接受相同的选项对象(除了一些根特有的选项),并提供相同的生命周期钩子。

我认为,理解和掌握组件是深入学习和精通 Vue.js 的关键。希望这个解释对你有所帮助!

请描述组件的渲染流程

Vue.js 的组件渲染流程是一个复杂的过程,涉及到许多步骤和技术,包括响应式系统、虚拟 DOM、diff 算法等。以下是一个简化的描述:

  1. 解析模板:首先,Vue 会解析组件的模板,将其转化为渲染函数。模板中的指令、过滤器、组件等都会在这个过程中被处理。

  2. 响应式数据:然后,Vue 会遍历组件的 data 和 props,使用 Object.defineProperty 或 Proxy 创建响应式数据。这使得当数据被修改时,Vue 能够追踪到这些变化。

  3. 观察者:对于每个响应式数据,Vue 会创建一个观察者对象,用于在数据变化时通知依赖。

  4. 计算属性和侦听器:接着,Vue 会处理组件的 computed 属性和 watch 侦听器。计算属性会被添加到响应式系统中,而侦听器则会在对应的数据变化时被调用。

  5. 创建虚拟 DOM:然后,Vue 会调用渲染函数,创建一个虚拟 DOM 树。渲染函数会根据当前的数据状态,使用虚拟 DOM API(如 h 函数)创建一个虚拟 DOM 树。

  6. diff 和更新:接着,Vue 会使用 diff 算法比较新旧两个虚拟 DOM 树,计算出最小的一系列修改操作,然后应用到实际的 DOM 树上。

  7. 生命周期钩子:在整个渲染流程中,Vue 会在适当的时机调用组件的生命周期钩子,如 createdmountedupdateddestroyed

以上就是 Vue.js 组件的渲染流程的简化描述。实际的过程可能会更复杂,涉及到更多的细节和优化。希望这个解释对你有所帮助!

image.png

请描述 Vue 组件的更新流程

Vue 组件的更新流程主要包括以下步骤:

  1. 依赖收集:当一个组件被渲染时,Vue 会跟踪所有被访问的响应式依赖属性。这样,后续当依赖项更新时,Vue 知道需要重新渲染此组件。

  2. 派发更新:当组件的数据(例如,props,data,computed properties 等)发生变化时,Vue 会触发更新过程。这包括虚拟 DOM 的重新渲染和对比,以及必要的 DOM 更新。

  3. 虚拟 DOM 重新渲染和对比:Vue 使用虚拟 DOM 来跟踪实际 DOM 应该呈现的状态。当一个组件的数据发生变化时,Vue 会创建一个新的虚拟 DOM,并将其与旧的虚拟 DOM 进行对比。这个过程被称为“diffing”。

  4. DOM 更新:基于虚拟 DOM 的对比结果,Vue 会精确地知道在实际 DOM 中需要改变什么。然后,Vue 会在 DOM 中应用这些变化,以使 DOM 匹配新的虚拟 DOM 的状态。

  5. 生命周期钩子:在更新过程中,Vue 还会调用相应的生命周期钩子,如beforeUpdateupdated。这为在更新过程的不同阶段运行自定义逻辑提供了可能。

以上就是 Vue 组件的更新流程。希望对你有所帮助!

image.png

Vue CLI 3 有哪些特性?

Vue CLI 3 是一个用于快速构建 Vue.js 项目的官方脚手架工具。它引入了一些新的特性和改进,使得开发 Vue.js 应用更加简单和灵活。下面是 Vue CLI 3 的一些主要特性:

  1. 零配置原则:Vue CLI 3 采用了零配置的原则,减少了项目初始化的配置步骤。默认情况下,它会根据你选择的特性和插件进行智能推测,并生成一个合适的配置。

  2. 插件系统:Vue CLI 3 通过插件系统扩展了其功能。你可以轻松添加、更新和删除插件,以满足项目的需求。Vue CLI 官方提供了许多插件,如 Vuex、Vue Router 等,也可以自定义开发插件。

  3. 图形化界面:Vue CLI 3 提供了一个图形化界面(GUI),可以通过命令 vue ui 启动。该界面提供了可视化的项目管理和配置,方便开发者进行项目的管理和操作。

  4. 预设(Presets):预设是一套预定义的配置集合,可以帮助你快速创建具有特定功能的项目。Vue CLI 3 默认提供了预设选项,如默认(Default)、手动(Manually)和大规模(Large)等,你可以根据项目需求进行选择。

  5. 环境变量管理:Vue CLI 3 支持在不同环境下配置和管理环境变量。你可以创建不同的环境配置文件,并在代码中访问这些环境变量。

  6. 逐步升级:Vue CLI 3 提供了一种逐步升级的方式,可以方便地将旧版本的 Vue CLI 迁移到最新版本,而不会影响现有的配置和代码。

  7. 内置的优化和构建:Vue CLI 3 集成了优化和构建工具,包括基于 webpack 的打包工具和代码压缩、按需加载等功能,可以帮助你优化应用的性能和文件大小。

这些特性使得 Vue CLI 3 成为一个强大且灵活的工具,可以简化 Vue.js 项目的开发和管理过程。它提供了一种快速启动项目的方式,并提供了丰富的功能和可定制性,以满足不同项目的需求。

能对比一下 Create React App 和 Vue CLI 3 吗?

当涉及到前端项目构建和开发的脚手架工具时,Create React App 和 Vue CLI 3 都是非常流行和常用的选择。它们都旨在简化项目的初始化和配置过程,并提供一些便利的功能。以下是 Create React App 和 Vue CLI 3 的一些比较:

  1. 生态系统和社区支持:

    • Create React App 是 React 官方提供的脚手架工具,因此与 React 生态系统和社区紧密集成。React 生态系统具有庞大的社区,提供了大量的第三方库、组件和工具,对于解决常见问题或添加特定功能非常有帮助。
    • Vue CLI 3 是 Vue 官方提供的脚手架工具,与 Vue 生态系统和社区紧密结合。Vue 生态系统也有一个活跃的社区,提供了许多有用的插件、库和组件,适用于构建 Vue.js 应用程序。
  2. 配置和灵活性:

    • Create React App 遵循 "开箱即用" 的原则,提供了一个预配置的项目结构和构建流程,无需手动配置即可快速启动 React 项目。这对于快速原型开发和小型项目非常方便。如果需要更多的自定义配置,可以使用 react-scripts 提供的配置选项进行调整。
    • Vue CLI 3 采用了零配置原则,使用预设(Presets)来快速初始化项目,并根据预设的选项自动生成配置。同时,Vue CLI 3 也提供了强大的配置系统,允许开发者根据项目需求进行更深入的配置调整。它还支持插件系统,可以通过插件扩展和定制构建过程。
  3. 项目管理和可视化界面:

    • Create React App 没有提供官方的图形化界面(GUI),主要通过命令行进行项目管理和操作。你可以使用命令行工具来启动开发服务器、运行测试、构建应用等。
    • Vue CLI 3 提供了一个图形化界面(GUI),可以通过命令 vue ui 启动。该界面提供了可视化的项目管理和配置,包括项目创建、插件管理、任务运行和依赖管理等功能。
  4. 前端框架支持:

    • Create React App 是专门为 React 开发的脚手架工具,提供了针对 React 的特定配置和优化,如 JSX 语法支持、React 热重载和代码分割等。
    • Vue CLI 3 是专为 Vue.js 开发的脚手架工具,提供了对 Vue.js 的特定支持,如单文件组件(SFC)、Vue Router 和 Vuex 等。

需要注意的是,Create React App 和 Vue CLI 3 都是成熟和功能强大的工具,可以帮助开发者快速启动和管理前端项目。选择哪个工具取决于你所使用的前端框架和项目需求,以及个人或团队的偏好。无论选择哪个工具,都可以通过扩展和自定义进行灵活的配置和调整,以满足具体项目的需求。

Vue.js 整个实现原理?

这里简单的描述一下 Vue 2.x 的运行机制(需要注意分析的是 Runtime + Compiler 的 Vue.js)。

初始化流程:

  • 创建 Vue 实例对象

  • init过程会初始化生命周期,初始化事件中心,初始化渲染、执行beforeCreate周期函数、初始化 datapropscomputedwatcher、执行created周期函数等。

  • 初始化后,调用$mount方法对 Vue 实例进行挂载(挂载的核心过程包括模板编译渲染以及更新三个过程)。

  • 如果没有在 Vue 实例上定义render方法而是定义了template,那么需要经历编译阶段。需要先将template字符串编译成render functiontemplate字符串编译步骤如下 :

    • parse正则解析template字符串形成 AST(抽象语法树,是源代码的抽象语法结构的树状表现形式)
    • optimize标记静态节点跳过 DIFF 算法(DIFF 算法是逐层进行比对,只有同层级的节点进行比对,因此时间的复杂度只有 O(n)。如果对于时间复杂度不是很清晰的,可以查看我写的文章ziyi2/algorithms-javascript/渐进记号
    • generate将 AST 转化成render function字符串
  • 编译成render function 后,调用$mountmountComponent方法,先执行beforeMount钩子函数,然后核心是实例化一个渲染Watcher,在它的回调函数(初始化的时候执行,以及组件实例中监测到数据发生变化时执行)中调用updateComponent方法(此方法调用render方法生成虚拟 Node,最终调用update方法更新 DOM)。

  • 调用render方法将render function渲染成虚拟的 Node(真正的 DOM 元素是非常庞大的,因为浏览器的标准就把 DOM 设计的非常复杂。如果频繁的去做 DOM 更新,会产生一定的性能问题,而 Virtual DOM 就是用一个原生的 JavaScript 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多,而且修改属性也很轻松,还可以做到跨平台兼容),render方法的第一个参数是createElement(或者说是h函数),这个在官方文档也有说明。

  • 生成虚拟 DOM 树后,需要将虚拟 DOM 树转化成真实的 DOM 节点,此时需要调用update方法,update方法又会调用pacth方法把虚拟 DOM 转换成真正的 DOM 节点。需要注意在图中忽略了新建真实 DOM 的情况(如果没有旧的虚拟 Node,那么可以直接通过createElm创建真实 DOM 节点),这里重点分析在已有虚拟 Node 的情况下,会通过sameVnode判断当前需要更新的 Node 节点是否和旧的 Node 节点相同(例如我们设置的key属性发生了变化,那么节点显然不同),如果节点不同那么将旧节点采用新节点替换即可,如果相同且存在子节点,需要调用patchVNode 方法执行 DIFF 算法更新 DOM,从而提升 DOM 操作的性能。

需要注意在初始化阶段,没有详细描述数据的响应式过程,这个在响应式流程里做说明。

响应式流程:

  • init的时候会利用Object.defineProperty方法(不兼容 IE8)监听 Vue 实例的响应式数据的变化从而实现数据劫持能力(利用了 JavaScript 对象的访问器属性getset,在未来的 Vue3 中会使用 ES6 的Proxy来优化响应式原理)。在初始化流程中的编译阶段,当render function被渲染的时候,会读取 Vue 实例中和视图相关的响应式数据,此时会触发getter函数进行依赖收集(将观察者Watcher对象存放到当前闭包的订阅者Depsubs中),此时的数据劫持功能和观察者模式就实现了一个 MVVM 模式中的 Binder,之后就是正常的渲染和更新流程。
  • 当数据发生变化或者视图导致的数据发生了变化时,会触发数据劫持的setter函数,setter会通知初始化依赖收集中的Dep中的和视图相应的Watcher,告知需要重新渲染视图,Wather就会再次通过update方法来更新视图。

可以发现只要视图中添加监听事件,自动变更对应的数据变化时,就可以实现数据和视图的双向绑定了。

链接:https://juejin.cn/post/6844904093425598471

Vue.js 源码的入口主要做了些什么处理?

Vue.js 源码的入口主要做了以下几个处理:

  1. 环境检测和版本提示:Vue.js 的入口文件首先会进行环境检测,判断当前的执行环境是浏览器还是 Node.js,并根据环境加载不同的代码。同时,它还会检测 Vue.js 的版本,并在开发环境中给出一些版本提示和警告信息。

  2. 引入依赖模块:接下来,入口文件会引入一些依赖模块,包括从其他文件导入的功能模块、工具函数和全局变量等。这些依赖模块包括 Vue 的核心功能模块、观察者模块、虚拟 DOM 模块、编译器模块以及各种工具函数和辅助模块。

  3. 定义全局变量和方法:入口文件会定义一些全局变量和方法,以便在整个 Vue.js 库中使用。例如,它会定义全局的 Vue 对象,用于创建 Vue 实例和访问 Vue 的全局 API。同时,它还会定义一些全局的工具函数和方法,如 warnerror 等,用于输出警告和错误信息。

  4. 扩展 Vue 的原型和全局方法:在入口文件中,Vue.js 还会对 Vue 的原型进行扩展,添加一些实例方法,例如 $watch$on$emit 等。这些方法可以在 Vue 实例中直接调用。此外,它还会扩展全局的 Vue 方法,如 Vue.useVue.component 等,用于注册全局组件和插件。

  5. 导出 Vue.js:最后,入口文件会通过 export 导出 Vue.js 的主要对象或函数,使其可以在其他地方引入和使用。

这些处理使得 Vue.js 能够提供一系列的功能和特性,包括数据响应式、模板编译、组件化开发、虚拟 DOM 管理等。它们为 Vue.js 应用程序的构建和运行提供了必要的基础设置和功能支持。

Vue.js 中的数据劫持是怎么实现的?浏览器兼容性呢?

在 Vue.js 中,数据劫持是通过使用 JavaScript 的 Object.defineProperty 方法来实现的。这个方法允许我们定义对象的属性,并可以在属性的读取(get)和写入(set)时执行特定的操作。Vue.js 利用这个特性,在数据对象的属性上设置 getter 和 setter,从而实现对数据的劫持和监听。

具体来说,Vue.js 在实例化 Vue 对象时,会通过递归地遍历数据对象,将对象的属性转换为 getter 和 setter。当访问数据对象的属性时,会触发 getter 方法,Vue.js 可以进行依赖收集,将当前的 Watcher(观察者)对象添加到依赖列表中。当修改数据对象的属性时,会触发 setter 方法,Vue.js 可以通知相关的 Watcher 对象进行更新。

关于浏览器兼容性,Object.defineProperty 方法在 ECMAScript 5 中引入,因此 Vue.js 的数据劫持特性在支持 ECMAScript 5 的浏览器中可以正常工作。具体而言,以下浏览器支持 Object.defineProperty

  • Chrome 5+
  • Firefox 4+
  • Safari 5+
  • Opera 12+
  • Internet Explorer 9+
  • Microsoft Edge

需要注意的是,Object.defineProperty 在一些旧版本的浏览器中可能存在限制或不完全支持。为了解决这个问题,Vue.js 在运行时会检测浏览器是否支持原生的 Object.defineProperty,如果不支持,则会退而使用其他手段模拟实现数据劫持的功能。这样可以保证 Vue.js 在大多数现代浏览器中都能正常运行。

Vue.js 中的依赖收集是怎么处理的?和闭包有什么关联吗?

在 Vue.js 中,依赖收集是通过 Watcher 对象和观察者模式实现的。每个 Vue 实例都有一个 Watcher 对象,用于追踪数据对象属性的变化。当访问数据对象的属性时,会触发 getter 方法,在 getter 方法中,Watcher 对象会将自身添加到当前属性的依赖列表中。这样,在数据对象的属性发生改变时,可以通知相关的 Watcher 对象进行更新。

依赖收集的关键在于如何将 Watcher 对象和数据对象的属性关联起来。在 Vue.js 中,依赖收集是在数据劫持的过程中完成的。当 Vue.js 在实例化过程中遍历数据对象的属性时,会在 getter 方法中进行依赖收集。具体来说,getter 方法会调用一个全局的 Dep 对象,这个 Dep 对象维护了一个依赖列表(subs),用于存储当前属性的所有 Watcher 对象。

在 getter 方法中,Watcher 对象会被添加到当前属性的依赖列表中。这样,当数据对象的属性发生改变时,Vue.js 就可以通过 Dep 对象遍历依赖列表,并通知相关的 Watcher 对象进行更新。

闭包在依赖收集中起到了重要的作用。当定义 Watcher 对象时,它会在当前作用域中创建一个闭包,用于保存当前 Watcher 对象的引用。这样,当 Watcher 对象被添加到依赖列表中时,它仍然可以访问到正确的 Watcher 对象,即使在属性的 getter 方法被调用时,当前作用域已经结束。

通过使用闭包,Vue.js 实现了对 Watcher 对象的引用保留,并能够将 Watcher 对象与数据对象的属性关联起来。这样,当属性发生变化时,可以通过依赖列表通知到相关的 Watcher 对象进行更新。

总结起来,Vue.js 中的依赖收集是通过在数据劫持过程中,使用 Watcher 对象和 Dep 对象实现的。闭包的使用确保了正确的引用关系,使得依赖收集能够正常工作。

Vue.js 中的模板解析需要经历哪几个阶段?

在 Vue.js 中,模板解析(Template Compilation)经历以下几个阶段:

  1. 解析阶段(Parsing):在解析阶段,Vue.js 将模板字符串解析为抽象语法树(AST)。它会遍历模板字符串,并识别其中的标签、指令、表达式、文本内容等,并构建对应的 AST。解析阶段使用的是类似 HTML 解析器的算法,通过词法分析和语法分析来生成 AST。

  2. 优化阶段(Optimization):在优化阶段,Vue.js 对生成的 AST 进行优化处理。它会应用一些静态分析的技术,找到静态节点(Static Node)和静态根节点(Static Root),并进行标记和优化。静态节点是指在编译时不会发生变化的节点,可以在渲染过程中完全跳过。优化阶段的目标是减少运行时的开销,提高渲染性能。

  3. 代码生成阶段(Codegen):在代码生成阶段,Vue.js 将优化后的 AST 转换为渲染函数。渲染函数是一个 JavaScript 函数,它接收数据作为输入,并生成虚拟 DOM(Virtual DOM)用于渲染视图。代码生成阶段会遍历优化后的 AST,并根据节点类型和属性生成相应的渲染函数代码。生成的渲染函数代码可以用于后续的渲染过程。

  4. 渲染阶段(Render):在渲染阶段,Vue.js 使用生成的渲染函数进行实际的渲染操作。渲染函数会根据输入的数据生成对应的虚拟 DOM,并与之前的虚拟 DOM 进行比较,找出需要更新的部分,并进行 DOM 操作进行更新。渲染阶段会根据数据的变化,动态更新视图,保持视图与数据的同步。

这些阶段共同构成了 Vue.js 中的模板解析过程。通过将模板解析为渲染函数,并将渲染函数应用于数据,Vue.js 实现了数据驱动的视图更新机制。这使得开发者可以通过编写简洁的模板来描述视图,而无需直接操作 DOM,提高了开发效率和可维护性。

Vue.js 中的虚拟节点优势是什么?

Vue.js 中使用虚拟节点(Virtual DOM)的主要优势包括以下几点:

  1. 性能优化:虚拟节点可以在内存中进行操作,而不需要直接操作真实的 DOM。通过在内存中构建虚拟节点树,可以避免频繁的 DOM 操作,从而提高性能。在更新视图时,Vue.js 会比较新旧虚拟节点树的差异,并只更新必要的部分,减少了不必要的 DOM 操作,提高了渲染效率。

  2. 跨平台能力:由于虚拟节点是以 JavaScript 对象的形式存在,因此可以在不同的平台上使用相同的渲染逻辑,例如在浏览器、服务器端(使用 Node.js)或移动端(使用 Vue Native)等。这样,开发者可以共享和重用他们的代码,提高了开发效率和代码的可维护性。

  3. 抽象化的视图表示:通过使用虚拟节点,开发者可以将视图抽象化为一个状态树,从而更方便地管理和操作视图。虚拟节点提供了一种声明式的方式来描述视图的结构和状态,使得开发者可以更专注于视图的逻辑,而无需过多关注底层的 DOM 操作。

  4. 响应式更新:虚拟节点与 Vue.js 的响应式系统紧密结合。当数据发生变化时,Vue.js 会重新生成新的虚拟节点树,并与旧的虚拟节点树进行比较,找出需要更新的部分。这样,Vue.js 可以根据数据的变化,自动更新视图,保持视图与数据的同步。

  5. 生态系统支持:虚拟节点在 Vue.js 生态系统中得到广泛支持。许多插件和工具,如 Vue Router、Vuex 等,都基于虚拟节点的概念构建。这使得开发者可以充分利用丰富的生态系统,扩展和增强他们的 Vue.js 应用。

总的来说,使用虚拟节点可以提供更高的性能、跨平台能力、抽象化的视图表示和响应式更新,以及丰富的生态系统支持。这些优势使得 Vue.js 成为一个强大的前端开发框架,能够满足复杂应用的需求并提供良好的开发体验。

Vue.js 中的 DIFF 算法是怎么处理的?

在 Vue.js 中,DIFF 算法用于比较新旧虚拟节点树的差异,并只更新必要的部分来最小化 DOM 操作,从而提高性能。Vue.js 使用了一种高效的 DIFF 算法,称为"patch"算法,它基于以下几个原则:

  1. 同级比较:Vue.js 只会比较同级的节点,并且假设不同的节点类型会产生不同的子树,因此在进行比较时,它会跳过不同的节点及其子树,而不会继续递归比较。

  2. 唯一标识:每个节点都应该有一个唯一的标识符(key),这样 Vue.js 可以通过标识符快速判断节点是否相同,从而减少比较的次数。如果节点没有提供 key,Vue.js 会使用节点的索引作为默认的标识符。

  3. 模糊比较:在进行节点比较时,Vue.js 使用一系列的启发式规则进行模糊比较,以尽量找到最优的更新策略。这些规则包括比较节点的标签名、关键属性和子节点等,以确定节点是否相同或可复用。

基于以上原则,DIFF 算法的大致过程如下:

  1. 根据新旧虚拟节点树的根节点进行比较,判断它们是否相同。如果不同,直接替换整个节点树;如果相同,进入下一步。

  2. 对比新旧节点的子节点列表,使用指针和索引进行遍历。通过标识符(key)进行节点的匹配,找到相同的节点并进行比较。如果节点不同,直接替换节点;如果节点相同,进入下一步。

  3. 对相同的节点进行递归比较,重复以上步骤,直到遍历完所有子节点。

  4. 如果新节点的子节点列表比旧节点短,说明有节点被移除了,将多余的旧节点删除。

  5. 如果新节点的子节点列表比旧节点长,说明有新节点被添加了,将多余的新节点插入到正确的位置。

通过这种 DIFF 算法,Vue.js 可以高效地比较新旧虚拟节点树,并只更新必要的部分,从而减少了不必要的 DOM 操作,提高了渲染性能。DIFF 算法的实现细节在 Vue.js 内部进行了优化,以提供更快速和高效的更新过程。

Vue.js 中 DIFF 算法的时间复杂度是多少?为什么?

Vue.js 中的 DIFF 算法的时间复杂度是 O(n),其中 n 是虚拟节点树中的节点数量。DIFF 算法的时间复杂度是线性的,因为它通过一次遍历比较新旧虚拟节点树的节点来确定差异,并执行相应的更新操作。

这是因为 DIFF 算法在比较节点时采用了一些优化策略和假设:

  1. 同级比较:DIFF 算法只会比较同级的节点,而不会递归比较不同级的节点。这样可以避免对整个节点树进行完全遍历,降低了比较的次数。

  2. 唯一标识:每个节点都应该有一个唯一的标识符(key),这样可以通过标识符快速判断节点是否相同。DIFF 算法使用标识符进行节点匹配,减少了比较的次数。

  3. 模糊比较:DIFF 算法使用一系列的启发式规则进行模糊比较,以尽量找到最优的更新策略。这些规则包括比较节点的标签名、关键属性和子节点等,减少了不必要的详细比较。

基于以上优化策略,DIFF 算法的时间复杂度是线性的,即 O(n)。在最坏的情况下,需要遍历每个节点进行比较,但由于 DIFF 算法采用了快速匹配和模糊比较的策略,实际上只会对相对较少的节点进行详细的比较,从而减少了比较的次数。

需要注意的是,DIFF 算法的时间复杂度是基于节点数量的线性增长,而不是基于节点深度的线性增长。这是因为 DIFF 算法是基于同级比较的,不会递归比较不同级的节点。

总的来说,DIFF 算法在 Vue.js 中提供了高效的虚拟节点树比较和更新过程,使得视图的渲染和更新操作可以更快速地完成。

Vue.js 中 computed / watch 实现的原理是什么?

在 Vue.js 中,computedwatch 都是用于响应式地监听数据变化并执行相应操作的功能。它们的实现原理如下:

  1. computed(计算属性)的实现原理:

    • 当定义一个 computed 属性时,Vue.js 会为其创建一个依赖关系,并建立与相关数据的关联。这样,当依赖的数据发生变化时,computed 属性会自动重新求值。
    • Vue.js 会使用缓存机制来优化计算属性的性能。当计算属性首次被访问时,Vue.js 会计算并缓存结果。在下次访问时,如果依赖的数据没有发生变化,Vue.js 会直接返回缓存的结果,避免重复计算。
  2. watch(侦听器)的实现原理:

    • 当定义一个 watch 属性时,Vue.js 会为其创建一个观察者(Watcher)。观察者会监听指定的数据,并在数据发生变化时执行相应的回调函数。
    • Vue.js 使用了底层的依赖追踪系统来收集与 watch 相关的数据依赖关系。当观察者被创建时,Vue.js 会自动追踪依赖的数据,并建立与这些数据之间的关联。
    • 当被观察的数据发生变化时,Vue.js 会通知相应的观察者,并执行对应的回调函数。这样,开发者可以在回调函数中处理数据变化的逻辑。

需要注意的是,computedwatch 的使用场景和用法略有不同:

  • computed 适用于派生出其他数据的场景,例如基于某些数据计算出一个新的值。它可以提供一个类似于属性的方式来访问计算结果,自动地根据依赖的数据进行更新。
  • watch 适用于需要在数据变化时执行异步操作或执行一些副作用操作的场景。通过监听指定的数据,开发者可以在数据变化时触发相应的回调函数,并执行相应的操作。

总结起来,computed 利用缓存机制和依赖追踪系统实现了响应式的计算属性,而 watch 则利用观察者模式和依赖追踪系统实现了响应式的数据变化监听。这些功能在 Vue.js 中提供了一种便捷的方式来处理数据的变化和衍生,使得开发者可以专注于业务逻辑的实现。

Vue.js 中有哪些周期函数?这些周期函数都是在什么时机执行的?

在 Vue.js 中,组件的生命周期函数可以分为两类:实例生命周期函数和钩子函数。

实例生命周期函数是在组件实例创建和销毁的过程中执行的,包括以下几个函数:

  1. beforeCreate:在实例刚被创建之后,数据观测 (data observer) 和事件/生命周期事件初始化之前调用。

  2. created:在实例创建完成后调用。此时实例已完成数据观测 (data observer),属性和方法的运算,挂载阶段尚未开始,$el 属性尚不可用。

  3. beforeMount:在挂载开始之前被调用。相关的 render 函数首次被调用。

  4. mounted:在挂载完成后调用,即组件被添加到 DOM 中。此时,组件的根 DOM 元素 ($el) 已经被创建和插入。

  5. beforeUpdate:在数据更新之前,发生在虚拟 DOM 重新渲染和打补丁之前。可以在此时对更新之前的状态进行更改。

  6. updated:在数据更新之后调用,发生在虚拟 DOM 重新渲染和打补丁之后。此时,组件更新完成。

  7. beforeDestroy:在实例销毁之前调用。可以在此时进行善后处理,如清除定时器、解绑全局事件等。

  8. destroyed:在实例销毁后调用。此时,实例中的所有指令和事件监听器都已被移除,组件的所有子实例也都被销毁。

除了实例生命周期函数,Vue.js 还提供了一些钩子函数,用于在特定的情况下执行相应的操作,包括:

  1. beforeRouteEnter:在路由进入组件之前调用,可以在此获取组件实例,但不能访问组件实例的数据和 DOM。

  2. beforeRouteUpdate:在路由更新但复用组件时调用,可以访问组件实例以及之前的路由和新的路由。

  3. beforeRouteLeave:在路由离开组件时调用,可以访问组件实例以及即将离开的路由。

这些生命周期函数和钩子函数提供了灵活的扩展点,允许开发者在不同的阶段执行特定的操作,如数据初始化、异步请求、DOM 操作等,以满足组件的需求。

Vue.js 中的 $nextTick 的原理是什么?它主要经历了哪些变化?为什么?

在 Vue.js 中,$nextTick 是一个实例方法,用于在 DOM 更新之后执行回调函数。它的原理是利用 JavaScript 的事件循环机制来延迟执行回调函数,以确保在下次 DOM 更新循环之后执行。

具体来说,当调用 $nextTick 方法时,Vue.js 会将回调函数推入一个队列中,并设置一个标志位,表示需要执行回调函数。然后,Vue.js 会利用 JavaScript 的事件循环机制,在当前执行栈执行完毕后,在下一个事件循环中执行回调函数。

$nextTick 主要经历了以下变化:

  1. Vue.js 2.x 版本:在 Vue.js 2.x 中,$nextTick 的实现使用了微任务(microtask)和宏任务(macrotask)的机制。在当前任务执行完毕后,Vue.js 会先检查是否支持 Promise,如果支持就使用 Promise.then 创建一个微任务,否则使用 MutationObserver 创建一个微任务。微任务会在当前任务执行完毕后立即执行,确保回调函数在 DOM 更新之后执行。

  2. Vue.js 3.x 版本:在 Vue.js 3.x 中,$nextTick 的实现发生了变化,不再使用微任务和宏任务的机制。相反,Vue.js 3.x 使用了一个特殊的全局函数 queuePostFlushCb 来处理 nextTick 的回调函数。当调用 $nextTick 时,Vue.js 会将回调函数推入到一个队列中,然后在下一个 DOM 更新循环的 post-flush 阶段执行回调函数。这种改变是为了进一步优化性能,减少不必要的微任务调度。

这些变化的原因主要是为了改进 Vue.js 的性能和响应性能。通过使用微任务和宏任务机制或者特殊的全局函数,Vue.js 可以在 DOM 更新之后异步执行回调函数,以避免阻塞主线程和提高用户界面的响应速度。在 Vue.js 3.x 中,进一步优化了 DOM 更新和回调函数的调度机制,以提升整体的性能和用户体验。

Vue.js 对 DOM 的更新做了哪些标记优化处理?

Vue.js 通过对 DOM 更新的标记优化处理提高了性能,主要包括以下几个方面:

  1. 虚拟 DOM(Virtual DOM):Vue.js 使用虚拟 DOM 进行高效的 DOM diff 算法,以减少对实际 DOM 的操作次数。虚拟 DOM 是一个轻量级的 JavaScript 对象结构,表示真实 DOM 的层次结构。在更新过程中,Vue.js 会先将变化应用到虚拟 DOM 上,然后通过 diff 算法找出实际需要更新的部分,并最小化对实际 DOM 的操作。

  2. 响应式更新追踪:Vue.js 使用响应式系统追踪数据的变化,并将需要更新的组件进行标记。只有被标记的组件会进行更新,而没有变化的组件会被跳过,避免不必要的 DOM 操作。

  3. 异步更新队列:Vue.js 通过将更新操作放入异步更新队列中,批量处理多个更新操作,减少了实际 DOM 操作的次数。在同一个事件循环中,多个数据变化会被合并为一次更新,避免了过多的重复计算和重复渲染。

  4. 列表渲染的 key:在使用 v-for 进行列表渲染时,为每个列表项指定唯一的 key 值。这样,Vue.js 在进行列表更新时,可以通过 key 的变化判断出哪些列表项是新增、删除或移动的,从而减少 DOM 操作的数量。

  5. 异步组件:Vue.js 中的异步组件使用动态导入的方式进行加载,延迟加载组件的代码和相关资源,提高了初始页面加载速度。只有在需要渲染异步组件时才会进行加载和解析,从而减少了初始渲染时的工作量。

这些优化处理使得 Vue.js 在更新 DOM 时能够更加高效地计算和渲染,减少重复工作并提升性能。通过合理的标记和追踪数据变化,Vue.js 可以最小化对实际 DOM 的操作,从而提供更快速、响应式的用户界面。

Vue.js 在语法层面可以做哪些优化处理?

Vue.js 在语法层面可以进行多种优化处理,以提高应用程序的性能和开发效率。以下是一些常见的语法层面的优化处理:

  1. 计算属性 (Computed Properties):使用计算属性可以缓存对响应式数据的计算结果,只有在相关数据变化时才会重新计算。避免重复计算可以提高性能,并使代码更加清晰和可维护。

  2. 侦听属性 (Watchers):通过使用侦听属性,可以监视特定数据的变化并执行相应的操作。将复杂的逻辑拆分为多个侦听属性可以提高代码的可读性和可维护性。

  3. v-if 和 v-show 指令:根据具体的需求选择使用 v-if 还是 v-show 指令。v-if 指令在条件为假时会完全销毁和重建元素,而 v-show 指令只是通过 CSS 控制元素的显示和隐藏。合理使用这两个指令可以优化页面的渲染性能。

  4. 列表渲染优化:在使用 v-for 进行列表渲染时,避免使用索引作为 key,而是使用唯一且稳定的值作为 key。这样可以提高列表的更新性能,避免不必要的 DOM 操作。

  5. 合理使用组件和组件拆分:将大型组件拆分为多个小组件,提高代码的可维护性和重用性。合理使用组件的懒加载和异步加载,减小初始加载的体积,提升应用程序的性能。

  6. 合理使用 v-bind 和 v-on 指令的简写:Vue.js 提供了 :@ 等指令的简写形式。合理使用简写形式可以提高代码的可读性,减少冗余代码。

  7. 合理使用 v-model 指令:v-model 指令可以实现表单元素和数据的双向绑定。在使用 v-model 指令时,根据实际需求选择使用修饰符和计算属性,以提高表单的交互性能和数据处理能力。

  8. 合理使用 Vue.js 的特性和插件:Vue.js 提供了丰富的特性和插件,如过渡动画、路由、状态管理等。合理使用这些特性和插件可以提高开发效率和用户体验。

通过以上的优化处理,可以使 Vue.js 应用在语法层面上更加高效、可维护和可扩展,提升应用程序的性能和开发效率。

Vue.js 2.x 中的 Proxy 代理主要做了些什么工作?

在 Vue.js 2.x 中,Proxy 代理主要用于实现响应式系统。它在 Vue.js 内部被用来代理和监听对象的访问和修改操作,以便在属性被访问或修改时触发相应的响应式更新。

具体来说,Proxy 代理在 Vue.js 2.x 中主要完成以下几项工作:

  1. 属性访问拦截:当访问对象的属性时,Proxy 代理会拦截这个操作,并触发响应式系统的追踪机制。这样,Vue.js 就能够知道哪些属性被访问了,从而建立属性与依赖关系,以便在属性发生变化时进行更新。

  2. 属性修改拦截:当修改对象的属性时,Proxy 代理会拦截这个操作,并触发响应式系统的更新机制。它会检查新值是否与旧值相同,如果不同,就会触发相应的更新操作,通知相关的依赖进行更新。

  3. 数组变异方法拦截:Vue.js 通过 Proxy 代理还拦截了数组的变异方法,如 pushpopsplice 等。这些变异方法会被修改,以便在调用它们时能够触发响应式系统的更新机制,并通知相关的依赖进行更新。

通过使用 Proxy 代理,Vue.js 2.x 实现了一个高效的响应式系统,能够自动追踪对象属性的访问和修改,并在属性发生变化时进行相应的更新。这使得开发者可以方便地编写具有响应式数据的组件,以实现数据驱动的视图更新。

Vue.js 2.x 中如何支持 TypeScript ?

在 Vue.js 2.x 中,可以通过以下步骤来支持 TypeScript:

  1. 安装 TypeScript:首先,确保你的项目中已经安装了 TypeScript。可以使用 npm 或者 yarn 来进行安装。

  2. 创建 tsconfig.json:在项目的根目录下创建一个 tsconfig.json 文件,用于配置 TypeScript 编译选项。可以使用以下命令来生成默认的 tsconfig.json 文件:

npx tsc --init
  1. 配置 tsconfig.json:根据项目的需要,对 tsconfig.json 文件进行相应的配置。可以设置编译目标、模块解析方式、输出路径等选项。

  2. .vue 文件与 TypeScript 关联:Vue.js 的单文件组件使用了 .vue 扩展名,其中包含了 HTML 模板、JavaScript 代码和 CSS 样式。为了让 TypeScript 能够正确地解析和编译这些文件,需要安装相应的插件。

    • 安装 vue 类型声明文件:在项目中安装 @types/vue 类型声明文件。
    npm install --save-dev @types/vue
    • 安装 vue-class-componentvue-property-decorator:这两个库提供了用于编写基于类的组件和装饰器的支持。
    npm install --save vue-class-component vue-property-decorator
  3. 使用 TypeScript 编写 Vue 组件:现在可以使用 TypeScript 来编写 Vue 组件了。可以使用 lang="ts" 属性指定 <script> 标签的语言为 TypeScript,并编写相应的 TypeScript 代码。

    <template>
    <!-- 模板内容 -->
    </template>

    <script lang="ts">
    import { Vue, Component } from "vue-property-decorator";

    @Component
    export default class MyComponent extends Vue {
    // 组件逻辑
    }
    </script>

    <style>
    /* 样式内容 */
    </style>
  4. 构建和运行项目:使用构建工具(如 webpack)来编译和打包项目,并运行应用程序。

通过上述步骤,你就可以在 Vue.js 2.x 中使用 TypeScript 来编写类型安全的组件和应用程序。 TypeScript 提供了静态类型检查和编辑器支持,可以提高代码的可靠性和开发效率。

Vue 3.x 的源码相对 Vue 2.x 主要做了哪些变化?

Vue 3.x 在源码层面相对于 Vue 2.x 进行了一系列的变化和改进,主要包括以下几个方面:

  1. 重构响应式系统:Vue 3.x 对响应式系统进行了全面的重构。引入了 Proxy 代理作为默认的响应式实现,取代了 Vue 2.x 中的 Object.defineProperty。这样可以提供更好的性能和更广泛的支持,同时还解决了 Vue 2.x 中存在的一些限制和问题。

  2. 重写虚拟 DOM:Vue 3.x 重写了虚拟 DOM 实现,引入了基于模板的编译器(Vue Template Compiler)和更高效的渲染机制。新的虚拟 DOM 实现在性能方面有所提升,并且支持了更多的编译优化和特性。

  3. Composition API:Vue 3.x 引入了 Composition API,这是一种基于函数的 API 风格,用于编写组件的逻辑。相比于 Vue 2.x 的 Options API,Composition API 提供了更灵活、可组合和可重用的方式来组织和管理组件的逻辑代码。

  4. 更小的包体积:Vue 3.x 对代码进行了优化和精简,使得整体包的体积更小。这得益于新的响应式系统和虚拟 DOM 的改进,以及对编译器和渲染机制的优化。

  5. TypeScript 支持:Vue 3.x 在源码层面增加了对 TypeScript 的原生支持,使用 TypeScript 进行开发时会有更好的类型推导和类型检查。

  6. 更好的 Tree-shaking 支持:Vue 3.x 的模块组织方式更加适应现代的构建工具,使得 Tree-shaking 变得更加高效,可以更准确地剔除未使用的代码,减小最终打包的体积。

  7. 更好的性能和渲染优化:Vue 3.x 在性能方面进行了一系列的优化,包括更新算法的改进、编译器的优化、渲染函数的改进等,使得应用程序在性能方面有所提升。

总体而言,Vue 3.x 在源码层面进行了重大的变化和改进,包括重构响应式系统、重新设计虚拟 DOM、引入 Composition API 等,旨在提供更好的性能、更好的开发体验和更小的包体积。这些变化使得 Vue 3.x 成为一个更现代、更强大的前端框架。

Vue.js 中的 M / V / VM 分别指的是哪些?

在 Vue.js 中,M/V/VM 是一种常见的架构模式的简称,代表了 Model(模型)、View(视图)和 ViewModel(视图模型)。

下面是每个部分的详细解释:

  1. Model(模型):模型表示应用程序的数据和业务逻辑。它负责管理数据的状态、获取和存储数据,以及执行与数据相关的操作。在 Vue.js 中,模型可以是从后端 API 获取的数据,也可以是组件内部的数据。

  2. View(视图):视图是用户界面的可视部分,通常由 HTML、CSS 和模板组成。在 Vue.js 中,视图由 Vue 组件定义,并通过数据绑定将模型的数据显示给用户。视图负责展示数据,并且与用户进行交互。

  3. ViewModel(视图模型):视图模型是连接模型和视图的中间层,它负责处理视图和模型之间的交互和数据传递。视图模型包含了与视图相关的业务逻辑,以及处理用户输入和响应的逻辑。在 Vue.js 中,视图模型通常由 Vue 组件的实例来扮演,它们通过双向数据绑定将模型的数据同步到视图,并处理视图事件和用户交互。

总体而言,M/V/VM 是一种将应用程序的数据、界面和逻辑进行分离的模式,它有助于更好地组织和管理代码,提高代码的可维护性和可测试性。在 Vue.js 中,这种模式可以通过使用 Vue 组件和其响应式系统来实现。模型负责管理数据和业务逻辑,视图负责展示数据,视图模型充当连接视图和模型的桥梁,处理交互和数据传递。

Vue-loader 主要有哪些特性?

Vue-loader 是一个用于将 Vue 单文件组件解析和转换为 JavaScript 模块的 webpack 加载器。它是 Vue.js 官方提供的 webpack 插件,用于开发环境中的模块化构建。

以下是 Vue-loader 的主要特性:

  1. 单文件组件解析:Vue-loader 能够解析 .vue 后缀的单文件组件,并将其拆分为模板、脚本和样式部分。这样可以更好地组织和管理组件的代码。

  2. 预处理器支持:Vue-loader 支持在单文件组件的模板和样式中使用各种预处理器,如 Pug(以前称为 Jade)、Stylus、Less 和 Sass/SCSS。这样可以在组件中使用更丰富的语法和功能。

  3. 模块热替换(HMR):Vue-loader 集成了 webpack 的热模块替换功能,使得在开发过程中可以实时预览和调试组件的变化,而不需要刷新整个页面。

  4. 自动 CSS 提取:Vue-loader 可以自动将组件中的样式提取为单独的 CSS 文件,以便于缓存和并行加载。这可以通过配置选项来控制,以满足不同的构建需求。

  5. Scoped CSS:Vue-loader 支持将样式作用域限制在组件范围内,以避免样式污染和冲突。它通过添加唯一的选择器或设置 scoped 属性来实现。

  6. 自动前缀处理:Vue-loader 可以自动为 CSS 添加浏览器前缀,以确保在不同浏览器中的兼容性。

  7. TypeScript 支持:Vue-loader 对 TypeScript 有原生支持,可以在单文件组件的脚本部分使用 TypeScript 进行开发。

  8. 其他特性:Vue-loader 还提供了其他一些特性,如自动注入 Vue 实例、自定义块处理、异步组件等,以增强开发体验和提供更灵活的功能。

通过这些特性,Vue-loader 让开发者能够更方便地使用 Vue 单文件组件进行开发,并与 webpack 整合,以实现模块化的构建和优化。

Vue.js 如何做 ESLint 校验?

要在 Vue.js 项目中进行 ESLint 校验,你可以按照以下步骤进行设置:

  1. 安装 ESLint:首先,确保你的项目中已经安装了 ESLint。你可以在项目根目录下执行以下命令来进行安装:

    npm install eslint --save-dev
  2. 初始化 ESLint 配置:执行以下命令来初始化 ESLint 配置文件:

    npx eslint --init

    这将引导你完成一系列问题以生成 .eslintrc.js.eslintrc.json 配置文件。你可以根据自己的需求选择合适的配置选项,例如选择使用 Vue.js 的官方推荐配置或者自定义配置。

  3. 安装 Vue.js 相关的 ESLint 插件:在进行 Vue.js 开发时,你还需要安装一些与 Vue.js 相关的 ESLint 插件。执行以下命令来安装这些插件:

    npm install eslint-plugin-vue --save-dev
  4. 配置 ESLint 规则:在生成的 ESLint 配置文件中,你可以配置一系列规则来定义代码的风格和质量要求。你可以根据自己的项目需求进行相应的配置。

    例如,如果你选择了 Vue.js 官方推荐的配置,你可以在配置文件中添加以下内容来启用规则:

    module.exports = {
    // ...其他配置
    extends: [
    // ...其他扩展配置
    "plugin:vue/vue3-recommended",
    ],
    // ...其他配置
    };
  5. 在构建工具中集成 ESLint:如果你使用的是构建工具(如 webpack),你可以将 ESLint 集成到构建过程中。例如,在 webpack 的配置文件中添加以下代码来在构建时进行 ESLint 校验:

    module.exports = {
    // ...其他配置
    module: {
    rules: [
    // ...其他规则
    {
    test: /\.(js|vue)$/,
    loader: "eslint-loader",
    enforce: "pre",
    exclude: /node_modules/,
    },
    ],
    },
    // ...其他配置
    };

    这样,在每次构建时,ESLint 将会对 JavaScript 和 Vue 单文件组件进行校验。

  6. 运行 ESLint 校验:配置完成后,你可以在命令行中执行以下命令来运行 ESLint 校验:

    npx eslint your-file.js

    your-file.js 替换为你需要校验的具体文件路径。你也可以指定目录来批量校验文件。

以上就是在 Vue.js 项目中进行 ESLint 校验的一般步骤。根据你的需求和项目配置,你可以进一步定制和调整 ESLint 的规则和配置。

Vue.js 如何做单元测试?

在 Vue.js 中进行单元测试可以通过使用不同的测试框架和工具来实现。以下是一种常见的方法来进行 Vue.js 单元测试:

  1. 安装测试工具:首先,确保你的项目中已经安装了测试工具。常见的 Vue.js 单元测试工具包括 Jest、Mocha、Chai 等。你可以根据个人偏好选择适合你的测试工具,并将其作为开发依赖项进行安装。例如,使用 Jest:

    npm install --save-dev jest vue-jest @vue/test-utils
  2. 编写测试用例:创建一个与组件对应的测试文件,并编写测试用例。测试用例应该覆盖组件的各个功能和边界情况,以确保组件的正确性。在测试用例中,你可以使用测试工具提供的断言库来验证组件的行为和输出。

    例如,以下是一个使用 Jest 编写的简单测试用例:

    import { shallowMount } from "@vue/test-utils";
    import MyComponent from "@/components/MyComponent.vue";

    describe("MyComponent", () => {
    it("renders correctly", () => {
    const wrapper = shallowMount(MyComponent);
    expect(wrapper.text()).toContain("Hello, World!");
    });
    });
  3. 运行测试:配置完成后,你可以执行以下命令来运行测试:

    npx jest

    Jest 将会查找项目中的测试文件,并执行其中的测试用例。你也可以指定特定的测试文件或目录来运行部分测试。

  4. 可选:配置额外的插件和工具:除了基本的测试工具外,你还可以使用其他插件和工具来增强单元测试的功能。例如,你可以使用 Vue Test Utils 提供的辅助函数来模拟用户交互和测试组件的异步行为。

    另外,如果你使用的是 Vue CLI 创建的项目,Vue CLI 提供了内置的单元测试支持,可以更轻松地进行单元测试。你可以通过运行以下命令来生成默认的单元测试配置:

    vue add @vue/unit-jest

    这将自动配置 Jest 和相关的插件,并为你的项目生成默认的测试文件和配置。

通过以上步骤,你就可以在 Vue.js 项目中进行单元测试了。单元测试可以帮助你验证组件的行为和逻辑,确保代码的质量和稳定性。你可以根据项目的需求和复杂度选择适合的测试框架和工具,并编写相应的测试用例来覆盖组件的各个方面。

发布 / 订阅模式和观察者模式的区别是什么?

发布/订阅模式和观察者模式是两种常见的软件设计模式,它们用于实现组件间的解耦和消息通信。尽管它们在某些方面有相似之处,但也存在一些关键区别:

发布/订阅模式(Publish/Subscribe Pattern):

  • 发布者(Publisher)和订阅者(Subscriber)之间存在一个中间组件,通常称为消息代理或事件总线。
  • 发布者将消息发送到消息代理中,而不需要直接与订阅者通信。
  • 订阅者可以通过向消息代理注册自己来接收感兴趣的消息。
  • 消息代理负责将消息传递给所有订阅者,订阅者之间相互独立,彼此不知道对方的存在。
  • 发布者和订阅者之间没有直接关联,它们通过消息代理进行间接通信。
  • 发布/订阅模式通常用于解耦复杂的系统,允许组件之间的松耦合通信。

观察者模式(Observer Pattern):

  • 观察者(Observer)直接订阅并监听目标对象(Subject)的状态变化。
  • 目标对象维护一个观察者列表,并在状态发生变化时通知所有观察者。
  • 观察者必须事先知道目标对象,并显式地注册为观察者。
  • 目标对象发出通知后,观察者会被调用以执行相应的操作。
  • 观察者与目标对象之间存在直接关联,它们通过事件或回调函数进行通信。
  • 观察者模式通常用于一对多的场景,当一个对象的状态改变需要通知多个其他对象时。

总结:

  • 发布/订阅模式使用消息代理作为中间件,发布者和订阅者之间没有直接关联,彼此解耦。订阅者通过注册来接收感兴趣的消息。
  • 观察者模式中,观察者直接订阅目标对象,它们之间存在直接关联,目标对象通知观察者状态变化。

这些模式在实际应用中都有广泛的用途,选择适当的模式取决于你的需求和系统架构。

简述 MVC / MVP / MVVM 的区别?

MVC(Model-View-Controller)、MVP(Model-View-Presenter)和 MVVM(Model-View-ViewModel)是常见的软件架构模式,用于组织和管理应用程序的代码。它们的主要区别如下:

MVC(Model-View-Controller):

  • 模型(Model)代表应用程序的数据和业务逻辑。
  • 视图(View)负责呈现数据和与用户交互的界面。
  • 控制器(Controller)处理用户输入并更新模型和视图之间的交互。
  • MVC 将应用程序分为三个组件,其中控制器作为中介协调模型和视图之间的通信。
  • 视图和控制器之间的交互是通过事件和回调机制实现的。

MVP(Model-View-Presenter):

  • 模型(Model)代表应用程序的数据和业务逻辑。
  • 视图(View)负责呈现数据和与用户交互的界面。
  • 主持人(Presenter)作为视图和模型之间的中介,处理用户输入和更新视图。
  • MVP 将应用程序分为三个组件,类似于 MVC,但主持人(Presenter)在控制器(Controller)和视图(View)之间承担更多的责任。
  • 视图和主持人之间的交互通常使用接口进行定义,以实现解耦和可测试性。

MVVM(Model-View-ViewModel):

  • 模型(Model)代表应用程序的数据和业务逻辑。
  • 视图(View)负责呈现数据和与用户交互的界面。
  • 视图模型(ViewModel)是视图和模型之间的中介,负责转换模型数据以供视图使用,并处理视图的状态和行为。
  • MVVM 将应用程序分为三个组件,并通过数据绑定实现视图和视图模型之间的自动同步。
  • 视图模型通过暴露属性和命令供视图绑定,从而使视图能够直接与视图模型交互。

总结:

  • MVC、MVP 和 MVVM 都是常见的软件架构模式,用于组织和管理应用程序的代码。
  • MVC 强调控制器的作用,将模型、视图和控制器分离。
  • MVP 强调主持人的作用,将模型、视图和主持人分离。
  • MVVM 强调数据绑定和视图模型的作用,将模型、视图和视图模型分离。

选择适当的架构模式取决于你的需求和项目特点。每种模式都有其优点和适用场景,可以根据项目的规模、复杂度和团队的需求来进行选择。

如果熟悉 Nuxt 等可能会问 SSR 的实现原理?

当涉及到 Nuxt.js 或其他 SSR(服务器端渲染)框架时,以下是 SSR 实现的一般原理:

  1. 服务器端路由:SSR 框架(如 Nuxt.js)在服务器端实现了路由系统。当客户端发起请求时,服务器会根据请求的 URL 确定要渲染的页面组件。

  2. 数据获取:在服务器端渲染之前,SSR 框架会根据当前请求的页面组件,调用相应的数据获取方法(如 asyncDatafetch)来获取页面所需的数据。这些方法可以在页面组件中定义,并在服务器端和客户端均执行。

  3. 数据注入:获取到的数据会在服务器端注入到页面组件中,以供后续渲染使用。这样,服务器端渲染的页面就具备了所需的数据。

  4. 组件渲染:服务器端根据页面组件和注入的数据,将页面组件渲染为 HTML 字符串。

  5. 客户端注水:生成的 HTML 字符串会随响应发送到客户端。客户端在接收到 HTML 之后,会解析 HTML 并重新创建页面组件的客户端实例。

  6. 客户端激活:客户端实例会接管页面的交互和渲染,并与服务器端渲染的页面保持一致。客户端实例会接管事件处理、动态更新等客户端特定的操作。

通过这种方式,SSR 框架能够在服务器端预渲染页面,将初始的 HTML 字符串发送到客户端,从而提供更快的首次加载时间和更好的 SEO(搜索引擎优化)效果。同时,客户端在接管页面后也能够提供更好的交互性和动态更新。

需要注意的是,SSR 的实现可能因具体的框架和工具而有所不同。上述步骤提供了一个一般的概述,但具体的实现细节可能会有所差异。

平常遇到 Vue.js 报 error / warning 的时候有深入追踪错误栈的习惯吗?

作为一个开发者,在遇到 Vue.js 报错或警告时,深入追踪错误栈是一个好的习惯。这可以帮助你更好地了解问题的来源,方便你定位和修复错误。以下是一些推荐的方法和工具来追踪错误栈:

  1. 控制台错误信息:Vue.js 在浏览器的开发者工具控制台中显示有关错误的详细信息。查看控制台中的错误消息和堆栈跟踪,可以获得关于错误发生的位置和可能的原因的线索。

  2. 堆栈追踪:错误消息通常包含堆栈跟踪,它显示了错误发生时的函数调用链。通过仔细阅读堆栈跟踪,可以逐级追踪调用的函数和组件,找出错误发生的具体位置。

  3. 开发者工具:现代浏览器提供了强大的开发者工具,可以帮助你追踪错误栈。利用调试器功能,你可以在代码中设置断点,逐行查看代码执行过程,并观察变量的值和函数调用栈。

  4. Vue.js Devtools:Vue.js 提供了一个官方的浏览器插件,称为 Vue.js Devtools。它提供了一组强大的工具,可以帮助你在浏览器中调试和检查 Vue.js 组件层次结构、状态和事件。通过 Vue.js Devtools,你可以更方便地追踪错误栈和查看组件的数据和状态变化。

  5. 错误边界(Error Boundary):在 Vue.js 应用中,你可以使用错误边界来捕获和处理组件内部的错误。错误边界是一个特殊的组件,它可以捕获子组件中的错误,防止错误的扩散,并提供自定义的错误处理逻辑。通过错误边界,你可以在错误发生时得到更具体和有意义的错误信息。

当遇到 Vue.js 报错或警告时,建议使用上述方法进行调试和追踪错误栈。这样可以更高效地定位和解决问题,提升开发效率和代码质量。

Vue 2.x 中的数据劫持能否采用发布 / 订阅模式实现?采用观察者模式带来的优势和劣势有哪些?

在 Vue 2.x 中,数据劫持采用的是观察者模式来实现响应式系统,而不是发布/订阅模式。

在观察者模式中,有以下几个关键角色:

  1. 主题(Subject):被观察的对象,它维护了一组观察者对象,并在状态变化时通知观察者。
  2. 观察者(Observer):订阅主题的对象,它定义了在主题状态变化时应该采取的操作。
  3. 依赖追踪(Dependency Tracking):通过在 getter 内部收集观察者,在数据发生变化时自动通知相关观察者。

Vue 2.x 的响应式系统使用了观察者模式的思想来实现数据劫持和依赖追踪。具体而言,当 Vue 实例初始化时,它会遍历数据对象的属性,通过 Object.defineProperty 方法将每个属性转换为 getter 和 setter。在 getter 中,Vue 会收集依赖(观察者),建立属性与观察者之间的关联。当属性值发生变化时,setter 会通知相关的观察者进行更新。

优势:

  1. 响应式:观察者模式使 Vue 能够实现数据的响应式。当数据发生变化时,相关的观察者会自动更新视图,保持视图与数据的同步。
  2. 精确追踪:Vue 的观察者模式能够精确追踪到数据的变化,只更新受影响的部分,避免了全局更新的性能损耗。

劣势:

  1. 初始性能开销:在初始化过程中,Vue 需要遍历对象的所有属性并进行转换,这可能会带来一定的初始性能开销。
  2. 对象新增属性的响应:Vue 的观察者模式只能对已经存在的属性进行响应式处理,而对于新增的属性,需要使用 Vue.setvm.$set 方法进行手动触发响应式。

需要注意的是,Vue 3.x 中采用了基于 Proxy 的响应式系统,代替了 Vue 2.x 中的观察者模式。Vue 3.x 的响应式系统在性能和功能上都有一定的优化,可以更好地处理对象新增属性的响应和提供更好的性能表现。

Vue 的整个框架的实现原理大致可分为哪几个部分?

Vue.js 框架的实现原理可以大致分为以下几个部分:

  1. 响应式系统(Reactivity System):Vue.js 的核心是其响应式系统,它负责将数据与视图进行关联。在 Vue.js 中,通过使用 Object.defineProperty 或 Proxy 来劫持数据对象,实现对数据的监听和触发相应的更新。当数据发生变化时,Vue.js 能够自动更新受影响的视图部分,保持数据和视图的同步。

  2. 模板编译(Template Compilation):Vue.js 使用基于 HTML 的模板语法来声明式地将数据绑定到视图上。在编译阶段,Vue.js 会将模板转换为渲染函数,生成虚拟 DOM(Virtual DOM)节点树。这个过程包括模板解析、静态优化、指令和表达式解析等步骤。编译后的渲染函数能够接受数据作为参数,并返回一个虚拟 DOM 树。

  3. 虚拟 DOM(Virtual DOM):Vue.js 使用虚拟 DOM 来表示视图的状态。虚拟 DOM 是一个轻量级的 JavaScript 对象树,与实际的 DOM 元素一一对应。当数据发生变化时,Vue.js 会通过比较新旧虚拟 DOM 树的差异,找出最小的变更,并只更新需要更新的部分,从而提高性能。

  4. 组件系统(Component System):Vue.js 通过组件化的方式构建用户界面。组件是可复用的、自包含的模块,具有自己的模板、数据、方法和样式。Vue.js 的组件系统支持父子组件的通信、自定义事件、插槽等特性,使得应用的代码可以组织为层次清晰、可维护的组件树。

  5. 生命周期钩子(Lifecycle Hooks):Vue.js 提供了一系列的生命周期钩子函数,它们允许开发者在组件不同的生命周期阶段执行自定义的逻辑。这些钩子函数包括 beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed 等,可以用于进行初始化、异步操作、销毁等操作。

  6. 插件系统(Plugin System):Vue.js 的插件系统允许开发者扩展 Vue.js 的功能。插件可以通过扩展 Vue 构造函数或添加全局方法、指令、过滤器等来增强 Vue.js 的能力。开发者可以编写自己的插件,并将其与 Vue.js 集成,以便于在应用中使用。

以上是 Vue.js 框架的主要部分和实现原理。通过这些核心机制,Vue.js 实现了响应式、组件化的开发方式,使得开发者可以更高效、灵活地构建交互性的前端应用。

$nextTick 执行的时机和 DOM 渲染的关系是什么?

$nextTick 方法是 Vue.js 提供的一个异步方法,用于在 DOM 更新之后执行回调函数。它的执行时机和 DOM 渲染之间存在密切的关系。

在 Vue.js 中,当数据发生变化时,Vue.js 会进行异步的 DOM 更新。这意味着在数据变化之后,实际的 DOM 更新并不会立即执行,而是会被放入一个队列中等待执行。而 $nextTick 方法就是用来在下一次 DOM 更新之后执行回调函数。

具体来说,当你调用 $nextTick 方法时,Vue.js 会将回调函数推入到一个队列中。在当前 JavaScript 执行环境的任务完成之后,Vue.js 会开始执行异步队列中的回调函数,同时进行 DOM 更新。这样,你就可以在回调函数中获取到最新的 DOM 渲染结果。

这种机制可以帮助我们处理 DOM 更新后的操作,例如在更新后获取元素的宽度、高度、位置等信息,或者在更新后执行一些特定的操作逻辑。通过将代码放在 $nextTick 的回调函数中,可以确保代码在 DOM 更新完成后执行,避免出现更新前的数据和更新后的 DOM 不一致的情况。

总结起来,$nextTick 方法的执行时机是在下一次 DOM 更新之后,它与 DOM 渲染紧密相关,可以用于在 DOM 更新之后执行代码,并确保获取到最新的 DOM 渲染结果。

Vue 里的 keep-alive 是怎么实现的?

<keep-alive> 是 Vue.js 提供的一个内置组件,用于在组件之间缓存和保持状态。它的实现原理主要涉及两个方面:组件实例的缓存和生命周期钩子的运用。

<keep-alive> 包裹的组件被切换时,它会将之前渲染过的组件实例缓存起来,而不是销毁它们。这样做的好处是可以保持组件的状态和避免重新创建组件的开销。

具体实现步骤如下:

  1. 首次渲染:当 <keep-alive> 内的组件第一次被渲染时,会创建该组件的实例,并将其缓存起来。

  2. 切换组件:当 <keep-alive> 内的组件切换时,Vue.js 会检查是否存在缓存的组件实例。

    • 存在缓存实例:如果存在缓存实例,则将缓存实例重新挂载到 DOM 上,而不是创建新的组件实例。这样可以保持之前的状态,并且不会触发组件的生命周期钩子(如 createdmounted)。

    • 不存在缓存实例:如果不存在缓存实例,则创建新的组件实例,并将其缓存起来。

  3. 缓存管理:Vue.js 使用一个缓存对象来管理被 <keep-alive> 缓存的组件实例。缓存对象采用 LRU(Least Recently Used,最近最少使用)的策略,当缓存达到一定的大小限制时,会自动销毁最不常用的组件实例。

  4. 生命周期钩子的运用:<keep-alive> 组件在切换时会使用两个特殊的生命周期钩子函数:activateddeactivated

    • activated:当组件被激活时(切换到当前组件),会触发 activated 钩子函数。在该钩子函数中,可以执行一些需要在组件激活时进行的逻辑操作。

    • deactivated:当组件被停用时(切换到其他组件),会触发 deactivated 钩子函数。在该钩子函数中,可以执行一些需要在组件停用时进行的逻辑操作。

通过以上的实现机制,<keep-alive> 组件能够帮助我们缓存和保持组件的状态,提高应用性能和用户体验。但需要注意的是,并不是所有的组件都适合使用 <keep-alive>,需要根据具体场景和需求进行合理的使用和配置。

说说 Vue 里的数据劫持在不同版本里是如何处理的?

Vue.js 在不同的版本中采用了不同的方式来实现数据劫持。

  1. Vue.js 1.x 版本:Vue.js 1.x 使用了 Object.defineProperty 来实现数据劫持。在这个版本中,Vue.js 通过递归地遍历数据对象的每个属性,并使用 Object.defineProperty 将其转化为 getter 和 setter。通过 getter 和 setter,Vue.js 能够监听数据的读取和修改操作,并在数据发生变化时通知相关的视图更新。

  2. Vue.js 2.x 版本:Vue.js 2.x 依然使用了 Object.defineProperty,但引入了更高效的响应式系统。在这个版本中,Vue.js 利用了 JavaScript 的原生 Proxy 对象来实现数据劫持。Proxy 是 ES6 中的特性,它可以拦截对目标对象的访问和修改操作。Vue.js 通过使用 Proxy 代理数据对象,实现对数据的监听和触发相应的更新。

    相较于 Object.defineProperty,Proxy 提供了更强大和灵活的功能,能够拦截更多类型的操作,如属性的新增、删除、遍历等。这使得 Vue.js 在 2.x 版本中能够更准确地追踪数据的变化,并实现更高效的响应式系统。

需要注意的是,由于 Proxy 是 ES6 的特性,它在一些古老的浏览器中可能不被支持。因此,Vue.js 2.x 在不支持 Proxy 的环境下会退回到使用 Object.defineProperty 的方式来实现数据劫持,但这可能会带来一些性能上的损失。

总结起来,Vue.js 1.x 使用 Object.defineProperty 来实现数据劫持,而 Vue.js 2.x 引入了 Proxy 对象来实现更高效的数据劫持。这些不同的实现方式都旨在实现 Vue.js 的响应式系统,将数据与视图进行关联,并在数据变化时自动更新受影响的视图部分。

Vue3.0 性能提升主要是体现在哪些方面

.响应式系统

  • Vue.js 2.x 中响应式系统的核心是 Object.defineProperty,劫持整个对象,然后进行深度遍历所有属性,给每个属性添加gettersetter,实现响应式

  • Vue.js 3.x 中使用 Proxy 对象重写响应式系统

    • 可以监听动态新增的属性
    • 可以监听删除的属性
    • 可以监听数组的索引和 length 属性
  • 实现原理:

    • 通过 Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。

    • 通过 Reflect(反射): 对源对象的属性进行操作。

    • MDN 文档中描述的 Proxy 与 Reflect:

       new Proxy(data, {
// 拦截读取属性值
get (target, prop) {
return Reflect.get(target, prop)
},
// 拦截设置属性值或添加新属性
set (target, prop, value) {
return Reflect.set(target, prop, value)
},
// 拦截删除属性
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})

proxy.name = 'tom' ![]
译阶段
  • Vue.js 2.x 通过标记静态节点,优化 diff 的过程

  • Vue.js 3.x

    • vue.js 3.x 中标记和提升所有的静态节点,diff 的时候只需要对比动态节点内容;
    • Fragments(升级 vetur 插件): template 中不需要唯一根节点,可以直接放文本或者同级标签
    • 静态提升(hoistStatic),当使用 hoistStatic 时,所有静态的节点都被提升到 render 方法之外.只会在应用启动的时候被创建一次,之后使用只需要应用提取的静态节点,随着每次的渲染被不停的复用。
    • patch flag, 在动态标签末尾加上相应的标记,只能带 patchFlag 的节点才被认为是动态的元素,会被追踪属性的修改,能快速的找到动态节点,而不用逐个逐层遍历,提高了虚拟 dom diff 的性能。
    • 缓存事件处理函数 cacheHandler,避免每次触发都要重新生成全新的 function 去更新之前的函数

3.源码体积

  • 相比 Vue2,Vue3 整体体积变小了,除了移出一些不常用的 AP

  • tree shanking

    • 任何一个函数,如 ref、reavtived、computed 等,仅仅在用到的时候才打包
    • 通过编译阶段的静态分析,找到没有引入的模块并打上标记,将这些模块都给摇掉

链接:https://juejin.cn/post/7202639428132274234

vue3 有哪些新的组件

ragment
  • 在 Vue2 中: 组件必须有一个根标签

  • 在 Vue3 中: 组件可以没有根标签, 内部会将多个标签包含在一个 Fragment 虚拟元素中

  • 好处: 减少标签层级, 减小内存占用

eleport

什么是 Teleport?—— Teleport 是一种能够将我们的组件 html 结构移动到指定位置的技术。

<teleport to="移动位置">
  <div v-if="isShow" class="mask">
      <div class="dialog">
          <h3>我是一个弹窗</h3>
          <button @click="isShow = false">关闭弹窗</button>
      </div>
  </div>
</teleport>

3.Suspense

等待异步组件时渲染一些额外内容,让应用有更好的用户体验

使用步骤:

  • 异步引入组件

    import { defineAsyncComponent } from "vue";
    const Child = defineAsyncComponent(() => import("./components/Child.vue"));
  • 使用Suspense包裹组件,并配置好defaultfallback

    <template>
    <div class="app">
    <h3>我是App组件</h3>
    <Suspense>
    <template v-slot:default>
    <Child />
    </template>
    <template v-slot:fallback>
    <h3>加载中.....</h3>
    </template>
    </Suspense>
    </div>
    </template>

链接:https://juejin.cn/post/7202639428132274234

Vue2.0 和 Vue3.0 有什么区别

  1. 响应式系统的重新配置,使用 proxy 替换 Object.defineProperty
  2. typescript 支持
  3. 新增组合 API,更好的逻辑重用和代码组织
  4. v-if 和 v-for 的优先级
  5. 静态元素提升
  6. 虚拟节点静态标记
  7. 生命周期变化
  8. 打包体积优化
  9. ssr 渲染性能提升
  10. 支持多个根节点

Vue 生命周期

ue2.x 的生命周期

ue3.0 的生命周期

  • Vue3.0 中可以继续使用 Vue2.x 中的生命周期钩子,但有有两个被更名:

    • beforeDestroy改名为 beforeUnmount

    • destroyed改名为 unmounted

  • Vue3.0 也提供了 Composition API 形式的生命周期钩子,与 Vue2.x 中钩子对应关系如下:

    • beforeCreate===>setup()

    • created====>setup()

    • beforeMount ===>onBeforeMount

    • mounted====>onMounted

    • beforeUpdate===>onBeforeUpdate

    • updated====>onUpdated

    • beforeUnmount ==>onBeforeUnmount

    • unmounted===>onUnmounted

Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是 Vue 的⽣命周期。 1、beforeCreate(创建前) :数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到 data、computed、watch、methods 上的方法和数据。 2、created(创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。 3、beforeMount(挂载前) :在挂载开始之前被调用,相关的 render 函数首次被调用。实例已完成以下的配置:编译模板,把 data 里面的数据和模板生成 html。此时还没有挂载 html 到页面上。 4、mounted(挂载后) :在 el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的 html 内容替换 el 属性指向的 DOM 对象。完成模板中的 html 渲染到 html 页面中。此过程中进行 ajax 交互。 5、beforeUpdate(更新前) :响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。 6、updated(更新后):在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM 已经更新,所以可以执行依赖于 DOM 的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。 7、beforeDestroy(销毁前) :实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例。 8、destroyed(销毁后) :实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。

都说 Composition API 和 React Hook 很像,请问他们的区别是什么

从 React Hook 从实现的角度来看,React Hook 是基于 useState 的调用顺序来确定下一个 render 渲染时间状态从哪个 useState 开始,所以有以下几个限制

  • 不在循环中、条件、调用嵌套函数 Hook
  • 你必须确保它总是在你这边 React Top level 调用函数 Hook
  • 使用效果、使用备忘录 依赖关系必须手动确定

和 Composition API 是基于 Vue 的响应系统,和 React Hook 相比

  • 在设置函数中,一个组件实例只调用一次设置,而 React Hook 每次重新渲染时,都需要调用 Hook,给 React 带来的 GC 比 Vue 更大的压力,性能也相对 Vue 对我来说也比较慢
  • Compositon API 你不必担心调用的顺序,它也可以在循环中、条件、在嵌套函数中使用
  • 响应式系统自动实现依赖关系收集,而且组件的性能优化是由 Vue 内部完成的,而 React Hook 的依赖关系需要手动传递,并且依赖关系的顺序必须得到保证,让路 useEffect、useMemo 等等,否则组件性能会因为依赖关系不正确而下降。

虽然 Compoliton API 看起来像 React Hook 来使用,但它的设计思路也是 React Hook 的参考。

Composition Api 与 Options Api 有什么不同

ptions Api

Options API,即大家常说的选项 API,即以vue为后缀的文件,通过定义methodscomputedwatchdata等属性与方法,共同处理页面逻辑

如下图:

可以看到Options代码编写方式,如果是组件状态,则写在data属性上,如果是方法,则写在methods属性上...

用组件的选项 (datacomputedmethodswatch) 组织逻辑在大多数情况下都有效

然而,当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解

omposition Api

在 Vue3 Composition API 中,组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(更加的高内聚,低耦合)

即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所有 API

3.对比

下面对Composition ApiOptions Api进行两大方面的比较

  • 逻辑组织
  • 逻辑复用
逻辑组织
Options API

假设一个组件是一个大型组件,其内部有很多处理逻辑关注点(对应下图不用颜色)

img

可以看到,这种碎片化使得理解和维护复杂组件变得困难

选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块

Compostion API

Compositon API正是解决上述问题,将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去

下面举个简单例子,将处理count属性相关的代码放在同一个函数了

function useCount() {
let count = ref(10);
let double = computed(() => {
return;
count.value * 2;
});
const handleConut = () => {
count.value = count.value * 2;
};
console.log(count);
return { count, double, handleConut };
}

组件上中使用count

export default defineComponent({
setup() {
const { count, double, handleConut } = useCount();
return { count, double, handleConut };
},
});

再来一张图进行对比,可以很直观地感受到 Composition API在逻辑组织方面的优势,以后修改一个属性功能的时候,只需要跳到控制该属性的方法中即可

img

逻辑复用

Vue2中,我们是用过mixin去复用相同的逻辑

下面举个例子,我们会另起一个mixin.js文件

export const MoveMixin = {
data() {
return {
x: 0,
y: 0,
};
},

methods: {
handleKeyup(e) {
console.log(e.code);
// 上下左右 x y
switch (e.code) {
case "ArrowUp":
this.y--;
break;
case "ArrowDown":
this.y++;
break;
case "ArrowLeft":
this.x--;
break;
case "ArrowRight":
this.x++;
break;
}
},
},

mounted() {
window.addEventListener("keyup", this.handleKeyup);
},

unmounted() {
window.removeEventListener("keyup", this.handleKeyup);
},
};

然后在组件中使用

<template>
<div>
Mouse position: x {{ x }} / y {{ y }}
</div>
</template>
<script>
import mousePositionMixin from './mouse'
export default {
mixins: [mousePositionMixin]
}
</script>

使用单个mixin似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时候

mixins: [mousePositionMixin, fooMixin, barMixin, otherMixin];

会存在两个非常明显的问题:

  • 命名冲突
  • 数据来源不清晰

现在通过Compositon API这种方式改写上面的代码

import { onMounted, onUnmounted, reactive } from "vue"; export function
useMove() { const position = reactive({ x: 0, y: 0, }); const handleKeyup = (e)
=> { console.log(e.code); // 上下左右 x y switch (e.code) { case "ArrowUp": //
y.value--; position.y--; break; case "ArrowDown": // y.value++; position.y++;
break; case "ArrowLeft": // x.value--; position.x--; break; case "ArrowRight":
// x.value++; position.x++; break; } }; onMounted(() => {
window.addEventListener("keyup", handleKeyup); }); onUnmounted(() => {
window.removeEventListener("keyup", handleKeyup); }); return { position }; }

在组件中使用

<template>
<div>Mouse position: x {{ x }} / y {{ y }}</div>
</template>

<script>
import { useMove } from "./useMove";
import { toRefs } from "vue";
export default {
setup() {
const { position } = useMove();
const { x, y } = toRefs(position);
return {
x,
y,
};
},
};
</script>

可以看到,整个数据来源清晰了,即使去编写更多的 hook 函数,也不会出现命名冲突的问题

小结
  • 在逻辑组织和逻辑复用方面,Composition API是优于Options API
  • 因为Composition API几乎是函数,会有更好的类型推断。
  • Composition APItree-shaking 友好,代码也更容易压缩
  • Composition API中见不到this的使用,减少了this指向不明的情况
  • 如果是小型组件,可以继续使用Options API,也是十分友好的

链接:https://juejin.cn/post/7202639428132274234

什么是 SPA 单页面应用,首屏加载你是如何优化的

单页 Web 应用(single page web application,SPA),就是只有一张 Web 页面的应用,是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的 Web 应用程序。我们开发的Vue项目大多是借助个官方的CLI脚手架,快速搭建项目,直接通过new Vue构建一个实例,并将el:'#app'挂载参数传入,最后通过npm run build的方式打包后生成一个index.html,称这种只有一个HTML的页面为单页面应用。

当然,vue也可以像jq一样引入,作为多页面应用的基础框架。

SPA 首屏优化方式

  • 减小入口文件积
  • 静态资源本地缓存
  • UI 框架按需加载
  • 图片资源的压缩
  • 组件重复打包
  • 开启 GZip 压缩
  • 使用 SSR

对 Vue 项目你做过哪些性能优化

1、v-ifv-show

  • 频繁切换时使用v-show,利用其缓存特性
  • 首屏渲染时使用v-if,如果为false则不进行渲染

2、v-forkey

  • 列表变化时,循环时使用唯一不变的key,借助其本地复用策略
  • 列表只进行一次渲染时,key可以采用循环的index

3、侦听器和计算属性

  • 侦听器watch用于数据变化时引起其他行为
  • 多使用compouter计算属性顾名思义就是新计算而来的属性,如果依赖的数据未发生变化,不会触发重新计算

4、合理使用生命周期

  • destroyed阶段进行绑定事件或者定时器的销毁
  • 使用动态组件的时候通过keep-alive包裹进行缓存处理,相关的操作可以在actived阶段激活

5、数据响应式处理

  • 不需要响应式处理的数据可以通过Object.freeze处理,或者直接通过this.xxx = xxx的方式进行定义
  • 需要响应式处理的属性可以通过this.$set的方式处理,而不是JSON.parse(JSON.stringify(XXX))的方式

6、路由加载方式

  • 页面组件可以采用异步加载的方式

7、插件引入

  • 第三方插件可以采用按需加载的方式,比如element-ui

8、减少代码量

  • 采用mixin的方式抽离公共方法
  • 抽离公共组件
  • 定义公共方法至公共js
  • 抽离公共css

9、编译方式

  • 如果线上需要template的编译,可以采用完成版vue.esm.js
  • 如果线上无需template的编译,可采用运行时版本vue.runtime.esm.js,相比完整版体积要小大约30%

10、渲染方式

  • 服务端渲染,如果是需要SEO的网站可以采用服务端渲染的方式
  • 前端渲染,一些企业内部使用的后端管理系统可以采用前端渲染的方式

11、字体图标的使用

  • 有些图片图标尽可能使用字体图标

Vue 组件通信的方式有哪些

vue 中 8 种常规的通信方案

  • 通过 props 传递
  • 通过 $emit 触发自定义事件
  • 使用 ref
  • EventBus
  • $parent$root
  • attrs 与 listeners
  • Provide 与 Inject
  • Vuex

组件间通信的分类可以分成以下

父子关系的组件数据传递选择 props 与 $emit进行传递,也可选择 ref 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent 进行传递 祖先与后代组件数据传递可选择 attrs 与 listeners 或者 Provide 与 Inject 复杂关系的组件数据传递可以通过 vuex 存放共享的变量

Vue 常用的修饰符有哪些

1、表单修饰符

(1).lazy

在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 ,可以添加 lazy 修饰符,从而转为在 change 事件之后进行同步:

<input v-model.lazy="msg">

(2).number

如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符:

<input v-model.number="age" type="number">

(3).trim

如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:

<input v-model.trim="msg">

2、事件修饰符

(1).stop

阻止单击事件继续传播。

<!--这里只会触发a-->
<div @click="divClick"><a v-on:click.stop="aClick">点击</a></div>

(2).prevent

阻止标签的默认行为。

<a href="http://www.baidu.com" v-on:click.prevent="aClick">点击</a>

(3).capture

事件先在有.capture修饰符的节点上触发,然后在其包裹的内部节点中触发。

<!--这里先执行divClick事件,然后再执行aClick事件-->
<div @click="divClick"><a v-on:click="aClick">点击</a></div>

(4).self

只当在 event.target 是当前元素自身时触发处理函数,即事件不是从内部元素触发的。

<!--在a标签上点击时只会触发aClick事件,只有点击phrase的时候才会触发divClick事件-->
<div @click.self="divClick">phrase<a v-on:click="aClick">点击</a></div>

(5).once

不像其它只能对原生的 DOM 事件起作用的修饰符,.once 修饰符还能被用到自定义的组件事件上,表示当前事件只触发一次。

<a v-on:click.once="aClick">点击</a>

(6).passive

.passive 修饰符尤其能够提升移动端的性能

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>

Vue 中的$nextTick 有什么作用

const callbacks = []
let pending = false

/**
* 完成两件事:
* 1、用 try catch 包装 flushSchedulerQueue 函数,然后将其放入 callbacks 数组
* 2、如果 pending 为 false,表示现在浏览器的任务队列中没有 flushCallbacks 函数
* 如果 pending 为 true,则表示浏览器的任务队列中已经被放入了 flushCallbacks 函数,
* 待执行 flushCallbacks 函数时,pending 会被再次置为 false,表示下一个 flushCallbacks 函数可以进入
* 浏览器的任务队列了
* pending 的作用:保证在同一时刻,浏览器的任务队列中只有一个 flushCallbacks 函数
* @param {*} cb 接收一个回调函数 => flushSchedulerQueue
* @param {*} ctx 上下文
* @returns
*/
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 用 callbacks 数组存储经过包装的 cb 函数
callbacks.push(() => {
if (cb) {
// 用 try catch 包装回调函数,便于错误捕获
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 执行 timerFunc,在浏览器的任务队列中(首选微任务队列)放入 flushCallbacks 函数
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

官方对其的定义

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

什么意思呢?

我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新

Vue 的异步更新机制的核心是利用了浏览器的异步任务队列来实现的,首选微任务队列,宏任务队列次之。

当响应式数据更新后,会调用 dep.notify 方法,通知 dep 中收集的 watcher 去执行 update 方法,watcher.update 将 watcher 自己放入一个 watcher 队列(全局的 queue 数组)。

然后通过 nextTick 方法将一个刷新 watcher 队列的方法(flushSchedulerQueue)放入一个全局的 callbacks 数组中。

如果此时浏览器的异步任务队列中没有一个叫 flushCallbacks 的函数,则执行 timerFunc 函数,将 flushCallbacks 函数放入异步任务队列。如果异步任务队列中已经存在 flushCallbacks 函数,等待其执行完成以后再放入下一个 flushCallbacks 函数。

flushCallbacks 函数负责执行 callbacks 数组中的所有 flushSchedulerQueue 函数。

flushSchedulerQueue 函数负责刷新 watcher 队列,即执行 queue 数组中每一个 watcher 的 run 方

链接:https://juejin.cn/post/7202639428132274234

如何理解双向数据绑定

我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

数据层(Model):应用的数据及业务逻辑 视图层(View):应用的展示效果,各类 UI 组件 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来 而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM 这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理

理解 ViewModel 它的主要职责就是:

数据变化后更新视图 视图变化后更新数据 当然,它还有两个主要部分组成

监听器(Observer):对所有数据的属性进行监听 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

v-show 和 v-if 有什么区别?你可以讲讲吗

v-show 与 v-if 的作用效果是相同的(不含 v-else),都能控制元素在页面是否显示,在用法上也是相同的

  • 区别 控制手段不同 编译过程不同 编译条件不同

控制手段:v-show 隐藏则是为该元素添加 css--display:none,dom 元素依旧还在。v-if 显示隐藏是将 dom 元素整个添加或删除

编译过程:v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show 只是简单的基于 css 切换

编译条件:v-if 是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染

v-show 由 false 变为 true 的时候不会触发组件的生命周期

v-if 由 false 变为 true 的时候,触发组件的 beforeCreate、create、beforeMount、mounted 钩子,由 true 变为 false 的时候触发组件的 beforeDestory、destoryed 方法

性能消耗:v-if 有更高的切换消耗;v-show 有更高的初始渲染消耗

有用过 keep-alive 吗?它有什么作用

vue中支持组件化,并且也有用于缓存的内置组件keep-alive可直接使用,使用场景为路由组件动态组件

  • activated表示进入组件的生命周期,deactivated表示离开组件的生命周期
  • include表示匹配到的才缓存,exclude表示匹配到的都不缓存
  • max表示最多可以缓存多少组件

关于 keep-alive 的基本用法:

<keep-alive>
<component :is="view"></component>
</keep-alive>
使用includes和exclude:

<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配

设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activated 与 deactivated):

首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > ... ... > beforeRouteLeave > deactivated

再次进入组件时:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated

你可以实现一个虚拟 DOM 吗

先看浏览器对HTML的理解

<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>

当浏览器读到这些代码时,它会建立一个 DOM 树来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。 上述 HTML 对应的 DOM 节点树如下图所示:

544ef95bdd7c96a19d700ce613ab425a_dom-tree.png

每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。

再看VueHTML template的理解

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

简言之,浏览器对 HTML 的理解是 DOM 树,Vue 对HTML的理解是虚拟 DOM,最后在patch阶段通过 DOM 操作的 api 将其渲染成真实的 DOM 节点。

如何实现虚拟 DOM

首先可以看看vueVNode的结构

源码位置:src/core/vdom/vnode.js

export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
functionalContext: Component | void; // only for functional component root nodes
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?

constructor(
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*当前节点的标签名*/
this.tag = tag;
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data;
/*当前节点的子节点,是一个数组*/
this.children = children;
/*当前节点的文本*/
this.text = text;
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm;
/*当前节点的名字空间*/
this.ns = undefined;
/*编译作用域*/
this.context = context;
/*函数化组件作用域*/
this.functionalContext = undefined;
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key;
/*组件的option选项*/
this.componentOptions = componentOptions;
/*当前节点对应的组件的实例*/
this.componentInstance = undefined;
/*当前节点的父节点*/
this.parent = undefined;
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false;
/*静态节点标志*/
this.isStatic = false;
/*是否作为跟节点插入*/
this.isRootInsert = true;
/*是否为注释节点*/
this.isComment = false;
/*是否为克隆节点*/
this.isCloned = false;
/*是否有v-once指令*/
this.isOnce = false;
}

// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next https://github.com/answershuto/learnVue*/
get child(): Component | void {
return this.componentInstance;
}
}

这里对VNode进行稍微的说明:

  • 所有对象的 context 选项都指向了 Vue 实例
  • elm 属性则指向了其相对应的真实 DOM 节点

vue是通过createElement生成VNode

源码位置:src/core/vdom/create-element.js

export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType);
}

上面可以看到createElement 方法实际上是对 _createElement 方法的封装,对参数的传入进行了判断

export function _createElement(
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context`
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
...
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if ( === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
// 创建VNode
...
}

可以看到_createElement接收 5 个参数:

  • context 表示 VNode 的上下文环境,是 Component 类型
  • tag 表示标签,它可以是一个字符串,也可以是一个 Component
  • data 表示 VNode 的数据,它是一个 VNodeData 类型
  • children 表示当前 VNode的子节点,它是任意类型的
  • normalizationType 表示子节点规范的类型,类型不同规范的方法也就不一样,主要是参考 render 函数是编译生成的还是用户手写的

根据normalizationType 的类型,children会有不同的定义

if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if ( === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}

simpleNormalizeChildren方法调用场景是 render 函数是编译生成的

normalizeChildren方法调用场景分为下面两种:

  • render 函数是用户手写的
  • 编译 slotv-for 的时候会产生嵌套数组

无论是simpleNormalizeChildren还是normalizeChildren都是对children进行规范(使children 变成了一个类型为 VNodeArray),这里就不展开说了

规范化children的源码位置在:src/core/vdom/helpers/normalzie-children.js

在规范化children后,就去创建VNode

let vnode, ns;
// 对tag进行判断
if (typeof tag === "string") {
let Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// 如果是内置的节点,则直接创建一个普通VNode
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
);
} else if (
isDef((Ctor = resolveAsset(context.$options, "components", tag)))
) {
// component
// 如果是component类型,则会通过createComponent创建VNode节点
vnode = createComponent(Ctor, data, context, children, tag);
} else {
vnode = new VNode(tag, data, children, undefined, undefined, context);
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}

createComponent同样是创建VNode

源码位置:src/core/vdom/create-component.js

export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return;
}
// 构建子类构造函数
const baseCtor = context.$options._base;

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}

// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== "function") {
if (process.env.NODE_ENV !== "production") {
warn(`Invalid Component definition: ${String(Ctor)}`, context);
}
return;
}

// async component
let asyncFactory;
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor;
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context);
if (Ctor === undefined) {
return createAsyncPlaceholder(asyncFactory, data, context, children, tag);
}
}

data = data || {};

// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor);

// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data);
}

// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag);

// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children);
}

// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on;
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn;

if (isTrue(Ctor.options.abstract)) {
const slot = data.slot;
data = {};
if (slot) {
data.slot = slot;
}
}

// 安装组件钩子函数,把钩子函数合并到data.hook中
installComponentHooks(data);

//实例化一个VNode返回。组件的VNode是没有children的
const name = Ctor.options.name || tag;
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
data,
undefined,
undefined,
undefined,
context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
);
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode);
}

return vnode;
}

稍微提下createComponent生成VNode的三个关键流程:

  • 构造子类构造函数Ctor
  • installComponentHooks安装组件钩子函数
  • 实例化 vnode

小结

createElement 创建 VNode 的过程,每个 VNodechildrenchildren 每个元素也是一个VNode,这样就形成了一个虚拟树结构,用于描述真实的DOM树结构

链接:https://juejin.cn/post/7202639428132274234

为什么 data 属性是一个函数而不是一个对象,具体原因是什么

是不是一定是函数,得看场景。并且,也无需担心什么时候该将data写为函数还是对象,因为vue内部已经做了处理,并在控制台输出错误信息。

场景一new Vue({data: ...})
这种场景主要为项目入口或者多个html页面各实例化一个Vue时,这里的data即可用对象的形式,也可用工厂函数返回对象的形式。因为,这里的data只会出现一次,不存在重复引用而引起的数据污染问题。

场景二:组件场景中的选项
在生成组件vnode的过程中,组件会在生成构造函数的过程中执行合并策略:

// data合并策略
strats.data = function (
parentVal,
childVal,
vm
) {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
);

return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}

return mergeDataOrFn(parentVal, childVal, vm)
};

如果合并过程中发现子组件的数据不是函数,即typeof childVal !== 'function'成立,进而在开发环境会在控制台输出警告并且直接返回parentVal,说明这里压根就没有把childVal中的任何data信息合并到options中去。

上面讲到组件 data 必须是一个函数,不知道大家有没有思考过这是为什么呢?

在我们定义好一个组件的时候,vue 最终都会通过 Vue.extend()构成组件实例

这里我们模仿组件构造函数,定义 data 属性,采用对象的形式

function Component(){

}
Component.prototype.data = {
count : 0
}

创建两个组件实例

const componentA = new Component()
const componentB = new Component()

修改 componentA 组件 data 属性的值,componentB 中的值也发生了改变

console.log(componentB.data.count)  // 0
componentA.data.count = 1
console.log(componentB.data.count) // 1

产生这样的原因这是两者共用了同一个内存地址,componentA 修改的内容,同样对 componentB 产生了影响

如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同)

function Component(){
this.data = this.data()
}
Component.prototype.data = function (){
return {
count : 0
}
}

修改 componentA 组件 data 属性的值,componentB 中的值不受影响

console.log(componentB.data.count) // 0 componentA.data.count = 1 console.log(componentB.data.count) // 0 vue 组件可能会有很多个实例,采用函数返回一个全新 data 形式,使每个实例对象的数据不会受到其他实例对象数据的污染

Vue2 的初始化过程你有过了解吗,做了哪些事情

new Vue 走到了 vue 的构造函数中:src\core\instance\index.js文件。

this._init(options)

然后从 Mixin 增加的原型方法看,initMixin(Vue),调用的是为 Vue 增加的原型方法_init

// src/core/instance/init.js

function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this; 创建vm,
...
// 合并options 到 vm.$options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
...
initLifecycle(vm); //初始生命周期
initEvents(vm); //初始化事件
initRender(vm); //初始render函数
callHook(vm, 'beforeCreate'); //执行 beforeCreate生命周期钩子
...
initState(vm); //初始化data,props,methods computed,watch
...
callHook(vm, 'created'); //执行 created 生命周期钩子

if (vm.$options.el) {
vm.$mount(vm.$options.el); //这里也是重点,下面需要用到
}
}

总结

所以,从上面的函数看来,new vue 所做的事情,就像一个流程图一样展开了,分别是

  • 合并配置
  • 初始化生命周期
  • 初始化事件
  • 初始化渲染
  • 调用 beforeCreate 钩子函数
  • init injections and reactivity(这个阶段属性都已注入绑定,而且被 $watch 变成 reactivity,但是 $el 还是没有生成,也就是 DOM 没有生成)
  • 初始化 state 状态(初始化了 data、props、computed、watcher)
  • 调用 created 钩子函数。

在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM。

Vue3 初始化的一个大概流程

  • 初始化的一个大概流程

createApp() => mount() => render() => patch() => processComponent() => mountComponent()

  • 简易版流程编写

    1.Vue.createApp() 实际执行的是 renderer 的 createApp()

    2.renderer 是 createRenderer 这个方法创建

    3.renderer 的 createApp()是 createAppAPI()返回的

    4.createAppApi 接受到 render 之后,创建一个 app 实例,定义 mount 方法

    5.mount 会调用 render 函数。将 vnode 转换为真实 dom

createRenderer() => renderer => renderer.createApp() <= createAppApi()

<div id="app"></div>

<script>
// 3.createAppAPI
const createAppAPI = (render) => {
return function createApp(rootComponent) {
// 返回应用程序实例
const app = {
mount(rootContainer) {
// 挂载vnode => dom
const vnode = {
tag: rootComponent,
};
// 执行渲染
render(vnode, rootContainer);
},
};
return app;
};
};

// 1. 创建createApp
const Vue = {
createApp(options) {
//实际执行的为renderer的createApp()
// 返回app实例
return renderer.createApp(options);
},
};

// 2.实现renderer工厂函数
const createRenderer = (options) => {
// 实现patch
const patch = (n1, n2, container) => {
// 获取根组件配置
const rootComponent = n2.tag;
const ctx = { ...rootComponent.data() };
// 执行render获取vnode
const vnode = rootComponent.render.call(ctx);

// 转换vnode => dom
const parent = options.querySelector(container);
const child = options.createElement(vnode.tag);
if (typeof vnode.children === "string") {
child.textContent = vnode.children;
} else {
//array
}
// 追加
options.insert(child, parent);
};

// 实现render
const render = (vnode, container) => {
patch(container._vnode || null, vnode, container);
container._vnode = vnode;
};

// 该对象就是renderer
return {
render,
createApp: createAppAPI(render),
};
};

const renderer = createRenderer({
querySelector(el) {
return document.querySelector(el);
},
createElement(tag) {
return document.createElement(tag);
},
insert(child, parent) {
parent.appendChild(child);
},
});

Vue.createApp({
data() {
return {
bar: "hello,vue3",
};
},
render() {
return {
tag: "h1",
children: this.bar,
};
},
}).mount("#app");
</script>

vue3 响应式 api 如何编写

var activeEffect = null; function effect(fn) {  activeEffect = fn;
 activeEffect();  activeEffect = null; } var depsMap = new WeakMap(); function
gather(target, key) {  // 避免例如console.log(obj1.name)而触发gather  if
(!activeEffect) return;  let depMap = depsMap.get(target);  if (!depMap) {  
 depsMap.set(target, (depMap = new Map())); }  let dep = depMap.get(key);  if
(!dep) {    depMap.set(key, (dep = new Set())); }  dep.add(activeEffect) }
function trigger(target, key) {  let depMap = depsMap.get(target);  if (depMap)
{    const dep = depMap.get(key);    if (dep) {      dep.forEach((effect) =>
effect());   } } } function reactive(target) {  const handle = {    set(target,
key, value, receiver) {      Reflect.set(target, key, value, receiver);    
 trigger(receiver, key); // 设置值时触发自动更新   },    get(target, key,
receiver) {      gather(receiver, key); // 访问时收集依赖      return
Reflect.get(target, key, receiver);   }, };  return new Proxy(target, handle); }
function ref(name){    return reactive(       {            value: name       }  
) }

在 Vue 项目中你是如何做的 SSR 渲染

与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:

  • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
  • 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序

服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行

  • Vue SSR 是一个在 SPA 上进行改良的服务端渲染
  • 通过 Vue SSR 渲染的页面,需要在客户端激活才能实现交互
  • Vue SSR 将包含两部分:服务端渲染的首屏,包含交互的 SPA

使用 ssr 不存在单例模式,每次用户请求都会创建一个新的 vue 实例 实现 ssr 需要实现服务端首屏渲染和客户端激活 服务端异步获取数据 asyncData 可以分为首屏异步获取和切换组件获取 首屏异步获取数据,在服务端预渲染的时候就应该已经完成 切换组件通过 mixin 混入,在 beforeMount 钩子完成数据获取

怎么看 Vue 的 diff 算法

diff 算法是一种通过同层的树节点进行比较的高效算法

diff 整体策略为:深度优先,同层比较 比较只会在同层级进行, 不会跨层级比较 比较的过程中,循环从两边向中间收拢

  • 当数据发生改变时,订阅者 watcher 就会调用 patch 给真实的 DOM 打补丁
  • 通过 isSameVnode 进行判断,相同则调用 patchVnode 方法
  • patchVnode 做了以下操作:
    • 找到对应的真实 dom,称为 el
    • 如果都有都有文本节点且不相等,将 el 文本节点设置为 Vnode 的文本节点
    • 如果 oldVnode 有子节点而 VNode 没有,则删除 el 子节点
    • 如果 oldVnode 没有子节点而 VNode 有,则将 VNode 的子节点真实化后添加到 el
    • 如果两者都有子节点,则执行 updateChildren 函数比较子节点
  • updateChildren 主要做了以下操作:
    • 设置新旧 VNode 的头尾指针
    • 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用 patchVnode 进行 patch 重复流程、调用 createElem 创建一个新节点,从哈希表寻找 key 一致的 VNode 节点再分情况操作

01构建一个Vue项目你需要做哪些内容

  • 架子:选用合适的初始化脚手架(vue-cli2.0或者vue-cli3.0)
  • 请求:数据axios请求的配置
  • 登录:登录注册系统
  • 路由:路由管理页面
  • 数据:vuex全局数据管理
  • 权限:权限管理系统
  • 埋点:埋点系统
  • 插件:第三方插件的选取以及引入方式
  • 错误:错误页面
  • 入口:前端资源直接当静态资源,或者服务端模板拉取
  • SEO:如果考虑SEO建议采用SSR方案
  • 组件:基础组件/业务组件
  • 样式:样式预处理起,公共样式抽取
  • 方法:公共方法抽离

删除数组用 delete 和 Vue.delete 有什么区别?

  • delete:只是被删除数组成员变为 empty / undefined,其他元素键值不变
  • Vue.delete:直接删了数组成员,并改变了数组的键值(对象是响应式的,确保删除能触发更新视图,这个方法主要用于避开 Vue 不能检测到属性被删除的限制)

在 vue 项目中如何引入第三方库(比如 jQuery)?

// 1、绝对路径直接引入
// 在index.html 中用script 引入
<script src="./static/jquery-1.12.4.js"></script>
// 然后在webpack 中配置external
externals: { 'jquery': 'jQuery' }
// 在组件中使用时import
import $ from 'jquery'
// 2 、在webpack 中配置alias
resolve: { extensions: ['.js', '.vue', '.json'], alias: { '@': resolve('src'), 'jquery':
resolve('static/jquery-1.12.4.js') } }
// 然后在组件中import
// 3、在webpack 中配置plugins
plugins: [ new webpack.ProvidePlugin({ $: 'jquery' }) ]
// 全局使用,但在使用eslint 情况下会报错,需要在使用了$ 的代码前添加/*eslint-disable*/ 来去掉ESLint 的检查。

Vue3.0 里为什么要用 Proxy API 替代 defineProperty API?

响应式优化。

  1. defineProperty API 的局限性最大原因是它只能针对单例属性做监听。Vue2.x 中的响应式实现正是基于 defineProperty 中的 descriptor,对 data 中的属性做了遍历+ 递归,为每个属性设置了 getter、setter。这也就是为什么 Vue 只能对 data 中预定义过的属性做出响应的原因,在 Vue 中使用下标的方式直接修改属性的值或者添加一个预先不存在的对象属性是无法做到 setter 监听的,这是 defineProperty 的局限性。
  2. Proxy API 的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性,将会带来很大的性能提升和更优的代码。Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
  3. 响应式是惰性的在 Vue.js 2.x 中,对于一个深层属性嵌套的对象,要劫持它内部深层次的变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的,这无疑会有很大的性能消耗。在 Vue.js 3.0 中,使用 Proxy API 并不能监听到对象内部深层次的属性变化,因此它的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部属性才会变成响应式,简单的可以说是按需实现响应式,减少性能消耗。 基础用法:
let datas = {
num: 0,
};
let proxy = new Proxy(datas, {
get(target, property) {
return target[property];
},
set(target, property, value) {
target[property] += value;
},
});

Vue3.0 编译做了哪些优化?

  1. 生成 Block tree

Vue.js 2.x 的数据更新并触发重新渲染的粒度是组件级的,单个组件内部需要遍历该组 件的整个 vnode 树。在 2.0 里,渲染效率的快慢与组件大小成正相关:组件越大,渲染 效率越慢。并且,对于一些静态节点,又无数据更新,这些遍历都是性能浪费。 Vue.js 3.0 做到了通过编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的, 每个区块只需要追踪自身包含的动态节点。所以,在 3.0 里,渲染效率不再与模板大小 成正相关,而是与模板中动态节点的数量成正相关。

  1. slot 编译优化 Vue.js 2.x 中,如果有一个组件传入了 slot,那么每次父组件更新的时候,会强制使子组 件 update,造成性能的浪费。

Vue.js 3.0 优化了 slot 的生成,使得非动态 slot 中属性的更新只会触发子组件的更新。 动态 slot 指的是在 slot 上面使用 v-if,v-for,动态 slot 名字等会导致 slot 产生运行时动 态变化但是又无法被子组件 track 的操作。

  1. diff 算法优化

Vue2.x 中的虚拟 dom 是进行全量的对比。 Vue3.0 中新增了静态标记(PatchFlag):在与上次虚拟结点进行对比的时候,值对比 带有 patch flag 的节点,并且 ��� 以通过 flag 的信息得知当前节点要对比的具体内容化。

  1. hoistStatic 静态提升 Vue2.x : 无论元素是否参与更新,每次都会重新创建。 Vue3.0 : 对不参与更新的元素,只会被创建一次,之后会在每次渲染时候被不停的复用。

  2. cacheHandlers 事件侦听器缓存

默认情况下 onClick 会被视为动态绑定,所以每次都会去追踪它的变化但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可。

Vue 组件之间通信方式有哪些

vue 是组件化开发框架,所以对于 vue 应用来说组件间的数据通信非常重要。 此题主要考查大家 vue 基本功,对于 vue 基础 api 运用熟练度。 另外一些边界知识如 provide/inject/$attrs 则提现了面试者的知识广度。


组件传参的各种方式 img


思路分析:

  1. 总述知道的所有方式
  2. 按组件关系阐述使用场景

回答范例:

  1. 组件通信常用方式有以下 8 种:
  • props
  • $emit/$on
  • $children/$parent
  • $attrs/$listeners
  • ref
  • $root
  • eventbus
  • vuex

注意 vue3 中废弃的几个 API

v3-migration.vuejs.org/breaking-ch…

v3-migration.vuejs.org/breaking-ch…

v3-migration.vuejs.org/breaking-ch…


  1. 根据组件之间关系讨论组件通信最为清晰有效
  • 父子组件
    • props/$emit/$parent/ref/$attrs
  • 兄弟组件
    • $parent/$root/eventbus/vuex
  • 跨层级关系
    • eventbus/vuex/provide+inject

v-if 和 v-for 哪个优先级更高?

分析:

此题考查常识,文档中曾有详细说明v2|v3;也是一个很好的实践题目,项目中经常会遇到,能够看出面试者 api 熟悉程度和应用能力。


思路分析:
  1. 先给出结论
  2. 为什么是这样的,说出细节
  3. 哪些场景可能导致我们这样做,该怎么处理
  4. 总结,拔高

回答范例:
  1. 实践中不应该把 v-for 和 v-if 放一起
  2. vue2 中v-for 的优先级是高于 v-if,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件,哪怕我们只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表,这会比较浪费;另外需要注意的是在vue3 中则完全相反,v-if 的优先级高于 v-for,所以 v-if 执行时,它调用的变量还不存在,就会导致异常
  3. 通常有两种情况下导致我们这样做:
    • 为了过滤列表中的项目 (比如 v-for="user in users" v-if="user.isActive")。此时定义一个计算属性 (比如 activeUsers),让其返回过滤后的列表即可(比如users.filter(u=>u.isActive))。
    • 为了避免渲染本应该被隐藏的列表 (比如 v-for="user in users" v-if="shouldShowUsers")。此时把 v-if 移动至容器元素上 (比如 ulol)或者外面包一层template即可。
  4. 文档中明确指出永远不要把 v-ifv-for 同时用在同一个元素上,显然这是一个重要的注意事项。
  5. 源码里面关于代码生成的部分,能够清晰的看到是先处理 v-if 还是 v-for,顺序上 vue2 和 vue3 正好相反,因此产生了一些症状的不同,但是不管怎样都是不能把它们写在一起的。

知其所以然:

做个测试,test.html 两者同级时,渲染函数如下:

ƒ anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},_l((items),function(item){return (item.isActive)?_c('div',{key:item.id},[_v("\n "+_s(item.name)+"\n ")]):_e()}),0)}
}


做个测试,test-v3.html

image-20220210104854185


源码中找答案

v2:github1s.com/vuejs/vue/b…

v3:github1s.com/vuejs/core/…


简述 Vue 的生命周期以及每个阶段做的事

必问题目,考查 vue 基础知识。

思路
  1. 给出概念
  2. 列举生命周期各阶段
  3. 阐述整体流程
  4. 结合实践
  5. 扩展:vue3 变化

回答范例

1.每个 Vue 组件实例被创建后都会经过一系列初始化步骤,比如,它需要数据观测,模板编译,挂载实例到 dom 上,以及数据变化时更新 dom。这个过程中会运行叫做生命周期钩子的函数,以便用户在特定阶段有机会添加他们自己的代码。

2.Vue 生命周期总共可以分为 8 个阶段:创建前后, 载入前后, 更新前后, 销毁前后,以及一些特殊场景的生命周期。vue3 中新增了三个用于调试和服务端渲染场景。


生命周期 v2生命周期 v3描述
beforeCreatebeforeCreate组件实例被创建之初
createdcreated组件实例已经完全创建
beforeMountbeforeMount组件挂载之前
mountedmounted组件挂载到实例上去之后
beforeUpdatebeforeUpdate组件数据发生变化,更新之前
updatedupdated数据数据更新之后
beforeDestroybeforeUnmount组件实例销毁之前
destroyedunmounted组件实例销毁之后

生命周期 v2生命周期 v3描述
activatedactivatedkeep-alive 缓存的组件激活时
deactivateddeactivatedkeep-alive 缓存的组件停用时调用
errorCapturederrorCaptured捕获一个来自子孙组件的错误时被调用
-renderTracked调试钩子,响应式依赖被收集时调用
-renderTriggered调试钩子,响应式依赖被触发时调用
-serverPrefetchssr only,组件实例在服务器上被渲染前调用

3.Vue生命周期流程图

Component lifecycle diagram


4.结合实践:

beforeCreate:通常用于插件开发中执行一些初始化任务

created:组件初始化完毕,可以访问各种数据,获取接口数据等

mounted:dom 已创建,可用于获取访问数据和 dom 元素;访问子组件等。

beforeUpdate:此时view层还未更新,可用于获取更新前各种状态

updated:完成view层的更新,更新后,所有状态已是最新

beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消

unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

可能的追问
  1. setup 和 created 谁先执行?
  2. setup 中为什么没有 beforeCreate 和 created?

知其所以然

vue3 中生命周期的派发时刻:

github1s.com/vuejs/core/…

vue2 中声明周期的派发时刻:

github1s.com/vuejs/vue/b…


能说一说双向绑定使用和原理吗?

题目分析:

双向绑定是vue的特色之一,开发中必然会用到的知识点,然而此题还问了实现原理,升级为深度考查。


思路分析:
  1. 给出双绑定义
  2. 双绑带来的好处
  3. 在哪使用双绑
  4. 使用方式、使用细节、vue3 变化
  5. 原理实现描述

回答范例:
  1. vue 中双向绑定是一个指令v-model,可以绑定一个响应式数据到视图,同时视图中变化能改变该值。
  2. v-model是语法糖,默认情况下相当于:value@input。使用v-model可以减少大量繁琐的事件处理代码,提高开发效率。
  3. 通常在表单项上使用v-model,还可以在自定义组件上使用,表示某个值的输入和输出控制。
  4. 通过<input v-model="xxx">的方式将 xxx 的值绑定到表单元素 value 上;对于 checkbox,可以使用true-value和 false-value 指定特殊的值,对于 radio 可以使用 value 指定特殊的值;对于 select 可以通过 options 元素的 value 设置特殊的值;还可以结合.lazy,.number,.trim 对 v-mode 的行为做进一步限定;v-model用在自定义组件上时又会有很大不同,vue3 中它类似于sync修饰符,最终展开的结果是 modelValue 属性和 update:modelValue 事件;vue3 中我们甚至可以用参数形式指定多个不同的绑定,例如 v-model:foo 和 v-model:bar,非常强大!
  5. v-model是一个指令,它的神奇魔法实际上是 vue 的编译器完成的。我做过测试,包含v-model的模板,转换为渲染函数之后,实际上还是是 value 属性的绑定以及 input 事件监听,事件回调函数中会做相应变量更新操作。编译器根据表单元素的不同会展开不同的 DOM 属性和事件对,比如 text 类型的 input 和 textarea 会展开为 value 和 input 事件;checkbox 和 radio 类型的 input 会展开为 checked 和 change 事件;select 用 value 作为属性,用 change 作为事件。

可能的追问:
  1. v-modelsync修饰符有什么区别
  2. 自定义组件使用v-model如果想要改变事件名或者属性名应该怎么做

知其所以然:

测试代码,test.html

观察输出的渲染函数:

// <input type="text" v-model="foo">
_c("input", {
directives: [
{ name: "model", rawName: "v-model", value: foo, expression: "foo" },
],
attrs: { type: "text" },
domProps: { value: foo },
on: {
input: function ($event) {
if ($event.target.composing) return;
foo = $event.target.value;
},
},
});

// <input type="checkbox" v-model="bar">
_c("input", {
directives: [
{ name: "model", rawName: "v-model", value: bar, expression: "bar" },
],
attrs: { type: "checkbox" },
domProps: {
checked: Array.isArray(bar) ? _i(bar, null) > -1 : bar,
},
on: {
change: function ($event) {
var $$a = bar,
$$el = $event.target,
$$c = $$el.checked ? true : false;
if (Array.isArray($$a)) {
var $$v = null,
$$i = _i($$a, $$v);
if ($$el.checked) {
$$i < 0 && (bar = $$a.concat([$$v]));
} else {
$$i > -1 && (bar = $$a.slice(0, $$i).concat($$a.slice($$i + 1)));
}
} else {
bar = $$c;
}
},
},
});

// <select v-model="baz">
// <option value="vue">vue</option>
// <option value="react">react</option>
// </select>
_c(
"select",
{
directives: [
{ name: "model", rawName: "v-model", value: baz, expression: "baz" },
],
on: {
change: function ($event) {
var $$selectedVal = Array.prototype.filter
.call($event.target.options, function (o) {
return o.selected;
})
.map(function (o) {
var val = "_value" in o ? o._value : o.value;
return val;
});
baz = $event.target.multiple ? $$selectedVal : $$selectedVal[0];
},
},
},
[
_c("option", { attrs: { value: "vue" } }, [_v("vue")]),
_v(" "),
_c("option", { attrs: { value: "react" } }, [_v("react")]),
]
);

Vue 中如何扩展一个组件

此题属于实践题,考察大家对 vue 常用 api 使用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同。

答题思路:
  1. 按照逻辑扩展和内容扩展来列举,
    • 逻辑扩展有:mixins、extends、composition api;
    • 内容扩展有 slots;
  2. 分别说出他们使用方法、场景差异和问题。
  3. 作为扩展,还可以说说 vue3 中新引入的 composition api 带来的变化

回答范例:
  1. 常见的组件扩展方法有:mixins,slots,extends 等

  2. 混入 mixins 是分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。

    // 复用代码:它是一个配置对象,选项和组件里面一样
    const mymixin = {
    methods: {
    dosomething() {},
    },
    };
    // 全局混入:将混入对象传入
    Vue.mixin(mymixin);

    // 局部混入:做数组项设置到mixins选项,仅作用于当前组件
    const Comp = {
    mixins: [mymixin],
    };

  1. 插槽主要用于 vue 组件中的内容分发,也可以用于组件扩展。

    子组件 Child

    <div>
    <slot>这个内容会被父组件传递的内容替换</slot>
    </div>

    父组件 Parent

    <div>
    <Child>来自老爹的内容</Child>
    </div>

    如果要精确分发到不同位置可以使用具名插槽,如果要使用子组件中的数据可以使用作用域插槽。


  1. 组件选项中还有一个不太常用的选项 extends,也可以起到扩展组件的目的

    // 扩展对象
    const myextends = {
    methods: {
    dosomething() {},
    },
    };
    // 组件扩展:做数组项设置到extends选项,仅作用于当前组件
    // 跟混入的不同是它只能扩展单个对象
    // 另外如果和混入发生冲突,该选项优先级较高,优先起作用
    const Comp = {
    extends: myextends,
    };

  1. 混入的数据和方法不能明确判断来源且可能和当前组件内变量产生命名冲突,vue3 中引入的 composition api,可以很好解决这些问题,利用独立出来的响应式模块可以很方便的编写独立逻辑并提供响应式的数据,然后在 setup 选项中组合使用,增强代码的可读性和维护性。例如:

    // 复用逻辑1
    function useXX() {}
    // 复用逻辑2
    function useYY() {}
    // 逻辑组合
    const Comp = {
    setup() {
    const { xx } = useXX();
    const { yy } = useYY();
    return { xx, yy };
    },
    };

可能的追问

Vue.extend 方法你用过吗?它能用来做组件扩展吗?


知其所以然

mixins 原理:

github1s.com/vuejs/core/…

github1s.com/vuejs/core/…

slots 原理:

github1s.com/vuejs/core/…

github1s.com/vuejs/core/…

github1s.com/vuejs/core/…


子组件可以直接改变父组件的数据么,说明原因

分析

这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题。

参考文档:staging.vuejs.org/guide/compo…


思路
  1. 讲讲单项数据流原则,表明为何不能这么做
  2. 举几个常见场景的例子说说解决方案
  3. 结合实践讲讲如果需要修改父组件状态应该如何做

回答范例
  1. 所有的 prop 都使得其父子之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器控制台中发出警告。

    const props = defineProps(['foo'])
    // ❌ 下面行为会被警告, props是只读的!
    props.foo = 'bar'


  1. 实际开发过程中有两个场景会想要修改一个属性:

    • 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:

      const props = defineProps(["initialCounter"]);
      const counter = ref(props.initialCounter);
    • 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:

      const props = defineProps(["size"]);
      // prop变化,计算属性自动更新
      const normalizedSize = computed(() => props.size.trim().toLowerCase());
  2. 实践中如果确实想要改变父组件属性应该 emit 一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的 prop,但是我们还是能够直接改内嵌的对象或属性。


Vue 要做权限管理该怎么做?控制到按钮级别的权限怎么做?

分析

综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。

权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。

img


思路
  1. 权限管理需求分析:页面和按钮权限
  2. 权限管理的实现方案:分后端方案和前端方案阐述
  3. 说说各自的优缺点

回答范例
  1. 权限管理一般需求是页面权限按钮权限的管理

  2. 具体实现的时候分后端和前端两种方案:

    前端方案会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个asyncRoutes数组,需要认证的页面在其路由的meta中添加一个roles字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)方式动态添加路由即可。

    后端方案会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过addRoutes动态添加路由信息

    按钮权限的控制通常会实现一个指令,例如v-permission将按钮要求角色通过值传给 v-permission 指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。

  3. 纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!


知其所以然

路由守卫

github1s.com/PanJiaChen/…

路由生成

github1s.com/PanJiaChen/…

动态追加路由

github1s.com/PanJiaChen/…


可能的追问
  1. 类似Tabs这类组件能不能使用v-permission指令实现按钮权限控制?

    <el-tabs>
    <el-tab-pane label="⽤户管理" name="first">⽤户管理</el-tab-pane>
    <el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane>
    </el-tabs>

  1. 服务端返回的路由信息如何添加到路由器中?

    // 前端组件名和组件映射表
    const map = {
    //xx: require('@/views/xx.vue').default // 同步的⽅式
    xx: () => import('@/views/xx.vue') // 异步的⽅式
    }
    // 服务端返回的asyncRoutes
    const asyncRoutes = [
    { path: '/xx', component: 'xx',... }
    ]
    // 遍历asyncRoutes,将component替换为map[component]
    function mapComponent(asyncRoutes) {
    asyncRoutes.forEach(route => {
    route.component = map[route.component];
    if(route.children) {
    route.children.map(child => mapComponent(child))
    }
    })
    }
    mapComponent(asyncRoutes)


说一说你对 vue 响应式理解?

分析

这是一道必问题目,但能回答到位的比较少。如果只是看看一些网文,通常没什么底气,经不住面试官推敲,但像我们这样即看过源码还造过轮子的,回答这个问题就会比较有底气啦。

答题思路:
  1. 啥是响应式?
  2. 为什么 vue 需要响应式?
  3. 它能给我们带来什么好处?
  4. vue 的响应式是怎么实现的?有哪些优缺点?
  5. vue3 中的响应式的新变化

回答范例:
  1. 所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制
  2. MVVM 框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。
  3. 以 vue 为例说明,通过数据响应式加上虚拟 DOM 和 patch 算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的 DOM 操作,从而大大提升开发效率,降低开发难度。
  4. vue2 中的数据响应式会根据数据类型来做不同处理,如果是对象则采用 Object.defineProperty()**的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是**数组则通过覆盖数组对象原型的 7 个变更方法,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用 Vue.set/delete 这样特殊的 api 才能生效;对于 es6 中新产生的 Map、Set 这些数据结构不支持等问题。
  5. 为了解决这些问题,vue3 重新编写了这一部分的实现:利用 ES6 的 Proxy 代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊 api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的 reactivity 包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了。

知其所以然

vue2 响应式:

github1s.com/vuejs/vue/b…

vue3 响应式:

github1s.com/vuejs/core/…

github1s.com/vuejs/core/…


说说你对虚拟 DOM 的理解?

分析

现有框架几乎都引入了虚拟 DOM 来对真实 DOM 进行抽象,也就是现在大家所熟知的 VNode 和 VDOM,那么为什么需要引入虚拟 DOM 呢?围绕这个疑问来解答即可!

思路
  1. vdom 是什么
  2. 引入 vdom 的好处
  3. vdom 如何生成,又如何成为 dom
  4. 在后续的 diff 中的作用

回答范例
  1. 虚拟 dom 顾名思义就是虚拟的 dom 对象,它本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构。

  2. 通过引入 vdom 我们可以获得如下好处:

    将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能

    • 直接操作 dom 是有限制的,比如:diff、clone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了。
    • 操作 dom 是比较昂贵的操作,频繁的 dom 操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作 dom 的次数,从而减少页面重绘和回流。

    方便实现跨平台

    • 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
    • Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染。

  1. vdom 如何生成?在 vue 中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler 编译为渲染函数,在接下来的挂载(mount)过程中会调用 render 函数,返回的对象就是虚拟 dom。但它们还不是真正的 dom,所以会在后续的 patch 过程中进一步转化为 dom。

    image-20220209153820845

  2. 挂载过程结束后,vue 程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新 render,此时就会生成新的 vdom,和上一次的渲染结果 diff 就能得到变化的地方,从而转换为最小量的 dom 操作,高效更新视图。


知其所以然

vnode 定义:

github1s.com/vuejs/core/…

观察渲染函数:21-vdom/test-render-v3.html

创建 vnode:

  • createElementBlock:

github1s.com/vuejs/core/…

  • createVnode:

github1s.com/vuejs/core/…

  • 首次调用时刻:

github1s.com/vuejs/core/…


mount:

github1s.com/vuejs/core/…

调试 mount 过程:mountComponent

21-vdom/test-render-v3.html


你了解 diff 算法吗?

分析

必问题目,涉及 vue 更新原理,比较考查理解深度。

img


思路
  1. diff 算法是干什么的
  2. 它的必要性
  3. 它何时执行
  4. 具体执行方式
  5. 拔高:说一下 vue3 中的优化

回答范例

1.Vue 中的 diff 算法称为 patching 算法,它由 Snabbdom 修改而来,虚拟 DOM 要想转化为真实 DOM 就需要通过 patch 方法转换。

2.最初 Vue1.x 视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟 DOM 和 patching 算法支持,但是这样粒度过细导致 Vue1.x 无法承载较大应用;Vue 2.x 中为了降低 Watcher 粒度,每个组件只有一个 Watcher 与之对应,此时就需要引入 patching 算法才能精确找到发生变化的地方并高效更新。

3.vue 中 diff 执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行 render 函数获得最新的虚拟 DOM,然后执行 patch 函数,并传入新旧两次虚拟 DOM,通过比对两者找到变化的地方,最后将其转化为对应的 DOM 操作。


4.patch 过程是一个递归过程,遵循深度优先、同层比较的策略;以 vue3 的 patch 为例:

  • 首先判断两个节点是否为相同同类节点,不同则删除重新创建
  • 如果双方都是文本则更新文本内容
  • 如果双方都是元素节点则递归更新子元素,同时更新元素属性
  • 更新子节点时又分了几种情况:
    • 新的子节点是文本,老的子节点是数组则清空,并设置文本;
    • 新的子节点是文本,老的子节点是文本则直接更新文本;
    • 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
    • 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节 blabla
  1. vue3 中引入的更新策略:编译期优化 patchFlags、block 等

知其所以然

patch 关键代码

github1s.com/vuejs/core/…

调试 test-v3.html


你知道哪些 vue3 新特性

分析

官网列举的最值得注意的新特性:v3-migration.vuejs.org/

image-20220210165307624


也就是下面这些:

  • Composition API
  • SFC Composition API 语法糖
  • Teleport 传送门
  • Fragments 片段
  • Emits 选项
  • 自定义渲染器
  • SFC CSS 变量
  • Suspense

以上这些是 api 相关,另外还有很多框架特性也不能落掉。


回答范例
  1. api 层面 Vue3 新特性主要包括:Composition API、SFC Composition API 语法糖、Teleport 传送门、Fragments 片段、Emits 选项、自定义渲染器、SFC CSS 变量、Suspense
  2. 另外,Vue3.0 在框架层面也有很多亮眼的改进:
  • 更快
    • 虚拟 DOM 重写
    • 编译器优化:静态提升、patchFlags、block 等
    • 基于 Proxy 的响应式系统
  • 更小:更好的摇树优化
  • 更容易维护:TypeScript + 模块化
  • 更容易扩展
    • 独立的响应化模块
    • 自定义渲染器

知其所以然

体验编译器优化

sfc.vuejs.org/

reactive 实现

github1s.com/vuejs/core/…


怎么定义动态路由?怎么获取传过来的动态参数?

分析

API 题目,考查基础能力,不容有失,尽可能说的详细。

思路
  1. 什么是动态路由
  2. 什么时候使用动态路由,怎么定义动态路由
  3. 参数如何获取
  4. 细节、注意事项

回答范例
  1. 很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由。
  2. 例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router 中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User },其中:id就是路径参数
  3. 路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。
  4. 参数还可以有多个,例如/users/:username/posts/:postId;除了 $route.params 之外,$route 对象还公开了其他有用的信息,如 $route.query$route.hash 等。

可能的追问
  1. 如何响应动态路由参数的变化

router.vuejs.org/zh/guide/es…

  1. 我们如何处理 404 Not Found 路由

router.vuejs.org/zh/guide/es…


如果让你从零开始写一个 vue 路由,说说你的思路

思路分析:

首先思考 vue 路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。

  • 借助 hash 或者 history api 实现 url 跳转页面不刷新
  • 同时监听 hashchange 事件或者 popstate 事件处理跳转
  • 根据 hash 值或者 state 值从 routes 表中匹配对应 component 并渲染之

回答范例:

一个 SPA 应用的路由需要解决的问题是页面跳转内容改变同时不刷新,同时路由还需要以插件形式存在,所以:

  1. 首先我会定义一个

    createRouter

    函数,返回路由器实例,实例内部做几件事:

    • 保存用户传入的配置项
    • 监听 hash 或者 popstate 事件
    • 回调里根据 path 匹配对应路由
  2. 将 router 定义成一个 Vue 插件,即实现 install 方法,内部做两件事:

    • 实现两个全局组件:router-link 和 router-view,分别实现页面跳转和内容显示
    • 定义两个全局变量:$route和$router,组件内可以访问当前路由和路由器实例

知其所以然:
  • createRouter 如何创建实例

github1s.com/vuejs/route…

  • 事件监听

github1s.com/vuejs/route… RouterView

  • 页面跳转 RouterLink

github1s.com/vuejs/route…

  • 内容显示 RouterView

github1s.com/vuejs/route…


能说说 key 的作用吗?

分析:

这是一道特别常见的问题,主要考查大家对虚拟 DOM 和 patch 细节的掌握程度,能够反映面试者理解层次。


思路分析:
  1. 给出结论,key 的作用是用于优化 patch 性能
  2. key 的必要性
  3. 实际使用方式
  4. 总结:可从源码层面描述一下 vue 如何判断两个节点是否相同

回答范例:
  1. key 的作用主要是为了更高效的更新虚拟 DOM。
  2. vue 在 patch 过程中判断两个节点是否是相同节点是 key 是一个必要条件,渲染一组列表时,key 往往是唯一标识,所以如果不定义 key 的话,vue 只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个 patch 过程比较低效,影响性能。
  3. 实际使用中在渲染一组列表时 key 必须设置,而且必须是唯一标识,应该避免使用数组索引作为 key,这可能导致一些隐蔽的 bug;vue 中在使用相同标签元素过渡切换时,也会使用 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。
  4. 从源码中可以知道,vue 判断两个节点是否相同时主要判断两者的 key 和元素类型等,因此如果不设置 key,它的值就是 undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的 dom 更新操作,明显是不可取的。

知其所以然

测试代码,test-v3.html

上面案例重现的是以下过程

img

不使用 key

image-20220214110059028


如果使用 key

// 首次循环patch A
A B C D E
A B F C D E

// 第2次循环patch B
B C D E
B F C D E

// 第3次循环patch E
C D E
F C D E

// 第4次循环patch D
C D
F C D

// 第5次循环patch C
C
F C

// oldCh全部处理结束,newCh中剩下的F,创建F并插入到C前面


源码中找答案:

判断是否为相同节点

github1s.com/vuejs/core/…

更新时的处理

github1s.com/vuejs/core/…


不使用 key

image-20220214110059028

如果使用 key

// 首次循环patch A
A B C D E
A B F C D E

// 第2次循环patch B
B C D E
B F C D E

// 第3次循环patch E
C D E
F C D E

// 第4次循环patch D
C D
F C D

// 第5次循环patch C
C
F C

// oldCh全部处理结束,newCh中剩下的F,创建F并插入到C前面

源码中找答案:

判断是否为相同节点

github1s.com/vuejs/core/…

更新时的处理

github1s.com/vuejs/core/…


说说 nextTick 的使用和原理?

分析

这道题及考察使用,有考察原理,nextTick 在开发过程中应用的也较少,原理上和 vue 异步更新有密切关系,对于面试者考查很有区分度,如果能够很好回答此题,对面试效果有极大帮助。

答题思路
  1. nextTick 是做什么的?
  2. 为什么需要它呢?
  3. 开发时何时使用它?抓抓头,想想你在平时开发中使用它的地方
  4. 下面介绍一下如何使用 nextTick
  5. 原理解读,结合异步更新和 nextTick 生效方式,会显得你格外优秀

回答范例:
  1. nextTick是等待下一次 DOM 更新刷新的工具方法。
  2. Vue 有个异步更新策略,意思是如果数据变化,Vue 不会立刻更新 DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在 DOM 上,此时如果想要获取更新后的 DOM 状态,就需要使用 nextTick。
  3. 开发时,有两个场景我们会用到 nextTick:
  • created 中想要获取 DOM 时;
  • 响应式数据变化后获取 DOM 更新后的状态,比如希望获取列表更新后的高度。
  1. nextTick 签名如下:function nextTick(callback?: () => void): Promise<void>

    所以我们只需要在传入的回调函数中访问最新 DOM 状态即可,或者我们可以 await nextTick()方法返回的 Promise 之后做这件事。

  2. 在 Vue 内部,nextTick 之所以能够让我们看到 DOM 更新后的结果,是因为我们传入的 callback 会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有 DOM 操作也就结束了,callback 自然能够获取到最新的 DOM 值。


知其所以然:
  1. 源码解读:

组件更新函数入队:

github1s.com/vuejs/core/…

入队函数:

github1s.com/vuejs/core/…

nextTick 定义:

github1s.com/vuejs/core/…

  1. 测试案例,test-v3.html

watch 和 computed 的区别以及选择?

两个重要 API,反应应聘者熟练程度。

思路分析
  1. 先看computed, watch两者定义,列举使用上的差异
  2. 列举使用场景上的差异,如何选择
  3. 使用细节、注意事项
  4. vue3 变化

computed 特点:具有响应式的返回值

const count = ref(1);
const plusOne = computed(() => count.value + 1);

watch 特点:侦测变化,执行回调

const state = reactive({ count: 0 });
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
);

回答范例
  1. 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computed 和 methods 的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch 没有返回值,但可以执行异步操作等复杂逻辑。
  2. 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的 DOM 操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性。
  3. 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch 可以传递对象,设置 deep、immediate 等选项。
  4. vue3 中 watch 选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API 中新出现了 watch、watchEffect 可以完全替代目前的 watch 选项,且功能更加强大。

回答范例
  1. 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computed 和 methods 的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch 没有返回值,但可以执行异步操作等复杂逻辑。
  2. 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的 DOM 操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性。
  3. 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch 可以传递对象,设置 deep、immediate 等选项。
  4. vue3 中 watch 选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API 中新出现了 watch、watchEffect 可以完全替代目前的 watch 选项,且功能更加强大。

可能追问
  1. watch 会不会立即执行?
  2. watch 和 watchEffect 有什么差异

知其所以然

computed 的实现

github1s.com/vuejs/core/…

ComputedRefImpl

github1s.com/vuejs/core/…

缓存性

github1s.com/vuejs/core/…

github1s.com/vuejs/core/…

watch 的实现

github1s.com/vuejs/core/…


说一下 Vue 子组件和父组件创建和挂载顺序

这题考查大家对创建过程的理解程度。

思路分析
  1. 给结论
  2. 阐述理由

回答范例
  1. 创建过程自上而下,挂载过程自下而上;即:
    • parent created
    • child created
    • child mounted
    • parent mounted
  2. 之所以会这样是因为 Vue 创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加 mounted 钩子到队列,等到 patch 结束再执行它们,可见子组件的 mounted 钩子是先进入到队列中的,因此等到 patch 结束执行这些钩子时也先执行。

知其所以然

观察 beforeCreated 和 created 钩子的处理

github1s.com/vuejs/core/…

github1s.com/vuejs/core/…

观察 beforeMount 和 mounted 钩子的处理

github1s.com/vuejs/core/…

测试代码,test-v3.html


怎么缓存当前的组件?缓存后怎么更新?

缓存组件使用 keep-alive 组件,这是一个非常常见且有用的优化手段,vue3 中 keep-alive 有比较大的更新,能说的点比较多。

思路
  1. 缓存用 keep-alive,它的作用与用法
  2. 使用细节,例如缓存指定/排除、结合 router 和 transition
  3. 组件缓存后更新可以利用 activated 或者 beforeRouteEnter
  4. 原理阐述

回答范例
  1. 开发中缓存组件使用 keep-alive 组件,keep-alive 是 vue 内置组件,keep-alive 包裹动态组件 component 时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。

    <keep-alive>
    <component :is="view"></component>
    </keep-alive>
  2. 结合属性 include 和 exclude 可以明确指定缓存哪些组件或排除缓存指定组件。vue3 中结合 vue-router 时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive

    <router-view v-slot="{ Component }">
    <keep-alive>
    <component :is="Component"></component>
    </keep-alive>
    </router-view>

  1. 缓存后如果要获取数据,解决方案可以有以下两种:

    • beforeRouteEnter:在有 vue-router 的项目,每次进入路由的时候,都会执行beforeRouteEnter

      beforeRouteEnter(to, from, next){
      next(vm=>{
      console.log(vm)
      // 每次进入路由执行
      vm.getData() // 获取数据
      })
      },

    • actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子

      activated(){
      this.getData() // 获取数据
      },


  1. keep-alive 是一个通用组件,它内部定义了一个 map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的 component 组件对应组件的 vnode,如果该组件在 map 中存在就直接返回它。由于 component 的 is 属性是个响应式数据,因此只要它变化,keep-alive 的 render 函数就会重新执行。

知其所以然

KeepAlive 定义

github1s.com/vuejs/core/…

缓存定义

github1s.com/vuejs/core/…

缓存组件

github1s.com/vuejs/core/…

获取缓存组件

github1s.com/vuejs/core/…

测试缓存特性,test-v3.html


从 0 到 1 自己构架一个 vue 项目,说说有哪些步骤、哪些重要插件、目录结构你会怎么组织

综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可。

思路
  1. 构建项目,创建项目基本结构
  2. 引入必要的插件:
  3. 代码规范:prettier,eslint
  4. 提交规范:husky,lint-staged
  5. 其他常用:svg-loader,vueuse,nprogress
  6. 常见目录结构

回答范例
  1. 从 0 创建一个项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件
  2. 目前 vue3 项目我会用 vite 或者 create-vue 创建项目
  3. 接下来引入必要插件:路由插件 vue-router、状态管理 vuex/pinia、ui 库我比较喜欢 element-plus 和 antd-vue、http 工具我会选 axios
  4. 其他比较常用的库有 vueuse,nprogress,图标可以使用 vite-svg-loader
  5. 下面是代码规范:结合 prettier 和 eslint 即可
  6. 最后是提交规范,可以使用 husky,lint-staged,commitlint

  1. 目录结构我有如下习惯: .vscode:用来放项目中的 vscode 配置

    plugins:用来放 vite 插件的 plugin 配置

    public:用来放一些诸如 页头 icon 之类的公共文件,会被打包到 dist 根目录下

    src:用来放项目代码文件

    api:用来放 http 的一些接口配置

    assets:用来放一些 CSS 之类的静态资源

    components:用来放项目通用组件

    layout:用来放项目的布局

    router:用来放项目的路由配置

    store:用来放状态管理 Pinia 的配置

    utils:用来放项目中的工具方法类

    views:用来放项目的页面文件


实际工作中,你总结的 vue 最佳实践有哪些?

看到这样的题目,可以用以下图片来回答:

img


思路

查看 vue 官方文档:

风格指南:vuejs.org/style-guide…

性能:vuejs.org/guide/best-…

安全:vuejs.org/guide/best-…

访问性:vuejs.org/guide/best-…

发布:vuejs.org/guide/best-…


回答范例

我从编码风格、性能、安全等方面说几条:

  1. 编码风格方面:
    • 命名组件时使用“多词”风格避免和 HTML 元素冲突
    • 使用“细节化”方式定义属性而不是只有一个属性名
    • 属性名声明时使用“驼峰命名”,模板或 jsx 中使用“肉串命名”
    • 使用 v-for 时务必加上 key,且不要跟 v-if 写在一起
  2. 性能方面:
    • 路由懒加载减少应用尺寸
    • 利用 SSR 减少首屏加载时间
    • 利用 v-once 渲染那些不需要更新的内容
    • 一些长列表可以利用虚拟滚动技术避免内存过度占用
    • 对于深层嵌套对象的大数组可以使用 shallowRef 或 shallowReactive 降低开销
    • 避免不必要的组件抽象

  1. 安全:
    • 不使用不可信模板,例如使用用户输入拼接模板:template: <div> + userProvidedString + </div>
    • 小心使用 v-html,:url,:style 等,避免 html、url、样式等注入
  2. 等等......

简单说一说你对 vuex 理解?

img

思路
  1. 给定义
  2. 必要性阐述
  3. 何时使用
  4. 拓展:一些个人思考、实践经验等

范例
  1. Vuex 是一个专为 Vue.js 应用开发的状态管理模式 + 库。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  2. 我们期待以一种简单的“单向数据流”的方式管理应用,即状态 -> 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是 vuex 存在的必要性,它和 react 生态中的 redux 之类是一个概念。
  3. Vuex 解决状态管理的同时引入了不少概念:例如 state、mutation、action 等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用 Vuex 反而是繁琐冗余的,一个简单的 store 模式就足够了。但是,如果要构建一个中大型单页应用,Vuex 基本是标配。
  4. 我在使用 vuex 过程中感受到一些 blabla

可能的追问
  1. vuex 有什么缺点吗?你在开发过程中有遇到什么问题吗?
  2. action 和 mutation 的区别是什么?为什么要区分它们?

说说从 template 到 render 处理过程

分析

问我们 template 到 render 过程,其实是问 vue编译器工作原理。

思路
  1. 引入 vue 编译器概念
  2. 说明编译器的必要性
  3. 阐述编译器工作流程
回答范例
  1. Vue 中有个独特的编译器模块,称为“compiler”,它的主要作用是将用户编写的 template 编译为 js 中可执行的 render 函数。
  2. 之所以需要这个编译过程是为了便于前端程序员能高效的编写视图模板。相比而言,我们还是更愿意用 HTML 来编写视图,直观且高效。手写 render 函数不仅效率底下,而且失去了编译期的优化能力。
  3. 在 Vue 中编译器会先对 template 进行解析,这一步称为 parse,结束之后会得到一个 JS 对象,我们成为抽象语法树 AST,然后是对 AST 进行深加工的转换过程,这一步成为 transform,最后将前面得到的 AST 生成为 JS 代码,也就是 render 函数。
知其所以然

vue3 编译过程窥探:

github1s.com/vuejs/core/…

测试,test-v3.html

可能的追问
  1. Vue 中编译器何时执行?
  2. react 有没有编译器?

Vue 实例挂载的过程中发生了什么?

分析

挂载过程完成了最重要的两件事:

  1. 初始化
  2. 建立更新机制

把这两件事说清楚即可!

回答范例
  1. 挂载过程指的是 app.mount()过程,这个过程中整体上做了两件事:初始化建立更新机制

  2. 初始化会创建组件实例、初始化组件状态,创建各种响应式数据

  3. 建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行 patch 将前面获得 vnode 转换为 dom;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。

  4. 知其所以然

    测试代码,test-v3.html mount 函数定义

    github1s.com/vuejs/core/…

    首次 render 过程

    github1s.com/vuejs/core/…

可能的追问

  1. 响应式数据怎么创建
  2. 依赖关系如何建立

MVVM 模型?

MVVM,是Model-View-ViewModel的简写,其本质是MVC模型的升级版。其中 Model 代表数据模型,View 代表看到的页面,ViewModelViewModel之间的桥梁,数据会绑定到ViewModel层并自动将数据渲染到页面中,视图变化的时候会通知ViewModel层更新数据。以前是通过操作DOM来更新视图,现在是数据驱动视图

Vue 的生命周期

Vue 的生命周期可以分为 8 个阶段:创建前后、挂载前后、更新前后、销毁前后,以及一些特殊场景的生命周期。Vue 3 中还新增了是 3 个用于调试和服务端渲染的场景。

Vue 2 中的生命周期钩子Vue 3 选项式 API 的生命周期选项Vue 3 组合 API 中生命周期钩子描述
beforeCreatebeforeCreatesetup()创建前,此时datamethods的数据都还没有初始化
createdcreatedsetup()创建后,data中有值,尚未挂载,可以进行一些Ajax请求
beforeMountbeforeMountonBeforeMount挂载前,会找到虚拟DOM,编译成Render
mountedmountedonMounted挂载后,DOM已创建,可用于获取访问数据和DOM元素
beforeUpdatebeforeUpdateonBeforeUpdate更新前,可用于获取更新前各种状态
updatedupdatedonUpdated更新后,所有状态已是最新
beforeDestroybeforeUnmountonBeforeUnmount销毁前,可用于一些定时器或订阅的取消
destroyedunmountedonUnmounted销毁后,可用于一些定时器或订阅的取消
activatedactivatedonActivatedkeep-alive缓存的组件激活时
deactivateddeactivatedonDeactivatedkeep-alive缓存的组件停用时
errorCapturederrorCapturedonErrorCaptured捕获一个来自子孙组件的错误时调用
renderTrackedonRenderTracked调试钩子,响应式依赖被收集时调用
renderTriggeredonRenderTriggered调试钩子,响应式依赖被触发时调用
serverPrefetchonServerPrefetch组件实例在服务器上被渲染前调用

关于 Vue 3 中的生命周期建议阅读官方文档!!!!

组合式 API:生命周期钩子--官方文档 选项式 API:生命周期选项--官方文档

父子组件的生命周期:

  • 加载渲染阶段:父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
  • 更新阶段:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
  • 销毁阶段:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

Vue.$nextTick

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

nextTick 是 Vue 提供的一个全局 API,由于 Vue 的异步更新策略,导致我们对数据修改后不会直接体现在 DOM 上,此时如果想要立即获取更新后的 DOM 状态,就需要借助该方法。

Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue 将开启一个异步更新队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入队列一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的 DOM 操作完成后才调用。

使用场景:

  1. 如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()
  2. created生命周期中进行DOM操作

Vue 实例挂载过程中发生了什么?

挂载过程指的是 app.mount()过程,这是一个初始化过程,整体上做了两件事情:初始化建立更新机制

初始化会创建组件实例、初始化组件状态、创建各种响应式数据。

建立更新机制这一步会立即执行一次组件的更新函数,这会首次执行组件渲染函数并执行patchvnode 转换为 dom; 同时首次执行渲染函数会创建它内部响应式数据和组件更新函数之间的依赖关系,这使得以后数据发生变化时会执行对应的更新函数。

Vue 的模版编译原理

Vue 中有个独特的编译器模块,称为compiler,它的主要作用是将用户编写的template编译为 js 中可执行的render函数。 在 Vue 中,编译器会先对template进行解析,这一步称为parse,结束之后得到一个 JS 对象,称之为抽象语法树AST;然后是对AST进行深加工的转换过程,这一步称为transform,最后将前面得到的AST生成 JS 代码,也就是render函数。

Vue 的响应式原理

  1. Vue 2 中的数据响应式会根据数据类型做不同的处理。如果是对象,则通过

    Object.defineProperty(obj,key,descriptor)

    拦截对象属性访问,当数据被访问或改变时,感知并作出反应;如果是数组,则通过覆盖数组原型的方法,扩展它的 7 个变更方法(push、pop、shift、unshift、splice、sort、reverse),使这些方法可以额外的做更新通知,从而做出响应。

    缺点:

    • 初始化时的递归遍历会造成性能损失;
    • 通知更新过程需要维护大量 dep 实例和 watcher 实例,额外占用内存较多;
    • 新增或删除对象属性无法拦截,需要通过 Vue.setdelete 这样的 API 才能生效;
    • 对于ES6中新产生的MapSet这些数据结构不支持。
  2. Vue 3 中利用ES6Proxy机制代理需要响应化的数据。可以同时支持对象和数组,动态属性增、删都可以拦截,新增数据结构均支持,对象嵌套属性运行时递归,用到时才代理,也不需要维护特别多的依赖关系,性能取得很大进步。

虚拟 DOM

  1. 概念: 虚拟 DOM,顾名思义就是虚拟的 DOM 对象,它本身就是一个 JS 对象,只不过是通过不同的属性去描述一个视图结构。
  2. 虚拟 DOM 的好处: (1) 性能提升 直接操作 DOM 是有限制的,一个真实元素上有很多属性,如果直接对其进行操作,同时会对很多额外的属性内容进行了操作,这是没有必要的。如果将这些操作转移到 JS 对象上,就会简单很多。另外,操作 DOM 的代价是比较昂贵的,频繁的操作 DOM 容易引起页面的重绘和回流。如果通过抽象 VNode 进行中间处理,可以有效减少直接操作 DOM 次数,从而减少页面的重绘和回流。 (2) 方便跨平台实现 同一 VNode 节点可以渲染成不同平台上对应的内容,比如:渲染在浏览器是 DOM 元素节点,渲染在 Native(iOS、Android)变为对应的控件。Vue 3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染。
  3. 结构: 没有统一的标准,一般包括tagpropschildren三项。 tag:必选。就是标签,也可以是组件,或者函数。 props:非必选。就是这个标签上的属性和方法。 children:非必选。就是这个标签的内容或者子节点。如果是文本节点就是字符串;如果有子节点就是数组。换句话说,如果判断children是字符串的话,就表示一定是文本节点,这个节点肯定没有子元素。

diff 算法

  1. 概念: diff算法是一种对比算法,通过对比旧的虚拟 DOM 和新的虚拟 DOM,得出是哪个虚拟节点发生了改变,找出这个虚拟节点并只更新这个虚拟节点所对应的真实节点,而不用更新其他未发生改变的节点,实现精准地更新真实 DOM,进而提高效率。
  2. 对比方式: diff算法的整体策略是:深度优先,同层比较。比较只会在同层级进行, 不会跨层级比较;比较的过程中,循环从两边向中间收拢。
  • 首先判断两个节点的tag是否相同,不同则删除该节点重新创建节点进行替换。

  • tag

    相同时,先替换属性,然后对比子元素,分为以下几种情况:

    • 新旧节点都有子元素时,采用双指针方式进行对比。新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻找 key一致的VNode节点再分情况操作。
    • 新节点有子元素,旧节点没有子元素,则将子元素虚拟节点转化成真实节点插入即可。
    • 新节点没有子元素,旧节点有子元素,则清空子元素,并设置为新节点的文本内容。
    • 新旧节点都没有子元素时,即都为文本节点,则直接对比文本内容,不同则更新。

Vue 中 key 的作用?

key的作用主要是为了更加高效的更新虚拟 DOM

Vue 判断两个节点是否相同时,主要是判断两者的key元素类型tag。因此,如果不设置key ,它的值就是 undefined,则可能永远认为这是两个相同的节点,只能去做更新操作,将造成大量的 DOM 更新操作。

为什么组件中的 data 是一个函数?

在 new Vue() 中,可以是函数也可以是对象,因为根实例只有一个,不会产生数据污染。

在组件中,data 必须为函数,目的是为了防止多个组件实例对象之间共用一个 data,产生数据污染;而采用函数的形式,initData 时会将其作为工厂函数都会返回全新的 data 对象。

Vue 中组件间的通信方式?

  1. 父子组件通信:

    父向子传递数据是通过props,子向父是通过$emit触发事件;通过父链/子链也可以通信($parent/$children);ref也可以访问组件实例;provide/inject$attrs/$listeners

  2. 兄弟组件通信:

    全局事件总线EventBusVuex

  3. 跨层级组件通信:

    全局事件总线EventBusVuexprovide/inject

v-show 和 v-if 的区别?

  1. 控制手段不同。v-show是通过给元素添加 css 属性display: none,但元素仍然存在;而v-if控制元素显示或隐藏是将元素整个添加或删除。
  2. 编译过程不同。v-if切换有一个局部编译/卸载的过程,切换过程中合适的销毁和重建内部的事件监听和子组件;v-show只是简单的基于 css 切换。
  3. 编译条件不同。v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建,渲染条件为假时,并不做操作,直到为真才渲染。
  4. 触发生命周期不同。v-show由 false 变为 true 的时候不会触发组件的生命周期;v-if由 false 变为 true 的时候,触发组件的beforeCreatecreatedbeforeMountmounted钩子,由 true 变为 false 的时候触发组件的beforeDestorydestoryed钩子。
  5. 性能消耗不同。v-if有更高的切换消耗;v-show有更高的初始渲染消耗。

使用场景: 如果需要非常频繁地切换,则使用v-show较好,如:手风琴菜单,tab 页签等; 如果在运行时条件很少改变,则使用v-if较好,如:用户登录之后,根据权限不同来显示不同的内容。

computed 和 watch 的区别?

  • computed计算属性,依赖其它属性计算值,内部任一依赖项的变化都会重新执行该函数,计算属性有缓存,多次重复使用计算属性时会从缓存中获取返回值,计算属性必须要有return关键词。
  • watch侦听到某一数据的变化从而触发函数。当数据为对象类型时,对象中的属性值变化时需要使用深度侦听deep属性,也可在页面第一次加载时使用立即侦听immdiate属性。

运用场景: 计算属性一般用在模板渲染中,某个值是依赖其它响应对象甚至是计算属性而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

v-if 和 v-for 为什么不建议放在一起使用?

Vue 2 中,v-for的优先级比v-if高,这意味着v-if将分别重复运行于每一个v-for循环中。如果要遍历的数组很大,而真正要展示的数据很少时,将造成很大的性能浪费。

Vue 3 中,则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,会导致异常。

通常有两种情况导致要这样做:

  • 为了过滤列表中的项目,比如:v-for = "user in users" v-if = "user.active"。这种情况,可以定义一个计算属性,让其返回过滤后的列表即可。
  • 为了避免渲染本该被隐藏的列表,比如v-for = "user in users" v-if = "showUsersFlag"。这种情况,可以将v-if移至容器元素上或在外面包一层template即可。

Vue 2 中的 set 方法?

set是 Vue 2 中的一个全局 API。可手动添加响应式数据,解决数据变化视图未更新问题。当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,会发现页面并没有更新。这是因为Object.defineProperty()的限制,监听不到数据变化,可通过this.$set(数组或对象,数组下标或对象的属性名,更新后的值)解决。

keep-alive 是什么?

  • 作用:实现组件缓存,保持组件的状态,避免反复渲染导致的性能问题。
  • 工作原理:Vue.js 内部将 DOM 节点,抽象成了一个个的 VNode 节点,keep-alive组件的缓存也是基于 VNode 节点的。它将满足条件的组件在 cache 对象中缓存起来,重新渲染的时候再将 VNode 节点从 cache 对象中取出并渲染。
  • 可以设置以下属性: ① include:字符串或正则,只有名称匹配的组件会被缓存。 ② exclude:字符串或正则,任何名称匹配的组件都不会被缓存。 ③ max:数字,最多可以缓存多少组件实例。 匹配首先检查组件的name选项,如果name选项不可用,则匹配它的局部注册名称(父组件 components 选项的键值),匿名组件不能被匹配。

设置了keep-alive缓存的组件,会多出两个生命周期钩子:activateddeactivated。 首次进入组件时:beforeCreate --> created --> beforeMount --> mounted --> activated --> beforeUpdate --> updated --> deactivated 再次进入组件时:activated --> beforeUpdate --> updated --> deactivated

mixin

mixin(混入), 它提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。

使用场景: 不同组件中经常会用到一些相同或相似的代码,这些代码的功能相对独立。可以通过 mixin 将相同或相似的代码提出来。

缺点:

  1. 变量来源不明确
  2. 多 mixin 可能会造成命名冲突(解决方式:Vue 3 的组合 API)
  3. mixin 和组件出现多对多的关系,使项目复杂度变高。

插槽

slot插槽,一般在组件内部使用,封装组件时,在组件内部不确定该位置是以何种形式的元素展示时,可以通过slot占据这个位置,该位置的元素需要父组件以内容形式传递过来。slot分为:

  • 默认插槽:子组件用<slot>标签来确定渲染的位置,标签里面可以放DOM结构作为后备内容,当父组件在使用的时候,可以直接在子组件的标签内写入内容,该部分内容将插入子组件的<slot>标签位置。如果父组件使用的时候没有往插槽传入内容,后备内容就会显示在页面。

  • 具名插槽:子组件用name属性来表示插槽的名字,没有指定name的插槽,会有隐含的名称叫做 default。父组件中在使用时在默认插槽的基础上通过v-slot指令指定元素需要放在哪个插槽中,v-slot值为子组件插槽name属性值。使用v-slot指令指定元素放在哪个插槽中,必须配合<template>元素,且一个<template>元素只能对应一个预留的插槽,即不能多个<template> 元素都使用v-slot指令指定相同的插槽。v-slot的简写是#,例如v-slot:header可以简写为#header

  • 作用域插槽

    :子组件在

    <slot>

    标签上绑定

    props

    数据,以将子组件数据传给父组件使用。父组件获取插槽绑定 props 数据的方法:

    1. scope="接收的变量名":<template scope="接收的变量名">
    2. slot-scope="接收的变量名":<template slot-scope="接收的变量名">
    3. v-slot:插槽名="接收的变量名":<template v-slot:插槽名="接收的变量名">

Vue 中的修饰符有哪些?

在 Vue 中,修饰符处理了许多 DOM 事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理。Vue 中修饰符分为以下几种:

  1. 表单修饰符 lazy 填完信息,光标离开标签的时候,才会将值赋予给 value,也就是在change事件之后再进行信息同步。 number 自动将用户输入值转化为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值。 trim 自动过滤用户输入的首尾空格,而中间的空格不会被过滤。

  2. 事件修饰符 stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法。 prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法。 self 只当在 event.target 是当前元素自身时触发处理函数。 once 绑定了事件以后只能触发一次,第二次就不会触发。 capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理。 passive 告诉浏览器你不想阻止事件的默认行为。 native 让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件。

  3. 鼠标按键修饰符 left 左键点击。 right 右键点击。 middle 中键点击。

  4. 键值修饰符

    键盘修饰符是用来修饰键盘事件(

    onkeyup

    onkeydown

    )的,有如下:

    keyCode

    存在很多,但

    vue

    为我们提供了别名,分为以下两种:

    • 普通键(enter、tab、delete、space、esc、up...)
    • 系统修饰键(ctrl、alt、meta、shift...)

对 SPA 的理解?

  1. 概念: SPA(Single-page application),即单页面应用,它是一种网络应用程序或网站的模型,通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换时打断用户体验。在SPA中,所有必要的代码(HTML、JavaScript 和 CSS)都通过单个页面的加载而检索,或者根据需要(通常是响应用户操作)动态装载适当的资源并添加到页面。页面在任何时间点都不会重新加载,也不会将控制转移到其他页面。举个例子,就像一个杯子,上午装的是牛奶,中午装的是咖啡,下午装的是茶,变得始终是内容,杯子始终不变。

  2. SPAMPA的区别: MPA(Muti-page application),即多页面应用。在MPA中,每个页面都是一个主页面,都是独立的,每当访问一个页面时,都需要重新加载 Html、CSS、JS 文件,公共文件则根据需求按需加载。

    SPAMPA
    组成一个主页面和多个页面片段多个主页面
    url 模式hash 模式history 模式
    SEO 搜索引擎优化难实现,可使用 SSR 方式改善容易实现
    数据传递容易通过 url、cookie、localStorage 等传递
    页面切换速度快,用户体验良好切换加载资源,速度慢,用户体验差
    维护成本相对容易相对复杂
  3. SPA的优缺点: 优点:

    • 具有桌面应用的即时性、网站的可移植性和可访问性
    • 用户体验好、快,内容的改变不需要重新加载整个页面
    • 良好的前后端分离,分工更明确

    缺点:

    • 不利于搜索引擎的抓取
    • 首次渲染速度相对较慢

双向绑定?

  1. 概念: Vue 中双向绑定是一个指令v-model,可以绑定一个响应式数据到视图,同时视图的变化能改变该值。v-model是语法糖,默认情况下相当于:value@input,使用v-model可以减少大量繁琐的事件处理代码,提高开发效率。
  2. 使用: 通常在表单项上使用v-model,还可以在自定义组件上使用,表示某个值的输入和输出控制。
  3. 原理: v-model是一个指令,双向绑定实际上是 Vue 的编译器完成的,通过输出包含v-model模版的组件渲染函数,实际上还是value属性的绑定及input事件监听,事件回调函数中会做相应变量的更新操作。

子组件是否可以直接改变父组件的数据?

  1. 所有的prop都遵循着单项绑定原则,props因父组件的更新而变化,自然地将新状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。 另外,每次父组件更新后,所有的子组件中的props都会被更新为最新值,这就意味着不应该子组件中去修改一个prop,若这么做了,Vue 会在控制台上抛出警告。

  2. 实际开发过程中通常有两个场景导致要修改

    prop

    • prop被用于传入初始值,而子组件想在之后将其作为一个局部数据属性。这种情况下,最好是新定义一个局部数据属性,从props获取初始值即可。
    • 需要对传入的prop值做进一步转换。最好是基于该prop值定义一个计算属性。
  3. 实践中,如果确实要更改父组件属性,应emit一个事件让父组件变更。当对象或数组作为props被传入时,虽然子组件无法更改props绑定,但仍然可以更改对象或数组内部的值。这是因为 JS 的对象和数组是按引用传递,而对于 Vue 来说,禁止这样的改动虽然可能,但是有很大的性能损耗,比较得不偿失。

Vue Router 中的常用路由模式和原理?

  1. hash 模式:
  • location.hash的值就是 url 中 # 后面的东西。它的特点在于:hash 虽然出现 url 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
  • 可以为 hash 的改变添加监听事件window.addEventListener("hashchange", funcRef, false),每一次改变hash (window.location.hash),都会在浏览器的访问历史中增加一个记录,利用 hash 的以上特点,就可以实现前端路由更新视图但不重新请求页面的功能了。 特点:兼容性好但是不美观
  1. history 模式:

利用 HTML5 History Interface 中新增的pushState()replaceState()方法。 这两个方法应用于浏览器的历史记录栈,在当前已有的backforwardgo 的基础上,他们提供了对历史记录进行修改的功能。 这两个方法有个共同点:当调用他们修改浏览器历史记录栈后,虽然当前 url 改变了,但浏览器不会刷新页面,这就为单页面应用前端路由“更新视图但不重新请求页面”提供了基础 特点:虽然美观,但是刷新会出现 404 需要后端进行配置。

动态路由?

很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用动态路径参数(dynamic segment)来达到这个效果:{path: '/user/:id', compenent: User},其中:id就是动态路径参数。

对 Vuex 的理解?

  1. 概念: Vuex 是 Vue 专用的状态管理库,它以全局方式集中管理应用的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  2. 解决的问题: Vuex 主要解决的问题是多组件之间状态共享。利用各种通信方式,虽然也能够实现状态共享,但是往往需要在多个组件之间保持状态的一致性,这种模式很容易出问题,也会使程序逻辑变得复杂。Vuex 通过把组件的共享状态抽取出来,以全局单例模式管理,这样任何组件都能用一致的方式获取和修改状态,响应式的数据也能够保证简洁的单向流动,使代码变得更具结构化且易于维护。
  3. 什么时候用: Vuex 并非是必须的,它能够管理状态,但同时也带来更多的概念和框架。如果我们不打算开发大型单页应用或应用里没有大量全局的状态需要维护,完全没有使用 Vuex 的必要,一个简单的 store 模式就够了。反之,Vuex 将是自然而然的选择。
  4. 用法: Vuex 将全局状态放入state对象中,它本身是一颗状态树,组件中使用store实例的state访问这些状态;然后用配套的mutation方法修改这些状态,并且只能用mutation修改状态,在组件中调用commit方法提交mutation;如果应用中有异步操作或复杂逻辑组合,需要编写action,执行结束如果有状态修改仍需提交mutation,组件中通过dispatch派发action。最后是模块化,通过modules选项组织拆分出去的各个子模块,在访问状态(state)时需注意添加子模块的名称,如果子模块有设置namespace,那么提交mutation和派发action时还需要额外的命名空间前缀。

页面刷新后 Vuex 状态丢失怎么解决?

Vuex 只是在内存中保存状态,刷新后就会丢失,如果要持久化就需要保存起来。

localStorage就很合适,提交mutation的时候同时存入localStorage,在store中把值取出来作为state的初始值即可。

也可以使用第三方插件,推荐使用vuex-persist插件,它是为 Vuex 持久化储存而生的一个插件,不需要你手动存取storage,而是直接将状态保存至 cookie 或者 localStorage中。

关于 Vue SSR 的理解?

SSR服务端渲染(Server Side Render),就是将 Vue 在客户端把标签渲染成 html 的工作放在服务端完成,然后再把 html 直接返回给客户端。

  • 优点: 有着更好的 SEO,并且首屏加载速度更快。
  • 缺点: 开发条件会受限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。服务器会有更大的负载需求。

了解哪些 Vue 的性能优化方法?

  • 路由懒加载。有效拆分应用大小,访问时才异步加载。
  • keep-alive缓存页面。避免重复创建组件实例,且能保留缓存组件状态。
  • v-for遍历避免同时使用v-if。实际上在 Vue 3 中已经是一个错误用法了。
  • 长列表性能优化,可采用虚拟列表。
  • v-once。不再变化的数据使用v-once
  • 事件销毁。组件销毁后把全局变量和定时器销毁。
  • 图片懒加载。
  • 第三方插件按需引入。
  • 子组件分割。较重的状态组件适合拆分。
  • 服务端渲染。

Vue 和 React 的区别

从原理上说

Vue 的数据绑定依赖数据劫持 Object.defineProperty() 中的 gettersetter,更新视图使用的是 发布订阅模式(eventEmitter) 来监听值的变化,从而让 virtual DOM 驱动 Model 和 View 的更新,利用 v-model 这一语法糖能够轻易实现双向的数据绑定,这种模式被称为 MVVM: M <=> VM <=> V,但本质上还是 State -> View -> Actions 的单向数据流,只是使用了 v-model 不需要显式地编写 ViewModel 的更新。

React 则需要依赖 onChange/setState 模式来实现数据的双向绑定,因为它在诞生之初就是设计成单向数据流的。

组件通信的区别

父子之间都可以通过 props 绑定 datastate 进行传值,又或者通过绑定回调函数来传值。

兄弟之间都可以通过 发布订阅模式 来写一个 EventBus 来监听值的变化。

跨层级:React 可以通过 React.context 来进行跨层级通信;Vue 则可以使用 provide/inject 来实现跨层级注入数据。

模版渲染方式的区别

React 在 JSX 中使用原生的 JS 语法来实现插值,条件渲染,循环等。

Vue 则需要依赖指令进行,更容易上手,但封装程度更高,调试成本更大,难以定位 Bug。

性能差异

在 React 中组件的更新渲染是从数据发生变化的根组件开始往子组件逐层渲染,而组件的生命周期中有 shouldComponentUpdate 这一钩子函数可以给开发者优化组件在不需要更新的时候不要更新。

Vue 通过 watcher 监听到数据的变化之后,通过自己的 diff 算法,在 virtualDOM 中直接以最低成本更新视图。

什么是递归组件?举个例子说明下?

分析

递归组件我们用的比较少,但是在TreeMenu这类组件中会被用到。

体验

组件通过组件名称引用它自己,这种情况就是递归组件

<template>
<li>
<div>{{ model.name }}</div>
<ul v-show="isOpen" v-if="isFolder">
<!-- 注意这里:组件递归渲染了它自己 -->
<TreeItem class="item" v-for="model in model.children" :model="model">
</TreeItem>
</ul>
</li>
<script>
export default {
name: "TreeItem",
// ...
};
</script>
</template>

回答范例

  1. 如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
  2. 实际开发中类似TreeMenu这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
  3. 使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件name属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。
  4. 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给resolveComponent,这样实际获取的组件就是当前组件本身

原理

递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)

const _component_Comp = _resolveComponent("Comp", true);

就是在传递maybeSelfReference

export function resolveComponent(
name: string,
maybeSelfReference?: boolean
): ConcreteComponent | string {
return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name;
}

resolveAsset中最终返回的是组件自身:

if (!res && maybeSelfReference) {
// fallback to implicit self-reference
return Component;
}

说说你对 slot 的理解?slot 使用场景有哪些

一、slot 是什么

在 HTML 中 slot 元素 ,作为 Web Components 技术套件的一部分,是 Web 组件内的一个占位符

该占位符可以在后期使用自己的标记语言填充

举个栗子

<template id="element-details-template">
<slot name="element-name">Slot template</slot>
</template>
<element-details>
<span slot="element-name">1</span>
</element-details>
<element-details>
<span slot="element-name">2</span>
</element-details>

template不会展示到页面中,需要用先获取它的引用,然后添加到DOM中,

customElements.define(
"element-details",
class extends HTMLElement {
constructor() {
super();
const template = document.getElementById(
"element-details-template"
).content;
const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(
template.cloneNode(true)
);
}
}
);

Vue中的概念也是如此

Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口

二、使用场景

通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理

如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情

通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用

比如布局组件、表格列、下拉选、弹框显示内容等

ref 和 reactive 异同

这是Vue3数据响应式中非常重要的两个概念,跟我们写代码关系也很大

const count = ref(0);
console.log(count.value); // 0

count.value++;
console.log(count.value); // 1

const obj = reactive({ count: 0 });
obj.count++;
  • ref接收内部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象
  • 从定义上看ref通常用于处理单值的响应式,reactive用于处理对象类型的数据响应式
  • 两者均是用于构造响应式数据,但是ref主要解决原始值的响应式问题
  • ref返回的响应式数据在 JS 中使用需要加上.value才能访问其值,在视图中使用会自动脱ref,不需要.valueref可以接收对象或数组等非原始值,但内部依然是reactive实现响应式;reactive内部如果接收Ref 对象会自动脱ref;使用展开运算符(...)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。
  • reactive内部使用Proxy代理传入对象并拦截该对象各种操作,从而实现响应式。ref内部封装一个RefImpl类,并设置get value/set value,拦截用户对值的访问,从而实现响应式

为什么要使用异步组件

  1. 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。
  2. 核心就是包组件定义变成一个函数,依赖import() 语法,可以实现文件的分割加载。
components: {
AddCustomerSchedule: (resolve) => import("../components/AddCustomer"); // require([])
}

原理

export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend
// 第二次渲染时Ctor不为undefined
if (Ctor === undefined) {
return createAsyncPlaceholder( // 渲染占位符 空虚拟节点
asyncFactory,
data,
context,
children,
tag
)
}
}
}
function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void {
if (isDef(factory.resolved)) {
// 3.在次渲染时可以拿到获取的最新组件
return factory.resolved
}
const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true) //2. 强制更新视图重新渲染
} else {
owners.length = 0
}
})
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true forceRender(true)
}
})
const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后
sync = false
return factory.resolved
}

Watch 中的 deep:true 是如何实现的

当用户指定了 watch 中的 deep 属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新

源码相关

get () {
pushTarget(this) // 先将当前依赖放到 Dep.target上
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) { // 如果需要深度监控
traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法
}popTarget()
}

声明式导航

<router-link to="/about">Go to About</router-link>

编程式导航

// literal string path
router.push("/users/1");

// object with path
router.push({ path: "/users/1" });

// named route with params to let the router build the url
router.push({ name: "user", params: { username: "test" } });

回答范例

  • vue-router导航有两种方式:声明式导航和编程方式导航
  • 声明式导航方式使用router-link组件,添加to属性导航;编程方式导航更加灵活,可传递调用router.push(),并传递path字符串或者RouteLocationRaw对象,指定pathnameparams等信息
  • 如果页面中简单表示跳转链接,使用router-link最快捷,会渲染一个 a 标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航
  • 实际上内部两者调用的导航函数是一样的

参考 前端进阶面试题详细解答

怎么实现路由懒加载呢

这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效

// 将
// import UserDetails from './views/UserDetails'
// 替换为
const UserDetails = () => import("./views/UserDetails");

const router = createRouter({
// ...
routes: [{ path: "/users/:id", component: UserDetails }],
});

回答范例

  1. 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段
  2. 一般来说,对所有的路由都使用动态导入是个好主意
  3. component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:{ path: '/users/:id', component: () => import('./views/UserDetails') }
  4. 结合注释 () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue') 可以做webpack代码分块

组件中写 name 属性的好处

可以标识组件的具体名称方便调试和查找对应属性

// 源码位置 src/core/global-api/extend.js

// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub; // 记录自己 在组件中递归自己 -> jsx
}

Vue 组件为什么只能有一个根元素

vue3中没有问题

Vue.createApp({
components: {
comp: {
template: `
<div>root1</div>
<div>root2</div>
`,
},
},
}).mount("#app");
  1. vue2中组件确实只能有一个根,但vue3中组件已经可以多根节点了。
  2. 之所以需要这样是因为vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
  3. vue3中之所以可以写多个根节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来patch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新

Vue 修饰符有哪些

vue 中修饰符分为以下五种

  • 表单修饰符
  • 事件修饰符
  • 鼠标按键修饰符
  • 键值修饰符
  • v-bind修饰符

1. 表单修饰符

在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model

关于表单的修饰符有如下:

  • lazy

在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步

<input type="text" v-model.lazy="value" />
<p>{{value}}</p>
  • trim

自动过滤用户输入的首空格字符,而中间的空格不会过滤

<input type="text" v-model.trim="value" />
  • number

自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值

<input v-model.number="age" type="number" />

2. 事件修饰符

事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符

  • .stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法
<div @click="shout(2)">
<button @click.stop="shout(1)">ok</button>
</div>
//只输出1
  • .prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法
<form v-on:submit.prevent="onSubmit"></form>
  • .capture 使用事件捕获模式,使事件触发从包含这个元素的顶层开始往下触发
<div @click.capture="shout(1)">
obj1
<div @click.capture="shout(2)">
obj2
<div @click="shout(3)">
obj3
<div @click="shout(4)">obj4</div>
</div>
</div>
</div>
// 输出结构: 1 2 4 3
  • .self 只当在 event.target 是当前元素自身时触发处理函数
<div v-on:click.self="doThat">...</div>

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击

  • .once 绑定了事件以后只能触发一次,第二次就不会触发
<button @click.once="shout(1)">ok</button>
  • .passive 告诉浏览器你不想阻止事件的默认行为

在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
  • 不要把 .passive.prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。
  • passive 会告诉浏览器你不想阻止事件的默认行为
  • native 让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件
<my-component v-on:click.native="doSomething"></my-component>

<!-- 使用.native修饰符来操作普通HTML标签是会令事件失效的 -->

3. 鼠标按钮修饰符

鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:

  • .left 左键点击
  • .right 右键点击
  • .middle 中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>

4. 键盘事件的修饰符

键盘修饰符是用来修饰键盘事件(onkeyuponkeydown)的,有如下:

keyCode存在很多,但 vue 为我们提供了别名,分为以下两种:

  • 普通键entertabdeletespaceescupdownleftright...)
  • 系统修饰键ctrlaltmetashift...)
<!-- 只有按键为keyCode的时候才触发 -->
<input type="text" @keyup.keyCode="shout()" />

还可以通过以下方式自定义一些全局的键盘码别名

Vue.config.keyCodes.f2 = 113;

5. v-bind 修饰符

v-bind修饰符主要是为属性进行操作,用来分别有如下:

  • async 能对props进行一个双向绑定
//父组件
<comp :myMessage.sync="bar"></comp>
//子组件
this.$emit('update:myMessage',params);

以上这种方法相当于以下的简写

//父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){
this.bar = e;
}

//子组件js
func2(){
this.$emit('update:myMessage',params);
}

使用async需要注意以下两点:

  • 使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致
  • 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用
  • prop 设置自定义标签属性,避免暴露数据,防止污染 HTML 结构
<input id="uid" title="title1" value="1" :index.prop="index" />
  • camel 将命名变为驼峰命名法,如将view-Box属性名转换为 viewBox
<svg :viewBox="viewBox"></svg>

应用场景

根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景:

  • .stop:阻止事件冒泡
  • .native:绑定原生事件
  • .once:事件只执行一次
  • .self :将事件绑定在自身身上,相当于阻止事件冒泡
  • .prevent:阻止默认事件
  • .caption:用于事件捕获
  • .once:只触发一次
  • .keyCode:监听特定键盘按下
  • .right:右键

Vue 组件之间通信方式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。 Vue 组件间通信只要指以下 3 类通信父子组件通信隔代组件通信兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信

组件传参的各种方式

img

组件通信常用方式有以下几种

  • props / $emit

    适用 父子组件通信

    • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
  • ref

    $parent / $children(vue3废弃)

    适用 父子组件通信

    • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
    • $parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法
  • EventBus ($emit / $on)

    适用于 父子、隔代、兄弟组件通信

    • 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件
  • $attrs / $listeners(vue3废弃)

    适用于 隔代组件通信

    • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( classstyle 除外 )。当一个组件没有声明任何 prop时,这里会包含所有父作用域的绑定 ( classstyle 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用
    • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
  • provide / inject

    适用于 隔代组件通信

    • 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题, 不过它的使用场景,主要是子组件获取上级组件的状态 ,跨级组件间建立了一种主动提供与依赖注入的关系
  • $root 适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用

  • Vuex

    适用于 父子、隔代、兄弟组件通信

    • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )
    • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
    • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

根据组件之间关系讨论组件通信最为清晰有效

  • 父子组件:props/$emit/$parent/ref
  • 兄弟组件:$parent/eventbus/vuex
  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root

下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯

1. 父子组件通信

使用props,父组件可以使用props向子组件传递数据。

父组件vue模板father.vue:

<template>
<child :msg="message"></child>
</template>

<script>
import child from './child.vue';
export default {
components: {
child
},
data () {
return {
message: 'father message';
}
}
}
</script>

子组件vue模板child.vue:

<template>
<div>{{msg}}</div>
</template>

<script>
export default {
props: {
msg: {
type: String,
required: true,
},
},
};
</script>

回调函数(callBack)

父传子:将父组件里定义的method作为props传入子组件

// 父组件Parent.vue:
<Child :changeMsgFn="changeMessage">
methods: {
changeMessage(){
this.message = 'test'
}
}

// 子组件Child.vue:
<button @click="changeMsgFn">
props:['changeMsgFn']

子组件向父组件通信

父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件

父组件vue模板father.vue:

<template>
<child @msgFunc="func"></child>
</template>

<script>
import child from "./child.vue";
export default {
components: {
child,
},
methods: {
func(msg) {
console.log(msg);
},
},
};
</script>

子组件vue模板child.vue:

<template>
<button @click="handleClick">点我</button>
</template>

<script>
export default {
props: {
msg: {
type: String,
required: true
}
},
methods () {
handleClick () {
//........
this.$emit('msgFunc');
}
}
}
</script>

2. provide / inject 跨级访问祖先组件的数据

父组件通过使用provide(){return{}}提供需要传递的数据

export default {
data() {
return {
title: "我是父组件",
name: "poetry",
};
},
methods: {
say() {
alert(1);
},
},
// provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法
provide() {
return {
message: "我是祖先组件提供的数据",
name: this.name, // 传递属性
say: this.say,
};
},
};

子组件通过使用inject:[“参数1”,”参数2”,…]接收父组件传递的参数

<template>
<p>曾孙组件</p>
<p>{{message}}</p>
</template>
<script>
export default {
// inject 注入/接收祖先组件传递的所需要的数据即可
//接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}}
inject: ["message", "say"],
mounted() {
this.say();
},
};
</script>

3. parent+parent + parent+children 获取父组件实例和子组件实例的集合

  • this.$parent 可以直接访问该组件的父实例或组件
  • 父组件也可以通过 this.$children 访问它所有的子组件;需要注意 $children 并不保证顺序,也不是响应式的
<!-- parent.vue -->
<template>
<div>
<child1></child1>
<child2></child2>
<button @click="clickChild">$children方式获取子组件值</button>
</div>
</template>
<script>
import child1 from "./child1";
import child2 from "./child2";
export default {
data() {
return {
total: 108,
};
},
components: {
child1,
child2,
},
methods: {
funa(e) {
console.log("index", e);
},
clickChild() {
console.log(this.$children[0].msg);
console.log(this.$children[1].msg);
},
},
};
</script>

<!-- child1.vue -->
<template>
<div>
<button @click="parentClick">点击访问父组件</button>
</div>
</template>
<script>
export default {
data() {
return {
msg: "child1",
};
},
methods: {
// 访问父组件数据
parentClick() {
this.$parent.funa("xx");
console.log(this.$parent.total);
},
},
};
</script>

<!-- child2.vue -->
<template>
<div>child2</div>
</template>
<script>
export default {
data() {
return {
msg: "child2",
};
},
};
</script>

4. attrs+attrs + attrs+listeners 多级组件通信

$attrs 包含了从父组件传过来的所有props属性

// 父组件Parent.vue:
<Child :name="name" :age="age"/>

// 子组件Child.vue:
<GrandChild v-bind="$attrs" />

// 孙子组件GrandChild
<p>姓名:{{$attrs.name}}</p>
<p>年龄:{{$attrs.age}}</p>

$listeners包含了父组件监听的所有事件

// 父组件Parent.vue:
<Child :name="name" :age="age" @changeNameFn="changeName"/>

// 子组件Child.vue:
<button @click="$listeners.changeNameFn"></button>

5. ref 父子组件通信

// 父组件Parent.vue:
<Child ref="childComp"/>
<button @click="changeName"></button>
changeName(){
console.log(this.$refs.childComp.age);
this.$refs.childComp.changeAge()
}

// 子组件Child.vue:
data(){
return{
age:20
}
},
methods(){
changeAge(){
this.age=15
}
}

6. 非父子, 兄弟组件之间通信

vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用 Bus 事件触发和监听来实现通信和参数传递。Bus.js可以是这样:

// Bus.js

// 创建一个中央时间总线类
class Bus {
constructor() {
this.callbacks = {}; // 存放事件的名字
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || [];
this.callbacks[name].push(fn);
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach((cb) => cb(args));
}
}
}

// main.js
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上
// 另一种方式
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能

<template>
<button @click="toBus">子组件传给兄弟组件</button>
</template>

<script>
export default{
methods: {
toBus () {
this.$bus.$emit('foo', '来自兄弟组件')
}
}
}
</script>

另一个组件也在钩子函数中监听on事件

export default {
data() {
return {
message: "",
};
},
mounted() {
this.$bus.$on("foo", (msg) => {
this.message = msg;
});
},
};

7. $root 访问根组件中的属性或方法

  • 作用:访问根组件中的属性或方法
  • 注意:是根组件,不是父组件。$root只对根组件有用
var vm = new Vue({
el: "#app",
data() {
return {
rootInfo: "我是根元素的属性",
};
},
methods: {
alerts() {
alert(111);
},
},
components: {
com1: {
data() {
return {
info: "组件1",
};
},
template: "<p>{{ info }} <com2></com2></p>",
components: {
com2: {
template: "<p>我是组件1的子组件</p>",
created() {
this.$root.alerts(); // 根组件方法
console.log(this.$root.rootInfo); // 我是根元素的属性
},
},
},
},
},
});

8. vuex

  • 适用场景: 复杂关系的组件数据传递
  • Vuex 作用相当于一个用来存储共享变量的容器

img

  • state用来存放共享变量的地方
  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值
  • mutations用来存放修改state的方法。
  • actions也是用来存放修改 state 的方法,不过action是在mutations的基础上进行。常用来做一些异步操作

小结

  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref
  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递
  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject
  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

双向绑定的原理是什么

我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类 UI 组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理

理解 ViewModel

它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更新数据

当然,它还有两个主要部分组成

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

写过自定义指令吗?原理是什么

回答范例

  1. Vue有一组默认指令,比如v-modelv-for,同时Vue也允许用户注册自定义指令来扩展 Vue 能力
  2. 自定义指令主要完成一些可复用低层级DOM操作
  3. 使用自定义指令分为定义、注册和使用三步:
  • 定义自定义指令有两种方式:对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在mounted 和updated时执行
  • 注册自定义指令类似组件,可以使用app.directive()全局注册,使用{directives:{xxx}}局部注册
  • 使用时在注册名称前加上v-即可,比如v-focus
  1. 我在项目中常用到一些自定义指令,例如:
  • 复制粘贴 v-copy
  • 长按 v-longpress
  • 防抖 v-debounce
  • 图片懒加载 v-lazy
  • 按钮权限 v-premission
  • 页面水印 v-waterMarker
  • 拖拽指令 v-draggable
  1. vue3中指令定义发生了比较大的变化,主要是钩子的名称保持和组件一致,这样开发人员容易记忆,不易犯错。另外在v3.2之后,可以在setup中以一个小写v开头方便的定义自定义指令,更简单了

基本使用

当 Vue 中的核心内置指令不能够满足我们的需求时,我们可以定制自定义的指令用来满足开发的需求

我们看到的v-开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能,对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。除了核心功能默认内置的指令 (v-modelv-show),Vue 也允许注册自定义指令

// 指令使用的几种方式:
//会实例化一个指令,但这个指令没有参数
`v-xxx` // -- 将值传到指令中
`v-xxx="value"` // -- 将字符串传入到指令中,如`v-html="'<p>内容</p>'"`
`v-xxx="'string'"` // -- 传参数(`arg`),如`v-bind:class="className"`
`v-xxx:arg="value"` // -- 使用修饰符(`modifier`)
`v-xxx:arg.modifier="value"`;

注册一个自定义指令有全局注册与局部注册

// 全局注册注册主要是用过Vue.directive方法进行注册
// Vue.directive第一个参数是指令的名字(不需要写上v-前缀),第二个参数可以是对象数据,也可以是一个指令函数
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能
}
})

// 局部注册通过在组件options选项中设置directive属性
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能
}
}
}

// 然后你可以在模板中任何元素上使用新的 v-focus property,如下:

<input v-focus />

钩子函数

  1. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
  4. componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
  5. unbind:只调用一次,指令与元素解绑时调用。

所有的钩子函数的参数都有以下:

  • el:指令所绑定的元素,可以用来直接操作 DOM

  • binding

    :一个对象,包含以下

    property

    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
    • vnodeVue 编译生成的虚拟节点
    • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
<script>
Vue.directive("demo", function (el, binding) {
console.log(binding.value.color); // "white"
console.log(binding.value.text); // "hello!"
});
</script>

应用场景

使用自定义组件组件可以满足我们日常一些场景,这里给出几个自定义组件的案例:

  1. 防抖
// 1.设置v-throttle自定义指令
Vue.directive('throttle', {
bind: (el, binding) => {
let throttleTime = binding.value; // 防抖时间
if (!throttleTime) { // 用户若不设置防抖时间,则默认2s
throttleTime = 2000;
}
let cbFun;
el.addEventListener('click', event => {
if (!cbFun) { // 第一次执行
cbFun = setTimeout(() => {
cbFun = null;
}, throttleTime);
} else {
event && event.stopImmediatePropagation();
}
}, true);
},
});
// 2.为button标签设置v-throttle自定义指令
<button @click="sayHello" v-throttle>提交</button>

  1. 图片懒加载

设置一个v-lazy自定义组件完成图片懒加载

const LazyLoad = {
// install方法
install(Vue, options) {
// 代替图片的loading图
let defaultSrc = options.default;
Vue.directive("lazy", {
bind(el, binding) {
LazyLoad.init(el, binding.value, defaultSrc);
},
inserted(el) {
// 兼容处理
if ("InterpObserver" in window) {
LazyLoad.observe(el);
} else {
LazyLoad.listenerScroll(el);
}
},
});
},
// 初始化
init(el, val, def) {
// src 储存真实src
el.setAttribute("src", val);
// 设置src为loading图
el.setAttribute("src", def);
},
// 利用InterpObserver监听el
observe(el) {
let io = new InterpObserver((entries) => {
let realSrc = el.dataset.src;
if (entries[0].isIntersecting) {
if (realSrc) {
el.src = realSrc;
el.removeAttribute("src");
}
}
});
io.observe(el);
},
// 监听scroll事件
listenerScroll(el) {
let handler = LazyLoad.throttle(LazyLoad.load, 300);
LazyLoad.load(el);
window.addEventListener("scroll", () => {
handler(el);
});
},
// 加载真实图片
load(el) {
let windowHeight = document.documentElement.clientHeight;
let elTop = el.getBoundingClientRect().top;
let elBtm = el.getBoundingClientRect().bottom;
let realSrc = el.dataset.src;
if (elTop - windowHeight < 0 && elBtm > 0) {
if (realSrc) {
el.src = realSrc;
el.removeAttribute("src");
}
}
},
// 节流
throttle(fn, delay) {
let timer;
let prevTime;
return function (...args) {
let currTime = Date.now();
let context = this;
if (!prevTime) prevTime = currTime;
clearTimeout(timer);

if (currTime - prevTime > delay) {
prevTime = currTime;
fn.apply(context, args);
clearTimeout(timer);
return;
}

timer = setTimeout(function () {
prevTime = Date.now();
timer = null;
fn.apply(context, args);
}, delay);
};
},
};
export default LazyLoad;
  1. 一键 Copy 的功能
import { Message } from "ant-design-vue";

const vCopy = {
//
/*
bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置
el: 作用的 dom 对象
value: 传给指令的值,也就是我们要 copy 的值
*/
bind(el, { value }) {
el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到
el.handler = () => {
if (!el.$value) {
// 值为空的时候,给出提示,我这里的提示是用的 ant-design-vue 的提示,你们随意
Message.warning("无复制内容");
return;
}
// 动态创建 textarea 标签
const textarea = document.createElement("textarea");
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = "readonly";
textarea.style.position = "absolute";
textarea.style.left = "-9999px";
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = el.$value;
// 将 textarea 插入到 body 中
document.body.appendChild(textarea);
// 选中值并复制
textarea.select();
// textarea.setSelectionRange(0, textarea.value.length);
const result = document.execCommand("Copy");
if (result) {
Message.success("复制成功");
}
document.body.removeChild(textarea);
};
// 绑定点击事件,就是所谓的一键 copy 啦
el.addEventListener("click", el.handler);
},
// 当传进来的值更新的时候触发
componentUpdated(el, { value }) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unbind(el) {
el.removeEventListener("click", el.handler);
},
};

export default vCopy;
  1. 拖拽
<div ref="a" id="bg" v-drag></div>

directives: {
drag: {
bind() {},
inserted(el) {
el.onmousedown = (e) => {
let x = e.clientX - el.offsetLeft;
let y = e.clientY - el.offsetTop;
document.onmousemove = (e) => {
let xx = e.clientX - x + "px";
let yy = e.clientY - y + "px";
el.style.left = xx;
el.style.top = yy;
};
el.onmouseup = (e) => {
document.onmousemove = null;
};
};
},
},
}

原理

  • 指令本质上是装饰器,是 vueHTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。
  • 自定义指令有五个生命周期(也叫钩子函数),分别是 bindinsertedupdatecomponentUpdatedunbind

原理

  1. 在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性
  2. 通过 genDirectives 生成指令代码
  3. patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子
  4. 当执行指令对应钩子函数时,调用对应指令定义的方法

为什么 Vue 采用异步渲染

Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。核心思想 nextTick

img

源码相关

dep.notify() 通知 watcher进行更新, subs[i].update 依次调用 watcherupdatequeueWatcherwatcher 去重放入队列, nextTickflushSchedulerQueue )在下一tick中刷新watcher队列(异步)

update () { /* istanbul ignore else */
if (this.lazy) {
this.dirty = true
}
else if (this.sync) {
this.run()
}
else {
queueWatcher(this); // 当数据发生变化时会将watcher放到一个队列中批量更新
}
}

export function queueWatcher (watcher: Watcher) {
const id = watcher.id // 会对相同的watcher进行过滤
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue) // 调用nextTick方法 批量的进行更新
}
}
}

vue-router 路由钩子函数是什么 执行顺序是什么

路由钩子的执行流程, 钩子函数种类有:全局守卫路由守卫组件守卫

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入

子组件可以直接改变父组件的数据吗?

子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。

Vue 提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。

只能通过 **$emit** 派发一个自定义事件,父组件接收到后,由父组件修改。

Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

  • extend是构造一个组件的语法器。然后这个组件你可以作用到Vue.component这个全局注册方法里还可以在任意vue模板里使用组件。 也可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件。
  • Vue.component你可以创建 ,也可以取组件。

相关代码如下

export default function initExtend(Vue) {
let cid = 0; //组件的唯一标识
// 创建子类继承Vue父类 便于属性扩展
Vue.extend = function (extendOptions) {
// 创建子类的构造函数 并且调用初始化方法
const Sub = function VueComponent(options) {
this._init(options); //调用Vue初始化方法
};
Sub.cid = cid++;
Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
Sub.prototype.constructor = Sub; //constructor指向自己
Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
return Sub;
};
}

谈一谈对 Vue 组件化的理解

  • 组件化开发能大幅提高开发效率、测试性、复用性等
  • 常用的组件化技术:属性、自定义事件、插槽
  • 降低更新频率,只重新渲染变化的组件
  • 组件的特点:高内聚、低耦合、单向数据流

Composition API 与 Options API 有什么不同

分析

Vue3最重要更新之一就是Composition API,它具有一些列优点,其中不少是针对Options API暴露的一些问题量身打造。是Vue3推荐的写法,因此掌握好Composition API应用对掌握好Vue3至关重要

img

What is Composition API?(opens new window)

  • Composition API出现就是为了解决 Options API 导致相同功能代码分散的现象

img

img

体验

Composition API能更好的组织代码,下面用composition api可以提取为useCount(),用于组合、复用

img

compositon api 提供了以下几个函数:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命周期的hooks

回答范例

  1. Composition API是一组API,包括:Reactivity API生命周期钩子依赖注入,使用户可以通过导入函数方式编写vue组件。而Options API则通过声明组件选项的对象形式编写组件
  2. Composition API最主要作用是能够简洁、高效复用逻辑。解决了过去Options APImixins的各种缺点;另外Composition API具有更加敏捷的代码组织能力,很多用户喜欢Options API,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API则可以将它们有效组织在一起。最后Composition API拥有更好的类型推断,对 ts 支持更友好,Options API在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API时获得类型推断,然而还是没办法用在mixinsprovide/inject
  3. Vue3首推Composition API,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API会获得更大收益

可能的追问

  1. Composition API能否和Options API一起使用?

可以在同一个组件中使用两个script标签,一个使用 vue3,一个使用 vue2 写法,一起使用没有问题

<!-- vue3 -->
<script setup>
// vue3写法
</script>

<!-- 降级vue2 -->
<script>
export default {
data() {},
methods: {},
};
</script>

vue-router 中如何保护路由

分析

路由保护在应用开发过程中非常重要,几乎每个应用都要做各种路由权限管理,因此相当考察使用者基本功。

体验

全局守卫:

const router = createRouter({ ... })

router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})

路由独享守卫:

const routes = [
{
path: "/users/:id",
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false;
},
},
];

组件内的守卫:

const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
},
};

回答

  • vue-router中保护路由的方法叫做路由守卫,主要用来通过跳转或取消的方式守卫导航。
  • 路由守卫有三个级别:全局路由独享组件级。影响范围由大到小,例如全局的router.beforeEach(),可以注册一个全局前置守卫,每次路由导航都会经过这个守卫,因此在其内部可以加入控制逻辑决定用户是否可以导航到目标路由;在路由注册的时候可以加入单路由独享的守卫,例如beforeEnter,守卫只在进入路由时触发,因此只会影响这个路由,控制更精确;我们还可以为路由组件添加守卫配置,例如beforeRouteEnter,会在渲染该组件的对应路由被验证前调用,控制的范围更精确了。
  • 用户的任何导航行为都会走navigate方法,内部有个guards队列按顺序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会取消原有的导航。

原理

runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch流程取消原本导航

// 源码
runGuardQueue(guards)
.then(() => {
// check global guards beforeEach
guards = [];
for (const guard of beforeGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from));
}
guards.push(canceledNavigationCheck);

return runGuardQueue(guards);
})
.then(() => {
// check in components beforeRouteUpdate
guards = extractComponentsGuards(
updatingRecords,
"beforeRouteUpdate",
to,
from
);

for (const record of updatingRecords) {
record.updateGuards.forEach((guard) => {
guards.push(guardToPromiseFn(guard, to, from));
});
}
guards.push(canceledNavigationCheck);

// run the queue of per route beforeEnter guards
return runGuardQueue(guards);
})
.then(() => {
// check the route beforeEnter
guards = [];
for (const record of to.matched) {
// do not trigger beforeEnter on reused views
if (record.beforeEnter && !from.matched.includes(record)) {
if (isArray(record.beforeEnter)) {
for (const beforeEnter of record.beforeEnter)
guards.push(guardToPromiseFn(beforeEnter, to, from));
} else {
guards.push(guardToPromiseFn(record.beforeEnter, to, from));
}
}
}
guards.push(canceledNavigationCheck);

// run the queue of per route beforeEnter guards
return runGuardQueue(guards);
})
.then(() => {
// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>

// clear existing enterCallbacks, these are added by extractComponentsGuards
to.matched.forEach((record) => (record.enterCallbacks = {}));

// check in-component beforeRouteEnter
guards = extractComponentsGuards(
enteringRecords,
"beforeRouteEnter",
to,
from
);
guards.push(canceledNavigationCheck);

// run the queue of per route beforeEnter guards
return runGuardQueue(guards);
})
.then(() => {
// check global guards beforeResolve
guards = [];
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from));
}
guards.push(canceledNavigationCheck);

return runGuardQueue(guards);
})
// catch any navigation canceled
.catch((err) =>
isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
? err
: Promise.reject(err)
);

源码位置(opens new window)

作者:bb_xiaxia1998 链接: 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。