标签: VUE

以下是优化 Vue3 异步加载模块并避免 Vite 打包生成过大 JS 文件的关键方法:

  1. 使用 defineAsyncComponent 定义异步组件
    Vue3 提供 defineAsyncComponent 方法,允许组件在渲染时动态加载,减少初始包体积。例如:

    const AsyncComponent = defineAsyncComponent(() => import('./Component.vue'));
    

    这会将组件拆分为独立 chunk,按需加载 。

  2. 结合 Vite 的 Glob 导入批量加载
    利用 Vite 的 import.meta.glob 实现批量异步组件加载:

    const modules = import.meta.glob('./components/*.vue');
    const asyncComponents = Object.entries(modules).map(([path, loader]) => {
      const name = path.split('/').pop().replace('.vue', '');
      return { [name]: defineAsyncComponent(loader) };
    });
    

    此方法自动拆分模块,避免生成单一巨文件 。

  3. 路由懒加载
    在 Vue Router 中动态导入路由组件:

    { path: '/admin', component: () => import('./AdminPage.vue') }
    

    仅当访问路由时加载对应组件,显著减小初始包 。

  4. 动态导入(Dynamic Import)
    使用 ES6 动态导入语法异步加载模块:

    const module = await import('./module.js');
    

    结合 Webpack/Vite 的代码分割功能,自动生成独立 chunk 。

  5. 配置加载状态与错误处理
    通过 defineAsyncComponent 的选项增强用户体验:

    defineAsyncComponent({
      loader: () => import('./Component.vue'),
      loadingComponent: LoadingSpinner, // 加载中组件
      errorComponent: ErrorDisplay,     // 错误组件
      timeout: 3000                     // 超时时间
    });
    

    避免界面空白,提升交互体验 。

  6. 使用 <Suspense> 管理异步状态
    用 <Suspense> 包裹异步组件,统一处理加载和错误状态:

    <Suspense>
      <template #default> <AsyncComponent /> </template>
      <template #fallback> <LoadingSpinner /> </template>
    </Suspense>
    

    简化异步逻辑 。

总结
通过 defineAsyncComponent + Vite Glob 导入实现批量异步组件,结合路由懒加载和动态导入,将代码拆分为按需加载的小 chunk;用 <Suspense> 和错误处理优化用户体验。这些策略可减少初始包体积 60% 以上,避免生成超大 JS 文件。

在 Vue.js 应用中,使用 Vue Router 进行路由管理时,常常需要在不同的路由之间传递参数。Vue Router 提供了几种方式来实现路由传参,包括通过 URL 路径参数、查询参数和命名视图。以下是一些常见的方法和示例:

1. 路径参数(Route Parameters)

路径参数通常用于传递具有唯一标识意义的参数,如用户 ID、文章 ID 等。

定义路由

// router/index.js  
import Vue from 'vue';  
import Router from 'vue-router';  
import User from '@/components/User.vue';  
import Post from '@/components/Post.vue';  
  
Vue.use(Router);  
  
export default new Router({  
  routes: [  
    {  
      path: '/user/:id', // 这里的 `:id` 是一个动态段  
      name: 'User',  
      component: User  
    },  
    {  
      path: '/post/:postId', // 这里的 `:postId` 是一个动态段  
      name: 'Post',  
      component: Post  
    }  
  ]  
});

访问路由并传递参数

// 在某个组件中  
this.$router.push({ name: 'User', params: { id: 123 } });  
this.$router.push({ name: 'Post', params: { postId: 456 } });

在组件中获取参数

// User.vue  
<template>  
  <div>User ID: {{ $route.params.id }}</div>  
</template>  
  
<script>  
export default {  
  computed: {  
    userId() {  
      return this.$route.params.id;  
    }  
  }  
};  
</script>  
  
// Post.vue  
<template>  
  <div>Post ID: {{ $route.params.postId }}</div>  
</template>  
  
<script>  
export default {  
  computed: {  
    postId() {  
      return this.$route.params.postId;  
    }  
  }  
};  
</script>

2. 查询参数(Query Parameters)

查询参数通常用于传递非唯一标识意义的参数,如搜索条件、分页信息等。

定义路由(不需要特殊定义)

// router/index.js  
import Vue from 'vue';  
import Router from 'vue-router';  
import SearchResults from '@/components/SearchResults.vue';  
  
Vue.use(Router);  
  
export default new Router({  
  routes: [  
    {  
      path: '/search',  
      name: 'SearchResults',  
      component: SearchResults  
    }  
  ]  
});

访问路由并传递参数

// 在某个组件中  
this.$router.push({ name: 'SearchResults', query: { q: 'vue', page: 2 } });

在组件中获取参数

// SearchResults.vue  
<template>  
  <div>  
    <p>Search Query: {{ $route.query.q }}</p>  
    <p>Page: {{ $route.query.page }}</p>  
  </div>  
</template>  
  
<script>  
export default {  
  computed: {  
    searchQuery() {  
      return this.$route.query.q;  
    },  
    page() {  
      return this.$route.query.page;  
    }  
  }  
};  
</script>

3. 编程式导航中的 props 传参

Vue Router 还支持将路由参数作为 props 传递给组件,这样可以使组件更加解耦和可复用。

定义路由时使用 props: true

// router/index.js  
import Vue from 'vue';  
import Router from 'vue-router';  
import User from '@/components/User.vue';  
  
Vue.use(Router);  
  
export default new Router({  
  routes: [  
    {  
      path: '/user/:id',  
      name: 'User',  
      component: User,  
      props: true // 这样可以将参数作为 props 传递给 User 组件  
    }  
  ]  
});

在组件中接收 props

// User.vue  
<template>  
  <div>User ID: {{ id }}</div>  
</template>  
  
<script>  
export default {  
  props: ['id']  
};  
</script>

使用 props 传递参数时,不需要通过 $route 对象来访问参数,直接通过 props 接收即可。

总结

  • 路径参数:适用于传递具有唯一标识意义的参数,如用户 ID。
  • 查询参数:适用于传递非唯一标识意义的参数,如搜索条件。
  • 编程式导航中的 props:使组件更加解耦和可复用。

通过这些方法,你可以在 Vue.js 应用中灵活地传递和使用路由参数。

一、Vue哪里好?

我认为一个好的框架首先有以下几点必要的特点

  • 简单易学,文档完善,Api清晰易懂,容易上手
  • 不会给程序员的开发过程带来很大的心智负担
  • 社区完善,长期维护,有完整的生态系统
  • 对业务逻辑及数据的关注度优先于框架本身的使用

低学习成本和使用成本

我认为,前端的发展思路永远是:在保证性能的基础上,提升程序员的开发效率为先。

Vue 和其他前端框架相比,在结构、样式、业务分离等方面更清晰彻底,更符合前端多年来的编码习惯,更符合直觉、更容易学习和维护,非常容易与其它库或已有项目整合。

虽然React等视图框架同样有很多优势,也同样简单易学。但其JSX的编码不太符合编码习惯。另外,在操作数据时,React的单向绑定和较为开放的API,封装性较小,容易带来一些心智负担。而vue很多东西都是内置的,写起来方便一些。比如指令,数据监听机制、模板语法封装、计算属性等丰富的API等。

高性能和轻量级

Vue在轻量级的基础上,还兼顾了高性能。相比于其他框架,Vue有更小的编译后体积,以及更快的渲染性能。

HTML模板化的组件结构

如果你喜欢用模板搭建应用,请选择Vue。

对于Web开发者,模板更容易理解。一些资深开发者也更喜欢模板,因为模板可以更好的把布局和功能分割开来。

