前端VUE知识点汇总

背八股过程中,做一些精简记录,不定期更新。

比较全的前端框架知识 见以前的博文。

vue

Vue

一个构建数据驱动的渐进式框架,关于 Vue 的优点,主要有响应式编程、组件化开发、虚拟 DOM

响应式编程:通过 MVVM 思想实现数据的双向绑定

组件化开发:将应用各模块拆分到各组件中,提高开发效率、方便重复使用…

虚拟 DOM:传统开发中,jQuery或原生DOM操作会导致浏览器不断渲染DOM树,性能开销大;虚拟 DOM 将各种操作放在虚拟节点中,计算完毕后才统一提交。

双向绑定原理

vue2

采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为:

  1. 需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
  2. compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
  3. Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁
  4. MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

Vue3

Vue 3.0 中采用了 Proxy,抛弃了 Object.defineProperty 方法

  1. Object.defineProperty 无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应
  2. Object.defineProperty 只能劫持对象的属性,需要对每个对象,每个属性进行遍历; Proxy 可以劫持整个对象,并返回一个新的对象
  3. Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
  4. 多达 13 种拦截方法,作为新标准是趋势

Proxy 只会代理对象的第一层,那么 Vue3 又是怎样处理这个问题的呢?

判断当前 Reflect.get 的返回值是否为 Object,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。

虚拟DOM

如何理解虚拟DOM

从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。

将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。

通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。

虚拟DOM的解析过程

  • 对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,包含 TagName、props 和 Children 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
  • 当页面的状态发生改变,需要对页面 DOM 结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
  • 最后将有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。

为什么使用虚拟DOM

  1. 提升性能

如果渲染使用真实DOM,由于真实DOM的各项操作会带来大量性能损耗,极大降低渲染效率。使用虚拟 dom ,主要为解决渲染效率的问题。

  • 真实DOM∶ 生成HTML字符串+重建所有的DOM元素
  • 虚拟DOM∶ 生成vNode+ DOMDiff+必要的dom更新

虚拟DOM保证性能下限,提供过得去的性能

  1. 跨平台

Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。

diff算法

当组件创建和更新时,vue 均会执行内部的 update 函数,该函数使用 render 函数生成的虚拟 dom 树,将新旧两树进行对比,找到差异点,最终更新到真实 dom

对比差异的过程叫 diff,vue 在内部通过一个叫 patch 的函数完成该过程

在对比时,vue 采用深度优先、同层比较的方式进行比对。

在判断两个节点是否相同时,vue 是通过虚拟节点的 key 和 tag来进行判断

既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 监测差异 ?

现代前端框架有两种方式侦测变化,一种是 pull,一种是 push

pull

其代表为 React,我们可以回忆一下 React 是如何侦测到变化的。

我们通常会用 setState API 显式更新,然后 React 会进行一层层的 Virtual Dom Diff 操作找出差异,然后 PatchDOM 上,React 从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的 Diff 操作查找「哪发生变化了」

push

Vue 的响应式系统则是 push 的代表,当 Vue 程序初始化的时候就会对数据 data 进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知,因此 Vue 是一开始就知道是「在哪发生变化了」

这又会产生一个问题,通常绑定一个数据就需要一个 Watcher,一但我们的绑定细粒度过高就会产生大量的 Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此 Vue 的设计是选择中等细粒度的方案,在组件级别进行 push 侦测的方式

通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行 Virtual Dom Diff 获取更加具体的差异,而 Virtual Dom Diff 则是 pull 操作,Vuepush + pull 结合的方式进行变化侦测的。

Vue 为什么没有类似于 React 中 shouldComponentUpdate 的生命周期?

根本原因是 VueReact 的变化侦测方式有所不同

Reactpull 的方式侦测变化,当 React 知道发生变化后,会使用 Virtual Dom Diff 进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要用 shouldComponentUpdate 进行手动操作来减少 diff,从而提高程序整体的性能。

Vuepull+push 的方式侦测变化的,在一开始就知道那个组件发生了变化,因此在 push 的阶段并不需要手动控制 diff,而组件内部采用的 diff 方式实际上是可以引入类似于 shouldComponentUpdate 相关生命周期的,但是**通常合理大小的组件不会有过量的 diff,手动优化的价值有限**,因此目前 Vue 并没有考虑引入 shouldComponentUpdate 这种手动优化的生命周期。

setup

script 标签附上 setup 属性后,内部将不再通过 export default 抛出方式的语法。template 模板可以直接拿到 script 标签内声明的变量,并且支持响应式

生命周期

对于 vue 来讲,生命周期就是一个 vue 实例从创建到销毁的过程。

