前端知识点总结之VUE

639 阅读9分钟

Vue 2.x 的全链路运作机制?

初始化及mount

在执行new Vue()后,vue 会执行一个init()的方法,这里的初始化包括生命周期、事件、props、data、methods、computed、watch,以及对所有data进行observer。

初始化之后会执行$mount挂载组件,若是运行时编译(存在template,不存在render function),则会进行编译,将template编译为渲染函数render function。

compile()编译

compile阶段主要分为parse、optimize、和genarate阶段,最后生成render function。

  • parse: parse的作用是通过正则匹配整个template,来生成抽象语法树(AST)。
  • optimize:主要是用来标记静态节点。所谓静态节点简单理解就是页面中写死的部分(不涉及vue的内容)。标记静态节点的作用是再更新视图时,会有一个patch过程来对比新旧虚拟DOM,若是静态节点则可以直接跳过,这是vue中关于性能的一处优化。
  • genarate:主要是将AST转换为render function字符串。

响应式

init()的时候,会通过Object.defineProperty()方法设置getter和setter,它使得数据在读取的时候执行getter,在修改的时候执行setter。

vue在执行render function的时候会指向getter对数据进行依赖收集,所谓依赖收集就是为每一个data分配一个Dep,然后将依赖于这些数据的Watcher(一般一个vue组件对应一个Watcher),添加到对应的Dep中。

在数据修改的时候会执行setter对数据进行响应式,流程是通过Dep来执行notify方法,通知对应数据的wacher执行update方法,来更新视图。

  // 创建一个dep
  // get时用来进行依赖收集
  // set时用来通知watcher更新dom
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      if (Dep.target) {
        // 进行依赖收集
        dep.depend()
      }    
      return value
    },
    set: function reactiveSetter (newVal) {
      // 修改value的值
      val = newVal
      // 通知watcher更新
      dep.notify()
    }
  })

虚拟DOM

当我们执行编译后的render function时,会生成一个叫虚拟DOM的东西,是真实DOM的映射。最终可以通过一系列的操作生成真实DOM。

  • 将页面改变的内容应用到虚拟 DOM 上,而不是直接应用到 DOM 上。
  • 变化被应用到虚拟 DOM 上时,虚拟 DOM 并不急着去渲染页面,而仅仅是调整虚拟 DOM 的内部状态,这样操作虚拟 DOM 的代价就变得非常轻了。
  • 在虚拟 DOM 收集到足够的改变时,再把这些变化一次性应用到真实的 DOM 上。

Vue的生命周期

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

往往我们在开发项目时都经常用到 refs 来直接访问子组件的方法,ref 需要在dom渲染完成后才会有,在使用的时候确保dom已经渲染完成。比如在生命周期 mounted(){} 钩子中调用,或者在 this.$nextTick(()=>{}) 中调用。nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 nextTick,则可以在回调中获取更新后的 DOM。

Vue组件间的参数传递

Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信。

  • props / emit 适用 父子组件通信。

  • ref 与 parent , children 适用 父子组件通信。ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。children为当前组件的直接子组件,是一个无序的数组,父组件通过 children 访问子组件并传递数据,$ children 并不保证顺序,也不是响应式的,如果能清楚的知道子组件的顺序,可以使用下标来操作对应的子组件。

  • EventBus (emit , on) 适用于父子、隔代、兄弟组件通信。eventBus 就是一个vue实例来作为全局的事件总线,组件之间通过 eventBus. on和eventBus.on和eventBus.emit 注册触发事件来传递数据。

  • attrs , listeners 适用于 隔代组件通信。attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind=" attrs " 传入内部组件。通常配合 inheritAttrs 选项一起使用。listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=" listeners " 传入内部组件。

  • provide / inject 适用于 隔代组件通信。provide/inject 组合以允许一个祖先组件向其所有子孙后代组件注入一个依赖(属性和方法),不论组件层次有多深,并在其上下游关系成立的时间里始终生效,从而实现跨级父子组件通信,主要在开发高阶插件/组件库时使用

  • Vuex 适用于 父子、隔代、兄弟组件通信。Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化。

Vue的路由实现:hash模式 、 history模式、abstract模式

随着前端应用的业务功能起来越复杂,用户对于使用体验的要求越来越高,单面(SPA)成为前端应用的主流形式。大型单页应用最显著特点之一就是采用的前端路由系统,通过改变URL,在不重新请求页面的情况下,更新页面视图。

更新视图但不重新请求页面,是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有2种方式:

  • 利用URL中的hash("#");
  • 利用History interfaceHTML5中新增的方法;

vue-routerVue.js框架的路由插件,它是通过mode这一参数控制路由的实现模式的:

const router=new VueRouter({
    mode:'history',
    routes:[...]
})