而React重度依赖JSX,布局和功能代码组合到一起的写法,对于老开发者来说编码习惯不合适。

Vue3.0的加持

Vue3.x版本的上线,速度、性能、体积的优化,尤其是组合式API和函数式的编程思想、对TS更友好的支持是我坚持Vue的动力。

二、Vue打包后有哪些文件?

  • 资源文件:包括图片、字体、样式和直接拷贝的文件
  • app.js:项目中各个模块的逻辑代码,格式被压缩
  • chunk-vendors.js:导入的第三方依赖包。防止该文件体积过大,可以使用webpack的externals配置,可以声明无需打包的依赖。可在使用CDN资源引用。
  • 其他js:使用路由懒加载方式打包的模块逻辑代码
  • xx.js.map:Source map文件,方便我们开发时调试js代码使用
  • index.html:html入口文件,基本没什么内容

三、有没有自己封装过组件?

我用vue开发的所有项目,都是采用组件化的思想开发的。一般我在搭建项目的时候,会创建一个views目录和一个components目录。

  • views目录中放页面级的组件
  • components中放公共组件
  • views中在页面文件夹中也可以添加components文件放置页面内的局部组件或公共组件
  • views或components中可以创建hooks文件夹,用于抽离相同业务逻辑的代码

首先,组件可以提升整个项目的开发效率。能够把页面抽象成多个相对独立的模块,解决了我们传统项目开发:效率低、难维护、复用性低等问题。

组件可以单独引用,也可以使用app.component('TkBadge', TkBadge)函数注册全局组件。

组件的封装要尽量做到低耦合性,方便更好得复用和维护。传参要更加灵活自由。

组件封装要尽量遵循“单一职责原则”,不同组件承担不同职责,互不干扰,可自由组合。这样便于更好的可读性和复用性。

比如封装公共组件如:弹窗、错误提示、自定义右键菜单

UI框架二次封装如:表格二次封装(固定格式的json、分页、查询等)、表单二次封装

1、基本用法

基本用法基本是相同的,没有什么改动,vue3和vue2都可以这样用

2、具名插槽

具名插槽两者用法有稍微不同,Vue3使用v-slot:[name](缩写#[name]),而Vue2用的是slot="name"

vue3写法

vue2写法

这里有两点需要注意

  • vue3在父组件中使用具名插槽使用v-slot,而vue2使用slot
  • vue3必须把v-slot写在template标签中,而vue2中的slot可以写在任意标签中

3、作用域插槽

经常我们会想让父组件的slot能够访问子组件的数据。

vue3

vue2

有关Vue3.x插槽的更多写法详见文档

1. Vue3.0升级了哪些重要的功能?

  • createApp:创建vue实例的⽅式
  • emits属性,组件中的事件要先使⽤emits进⾏声明,然后在setup的形参引⼊
  • ⽣命周期
  • 多事件
  • fragment:不再限制唯⼀根节点
  • 移除.sync
  • 异步组件的写法:Vue2 直接import进来,Vue3需要使⽤defineAsyncComponent包裹⼀层
  • 移除filter
  • 新的组件:Fragment(⽚段)/Teleport(瞬移)/Suspense(不确定)
  • teleport:把组件直接to到某个dom
  • suspense:fallback,就是⼀个具名插槽
  • composition API

2. Vue3.0 生命周期及其与Vue2.x对比

3. setup中如何获取组件实例?

  • setup和其他Composition API中都没有this
  • 在Options API中仍然可以使⽤this
  • Composition API中可以使⽤getCurrentInstance()⽅法获取

4. Vite是什么?

5. Vue3插槽使用,具名插槽和匿名插槽使用,及与Vue2区别

Vue3插槽详见文档:https://cn.vuejs.org/guide/components/slots.html

与Vue2区别:https://code.zuifengyun.com/2022/09/2808.html

一、Vue3相比与Vue2的优势

1. 速度更快、性能更好

1) 基于Proxy的新响应式系统(数据劫持优化)

Vue3.0如何实现响应式?

由原来的Object.definePropertygettersetter,改成了ES6 Proxy 作为其观察机制(准确说是 Proxy 配合 ReflectReflect提供了一些操作Object对象的方法),初始化时无需递归遍历数据,初始化效率更高,而且也可以监控数组。速度加倍,节省了一半内存开销。

Proxy相当于在⽬标对象之前架设⼀层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了⼀种机制,可以对外界的访问进⾏过滤和改写。

Proxy 的性能本来⽐defineProperty好,Proxy可以拦截属性的访问、赋值、删除等操作,不需要初始化的时候遍历所有属性。另外有多层属性嵌套的话,只有访问某个属性的时候,才会递归处理下⼀级的属性。

  • 可以劫持整个对象
  • 可以监听对象动态属性的添加
  • 可以监听到数组的变化,包括数组的索引和数组length属性
  • 可以监听删除属性
  • 操作时不是对原对象操作,new Proxy 返回⼀个新对象(它不直接操作对象,而是代理模式,通过对象的代理对象进行操作)。

Vue2为啥不使用Proxy?答案是由于浏览器兼容性问题。目前来看,除不支持IE11及以下版本,其余浏览器均已支持。

2) 虚拟DOM重写

虚拟 DOM 重写,提供更多的编译时提示来减少运行时开销。使用更有效的代码来创建虚拟节点。

虚拟DOM静态属性缓存,避免重复patch。内存换时间。

3) Diff算法优化

算法改写,更加细粒度,效率更高。

增加了静态标记flag。标记和提升所有静态根节点,diff 的时候只⽐较动态节点内容。

Patch flag: 在动态标签末尾加上相应的标记,只能带 patchFlag 的节点才被认为是动态的元素,会被追踪属性的修改,能快速的找到动态节点,⽽不⽤逐个逐层遍历,提⾼了虚拟dom diff的性能。

将自定义组件和普通标签进行区分、将静态节点和动态节点进行区分。

4) 静态提升(hoistStatic),靶向更新,提升VNode更新性能

静态提升、靶向更新等优化点,来提高 patchVNode (diff算法递归打补丁)性能。

静态提升:Vue2中无论元素是否参与更新,每次都会重新创建(createVNode),然后再渲染(Vue2 中的虚拟dom节点是进⾏全量的更新)。但是在Vue3中使用了静态提升后,对于静态不需要发生变化的元素,只会被创建一次,静态节点都被提升到 render ⽅法之外,在渲染时直接复用即可。(静态提升避免了静态元素节点频繁重复创建

靶向更新:Vue3将 compileruntime 结合(编译和运行相结合),对动态节点(Block VNode )进行精准标记。靶向更新的本质是为了从一颗存在动态、静态节点的 VNode Tree 中筛选出动态的节点形成 Block Tree,即 dynamicChildren,然后在 patch 时实现精准、快速的更新。(Patch flag,跳过静态节点,直接对⽐动态节点)

5) SSR渲染优化

当有大量静态内容时,这些内容会被当成纯字符串推进一个buffer里面,即使存在动态的绑定,会通过插值嵌入进去。这样会比通过虚拟dom来渲染快很多。当静态内容大到一定量级的时候,会用_createStaticVNode方法在在客户端去生成一个static node这些静态node会被直接innerHTML就不需要创建对象,然后直接根据对象渲染。

6) 事件侦听器缓存(cacheHandles)

Vue3中的事件绑定的函数会被缓存起来,再次复用时就会提升性能。在patch过程中不会重新生成新的函数。