通俗的说,hook 就是在程序运行中,在某个特定的位置,框架的开发者设计好了一个钩子来告诉我们当前程序已经运行到特定的位置了,会触发一个回调函数,并提供给我们,让我们可以在生命周期的特定阶段进行相关业务代码的编写

先看看生命周期的对照表:

Vue 2.0 Vue3
beforeCreate setup()
created setup() 创建datamethod
beforeMount onBeforeMount 组件挂载前执行的函数
mounted onMounted 组件挂载后的函数
beforeUpdate onBeforeUpdate 更新前
updated onUpdated 更新后
beforeDestroy onBeforeUnmount 卸载前
destroyed onUnmounted 卸载后
activated onActivated keep-alive 缓存的组件激活时调用
deactivated onDeactivated 被 keep-alive 缓存的组件停用时调用
errorCaptured onErrorCaptured 捕获来自子孙组件的异常时激活钩子函数

setup 调用的时机是创建组件实例,然后初始化 props,紧接着就是调用 setup 函数。在 beforeCreate 钩子之前被调用,所以 setup 内是拿不到 this 上下文的。

vue3中,除去 beforeCreatecreated 之外,有 9 个旧的生命周期钩子,我们可以在 setup 方法中访问

父子组件中生命周期的调用顺序

  • 加载渲染过程:父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted
  • 子组件更新过程:父beforeUpdate->子beforeUpdate->子updated->父updated
  • 父组件更新过程:父 beforeUpdate -> 父 updated
  • 销毁过程:父beforeDestroy->子beforeDestroy->子destroyed->父 destroyed

添加响应式

reactive

reactive 接收一个 JS 对象 作为参数,返回一个该对象的proxy代理,允许多层嵌套

  • JS 对象为 对象、数组和 MapSet 这样的集合类型,而对 stringnumberboolean 这样的 原始类型 无效

  • 不可以随意地“替换”一个响应式对象,不可以将响应式对象的属性赋值或解构

样式穿透

需要使用 scoped 属性 保证组件间不会样式污染,但是有时候需要做样式穿透,有这么几种方式:

  1. 深度选择器 ::deep()
  2. vue 组件中定义一个全局的 style 标签,一个含有作用域的 style 标签

ref 的作用

ref 的作用是被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。其特点是:

  • 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素
  • 如果用在子组件上,引用就指向组件实例

常见的使用场景有:

  1. 基本用法,本页面获取 DOM 元素
  2. 获取子组件中的 data
  3. 调用子组件中的方法

说一下常用的Vue修饰符

  • 事件修饰符
    • .stop:阻止冒泡。
    • .prevent:阻止默认事件。
    • .capture:使用事件捕获模式。
    • .self:只在当前元素本身触发。
    • .once:只触发一次。
  • 按键修饰符
    • .enter:回车
    • .tab:制表键
    • .delete:捕获 “删除” 和 “退格” 键
    • .esc:返回
    • .space:空格
  • 表单修饰符
    • .lazy:在文本框失去焦点时才会渲染
    • .number:将文本框中所输入的内容转换为 number 类型
    • .trim:可以自动过滤输入首尾的空格

移动端如何实现一个常见的header组件

header组件一般分为 左右中 三部分。中间为主标题,可以通过vue props方式做成可配置向外暴露,左右两侧可以通过 vue slot 插槽的方式对外暴露以实现多样化,同时也可以提供 default slot 默认插槽来统一页面风格。

v-on可以实现监听多个方法吗

可以,几种写法:

1
2
3
4
5
6
7
写法一:
<div v-on="{ 事件类型: 事件处理函数, 事件类型: 事件处理函数 }"></div>
写法二:
<div @事件类型="“事件处理函数”" @事件类型="“事件处理函数”"></div>
写法三:在一个事件里面书写多个事件处理函数
<div @事件类型="“事件处理函数1,事件处理函数2”"></div>
写法四:在事件处理函数内部调用其他的函数

vue数据为什么频繁变化但只更新一次

DOM 更新是一个异步操作,在数据更新后会首先被 set 钩子监听到,但是不会马上执行 DOM 更新,而是在下一轮循环中执行更新。

具体实现是vue 中实现了一个 queue 队列用于存放本次事件循环中的所有 watcher 更新,并且同一个 watcher 的更新只会被推入队列一次。在本轮事件循环的微任务执行结束后执行此更新,这就是 DOM 只会更新一次的原因。

在下一个的事件循环“tick”中,vue 刷新队列并执行实际 (已去重的) 工作。vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

插槽和作用域插槽的区别

插槽的作用是子组件提供了可替换模板,父组件可以更换模板的内容。

作用域插槽给了子组件将数据返给父组件的能力,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。本质是子组件可以通过插槽的位置绑定一些数据,让父组件插槽位置可以用这个数据。

vue中相同逻辑如何进行抽离?

