02-面试之前端 Vue 基础

语法

Vue 中常用的一些指令

  1. v-model:用于表单输入,实现表单控件和数据的双向绑定。
  2. v-on:简写为@,基础事件绑定。
  3. v-bind:简写为:,动态绑定一些元素的属性,类型可以是:字符串、对象或数组。
  4. v-if:取值为 true/false,控制元素是否需要被渲染
  5. v-else:和 v-if 指令搭配使用,没有对应的值。当 v-if 的值 falsev-else 才会被渲染出来。
  6. v-show:指令的取值为 true/false,分别对应着显示/隐藏。
  7. v-for:遍历 data 中存放的数组数据,实现列表的渲染。
  8. v-once:使用 v-once 指令,你能执行一次性地插值,当数据改变时,插值处的内容不会更新

v-showv-if 有什么区别 ★★★★★

v-showv-if 都是用来显示隐藏元素,v-if 还有一个 v-else 配合使用,两者达到的效果都一样,但是 v-if 更消耗性能的,因为 v-if 在显示隐藏过程中有 DOM 的添加和删除,v-show 就简单多了,只是操作 css。

  • v-if: 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
  • v-show:就简单得多。不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。

所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

为什么避免 v-ifv-for 一起使用

  • vue2.x 版本中,当 v-ifv-for 一起使用时,v-for 具有比 v-if 更高的优先级。
  • vue3.x 版本中,当 v-ifv-for 一起使用时,v-if 具有比 v-for 更高的优先级。

官网明确指出:避免 v-ifv-for 一起使用,永远不要在一个元素上同时使用 v-ifv-for。可以先对数据在计算数据中进行过滤,然后再进行遍历渲染;操作和实现起来都没有什么问题,页面也会正常展示。但是会带来不必要的性能消耗;

computed 和 watch 的区别

  • computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算
  • watch: 更多的是「观察」的作用,监听某一个值,当被监听的值发生变化时,执行相关操作。

运用场景:

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

computed 与 method 的区别

相同点:

  • 如果作为模板的数据显示,二者能实现响应的功能,唯一不同的是 methods 定义的方法需要执行

不同点:

  1. computed 会基于响应数据缓存,methods 不会缓存
  2. diff 之前先看 data 里的数据是否发生变化,如果没有变化 computed 的方法不会执行,但 methods 里的方法会执行
  3. computed 是属性调用,methods 是函数调用

delete 与 vue.delete 区别?

delete 会删除数组的值,但是它依然会在内存中占位置

而 vue.delete 会删除数组在内存中的占位

1
2
3
4
5
6
let arr1 = [1,2,3];
let arr2 = [1,2,3];
delete arr1[1];
this.$delete(arr2,2);
console.log(arr1); // [1, empty, 3]
console.log(arr2) // [1,2]

Class 与 Style 如何动态绑定?