缓存事件处理函数cacheHandler,避免每次触发更新都要重新⽣成全新的function去更新之前的函数。

2. 体积更小

vue3整个源码体积相对减少,移出了一些不常用的Api,例如:inline-templatefilter.sync$set等。

优化了打包方法,引入tree-shaking,按需编译,避免打包无用模块(例如只打包用到的ref,reactive,components等),使得打包后的bundle的体积更小,提升了运行效率。(通过摇树优化核⼼库体积,减少不必要的代码量)

另外,Composition APItree-shaking 友好,代码也更容易压缩。(Tree shaking 详述

3. 更容易维护

语法API方面(compositon Api):

  • 支持组合API(compositon Api),可与现有的Options API一起使用
  • 灵活的逻辑组织逻辑复用
  • Vue3模块可以和其他框架搭配使用

compositon Api 可以解决业务分离问题,使代码有更好的复用性。同时, 也方便后续的维护和管理setup() 的出现,使得相关的业务代码得以集中起来,方便查找和维护。我们可以把不同的业务代码进行逻辑抽离,比如使用hooks形式,更容易维护。而Vue2不同的业务代码都混杂在options中,不便管理。

4. 更接近原生

compositon Api 使得 Vue 3 逻辑代码编写更趋向于原生。

可以自定义渲染 API:createRenderer函数可以自定义渲染逻辑。我们可以自定义渲染的逻辑,甚至还可以渲染到canvaswebview等其他指定的平台。

5. 更易使用

响应式 Api 和 Hooks 直接暴露出来,哪里需要直接引用。比如 ref()toRefs()watch()等和生命周期钩子onMounted()onUpdated()onUnmounted()onBeforeMount()等。

6. 更好的TS支持

基于typescipt编写,可以享受到类型提示。对Ts支持更好。

编辑器可提供强有力的类型检查和错误提示,方便更好得调试。

二、Vue3相较于Vue2解决了哪些问题?设计目标是什么?

1. Vue3设计的初衷有两点(尤大)

  1. 利用新的语言特性(es6)
  2. 解决Vue3的一些设计和架构的问题

2. Vue2长期以来存在的一些问题

  • 随着功能的增长,复杂组件的代码变得越来越难以维护
  • 数据量大后带来的渲染和更新性能问题
  • 缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机制
  • 类型推断不够友好
  • bundle的时间太久了

3. Vue3的设计目标

  • 更小
  • 更快
  • TypeScript支持
  • API设计一致性
  • 提高自身可维护性
  • 开放更多底层功能

三、Vue3编译方面有哪些优势

Vue3更快主要体现在编译方面:

四、Vue3做了哪些优化方案

vue3从很多层面都做了优化,可以分成三个方面:

1. 源码

源码可以从两个层面展开:

1)源码管理

更好的代码管理方式: monorepo(简单的说monorepo模式就是把你组内的所有项目管理在一个git仓库中)。

monorepo将不同的模块拆分到packages目录下不同子目录中,每个package有各自的API、类型定义和测试,这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。每个魔抗可独立于Vue使用,减少引用包的体积大小。

2)TypeScript

  • 编码期间可以进行类型检查,避免类型问题的错误
  • 有利于定义接口类型,利于IDE对变量类型的推导
  • Composition API 几乎是函数,也会有更好的类型推断

2. 性能

vue3是从什么哪些方面对性能进行进一步优化呢?

3. 语法API(Composition API 的出现带来哪些新的开发体验,为啥需要这个? )

这里当然说的就是Composition API,Vue3为什么要使用Composition API取代Options API呢?

如何看待Composition APIOptions API

通常使用Vue2开发的项目,普遍会存在以下问题:

  • 代码的可读性随着组件变大而变差,组件逻辑难以阅读和理解
  • 每一种代码复用的方式,都存在缺点
  • TypeScript支持有限

以上通过使用Composition Api都能迎刃而解,其两大显著的优化:

1)优化逻辑组织

Options API

包含⼀个描述组件选项(datamethodsprops等)的对象 options;开发时,同⼀个功能逻辑的代码会被拆分到不同选项中。

假设一个组件是一个大型组件,其内部有很多处理逻辑关注点,这种碎片化使得理解维护复杂组件变得困难。

选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块(比如在methodscomputed以及一些生命周期选项中反复横跳)。

⽤mixin重⽤公⽤代码,也有问题:命名冲突数据来源不清晰。(options选项不好拆分和重用)

Compostion API

基于函数的 api,可以更灵活的组织组件的逻辑。(一般为纯函数,没有副作用)

Composition ApiReact Hooks有异曲同工之妙,是为了解决复杂业务逻辑而设计的。

将某个逻辑关注点相关的代码全都放在一个函数里(更加的高内聚,低耦合),这样当需要修改一个功能时,就不再需要在文件中或各个选项中来跳去。也方便使用hooks的方式做逻辑抽离。

Composition API 解决了几个问题:

  • 根据业务逻辑组织代码,提⾼可读性和可维护性
  • 解决在⽣命周期函数经常包含不相关的逻辑,但⼜不得不把相关逻辑分离到了⼏个不同⽅法中的问题。(如在mounted中设置定时器,但需要在beforeDestroy中来清除)
  • 更好的重⽤逻辑代码,在Options API中通过MIxins重⽤逻辑代码,容易发⽣命名冲突数据来源不清晰
  • 很少使用到 this ,减少了 this 指向不明的情况
  • Composition API 都是函数,有更好的类型推导
  • 模块化更加解耦,可以单独引用某个模块而不用全量引入Vue对象

当然,Options API并没有被废弃,与Composition API可以共存,但并不推荐这样做(可能会引起混乱)。

小型项目,业务逻辑简单的可以使用Options API,复杂的大型项目还是使用Composition API更好。

2)优化逻辑的复用:mixins

使用单个mixins没太大问题。但当组件中存在大量多个的mixins后,会存在命名冲突数据来源不清晰问题,组合式API解决了这两个问题。即使去编写更多的 hook 函数,也不会出现这两个问题。

五、Vue3里为什么要用 Proxy 替代 defineProperty ?

Object.defineProperty() 只能遍历对象属性进行劫持,Proxy 直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的,且可以解决 defineProperty 存在的问题。且提升性能。

Object.defineProperty()

此方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。文档

为什么能实现响应式:

通过 defineProperty 两个属性,getset

get:属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值

set:属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined

存在弊端:

  • 检测不到对象属性的添加和删除
  • 数组API方法无法监听到
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

Proxy 对象

对对象的所有操作进行监听。文档

这里需要对 ES6 对应的 Proxy 新特性进行复盘。

六、说说Vue 3.0中Treeshaking特性?举例说明一下?

1. 是什么

Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术。

专业术语叫 Dead code elimination(死码消除)。简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码。

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

而Vue3源码引入tree shaking 特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中。

2. 原理

