目录
- Vue 响应式流程
- Vue的事件机制,DOM异步更新机制
- Object.defineProperty 和 ES6 的 Proxy 有什么区别
- 依赖收集器Dep 【作用:收集 与 通知相关Watcher更新视图】
- vue2生命周期?与 用了keep-alive的生命周期【用
activated代替了created和mounted】 - Vue 响应式原理
- methods、computed 和 watch 有什么区别?
- 附加图 与 MVVM 双向数据绑定
- 总结
① Vue 响应式流程
1.1 Vue初始化流程中的编译阶段进行render时
1.1.1 getter依赖收集流程【订阅发布模式】
render 模板时 -> 得到视图中使用到的响应式数据 -> 触发getter钩子进行依赖收集 -> 将观察者Watcher对象 -> 存放到当前闭包的订阅者 Dep -> 的subs中 ->依赖收集完成 -> 渲染
- 在
init的时候会利用Object.defineProperty方法(不兼容IE8)监听Vue实例的响应式数据的变化从而实现数据劫持能力(利用了JavaScript对象的访问器属性get和set,在Vue3中会使用ES6的Proxy来优化响应式原理)。在初始化流程中的编译阶段,当render function被渲染的时候,会读取Vue实例中和视图相关的响应式数据,此时会触发getter函数进行依赖收集(将观察者Watcher对象存放到当前闭包的订阅者Dep的subs中),此时的数据劫持功能和观察者模式就实现了一个MVVM模式中的Binder,之后就是正常的渲染和更新流程。
1.1.2 setter 数据变化时
数据变化时 -> 触发数据劫持的setter -> 通知依赖收集的Dep中的订阅者中的 -> Watcher对象 -> 调用update -> 重新渲染视图
- 当数据发生变化或者视图导致的数据发生了变化时,会触发数据劫持的
setter函数,setter会通知初始化依赖收集中的Dep中的和视图相应的Watcher,告知需要重新渲染视图,Wather就会再次通过update方法来更新视图。
② Watch的运行原理 【异步更新视图】
Vue的数据为什么频繁变化但只会更新一次
Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。
由于VUE的数据驱动视图更新是异步的,即修改数据的当下,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。在同一事件循环中的数据变化后,DOM完成更新,立即执行nextTick(callback)内的回调。
Vue的事件机制 【异步更新】
管理调度一个事件队列,会等待主线程执行空闲后进行调度。所以,并不是你修改data中的一个属性a,vue就会直接更新到视图。
举例子
在mounted时,test的值会被循环执行++1000次。每次++时,都会根据响应式触发
setter->Dep->Watch-> update -> run。 可想而知,如果vue没有做异步更新视图,那么每次++都会直接操作DOM更新视图。这是非常耗费性能的。
所以,Vue 实现了一个队列,在下一个tick (或者当前任务的微任务阶段),统一执行队列中的Watcher的run。 同时,每个Watcher 都是有ID的,拥有相当ID的Watcher不会被重复加入到队列中。所以,不会执行 1000 次 Watcher的run。最终,更新视图只会直接将test对应的DOM 的 0 变成 1000。
执行顺序
(数据修改)-> update -> queueWatcher -> 维护观察者队列(重复id的Watcher处理) -> waiting标志位处理(保证需要更新DOM或者Watcher视图更新的方法flushSchedulerQueue只会被推入异步执行的$nextTick回调数组一次) -> 处理$nextTick(在为微任务或者宏任务中异步更新DOM) (Promise.then不支持的话降级策略到setTimeout来执行此次更新任务)
③ Object.defineProperty 和 ES6 的 Proxy 有什么区别?
Proxy的优势如下
- Proxy可以直接监听整个对象而非属性。
- Proxy可以直接监听数组的变化。
- Proxy有13中拦截方法,如ownKeys、deleteProperty、has 等是 Object.defineProperty 不具备的。
- Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改;
- Proxy做为新标准将受到浏览器产商重点持续的性能优化,也就是传说中的新标准的性能红利。
Object.defineProperty 的优势如下
- 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平。
Object.defineProperty 不足在于:
- Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
- Object.defineProperty不能监听数组。是通过重写数据的那7个可以改变数据的方法来对数组进行监听的。
- Object.defineProperty 也不能对 es6 新产生的 Map,Set 这些数据结构做出监听。
- Object.defineProperty也不能监听新增和删除操作,通过 Vue.set()和 Vue.delete来实现响应式的。
④ 依赖收集器Dep 【作用:收集 与 通知相关Watcher更新视图】
4.1 Dep的作用
- Dep的作用是收集观察者以及当数据发生变动时通知观察者去更新
- 每一个属性都有自身的dep,接着添加watcher,在每次数据变动时,通知自身的dep,dep通知其中watcher去完成视图更新
class Dep {
constructor () {
this.subs = []
}
// 收集观察者
addSub (watcher) {
this.subs.push(watcher)
}
// 通知观察者去更新
notify () {
this.subs.forEach(w => w.update())
}
}
⑤ vue2生命周期?与 用了keep-alive的生命周期【用activated代替了created和mounted】
总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后。
创建前/后: 在beforeCreate阶段,vue实例的挂载元素el和数据对象data都为undefined,还未初始化。在created阶段,vue实例的数据对象data有了,el为undefined,还未初始化。
载入前/后:在beforeMount阶段,vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点,data.message还未替换。在mounted阶段,vue实例挂载完成,data.message成功渲染。
更新前/后:当data变化时,会触发beforeUpdate和updated方法
销毁前/后:在执行destroy方法后,对data的改变不会再触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在
5.1 Vue 之keep-alive的使用,【实现页面缓存】
有时候我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就需要用到
keep-alive组件。
keep-alive的应用场景
如果未使用
keep-alive组件,则在页面回退时仍然会重新渲染页面,触发created钩子,使用体验不好。 在以下场景中使用keep-alive组件会显著提高用户体验:
- 商品列表页点击商品跳转到商品详情,返回后仍显示原有信息
- 订单列表跳转到订单详情,返回,等等场景。
keep-alive的生命周期
-
初次进入时:
created>mounted>activated- 退出后触发
deactivated
-
再次进入:
- 只会触发
activated
- 只会触发
-
事件挂载的方法等,只执行一次的放在
mounted中;组件每次进去执行的方法放在activated中
⑥ Vue 响应式原理是怎么实现的?
-
响应式的核心是通过
Object.defineProperty拦截对数据的访问和设置 -
响应式的数据分为两类:
-
对象,循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依然为对象,则递归为属性值上的每个 key 设置 getter、setter
- 访问数据时(obj.key)进行依赖收集,在 dep 中存储相关的 watcher
- 设置数据时由 dep 通知相关的 watcher 去更新
-
数组,增强数组的那 7 个可以更改自身的原型方法,然后拦截对这些方法的操作
- 添加新数据时进行响应式处理,然后由 dep 通知 watcher 去更新
- 删除数据时,也要由 dep 通知 watcher 去更新
-
6.1 getter时 Dep 去依赖收集(Watcher)
Object.defineProperty -> 为每个属性设置getter和setter(属性为对象时递归) -> 访问数据时 -> 依赖收集 -> 在Dep中存储相关的Watcher ->
6.2 setter时 Dep 去通知 (Watcher)
设置数据时 -> dep 通知 -> 相关的Watcher -> update更新视图
⑦ methods、computed 和 watch 有什么区别?
- methods 一般用于封装一些较为复杂的处理逻辑(同步、异步)
- computed 一般用于封装一些简单的同步逻辑,将经过处理的数据返回,然后显示在模版中,以减轻模版的重量
- watch 一般用于当需要在数据变化时执行异步或开销较大的操作
参考
- 依赖收集与数据修改通知Watcher去更新视图
- 最全的 Vue 面试题+详解答案
- 手写 vue3.0 源码
- Vue 核心之数据双向绑定
- [Vue的运行机制]juejin.cn/post/684490…
⑧ 附加图
8.1, Vue 核心之数据双向绑定
8.2, 什么是 MVVM 数据双向绑定
MVVM 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示
即:
- 输入框内容变化时,
Data中的数据同步变化。即View=>Data的变化。 Data中的数据变化时,文本节点的内容同步变化。即Data=>View的变化。
其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以我们本文主要讨论如何根据 Data 变化更新 View。
我们会通过实现以下 4 个步骤,来实现数据的双向绑定:
1、实现一个监听器 Observer ,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;
2、实现一个订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理;
3、实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图;
4、实现一个解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。
8.3 Vue的运行机制
⑨ 总结
-
Vue 中某个数据被修改后,【根据响应式触发
setter -> Dep -> Watch -> update】 -
Vue 是异步更新Dom的,Dom的更新放在下一个宏任务或者当前宏任务的末尾(微任务)中进行执行
-
vue和react一样,对dom的修改都是异步的。它会在队列里记录你对dom的操作并进行diff操作,后一个操作会覆盖前一个,然后更新dom。
-
Vue 依赖收集流程
首次render时 -> getter -> Dep收集依赖(Watcher对象) -> 存放到Dep中 -> 开始render
-
Vue DOM更新流程
数据变化时 -> setter -> Dep去通知 -> 相关的Watcher对象 -> update更新视图
-
keep-alive的生命周期
首次进入时生命周期
created > mounted > activated > deactivated
后续进入时生命周期
activated > deactivated