Vue-router 导航守卫有哪些
- 全局前置/钩子:beforeEach、beforeResolve、afterEach
- 路由独享的守卫:beforeEnter
- 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(Vue 为什么不能检测数组变动 )。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组
push();
pop();
shift();
unshift();
splice();
sort();
reverse();
复制代码
由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
v-for 为什么要加 key
如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
如何从真实DOM到虚拟DOM
涉及到Vue中的模板编译原理,主要过程:
- 将模板转换成
ast
树,ast
用对象来描述真实的JS语法(将真实DOM转换成虚拟DOM) - 优化树
- 将
ast
树生成代码
为什么Vue采用异步渲染呢?
Vue
是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue
会在本轮数据更新后,在异步更新视图。核心思想 nextTick
。
dep.notify()
通知 watcher进行更新, subs[i].update
依次调用 watcher 的 update
, queueWatcher
将watcher 去重放入队列, nextTick( flushSchedulerQueue
)在下一tick中刷新watcher队列(异步)。
为什么vue组件中data必须是一个函数?
对象为引用类型,当复用组件时,由于数据对象都指向同一个data对象,当在一个组件中修改data时,其他重用的组件中的data会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object的实例),引用地址不同,则不会出现这个问题。
MVC 和 MVVM 区别
MVC
MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范
- Model(模型):是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据
- View(视图):是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的
- Controller(控制器):是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据
MVC 的思想:一句话描述就是 Controller 负责将 Model 的数据用 View 显示出来,换句话说就是在 Controller 里面把 Model 的数据赋值给 View。
MVVM
MVVM 新增了 VM 类
- ViewModel 层:做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
MVVM 与 MVC 最大的区别就是:它实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)
整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性
注意:Vue 并没有完全遵循 MVVM 的思想 这一点官网自己也有说明
那么问题来了 为什么官方要说 Vue 没有完全遵循 MVVM 思想呢?
- 严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。
Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?
1)Vue为什么要用vm.$set() 解决对象新增属性不能响应的问题
- Vue使用了Object.defineProperty实现双向数据绑定
- 在初始化实例时对属性执行 getter/setter 转化
- 属性必须在data对象上存在才能让Vue将它转换为响应式的(这也就造成了Vue无法检测到对象属性的添加或删除)
所以Vue提供了Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
2)接下来我们看看框架本身是如何实现的呢?
Vue 源码位置:vue/src/core/instance/index.js
export function set (target: Array | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
我们阅读以上源码可知,vm.$set 的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,
- 最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理
defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法
Vue3.0 和 2.0 的响应式原理区别
Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。
相关代码如下
import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑
import { isObject } from "./util"; // 工具方法
export function reactive(target) {
// 根据不同参数创建不同响应式对象
return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {
if (!isObject(target)) {
return target;
}
const observed = new Proxy(target, baseHandler);
return observed;
}
const get = createGetter();
const set = createSetter();
function createGetter() {
return function get(target, key, receiver) {
// 对获取的值进行放射
const res = Reflect.get(target, key, receiver);
console.log("属性获取", key);
if (isObject(res)) {
// 如果获取的值是对象类型,则返回当前对象的代理对象
return reactive(res);
}
return res;
};
}
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if (!hadKey) {
console.log("属性新增", key, value);
} else if (hasChanged(value, oldValue)) {
console.log("属性值被修改", key, value);
}
return result;
};
}
export const mutableHandlers = {
get, // 当获取属性时调用此方法
set, // 当修改属性时调用此方法
};
Vue模版编译原理知道吗,能简单说一下吗?
简单说,Vue的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
- 生成AST树
- 优化
- codegen
首先解析模版,生成AST语法树
(一种用JavaScript对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
MVVM的优缺点?
优点:
- 分离视图(View)和模型(Model),降低代码耦合,提⾼视图或者逻辑的重⽤性: ⽐如视图(View)可以独⽴于Model变化和修改,⼀个ViewModel可以绑定不同的"View"上,当View变化的时候Model不可以不变,当Model变化的时候View也可以不变。你可以把⼀些视图逻辑放在⼀个ViewModel⾥⾯,让很多view重⽤这段视图逻辑
- 提⾼可测试性: ViewModel的存在可以帮助开发者更好地编写测试代码
- ⾃动更新dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动dom中解放
缺点:
- Bug很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异常了,有可能是你View的代码有Bug,也可能是Model的代码有问题。数据绑定使得⼀个位置的Bug被快速传递到别的位置,要定位原始出问题的地⽅就变得不那么容易了。另外,数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的
- ⼀个⼤的模块中model也会很⼤,虽然使⽤⽅便了也很容易保证了数据的⼀致性,当时⻓期持有,不释放内存就造成了花费更多的内存
- 对于⼤型的图形应⽤程序,视图状态较多,ViewModel的构建和维护的成本都会⽐较⾼。
Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?
不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。
diff算法
时间复杂度: 个树的完全 diff
算法是一个时间复杂度为 O(n*3)
,vue进行优化转化成 O(n)
。
理解:
最小量更新,
key
很重要。这个可以是这个节点的唯一标识,告诉diff
算法,在更改前后它们是同一个DOM节点- 扩展
v-for
为什么要有key
,没有key
会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改DOM),加key
只会移动减少操作DOM。
- 扩展
- 只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。
- 只进行同层比较,不会进行跨层比较。
diff算法的优化策略:四种命中查找,四个指针
- 旧前与新前(先比开头,后插入和删除节点的这种情况)
- 旧后与新后(比结尾,前插入或删除的情况)
- 旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)
- 旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)
--- 问完上面这些如果都能很清楚的话,基本O了 ---
以下的这些简单的概念,你肯定也是没有问题的啦
Vue的优点
- 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十
kb
; - 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
- 双向数据绑定:保留了
angular
的特点,在数据操作方面更为简单; - 组件化:保留了
react
的优点,实现了html
的封装和重用,在构建单页面应用方面有着独特的优势; - 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
- 虚拟DOM:
dom
操作是非常耗费性能的,不再使用原生的dom
操作节点,极大解放dom
操作,但具体操作的还是dom
不过是换了另一种方式; - 运行速度更快:相比较于
react
而言,同样是操作虚拟dom
,就性能而言,vue
存在很大的优势。
vue-router 路由钩子函数是什么 执行顺序是什么
路由钩子的执行流程, 钩子函数种类有:全局守卫、路由守卫、组件守卫
完整的导航解析流程:
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫 (2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
Vue.js的template编译
简而言之,就是先转化成AST树,再得到的render函数返回VNode(Vue的虚拟DOM节点),详细步骤如下:
首先,通过compile编译器把template编译成AST语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile是createCompiler的返回值,createCompiler是用以创建编译器的。另外compile还负责合并option。
然后,AST会经过generate(将AST语法树转化成render funtion字符串的过程)得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,里面有(标签名、子节点、文本等等)
$nextTick 是什么?
Vue 实现响应式并不是在数据发生后立即更新 DOM,使用 vm.$nextTick
是在下次 DOM 更新循环结束之后立即执行延迟回调。在修改数据之后使用,则可以在回调中获取更新后的 DOM。
说说Vue的生命周期吧
什么时候被调用?
- beforeCreate :实例初始化之后,数据观测之前调用
- created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、
watch/event
事件回调。无$el
. - beforeMount:在挂载之前调用,相关
render
函数首次被调用 - mounted:了被新创建的
vm.$el
替换,并挂载到实例上去之后调用改钩子。 - beforeUpdate:数据更新前调用,发生在虚拟DOM重新渲染和打补丁,在这之后会调用改钩子。
- updated:由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用改钩子。
- beforeDestroy:实例销毁前调用,实例仍然可用。
- destroyed:实例销毁之后调用,调用后,Vue实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除
每个生命周期内部可以做什么?
- created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。
- mounted:实例已经挂载完成,可以进行一些DOM操作。
- beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。
- updated:可以执行依赖于DOM的操作,但是要避免更改状态,可能会导致更新无线循环。
- destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。
ajax放在哪个生命周期?:一般放在 mounted
中,保证逻辑统一性,因为生命周期是同步执行的, ajax
是异步执行的。单数服务端渲染 ssr
同一放在 created
中,因为服务端渲染不支持 mounted
方法。 什么时候使用beforeDestroy?:当前页面使用 $on
,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove
。
Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?
受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
来实现为对象添加响应式属性,那框架本身是如何实现的呢?
我们查看对应的 Vue 源码:vue/src/core/instance/index.js
export function set (target: Array | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
我们阅读以上源码可知,vm.$set 的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)