Tree shaking 是基于ES6模板语法(importexports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。

Tree shaking 无非就是做了两件事:

  • 编译阶段利用ES6 Module判断哪些模块已经加载
  • 判断哪些模块和变量未被使用,进而删除对应代码

3. 作用

通过 Tree shaking,Vue3 给我们带来的好处是:

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

七、都说 Composition API 和 React Hook 很像,请问他们的区别是什么?

React Hook 从实现的角度来看,React 是通过链表去实现 hooks 的调用的。需要确保每次更新时 hooks 的调用顺序一致,这让 React 能够在多次的 useStateuseEffect 调用之间保持 hook 状态的正确。

所以有以下几个限制:

  • 不在循环中、条件语句中、函数嵌套中调用 Hook
  • 你必须确保它总是在 React Top level 调用函数 Hook
  • 使用效果、依赖关系必须手动确定

Composition API 是基于 Vue 的响应系统,和React Hook 相比:

  • setup() 函数中,一个组件实例只执行一次,而React Hook 每次重新渲染时,都需要调用 Hook,给 React 带来的 GC 比 Vue 更大的压力,性能也相对 Vue 来说比较慢
  • Compositon API 不必担心调用的顺序,它也可以在循环中、条件、在嵌套函数中任意位置使用
  • 响应式系统自动实现依赖关系收集,而且组件的性能优化是由 Vue 内部完成的,而 React Hook 的依赖关系需要手动传递,并且依赖关系的顺序必须得到保证,尤其是使用 useEffectuseMemo 等 Hook 时,否则组件性能会因为依赖关系不正确而下降。
  • reactive + ref 属于响应式数据,⽐ react 的useState,要更难理解

虽然Compoliton API区别于React Hook,但它的设计思路也是来自React Hook的参考。

八、vue3.0有哪些改进(总结)

  1. 采用TS来编写,编辑器提供强有力的类型检查和错误提示,方便更好得调试。
  2. 支持composition API。
  3. 基于Proxy的响应式系统(由原来的Object.defineProperty的getter 和setter,改变成为了ES2015 Proxy 作为其观察机制),无需递归遍历数据,初始化效率更高,而且也可以监控数组。速度加倍,节省一半内存开销。但兼容性较差,不支持IE11。
  4. 虚拟 DOM 重写,提供更多的编译时提示来减少运行时开销。使用更有效的代码来创建虚拟节点。
  5. v-dom对比算法(diff算法)改写,更加细粒度,效率更高,比如将自定义组件和普通标签进行区分。只更新vdom绑定了动态数据的部分。
  6. v-dom静态属性缓存,避免重复patch。内存换时间。
  7. 虚拟dom编译优化,在compile阶段使用有 transform 对 AST (抽象语法树)做诸多层面的转化,使「Vue」 diff 过程更快。相比之下,「Vue2.x」的编译阶段没有完整的 transform,只是 optimize 优化了一下 AST。
  8. 静态提升、靶向更新等优化点,来提高 patchVNode (diff算法递归打补丁)过程。
  9. Composition API (结构化API)方便逻辑复用,代替原先options API(配置化)。
  10. 模块化更加解耦,可以单独引用某个模块而不用全部引入vue
  11. 2.x 版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用,而3.x版本中v-if优先级更高。
  12. 3.x版本使用:ref 绑定函数的方式取代$refs方式获取dom
  13. 组件的v-model重构。具体见文档。
  14. 总之就是:1. 更快 2. 更小 3. 更容易维护 4. 更加友好 5. 更容易使用
  15. vue3性能比vue2好的原因?1.diff算法优化;2.静态提升hoistStatic;3.事件侦听器缓存 cacheHandles

前端框架火了已经有些年头了。像React、Vue、Angular等,都是视图层数据驱动框架

当然现在最火的两大框架就是React和Vue。而React又出了v16.8版本的Hooks新特性,Vue出了3.x版本。

争论React和Vue哪个更好没有意义。每个框架各有优势,但他们没有本质的区别。

  • Vue更注重视图的自动同步(双向数据绑定),且封装性更好(比如框架封装了完善的事件机制、绑定规范、指令操作等)。使用习惯更偏向前端人员。
  • React更注重组件及其状态的管理(单向数据绑定),更加侧重于逻辑(JS优先)。习惯更偏向于程序人员(比如原先是做PHP的更易上手)。React更加透明,没有封装过多的隐含逻辑(比如指令、事件、数据监听等),便于程序员了解程序的执行过程,但也较Vue使用更复杂。

抛开框架具体本身,宏观上来谈近年来前端框架的发展,我认为前端的发展思路永远是:在保证性能的基础上,提升程序员的开发效率为先。

如果一个框架本身的学习成本和使用成本过高,给开发过程带来很大的心智负担,让程序员对于语言或框架本身使用的关注度超过了对业务逻辑及数据的关注度,那么这个框架一定不是一个好的框架。

简便易用

像React、Vue等,在使用方面有一个最大的特点就是:简便易用!(相对于不使用框架)

以前有人问我说,原来是用原生的,用JQuery的,现在学React,学Vue难不难,复不复杂。

其实新的框架要比老的框架要更加简便易用,这个其实是一个很容易理解的问题。他如果比原来的那玩意还难用,那我们为啥要用他呢?

使用这些框架后,我们可以把精力都放到业务逻辑和数据关系上,对于程序员来说,精力是很有限的。如果让你过多的去处理那些coding层面的细枝末节的东西,那么大的结构,整体的流程方面就会关注不足,很容易拉低效率,延误工时。

视图自动更新

框架最大的优势实际上就是帮我们维护我们的视图层。视图说白了就是页面上的元素。

如果你数据发生了变化,框架会自动将视图进行更新,那么我们的思路就得以解放,精力就不用过多的放到视图上,从而有更多的时间去关注业务逻辑。而业务逻辑、数据等才是决定一个程序根本表现的东西。

虚拟DOM

这个技术实际上老生常谈了。在这里我也不去谈那些没用的什么原理上的东西。无非就是把DOM元素用JS对象的形式进行处理。在视图更新时,通过一系列的diff对比算法对其更新,只更新对应的元素,不会重绘整个页面。

1. 简化操作

虚拟DOM的方式可以帮助我们进行简化操作,使得我们摆脱元素的创建、选取和操作的噩梦。比如React的jsx语法和Vue的模板语法,最终会创建虚拟DOM节点。

在不使用框架前,我们的HTML和JS是分开的。而在虚拟DOM使得我们的HTML标签和JS合二为一(JS优先)。使得我们无需去获取元素。如有需要,可以直接操作标签本身。所见即所得。

2. 提升性能

DOM操作是JS操作中很慢且性能很差的操作。实测一次的DOM操作足够进行成百上千次的其他JS运算。所以在DOM操作非常频繁的,比如带有大的动画特效的页面会非常卡,很吃性能。

而使用视图框架,他不会直接操作真实DOM,而是操作一套虚拟的DOM。以最小化更新真实DOM,从而减小不必要的性能损耗。

基于组件的开发

在开发过程中,实际上存在许多需要多次复用的单元。框架的发展,越来越倾向于使用组件化的开发去把大的视图单元和逻辑分离开来,变成一个个的小功能。

在组件化的开发模式下,只要把一个个的小东西做好了,就可以用组件拼凑出一个大型的应用。

1. 便于大型应用开发

一个大型应用往往比较复杂,往往一眼看去无法直接入手。

大型应用工作任务所需人员众多,如果像原来一样,一大块一个整体,是很难去细化任务的。

如果使用组件,便于页面开发的细化,分工也会更加方便和明确。便于降低整个程序的复杂度。

一个组件可能很简单,但是多个组件组合在一起,那么功能就可能非常强大,威力无穷。

2. 便于功能的复用

一个应用其实是有很多重复的内容和单元的。如果按照原始的开发流程,除了一些业务逻辑,视图结构是不方便复用的。

而组件化的开发就解决了这一难题。

3. 便于使用第三方的组件和开发自己的组件库

你不仅可以组装自己开发的组件,而且还能够使用大量第三方开源的组件库。这也是使用框架的优势之一。

而且你也可以开发自己的组件库,便于后期相关项目视图的统一性。比如我们公司是做法律相关的应用,那么我们可以开发相应的组件库,使得后期所有应用得以统一标准

甚至你可以把自己的组件库开源,为社区做点贡献也是很不错的。

JS优先的设计原则

理论上来说,Vue和React等这些视图层数据驱动框架有一个设计原则就是JS优先。这是与一些古典的模板引擎相对比而言的。

我们知道,视图框架涵盖了模板引擎的功能,但这个功能与模板引擎还有所不同。

模板引擎

模板为主,JS为辅。因为在模板引擎(如EJS、Handlebars等)诞生的年代,JS相对还比较简单,只是浏览器的一个小的脚本语言。像做个小东西如轮播图、请求个数据、做个简单的表单校验。

所以模板为主,优先保障的是静态HTML的结构。js是附加的,是额外的,是添头。

模板引擎更适合于传统开发,偏向于一些比较简单的、静态的、逻辑很少的页面,不利于扩展。比如做个企业站首页,没什么交互功能,只用于展示。

视图框架

JS为主,模板为辅。模板作为JS的一种数据类型。这点在React上尤为明显。

逻辑优先,模板是js逻辑的一部分,可以任意扩展。因为js的灵活程度比HTML强太多了。

适用于现代开发,尤其是大型、复杂的逻辑。

丰富的应用场景

视图框架有非常丰富的应用场景,不仅仅用于开发Web。

可用于移动端开发。比如 React Native 可以开发移动原生应用,比普通混合式应用性能更高。

也可用于开发服务端渲染(SSR静态web应用)的应用。比如React衍生框架Next、Vue衍生框架Nuxt。前后台渲染可以使用同一框架相互结合。

甚至可以用于开发跨平台桌面应用和多端小程序,比如Taro、Electron、Tauri。

介绍

Pinia 是西班牙语中菠萝发音,菠萝实际上是一组单独的花朵,它们结合在一起形成多个水果。与商店类似,每一家都是独立诞生的,但最终都是相互联系的。

Pinia 是 Vue.js 的轻量级状态管理库,最近很受欢迎。它使用 Vue 3 中的新反应系统来构建一个直观且完全类型化的状态管理库。

Pinia的成功可以归功于其管理存储数据的独特功能(可扩展性、存储模块组织、状态变化分组、多存储创建等)。

另一方面,Vuex也是为Vue框架建立的一个流行的状态管理库,它也是Vue核心团队推荐的状态管理库。Vuex高度关注应用程序的可扩展性、开发人员的工效和信心。它基于与Redux相同的流量架构。

Pinia 设置

Pinia 很容易上手,因为它只需要安装和创建一个store。

要安装 Pinia,您可以在终端中运行以下命令:

yarn add pinia@next 
# or with npm 
npm install pinia@next 

该版本与Vue 3兼容,如果你正在寻找与Vue 2.x兼容的版本,请查看v1分支。

Pinia是一个围绕Vue 3 Composition API的封装器。因此,你不必把它作为一个插件来初始化,除非你需要Vue devtools支持、SSR支持和webpack代码分割的情况:

//app.js 
import { createPinia } from 'pinia' 
app.use(createPinia()) 

在上面的片段中,你将Pinia添加到Vue.js项目中,这样你就可以在你的代码中使用Pinia的全局对象。这一步不可省略。

为了创建一个store,你用一个包含创建一个基本store所需的states、actions和getters的对象来调用 defineStore 方法。

import { defineStore } from 'pinia'

// main是Store的名称。它在您的应用程序中是唯一的。
// 并将出现在DevTools中
export const useMainStore = defineStore('main', {
  // a function that returns a fresh state
  state: () => ({
    counter: 0,
    name: 'Eduardo',
  }),
  // optional getters
  getters: {
    // getters receive the state as first parameter
    doubleCount: (state) => state.counter * 2,
    // use getters in other getters
    doubleCountPlusOne(): number {
      return this.doubleCount + 1
    },
  },
  // optional actions
  actions: {
    reset() {
      // `this` is the store instance
      this.counter = 0
    },
  },
})

Pinia使用

使用 Pinia,可以按如下方式访问该store:

import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'

export default defineComponent({ 
  setup() { 
    const main = useMainStore() 

    // extract specific store properties
    const { counter, doubleCount } = storeToRefs(main)
 
    return { 
      // whole store
      main,
      // state or getter
      counter,
      doubleCount,
      // 计算属性方式 
      _counter: computed(() => main.counter), 
    } 
  }, 
}) 

请注意,在访问其属性时省略了 store 的 state 对象。

文档

要了解有关 Pinia 的更多信息,请查看其文档

Vuex 和 Pinia 对比

Pinia试图尽可能地接近Vuex的理念。它的设计是为了测试Vuex的下一次迭代的建议,它是成功的,因为目前有一个开放的RFC,用于Vuex 5,其API与Pinea使用的非常相似。这个项目的个人意图是重新设计使用全局Store的体验,同时保持Vue的平易近人的理念。Pinea的API与Vuex一样接近,因为它不断向前发展,使人们很容易迁移到Vuex,甚至在未来融合两个项目(在Vuex下)。

尽管 Pinia 足以取代 Vuex,但取代 Vuex 并不是它的目标,因此 Vuex 仍然是 Vue.js 应用程序的推荐状态管理库。

在现阶段,由于Pinia是轻量级的,体积很小,它适合于中小型应用。它也适用于低复杂度的Vue.js项目,因为一些调试功能,如时间旅行和编辑仍然不被支持。

将 Vuex 用于中小型 Vue.js 项目是过度的,因为它是重量级的,对性能有很大影响。因此,Vuex 适用于大规模、高复杂度的 Vue.js 项目。

Ps:2022-8:看官网的说明,意思是Pinia将会取代Vuex。

这些是目前Pinia相比Vuex的优势。

  • 极其轻巧(体积约 1KB)
  • store 的 action 被调度为常规的函数调用,而不是使用 dispatch 方法或 MapAction 辅助函数,这在 Vuex 中很常见。
  • 支持多个Store。
  • 支持 Vue devtools、SSR 和 webpack 代码拆分。
  • 无需创建自定义的复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能地利用 TS 类型推断。与在 Vuex 中添加 TypeScript 相比,添加 TypeScript 更容易。
  • Pinia 不支持嵌套存储。相反,它允许你根据需要创建store。但是,store仍然可以通过在另一个store中导入和使用store来隐式嵌套
  • 存储器在被定义的时候会自动被命名。因此,不需要对模块进行明确的命名。
  • Pinia允许你建立多个store,让你的捆绑器代码自动分割它们
  • Pinia允许在其他getter中使用getter

1.vue-router导航守卫(生命周期钩子)

导航守卫主要⽤来对路由的跳转进⾏监控,控制它的跳转或取消,路由守卫有全局的, 单个路由独享的, 或者组件级的。

导航钩⼦有3个参数

  • to:即将要进⼊的⽬标路由对象;
  • from:当前导航即将要离开的路由对象;
  • next:调⽤该⽅法后,才能进⼊下⼀个钩⼦函数(afterEach)。

具体有哪些钩子

  1. 全局前置守卫:router.beforeEach
  2. 全局解析守卫:router.beforeResolve
  3. 全局后置钩子:router.afterEach
  4. 路由独享钩子:在路由配置中添加beforeEnter钩子
  5. 组件内钩子:beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave

简单理解 beforeEach

每次通过跳转路由时,都会触发beforeEach这个钩⼦函数,这个回调函数共有三个参数,to,from,next这三个参数,to表⽰我要跳转的⽬标路由对应的参数,from表⽰来⾃那个路由,就是当前导航即将要离开的路由对象,next是⼀个回调函数,⼀定要调⽤next⽅法来resolve这个钩⼦函数,否则无法进⼊下⼀个钩⼦函数,也无法加载路由。

详见文档

2.导航守卫解析流程

  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 的回调函数,创建好的组件实例会作为回调函数的参数传入。

3.路由懒加载如何实现,原理是什么?

路由懒加载需要导入异步组件,最常用的是通过import()来实现它。

function load(component) {
    return () => import(`views/${component}`)
}
// 箭头函数
const asyncPage = () => import('./views/home.vue')
// import()函数需要作为返回值,其返回Promise

编译打包后,会把每个路由组件的代码分割成一个js文件,初始化时不会加载这些js文件,只当激活路由时(访问该路由时)才会去按需加载对应的路由组件js文件。可以加快项⽬的加载速度。

在Vue3中,在组件中使用异步组件时需要使用defineAsyncComponent()函数才能实现懒加载,但在vue-router中同样可以使用import()方法

这时由于在 Vue 3 中,函数组件被定义为纯函数,异步组件定义需要通过将其包装在一个新的 defineAsyncComponent helper 中来显式定义。

import { defineAsyncComponent } from 'vue'
const home = defineAsyncComponent(() => import('@/views/home.vue'))
export default {
  name: 'async-components',
  components:{
    home
  }
};

4.vue-router如何做用户登录权限?

第一次打开页面或刷新页面时候可以在路由前置钩子router.beforeEach中查询用户信息。再根据返回的用户信息和to,from参数判断登录权限(这里要把用户的信息和登录状态做一个缓存),然后使用next回调控制路由跳转方向。具体的代码要根据具体的项目进行实施。

不是第一次打开页面情况,跳转时直接判断缓存中的登录状态和用户信息来判断是否拥有权限。若有权限则继续跳转到指定的路由。

5. vue-router 3.1.0 <router-link>新增的v-slot属性怎么⽤?

router-link 通过⼀个作⽤域插槽暴露底层的定制能⼒。方便我们更自由的定制导航链接的形式。记得把 custom 配置传递给 <router-link>,以防止它将内容包裹在 <a> 元素内。

详见文档

6. <router-view> 的 v-slot

详见文档

7. 如何实现⼀个路径渲染多个组件?

可以通过命名视图(router-view),它容许同⼀界⾯中拥有多个单独命名的视图,⽽不是只有⼀个单独的出⼝。如果router-view 没有设置名字,那么默认为default

<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>

一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。在配置routes时,确保正确使用 components 配置 (带上 s):

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: '/',
      components: {
        default: Home,
        // LeftSidebar: LeftSidebar 的缩写
        LeftSidebar,
        // 它们与 `<router-view>` 上的 `name` 属性匹配
        RightSidebar,
      },
    },
  ],
})