Class 可以通过对象语法和数组语法进行动态绑定:

  • 对象语法:

    1
    2
    3
    4
    5
    <div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
    data: {
    isActive: true,
    hasError: false
    }
  • 数组语法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
        <div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
    data: {
    activeClass: 'active',
    errorClass: 'text-danger'
    }
    ```

    Style 也可以通过对象语法和数组语法进行动态绑定:

    * 对象语法:
    data: { activeColor: 'red', fontSize: 30 }
    1
    2

    * 数组语法:
    data: { styleColor: { color: 'red' }, styleSize:{ fontSize:'23px' } }
    1
    2
    3


    ## 组件中 data 为什么是一个函数,然后 return 一个对象,而 new Vue 实例里 data 可以直接是一个对象?

// data
data() {
return {
message: “子组件”,
childName:this.name
}
}

// new Vue
new Vue({
el: ‘#app’,
router,
template: ‘‘,
components: {App}
})

1
2
3
4
5
6
7
8
9
10
11
12

因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;

而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。


## 既然函数是引用类型,为什么 Vue 的 data 还是可以用函数

JavaScript 只有函数构成作用域(注意理解作用域,对象的 `{}` 以及 `if(){}` 都不构成作用域),data 是一个函数时,每个组件实例都有自己的作用域,每个实例相互独立,不会相互影响。


## Vue 中的插槽

<slot:自定义 name=data中的属性对象>

1
2
3
4
5
6
7
8
9
10
11

Vue 中的插槽指的是子组件提供给父组件使用的一个占位符。

用标签表示,父组件可以在这个占位符中填充任何模板代码,比如 html、组件等,填充的内容会替换掉子组件的标签(替换占位符)


## Vue 中 data 的属性可以和 methods 中方法同名吗,为什么?

可以同名,methods 的方法名会被 data 的属性覆盖;调试台也会出现报错信息,但是不影响执行;

原因:源码定义的 initState 函数内部执行的顺序:`props>methods>data>computed>watch`

// initState 部分源码
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59


## Vue 中 `$nextTick` 作用与原理

作用:是为了可以获取更新后的 DOM。

由于 Vue DOM 更新是异步执行的,即修改数据时,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。

为了确保得到更新后的 DOM,所以设置了 `Vue.nextTick()`,就是在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

原理: 在下次 DOM 更新循环结束之后执行延迟回调。nextTick 主要使用了宏任务和微任务。根据执行环境分别尝试采用

* Promise
* MutationObserver
* setImmediate
* 如果以上都不行则采用 setTimeout

定义了一个异步方法,多次调用 nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。


# 虚拟 DOM

## 虚拟 DOM 的优缺点

**优点:**

* **保证性能下限:** 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
* **无需手动操作 DOM:** 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
* **跨平台:** 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

**缺点:**

* **无法进行极致优化:** 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。


## 虚拟 DOM 实现原理?

虚拟 DOM 的实现原理主要包括以下 3 部分:

* 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
* diff 算法:比较两棵虚拟 DOM 树的差异;
* pach 算法:将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

如果对以上 3 个部分还不是很了解的同学,可以查看另一篇详解虚拟 DOM 的文章《[深入剖析:Vue核心之虚拟DOM](https://juejin.cn/post/6844903895467032589#heading-14 "https://juejin.cn/post/6844903895467032589#heading-14")》


## Vue 中的 key 有什么作用

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

Vue 的 diff 过程可以概括为:oldCh 和 newCh 各有两个头尾的变量 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它们会新节点和旧节点会进行两两对比,即一共有4种比较方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 种比较都没匹配,如果设置了key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。

具体有无 key 的 diff 过程,可以查看另一篇详解虚拟 DOM 的文章《[深入剖析:Vue核心之虚拟DOM](https://juejin.cn/post/6844903895467032589#heading-14 "https://juejin.cn/post/6844903895467032589#heading-14")》

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

**更准确**:因为带 key 就不是就地复用了,在 sameNode 函数 `a.key === b.key` 对比中可以避免就地复用的情况。所以会更加准确。

**更快速**:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下:

function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66


## Vue 列表为什么加 key

Vue 中列表循环需加 `key="唯一标识"` 唯一标识且最好是静态的,因为 Vue 组件高度复用增加 Key 可以标识组件的唯一性,为了更好地区别各个组件 key 的作用主要是为了高效的更新虚拟 DOM

**解析:**

Vue 和 react 的虚拟 DOM 的 Diff 算法大致相同,其核心是基于两个简单的假设

首先讲一下 diff 算法的处理方法,对操作前后的 dom 树同一层的节点进行对比,一层一层对比。

![](/img/interview/frontend/01.png)

当某一层有很多相同的节点时,也就是列表节点时,Diff 算法的更新过程默认情况下也是遵循以上原则。

比如以下这个情况:

![](/img/interview/frontend/02.png)

可以在 B 和 C 之间加一个 F,Diff 算法默认执行起来是这样的:

![](/img/interview/frontend/03.png)

即把 C 更新成 F,D 更新成 C,E 更新成 D,最后再插入 E,是不是很没有效率?

所以我们需要使用 key 来给每个节点做一个唯一标识,Diff 算法就可以正确的识别此节点,找到正确的位置去插入新的节点。

![](/img/interview/frontend/04.png)


# 其他

## 为什么 Vue 采用异步渲染

Vue 是组件级更新,当前组件里的数据变了,它就会去更新这个组件。当数据更改一次组件就要重新渲染一次,性能不高,为了防止数据一更新就更新组件,所以做了个异步更新渲染。(核心的方法就是 nextTick)

源码实现原理:

当数据变化后会调用 notify 方法,将 watcher 遍历,调用 update 方法通知 watcher 进行更新,这时候 watcher 并不会立即去执行,在 update 中会调用 queueWatcher 方法将 watcher 放到了一个队列里,在 queueWatcher 会根据 watcher 的进行去重,多个属性依赖一个 watcher,如果队列中没有该 watcher 就会将该 watcher 添加到队列中,然后通过 nextTick 异步执行 flushSchedulerQueue 方法刷新 watcher 队列。

flushSchedulerQueue 中开始会触发一个 before 的方法,其实就是 beforeUpdate,然后 watcher.run() 才开始真正执行 watcher,执行完页面就渲染完成啦,更新完成后会调用 updated 钩子。


## Vue 为什么在 HTML 中监听事件

你可能注意到这种事件监听的方式违背了关注点分离(separation of concern)这个长期以来的优良传统。但不必担心,因为所有的 Vue 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。实际上,使用 `v-on` 或 `@` 有几个好处:

* 扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。
* 因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。
* 当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何清理它们。


## axios 是什么,其特点和常用语法

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。前端最流行的 ajax 请求库, react/Vue 官方都推荐使用 axios 发 ajax 请求。

特点: 基于 promise 的异步 ajax 请求库,支持 promise 所有的 API 浏览器端/node 端都可以使用,浏览器中创建 XMLHttpRequests

* 支持请求/响应拦截器
* 支持请求取消
* 可以转换请求数据和响应数据,并对响应回来的内容自动转换成 JSON 类型的数据
* 批量发送多个请求
* 安全性更高,客户端支持防御 XSRF,就是让你的每个请求都带一个从 cookie 中拿到的 key, 根据浏览器同源策略,假冒的网站是拿不到你 cookie 中得 key 的,这样,后台就可以轻松辨别出这个请求是否是用户在假冒网站上的误导输入,从而采取正确的策略。

常用语法:

axios(config): 通用/最本质的发任意类型请求的方式
axios(url[, config]): 可以只指定 url 发 get 请求
axios.request(config): 等同于 axios(config)
axios.get(url[, config]): 发 get 请求
axios.delete(url[, config]): 发 delete 请求
axios.post(url[, data, config]): 发 post 请求
axios.put(url[, data, config]): 发 put 请求
axios.defaults.xxx: 请求的默认全局配置
axios.interceptors.request.use(): 添加请求拦截器
axios.interceptors.response.use(): 添加响应拦截器
axios.create([config]): 创建一个新的 axios(它没有下面的功能)
axios.Cancel(): 用于创建取消请求的错误对象
axios.CancelToken(): 用于创建取消请求的 token 对象
axios.isCancel(): 是否是一个取消请求的错误
axios.all(promises): 用于批量执行多个异步请求
axios.spread(): 用来指定接收所有成功数据的回调函数的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17


### axios 中的路由的拦截器的作用?

顾名思义,拦截器主要是拦截,放到 Vue 里面指对 Vue 发出的 http 请求和响应进行拦截,并可以对拦截的请求或响应做一些特殊的处理。

[拦截器详细介绍](https://github.com/pagekit/Vue-resource/blob/master/docs/http.md)

拦截器能做的

* 添加统一的 request 的参数
比如 header 中加入 `X-Requested-With`,比如客户端需要实现 sign 和 token 的验证机制,比如你可以写 `$http.get('/files', params)`,拦截器帮你拼接成 `http://www.xxxx.com/1/files` 这样的请求地址
* 处理统一的 responseError
* 比如重连机制,拿到 `error.code` 错误码重连,比如 token 过期,重新拿到 token 再次 send request
* 比如统一报错信息,给所有返回的 404 来个提示也会很酷