作为参数传入的字符串属性mode只是一个标记,用来指示实际起作用的对象属性history的实现类,两者对应关系:

    modehistory:
        'history':HTML5History;
        'hash':HashHistory;
        'abstract':AbstractHistory;
  1. 在初始化对应的history之前,会对mode做一些校验:若浏览器不支持HTML5History方式(通过supportsPushState变量判断),则mode设为hash;若不是在浏览器环境下运行,则mode设为abstract;
  2. VueRouter类中的onReady(),push()等方法只是一个代理,实际是调用的具体history对象的对应方法,在init()方法中初始化时,也是根据history对象具体的类别执行不同操作。

**hash模式:**在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。

hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 www.xxx.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。

**history模式:**history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。通过back(),forward(),go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。

history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。所以要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。

一般的需求场景中,hash模式与history模式是差不多的,根据MDN的介绍,调用history.pushState()相比于直接修改hash主要有以下优势:

  • pushState设置的新url可以是与当前url同源的任意url,而hash只可修改#后面的部分,故只可设置与当前同文档的url
  • pushState设置的新url可以与当前url一模一样,这样也会把记录添加到栈中,而hash设置的新值必须与原来不一样才会触发记录添加到栈中
  • pushState通过stateObject可以添加任意类型的数据记录中,而hash只可添加短字符串
  • pushState可额外设置title属性供后续使用

vue-router路由懒加载以及三种实现方式

项目build打包后,一般情况下,会放在一个单独的js文件中,但是,如果很多的页面都放在同一个js文件中,必然会造成这个页面非常大。像vue这种单页面应用,如果没有应用懒加载,运用webpack打包后的文件将会异常的大。造成进入首页时,需要加载的内容过多,时间过长,会出现长时间的白屏,即使做了loading也是不利于用户体验。 而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时。

主要作用是将路由对应的组件打包成一个个的js代码块 ,只有在这个路由被访问到的时候,才加载对应的组件,否则不加载。

vue项目实现路由按需加载(路由懒加载)的三种方式:

  •  Vue异步组件

    { path: '/problem', name: 'problem', component: resolve => require(['../pages/home/problemList'], resolve) }

  •  ES6标准语法import( )---------推荐使用

    { path: '/problem', name: 'problem', component: () => import('../pages/home/problemList') }

  •  webpack的require.ensure( )

import和require的比较:

  • import 是解构过程并且是编译时执行 。
  • require 是赋值过程并且是运行时才执行,也就是异步加载 。
  • require的性能相对于import稍低,因为require是在运行时才引入模块并且还赋值给某个变量。

对v-model的了解

在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定, v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

text 和 textarea 元素使用 value 属性和 input 事件;
checkbox 和 radio 使用 checked 属性和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件。

<input v-model='something'>

相当于

<input :value="something" @input="something = $event.target.value">

其中@input是对输入事件的一个监听:value="something"是将监听事件中的数据放入到input,v-model不仅可以给input赋值还可以获取input中的数据,而且数据的获取是实时的,因为语法糖中是用@input对输入框进行监听的。

在自定义组件中

<my-component v-model="inputValue"></my-component>

相当于

<my-component v-bind:value="inputValue" v-on:input="inputValue = argument[0]"></my-component>

这个时候,inputValue接受的值就是input事件的回调函数的第一个参数,所以在自定义组件中,要实现数据绑定,还需要$emit去触发input的事件。

this.$emit('input', value)

vuex是什么?怎么使用?哪种功能场景使用它?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。简单来说就是:应用遇到多个组件共享状态时,使用vuex。

vuex的流程:页面通过mapAction异步提交事件到action。action通过commit把对应参数同步提交到mutation,mutation会修改state中对应的值。最后通过getter把对应值传出去,在页面的计算属性中,通过,mapGetter来动态获取state中的值。

vuex有哪几种属性

有五种,分别是State , Getter , Mutation , Action , Module (就是mapAction)。

  • state:vuex的基本数据,用来存储变量。
  • getter:从基本数据(state)派生的数据,相当于state的计算属性。
  • mutation:提交更新数据的方法,必须是同步的(如果需要异步使用action)。每个mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数,提交载荷作为第二个参数。
  • action:和mutation的功能大致相同,不同之处在于:1. Action 提交的是 mutation,而不是直接变更状态。 2. Action 可以包含任意异步操作。
  • modules:模块化vuex,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。

对 SPA 单页面的理解,它的优缺点分别是什么?

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小;
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

理解 Vue 的单向数据流?

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着不应该在一个子组件内部改变 prop。如果这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
有两种常见的试图改变一个 prop 的情形 :

这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:

props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}

这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性

props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

computed 和 watch 的区别和运用的场景?

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作

运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

父组件可以监听到子组件的生命周期吗?

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

// Parent.vue
<Child @mounted="doSomething"/>
    
// Child.vue
mounted() {
  this.$emit("mounted");
}

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>

doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},

//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
},        
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...     

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。