详见文档

8. 如何实现多个路径共享⼀个组件?

只需将多个路径的component字段的值设置为同⼀个组件即可。

9. 如何监测动态路由的变化

可以通过watch⽅法来对$route进⾏监听,或者通过导航守卫的钩⼦函数beforeRouteUpdate来监听它的变化。

在Vue3中可使用useRoute()onBeforeRouteUpdate

10. Vue 如何去除url中的 #

哈希字符(#)部分 URL 从未被发送到服务器,所以它不需要在服务器层面上进行任何特殊处理。不过,它在 SEO 中确实有不好的影响。

如果你担心这个问题,可以使用 HTML5 模式,将路由模式改为history

路由切换时本质是向history栈中添加一个路由,也就是添加一个history记录。

vue2(vue-router 3.x,已经弃用):

const router = new VueRouter({
  mode: 'history', // 也可设为 hash
  routes: [...]
})

vue3(vue-router 4.x):

import { createRouter, createWebHashHistory } from 'vue-router'

// hash 模式是用 createWebHashHistory() 创建的:
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    //...
  ],
})

// 用 createWebHistory() 创建 HTML5 模式,推荐使用这个模式:
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    //...
  ],
})

11. $route 和 $router 的区别

$route是用来获取路由信息的:

它是当前路由信息的⼀个对象,⾥⾯包含路由的⼀些基本属性,包括name(路由名称)、meta(配置的路由元信息)、path(当前路径)、hash(hash参数)、query(查询参数)、params(当前传递参数)、fullPath(当前全路径)、matched(匹配项)等等。