简单示例

import axios from ‘axios’

// 配置默认的host,假如你的API host是:http://api.htmlx.club
axios.defaults.baseURL = ‘http://api.htmlx.club

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error)
});

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error)
});



### Vue axios 拦截器和 router 导航守卫的区别

**导航守卫**:导航守卫只能在前端路由发生变化时作出判断,此时不一定会发起 ajax 请求,比如

*   检查请求头中是否带有 token,但是不能判断 token 是否失效
*   判断该路由的访问,该用户是否需有权限登录该页面

**axios 拦截器**:拦截的是 ajax 的请求


## Vue 项目前端开发环境请求服务器接口跨域问题

*   对于 vue-cli 2.x 版本在 config 文件夹配置服务器代理;

*   对于 vue-cli 3.x 版本前端配置服务器代理在 `vue.config.js` 中设置服务器代理;
    *   target: 对应的属性值为你准备向后端服务器发送请求的主机+端口,含义为:相当于把前端发送请求的主机+端口自动替换成挂载的主机和端口,这样前后端的主机端口都一一就不会存在跨域问题;
    *   ws: 表示WebSocket协议;
    *   changeOrigin: true;表示是否改变原域名;这个一定要选择为 true;
    
    这样发送请求的时候就不会出现跨域问题了。


## 既然 Vue 通过数据劫持可以精准探测数据在具体 dom 上的变化,为什么还需要虚拟 DOM diff 呢

**前置知识:** 依赖收集、虚拟 DOM、响应式系统

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

*   **pull:** 其代表为 React,我们可以回忆一下 React 是如何侦测到变化的,我们通常会用 setStateAPI 显式更新,然后 React 会进行一层层的 Virtual Dom Diff 操作找出差异,然后 Patch 到 DOM 上,React 从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的 Diff 操作查找「哪发生变化了」,另外一个代表就是 Angular 的脏检查操作。

*   **push:** Vue 的响应式系统则是 push 的代表,当 Vue 程序初始化的时候就会对数据 data 进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知。因此 Vue 是一开始就知道是「在哪发生变化了」,但是这又会产生一个问题,如果你熟悉 Vue 的响应式系统就知道,通常一个绑定一个数据就需要一个 Watcher,但我们的绑定细粒度过高就会产生大量的 Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此 Vue 的设计是选择中等细粒度的方案,在组件级别进行 push 侦测的方式,也就是那套响应式系统,通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行 Virtual Dom Diff 获取更加具体的差异,而 Virtual Dom Diff 则是 pull 操作,Vue 是 push+pull 结合的方式进行变化侦测的。

02-面试之前端 Vue 基础
https://flepeng.github.io/interview-20-开发语言类-21-frontend-03-Vue-02-面试之前端-Vue-基础/
作者
Lepeng
发布于
2023年8月8日
许可协议