组件中 data 为什么是一个函数?

  • vue中组件是用来复用的,为了防止data复用,将其定义为函数。 
  • vue组件中的data数据都应该是相互隔离,互不影响的,组件每复用一次,data数据就应该被复制一次,之后,当某一处复用的地方组件内data数据被改变时,其他复用地方组件的data数据不受影响,就需要通过data函数返回一个对象作为组件的状态。 
  • 当我们将组件中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,拥有自己的作用域,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。 
  • 当我们组件的date单纯的写成对象形式,这些实例用的是同一个构造函数,由于JavaScript的特性所导致,所有的组件实例共用了一个data,就会造成一个变了全都会变的结果。

使用Vue.$set()

由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。当把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。

受现代 JavaScript 的限制,Vue 不能检测到对象属性的添加或删除。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性。

Vue 也不能检测到以下数组的变动:当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue,当你修改数组的长度时,例如:vm.items.length = newLength。为了解决第一个问题,Vue 提供了以下操作方法:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

为了解决第二个问题,Vue 提供了以下操作方法:

// Array.prototype.splice
vm.items.splice(newLength)

vm.$set 的实现原理是:

  • 如果目标是数组,直接使用数组的 splice 方法触发响应式;
  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

Vue 中的 key 有什么作用?

当 Vue.js 用v-for正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。

Vue 中 key 的作用是:key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。

更准确:因为带 key 就不是就地复用了,在有了key属性之后,Vue会记住元素们的顺序,并根据这个顺序在适当的位置插入/删除元素来完成更新。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快。

vue3.0 特性的了解

更快:节点打tag + 事件开cache + 响应式proxy

  • 节点打tag:对静态节点和动态节点做标记的区分,对于静态节点在更新的时候不会做diff操作,节省开销,无论层级嵌套多深,更新时可以直接遍历动态节点。对开发者无感知。
  • 事件开cache:默认开启,避免template重复编译。
  • 响应式proxy:proxy在目标对象之上架了一个代理拦截,代理的是对象而不是对象的属性,极大提升性能。主要通过函数reactive()给对象新增一个Proxy对象监听内部的属性来实现数据监听。

更强大:强大的composition API + 强大的Teleport + 强大的Fragment + 强大的createRenderer

  • 强大的composition API:被定位为高级特性,旨在解决的问题主要出现在大型应用程序中。
    compostion api着力于JavaScript(逻辑)部分,将逻辑相关的代码放在一起,近而有利于代码的维护。
    在vue2的组件内,使用的是Option API风格(data/methods/mounted)来组织的代码,这样会让逻辑分散,举个例子就是我们完成一个计数器功能,要在data里声明变量,在methods定义响应函数,在mounted里初始化变量,如果在一个功能比较多、代码量比较大的组件里,你要维护这样一个功能,就需要在data/methods/mounted反复的切换到对应位置,然后进行代码的更改。
    Composition API顾名思义就是不再传入data、mounted等参数,通过引入的ref、onMounted等方法实现数据的双向绑定、生命周期函数的执行。在vue3中,使用setup函数。

  •   <template>
        <div>{{count}}</div>
        <button @click="addCount">添加</button>
      </template>
      
      <script lang="ts">
      import { defineComponent, ref, onMounted } from 'vue';
      export default defineComponent({
        name: 'App',
        setup () {
          const count = ref(0)
          const getCount = () => {
            count.value = Math.floor(Math.random() * 10)
          }
          const addCount = () => {
            count.value++
          }
          onMounted(() => {
            getCount()
          })
      
          return {
            count,
            addCount
          }
        }
      });
      </script>
    
  • 强大的Teleport:Teleport 是一种能够将我们的模板移动到 DOM 中 Vue app 之外的其他位置的技术。可以在组件的逻辑位置写模板代码,这意味着我们可以使用组件的 data 或 props,然后在 Vue 应用的范围之外渲染它。如下,在这种情况下,即使在不同的地方渲染 Modal,它仍是当前组件(调用 Modal 的组件)的子级,并将从中接收 prop
    **

      <teleport to="#modal-container">
        <!-- use the modal component, pass in the prop -->
        <modal :show="showModal" @close="showModal = false">
          <template #header>
            <h3>custom header</h3>
          </template>
        </modal>
      </teleport>
    
    import { ref } from 'vue';
    import Modal from './Modal.vue';
    export default {
      components: {
        Modal
      },
      setup() {
        // modal 的封装
        const showModal = ref(false);
        return {
          showModal
        }
      }
    }** 
    
  • 强大的Fragment:组件不需要有唯一根节点。

  • 强大的createRenderer:在 vue3 中,为了更好的实现多平台应用,将渲染器的创建函数 createRenderer 方法单独分离出来,不用像 weex 一样,需要 fork 一下,然后去改源码。

更小Tree shaking按需加载

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

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

import Vue from 'vue'

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

import { nextTick, observable } from 'vue'

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

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

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