每一个路由都会有一个$route对象,是一个局部的对象。

$router是用来操作路由的:

$router是VueRouter的一个实例,他包含了所有的路由,以及路由的跳转方法,钩子函数等,也包含一些子对象(例如history)。

12. Vue-router 使⽤params与query传参有什么区别

⽤法上

query要⽤path来引⼊,params要⽤name来引⼊,接收参数都是类似的,分别是this.$route.query.namethis.$route.params.name

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })

// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// `params` 不能与 `path` 一起使用,这时,params无法传递
router.push({ path: '/user', params: { username } }) // -> /user

// 占位符传参时,params参数对应的字段会被拼接到path中
// 使用 `name` 和 `params` 从自动 URL 编码中获益
const routes = [
  { name: 'user',path: '/user/:username' },
]
router.push({ name: 'user', params: { username } }) // -> /user/eduardo

展⽰上

  • query更加类似于我们ajax中get传参,params则类似于post,说的再简单⼀点,前者在浏览器地址栏中显⽰参数,后者则不显⽰
  • params是路由的⼀部分,必须要有,组件才能正常执行。query是拼接在url后⾯的参数,若没有也可正常执行组件。
  • query不设置也可以通过浏览器url进行传参,params不设置的时候,刷新页⾯或者返回参数会丢失
  • 刷新页面后,通过params传参会出现参数丢失的情况,可以通过query的传参⽅式或者在路由匹配规则加⼊占位符即可以解决参数丢失的情况。

13. Vue路由实现的底层原理

Vue的路由实现:hash 模式和 history 模式

hash模式:

早期前端路由的实现是基于window.location.hash 来实现的,window.location.hash 的值就是 URL中#后面的内容。

特点:hash虽然在URL中,但不被包括在HTTP请求中;只是用来指导浏览器动作,对服务端无作用,且hash的切换不会重加载页面。

hash 模式下,仅 # 号之前的内容会被包含在请求中,因此对于后端来说,即使路由切换错误,也不会返回 404 。

但是 hash 模式有两个缺点:

  1. url 不太美观
  2. 对SEO不太友好

history模式:

history采用HTML5的新特性;且提供了两个新方法:history.pushState()history.repalceState()可以对浏览器历史记录栈内存进行修改,且页面不会重载。

history.pushState方法接受三个参数,依次为:

  • state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
  • title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
  • url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

假定当前网址是example.com/1.html,我们使用pushState方法在浏览记录(history对象)中添加一个新记录。

var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');

这样地址栏就可以看到我们自己存的标识了。历史记录也就存进去了。

每当激活的历史记录发生变化时,都会触发popstate事件,调用history.pushState()或者history.replaceState()不会触发popstate事件。pushState事件只会在其他浏览器操作时触发, 比如点击后退按钮(或者在JavaScript中调用history.back()history.go()方法)。

window.addEventListener('popstate', function(event) {
  //做一些操作
  console.log(event.state) // history.pushState()中传入的state状态对象
});

缺点:若没有对服务器(如nginx)进行配置,刷新本不存在的(pushState新增的历史记录)url页面时会出现404状况。

一、响应式数据原理

什么是双向数据绑定:Vue将视图层与数据层做了一个双向的数据同步。甚至是组件之间也可以通过v-model方式进行数据绑定。最大的作用是方便。更效率的实现业务。