vue2 中采用混入(mixin)技术。

vue3 中采用组合式函数,组合式函数(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

vue为什么采用异步渲染

如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染;为了性能考虑,Vue 会在本轮数据更新后,再去异步更新视图

异步渲染的原理

  1. 调用 notify( ) 方法,通知 watcher 进行更新操作
  2. 依次调用 watcher 的 update 方法
  3. 对 watcher 进行去重操作(通过 id)放到队列里
  4. 执行完后异步清空这个队列,nextTick(flushSchedulerQueue)进行批量更新操作

$nextTick原理及作用

nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。

nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。

有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick了。

由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick中。

两种情况用到:

  1. 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在nextTick()的回调函数中;
  2. 在created()钩子进行DOM操作,也一定要放在nextTick()的回调函数中。因为在created()钩子函数中,页面的DOM还未渲染,没办法操作DOM,所以必须将代码放在nextTick()的回调函数中

v-if、v-show

  • v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就不会渲染;
  • v-show会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在节点的属性中修改show属性值,也就是常说的display;

两者区别:

  • 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
  • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
  • 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
  • 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
  • 使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。

v-model

v-model 实际上是一个语法糖

1
2
3
4
5
6
<input v-model="searchText"> 
全等于
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>

适用于表单元素、组件

如何理解v-model用在组件上,自定义组件和父组件的交互如下:

  1. 父组件将searchText变量传入custom-input 组件,使用的 prop 名为value
  2. custom-input 组件向父组件传出名为input的事件,父组件将接收到的值赋值给searchText

keep-alive

在组件切换的时候,保存一些组件的状态防止多次渲染,可以使用 keep-alive 组件包裹需要保存的组件。

VueRouter

hash模式和history模式

# 后面 hash 值的变化,不会导致浏览器向服务器发出请求,浏览器不发出请求,就不会刷新页面;

通过监听 hashchange 事件可以知道 hash 发生了哪些变化,然后根据 hash 变化来实现更新页面部分内容的操作。

history 模式的实现,主要是 HTML5 标准发布的两个 APIpushStatereplaceState,这两个 API 可以 改变url但又不刷新页面的效果。这样就可以监听 url 变化来实现更新页面部分内容的操作。

两者区别:

  • 首先是在 URL 的展示上,hash 模式有“#”,history 模式没有
  • 刷新页面时,hash 模式可以正常加载到 hash 值对应的页面,而 history 没有处理的话,会返回 404,一般需要后端将所有页面都配置重定向到首页路由
  • 在兼容性上,hash 可以支持低版本浏览器和 IE

router和route区别

  • $route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name ,component等路由信息参数
  • $router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。

路由懒加载

延迟加载,即在需要的时候进行加载,随用随载

如果能把不同路由对应的组件分割成不同的代码块,当路由被访问的时候才加载对应组件,这样就更加高效了。

非懒加载:

1
2
3
4
import List from "@/components/list.vue";
const router = new VueRouter({
routes: [{ path: "/list", component: List }],
});

懒加载:

1
2
3
4
const List = () => import("@/components/list.vue");
const router = new VueRouter({
routes: [{ path: "/list", component: List }],
});

对前端路由的理解

在前端技术早期,一个 url 对应一个页面,如果要从 A 页面切换到 B 页面,那么必然伴随着页面的刷新。这个体验并不好,不过在最初也是无奈之举——用户只有在刷新页面的情况下,才可以重新去请求数据。

Ajax 出现了,它允许人们在不刷新页面的情况下发起请求。与之共生的,还有不刷新页面即可更新页面内容这种需求。在这样的背景下,出现了 SPA(单页面应用)。

在 SPA 诞生之初,在内容切换前后,页面的 URL 都是一样的,这就有两个问题:

  • SPA 其实并不知道当前的页面“进展到了哪一步,一次刷新,一切就会被清零,必须重复之前的操作、才可以重新对内容进行定位
  • 有且仅有一个 URL 给页面做映射,这对 SEO 也不够友好,收集信息不全

为了解决这个问题,前端路由出现了

前端路由可以帮助我们在仅有一个页面的情况下,“记住”用户当前走到了哪一步——为 SPA 中的各个视图匹配一个唯一标识。刷新页面,因为当前的 URL 可以标识出他所处的位置,因此内容也不会丢失。

如何实现呢:

  1. 拦截用户的刷新操作,因为一旦刷新就重新请求资源,就重来了,要把刷新这个动作完全放到前端逻辑里消化掉。
  2. 感知 URL 的变化。这里不是说要改造 URL、凭空制造出 N 个 URL 来。我们可以给它做一些微小的处理,根据后续感知到的变化,用 JS 去给它生成不同的内容。

Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。

待续…