vue 实现数据双向绑定主要是:采用数据劫持结合发布-订阅者模式的方式,通过 Object.defineProperty() 来劫持data中对象的各个属性的 settergetter,在数据变动时发布消息给订阅者,触发相应回调。每个组件实例都有相应的 watcher (观察者)程序实例,当页面取相应的属性时,会进行依赖收集(收集组件的watcher),之后当依赖项属性的 setter 被调用时,会通知 watcher 重新计算,从而致使组件得以更新。

Object.defineProperty() 的问题主要有三个:不能监听数组的变化(因为数组可能项目过多,遍历数组太吃性能。需要重写数组的一些方法来实现数组的劫持)、必须遍历对象的每个属性、必须深层遍历嵌套的对象。

Vue在监听数组变化时用的不是Object.defineProperty(),而是把数组的原型方法进行了重写。当用户使用数组的一些方法操作数组时,走的就是VUE自己的方法,然后通知依赖更新。如果数组中包含引用类型如对象,会对其再次进行监控。Vue改写了数组的7个方法(push,pop,shift,unshift,splice,sort,reverse),因为只有这7个方法才能改变数组。另外通过引索改变数组是无法监听的,只能使用$set(arr,index,value)来更新。

在新版Vue3中使用Proxy代理取代Object.defineProperty()来劫持,可以监听整个数据的变化,无需进行层级递归。

整理如下:

  1. 观察者Observer(监听器):⾸先通过观察者对data中的属性使⽤object.defineproperty劫持数据的getter和setter,通知订阅者,触发他的update⽅法,对视图进⾏更新
  2. 解析器Compile:⽤来扫描和解析每个节点的相关指令,并替换模板数据,初始化视图,初始化相应的订阅器
  3. 订阅者Watcher:订阅者接到通知后,调⽤对应的更新函数update⽅法更新对应的视图
  4. 订阅器Dep:订阅者可能有多个,因此需要订阅器Dep来专门接收这些订阅者,并统⼀管理

但在vue3中抛弃了object.defineproperty⽅法,因为

  1. Object.defineproperty⽆法监测对象属性的添加和删除、数组索引和长度的变更,因此vue重写了数组的push/pop/shift/unshift/splice/sort/reverse⽅法
  2. Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进⾏遍历,这样很消耗性能

Vue3中实现数据双向绑定的原理是数据代理,使⽤proxy实现。Proxy 可以理解成,在⽬标对象之前架设⼀层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了⼀种机制,可以对外界的访问进⾏过滤和改写。

二、Vue响应式原理整体叙述(用自己的话)

  1. 通过数据劫持和发布订阅模式来实现。
  2. 初始化时会调用initData,并通过Observer方法对数据进行观测(这里会判断数据是否已被观测,观测时分对象和数组两种观测方法),如果是对象,对象会进行遍历和递归,然后利用Object.defindProperty()方法重新定义了data对象中所有的属性的get和set方法(这个过程如下图)。
  3. 当访问数据时触发对应的get,这时会通过dep.depend()进行依赖收集,收集当前的watcher(观察者,一个组件创建一个watcher,watcher实例与组件实例一一对应)
  4. 修改数据时触发对应的set。如果新的值与旧值不一样,这时会触发dep.notify()回调,通知数据更新。
  5. 由于一个watcher中可能包含有很多个观测的数据,当某个数据更新时怎样找到变化的地方而实现局部响应式更新,这时就需要虚拟DOM和diff算法的支持。
  6. 虚拟DOM是将真实DOM用对象的形式表示,与真实DOM是映射关系。数据变化会修改虚拟DOM对应的节点。然后再通过diff算法,比较新旧虚拟DOM的差异,从而通过打补丁的方式修改真实DOM。(在源码中,patchVnode是diff发生的地方,新旧节点对比方式为:深度优先(递归先找孩子,有孩子先比孩子,没有再比较同层),同层比较。)

三、Vue模板编译原理(template->v-dom->dom)

template -->parse(template) --> ast树(抽象语法树,vue中使用jquery之父的一个作品) -->gencode(ast,options) -->优化树 --> 生成 render 函数 --> 执行 render 函数生成 VNode(虚拟DOM)--> 通过虚拟DOM创建真实DOM

1)什么是模板编译?

把写在<template></template>标签中的类似于原生HTML的内容称之为模板。

在<template></template>标签中除了写一些原生HTML的标签,我们还会写一些变量插值,或者写一些Vue指令,这些东西都是在原生HTML语法中不存在的,不被接受的。render函数会将模板内容生成对应的VNode,VNode经过patch过程从而得到将要渲染的视图中的VNode,最后根据VNode创建真实的DOM节点并插入到视图中, 最终完成视图的渲染更新——模板编译过程。

2)整体渲染过程

在这里插入图片描述

3)模板编译内部流程

借助抽象语法树解析<template></template>标签中写的模板。

4)抽象语法树AST

抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。

将一堆字符串模板解析成抽象语法树AST后,我们就可以对其进行各种操作处理了,处理完后用处理后的AST来生成render函数。

  1. 模板解析阶段:将一堆模板字符串用正则等方式解析成抽象语法树AST;
  2. 优化阶段:遍历AST,找出其中的静态节点,并打上标记;
  3. 代码生成阶段:将AST转换成渲染函数;

四、nextTick实现原理

nextTick主要是利用js事件循环机制定义了一个异步方法。在这个异步方法中包含宏任务微任务(主线程先执行,异步线程(工作线程)在主线程执行结束后再将其推入主线程中,异步任务中的微任务会优先于宏任务先执行)。多次调用nextTick会将这些任务存入任务队列,在主线程任务执行完毕后会读取任务队列中的任务进行清空。

用户手动调用的nextTick(cb)也会将cb回调存入任务数组推入任务队列,先进先出原则进行执行。

Vue中的nextTick实现所用的方法:

Vue的nextTick将任务数组推入任务队列所使用的方法是微任务:PromiseMutationObserver;若这些方法不支持,会使用宏任务setImmediate(IE,优先于setTimeout)、setTimeout方法。

原理:

  • 先定义了一个 callbacks 存放所有的 nextTick 里的回调函数
  • 然后判断当前环境是否支持 Promise,如果支持,就用 Promise 来触发回调函数
  • 如果不支持 Promise 就判断是否支持 MutationObserver,通过观察文本节点发生变化,去触发执行所有异步回调函数
  • 如果不支持 MutationObserver 就判断是否支持 setImmediate,如果支持,就通过setImmediate 来触发回调函数
  • 如果以上都不支持就只能用 setTimeout 来完成异步执行

延迟调用优先级如下:

Promise > MutationObserver > setImmediate > setTimeout

$nextTick可以在vue的created生命周期中使用,在回调中可以用$refs获取dom元素。

nextTick作用:

在修改数据之后使⽤nextTick⽤于下次Dom更新循环结束之后执⾏延迟回调。在修改数据之后使⽤nextTick,则可以在回调中获取更新后的DOM。

五、插槽slot原理

插槽,可以分为两种,一种是普通插槽,一种是作用域插槽。普通插槽,又分为默认插槽和具名插槽。具名插槽可以将不同元素插入到不同slot占位符。

作用域插槽,就是使用子作用域数据(子组件属性数据)的插槽。

普通插槽原理:

  1. 父组件先解析,把插槽元素或字符串当做子元素处理,生成含有children的v-dom节点。
  2. 子组件解析,slot 作为一个占位符,会被解析成一个_xxx('default')函数。这个_xxx函数,传入 'default ' 参数并执行。如果给了名字,就传入插槽的名字。
  3. 这个函数的作用,是把第一步解析得到的插槽节点拿到,然后返回,那么子组件的节点就完整了,插槽也成功认了爹。

作用域插槽原理:

  1. 父子组件在生成v-dom节点时,父组件解析成一个含有props参数的函数,直接传入到children中未执行。
  2. 子组件slot占位符同样解析成一个函数,参数除了插槽名称,还有props数据对象。
  3. slot函数在执行时,通过插槽名称找到对应父组件的函数,并执行,将props传入,这样最终返回的就是含有作用域数据的节点

大概原理就是这样,当然在实际源码中,要复杂很多。

六、缓存组件keep-alive原理

用法:

  1. 用于Vue性能优化。缓存组件。
  2. 频繁切换,不需要重复渲染。
  3. keep-alive有include和exclude属性,这两个属性决定了哪些组件可以进入缓存。
  4. keep-alive还有一个max属性,通过它可以设置最大缓存数,当缓存的实例超过max的时候,vue会删除最久没有使用的缓存,属于LRU缓存策略。
  5. keep-alive其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是activated和deactivated,它们分别在组件激活和失活的时候触发。

可以利用keep-alive提供的include和exclude指定缓存哪些组件不缓存哪些组件,然后配合vuex等状态管理工具实现动态控制。

原理:

  • 维护了一个key数组和一个缓存对象,这个key数组记录目前缓存的组件的key值,
  • 如果这个组件没有指定key值,会自动生成一个唯一的key值
  • 缓存对象会以key值为键,vnode为值,用于缓存组件对应的虚拟DOM
  • 在keep-alive的渲染函数中,其基本逻辑是判断当前渲染的vnode是否有对应的缓存,如果有则从缓存中读取到对应的组件实例,没有就把它缓存。

七、路由组件router-view原理

router-view通过判断当前组件的嵌套层次,然后通过这个层次从route.matches数组中获取当前需要渲染的组件,最后调用全局的$createElement来创建对应的VNode完成渲染的。

八、计算属性computed原理

你给 computed 设置的 get 和 set 函数,会与 Object.defineProperty 关联起来。

所以 Vue 能监听捕捉到,读取 computed 和 赋值 computed 的操作。

在读取 computed 时,会执行设置的 get 函数,但是并没有这么简单,因为还有一层缓存的操作。如果数据没有被污染,不为脏数据(标识dirty),那将直接从缓存中取值,而不会去执行 get 函数。

赋值 computed 时,会执行所设置的 set 函数。这个就比较简单,会直接把 set 赋值给 Object.defineProperty - set。

computed缓存原理

  1. 一开始每个 computed 新建自己的 watcher时,会设置 watcher.dirty = true,以便于 computed 被使用时,会计算得到值
  2. 当依赖的数据变化了,通知 computed 时,会赋值 watcher.dirty = true,此时重新读取 computed 时,会执行 get 函数重新计算。
  3. computed 计算完成之后,会设置 watcher.dirty = false,以便于其他地方再次读取时,使用缓存,免于计算。

九、生命周期钩子原理

无非就是回调函数。在不同的节点插入不同的钩子。

十、前端路由URL跳转原理(hash和history路由

history路由:history.pushStatehistory.replaceState方法(Html5新增方法,用于在不刷新页面的情况下切换url路径)

hash路由:hash路由切换本身不会刷新页面。监听hash切换使用onhashchange事件

history.pushState(stateData,nameString,pathUrl)  //把路由页面放入历史记录
history.replaceState(stateData,nameString,pathUrl)  //不放入历史,直接切换
//stateData-传参,nameString-路由名,pathUrl-路由地址

十一、v-if、v-show指令原理

  1. v-if在内部使用with语法,使用三元运算符判断v-if的值的真假。若为真,则渲染节点。若为假,则不渲染。
  2. v-show在内部使用创建指令方式,判断值为假,则改变节点样式display:none,若为真,则使用原有样式。

十二、Vue中事件绑定原理

在template中绑定事件有两种方式。一种是原生dom绑定,如使用<button @click="fn"></button>,另一种是vue组件(添加到组件上的)自定义事件绑定<el-form @click="fn"><el-form>。两种的区别:

  1. 两者编译后的结果不同:原生结果{nativeOnOn:{click}},等价于普通元素on;自定义事件结果{on:{click}},会单独处理。
  2. 原生绑定采用addEventListener方法添加事件。而组件的自定义事件采用vm.$on()方法,使用vue的发布订阅模式绑定事件,需要使然$emit来触发事件。
  3. 在组件上添加原生事件的方法是使用native修饰符:<el-form @click.native="fn"><el-form>
  4. Vue中没有事件代理的机制。如果要在v-for渲染的多个节点上添加事件,会多次调用addEventListener方法,性能不高。可以在外层添加事件,采用代理模式区分每个子节点的事件。

十三、v-model的实现原理

v-model就是value+input事件的语法糖。

原理:会将组件的v-model转换成value+input

除input标签外,select标签等无法使用input事件的标签,其事件会使用change或自定义input

自定义组件实现v-model

Vue.component("el-checkbox",{
    template:`<input type="checkbox" :checked="check" @change="$emit("change",$event.target.checked)"/>`,
    model:{
        prop:"check",  // 默认为value,这里更改为check
        event:"change" // 默认为input,这里改为change
    },
    props:{
        check:boolean
    }
})

普通标签v-model原理:除value+input之外,还会添加指令。会根据元素的type在编译时绑定不同的属性和事件。比如checkbox会绑定checked属性和change事件。

当添加.lazy修饰符之后(v-model.lazy=""),改变input框中的内容并不会使得其他绑定的数据发生变化,当输入框失去焦点后触发change事件才会改变。

十四、vue.$set原理

强制更新。将未被劫持的数据重新劫持,然后再跑一遍Vue的响应式流程。在Vue3中已废弃。

十五、路由懒加载如何实现,原理是什么

路由懒加载需要导入异步组件,最常用的是通过import()来实现它。

function load(component) {
    return () => import(`views/${component}`)
}
// 箭头函数
const asyncPage = () => import('./views/home.vue')
// import()函数需要作为返回值,其返回Promise

编译打包后,会把每个路由组件的代码分割成一个js文件,初始化时不会加载这些js文件,只当激活路由时(访问该路由时)才会去按需加载对应的路由组件js文件。可以加快项⽬的加载速度。

在Vue3中,在组件中使用异步组件时需要使用defineAsyncComponent()函数才能实现懒加载,但在vue-router中同样可以使用import()方法

这时由于在 Vue 3 中,函数组件被定义为纯函数,异步组件定义需要通过将其包装在一个新的 defineAsyncComponent helper 中来显式定义。

import { defineAsyncComponent } from 'vue'
const home = defineAsyncComponent(() => import('@/views/home.vue'))
export default {
  name: 'async-components',
  components:{
    home
  }
};

十六. Vue3中proxy的原理

Proxy 可以理解成,在⽬标对象之前架设⼀层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了⼀种机制,可以对外界的访问进⾏过滤和改写。

主要通过Proxy对对象进⾏绑定监听处理,通过new Map对对象的属性操作进⾏处理,将要执⾏的函数匹配到存到对应的prop上⾯,通过每次的访问触发get⽅法,进⾏订阅操作,通过修改触发set⽅法,此时通过回调函数通知订阅者,触发他的update⽅法,对视图进⾏更新,达到修改数据和视图的 。