响应式对象核心是利用了 Object.defineProperty 给对象的属性添加 getter 和 setter
Vue 会把 props、data 等变成响应式对象,在创建过程中,发现子属性也为对象则递归把该对象变成响应式

getter

依赖收集

  1. get: function reactiveGetter() {
  2. const value = getter ? getter.call(obj) : val
  3. if (Dep.target) {
  4. dep.depend()
  5. if (childOb) {
  6. childOb.dep.depend()
  7. if (Array.isArray(value)) {
  8. dependArray(value)
  9. }
  10. }
  11. }
  12. return value
  13. }

当对数据进行访问的时候会触发 getter ,dep.depend() 实际上就是将 当前对象的 watch 收集起来添加到 this.subs 里面去。

依赖收集就是订阅数据变化的 watcher 的收集
依赖收集的目的是为了当这些响应式数据发送变化,触发它们的 setter 的时候,能知道应该通知哪些订阅者去做相应的逻辑处理。

setter

派发更新

  1. set: function reactiveSetter(newVal) {
  2. const value = getter ? getter.call(obj) : val
  3. if (newVal === value || (newVal !== newVal && value !== value)) {
  4. return
  5. }
  6. if (process.env.NODE_ENV !== 'production' && customSetter) {
  7. customSetter()
  8. }
  9. if (setter) {
  10. setter.call(obj, newVal)
  11. } else {
  12. val = newVal
  13. }
  14. childOb = !shallow && observe(newVal)
  15. dep.notify()
  16. }

派发更新就是当数据发生改变后,通知所有订阅了这个数据变化的 watcher 执行 update
派发更新的过程中会把所有要执行 update 的 watcher 推入到队列中,在 nextTick 后执行 flush 。

nextTick

nextTick 是把要执行的任务推入到一个队列中,在下一个 tick 同步执行
数据改变后触发渲染 watcher 的 update ,但是 watchers 的 flush 是在 nextTick 后,所以重新渲染是异步的。

计算属性

Watch属性

  • 计算属性的本质是 computed watcher
  • 侦听属性的本质是 user watcher ,它还支持 deep、sync、immediate 等配置
  • 计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

    组件更新

  • 组件更新的过程核心就是新旧 vnode diff,对新旧节点相同以及不同的情况分别做不同的处理。

  • 新旧节点不同的更新流程是创建新节点 -> 更新占位符节点 -> 删除旧节点
  • 新旧节点相同的更新流程是去获取它们的 children,根据不同情况做不同的更新逻辑。

响应式系统

设置响应式对象 - Observer

首先引入一个类 Observer ,这个类的目的是将数据变成响应式对象,利用 Object.defineProperty 对数据的 getter , setter 方法进行改写。在数据读取 getter 阶段进行依赖的收集,在数据的修改 setter 阶段,进行依赖的更新。因此,在数据初始化阶段,我们会利用 Observer 这个类将数据对象修改为响应式对象。

  1. class MyVue {
  2. initData(options) {
  3. if(!options.data) return;
  4. this.data = options.data;
  5. // 将数据重置getter,setter方法
  6. new Observer(options.data);
  7. }
  8. }
  9. // Observer类的定义
  10. class Observer {
  11. constructor(data) {
  12. // 实例化时执行walk方法对每个数据属性重写getter,setter方法
  13. this.walk(data)
  14. }
  15. walk(obj) {
  16. const keys = Object.keys(obj);
  17. for(let i = 0;i< keys.length; i++) {
  18. // Object.defineProperty的处理逻辑
  19. defineReactive(obj, keys[i])
  20. }
  21. }
  22. }

依赖本身 - Watcher

一个 Watcher 实例就是一个依赖,数据不管是在渲染模板时使用还是在用户计算时使用,都可以算做一个需要监听的依赖, watcher 中记录着这个依赖监听的状态,以及如何更新操作的方法。
在渲染数据到真实 DOM 时可以创建 watcher 。 $mount 会经历模板生成 render 函数和 render 函数渲染真实 DOM 的过程。

依赖管理 - Dep

watcher 如果理解为每个数据需要监听的依赖,那么 Dep 可以理解为对依赖的一种管理。数据可以在渲染中使用,也可以在计算属性中使用。相应的每个数据对应的 watcher 也有很多。而我们在更新数据时,如何通知到数据相关的每一个依赖,这就需要 Dep 进行通知管理了。并且浏览器同一时间只能更新一个 watcher ,所以也需要一个属性去记录当前更新的 watcher 。而 Dep 这个类只需要做两件事情,将依赖进行收集,派发依赖进行更新。

  1. let uid = 0;
  2. class Dep {
  3. constructor() {
  4. this.id = uid++;
  5. this.subs = []
  6. }
  7. // 依赖收集
  8. depend() {
  9. if(Dep.target) {
  10. // Dep.target是当前的watcher,将当前的依赖推到subs中
  11. this.subs.push(Dep.target)
  12. }
  13. }
  14. // 派发更新
  15. notify() {
  16. const subs = this.subs.slice();
  17. for (var i = 0, l = subs.length; i < l; i++) {
  18. // 遍历dep中的依赖,对每个依赖执行更新操作
  19. subs[i].update();
  20. }
  21. }
  22. }
  23. Dep.target = null;

依赖管理过程 - defineReactive

前面的 Observer 实例化最终会调用 defineReactive 重写 getter ,setter 方法。这个方法开始会实例化一个 Dep ,也就是创建一个数据的依赖管理。在重写的 getter 方法中会进行依赖的收集,也就是调用 dep.depend 的方法。在 setter 阶段,比较两个数不同后,会调用依赖的派发更新。即 dep.notify 。

  1. const defineReactive = (obj, key) => {
  2. const dep = new Dep();
  3. const property = Object.getOwnPropertyDescriptor(obj);
  4. let val = obj[key]
  5. if(property && property.configurable === false) return;
  6. Object.defineProperty(obj, key, {
  7. configurable: true,
  8. enumerable: true,
  9. get() {
  10. // 做依赖的收集
  11. if(Dep.target) {
  12. dep.depend()
  13. }
  14. return val
  15. },
  16. set(nval) {
  17. if(nval === val) return
  18. // 派发更新
  19. val = nval
  20. dep.notify();
  21. }
  22. })
  23. }

回过头来看 watcher ,实例化 watcher 时会将 Dep.target 设置为当前的 watcher ,执行完状态更新函数之后,再将 Dep.target 置空。这样在收集依赖时只要将 Dep.target 当前的 watcher push 到 Dep 的 subs 数组即可。而在派发更新阶段也只需要重新更新状态即可。

Vue2原理-Object.defineProperty

Vue3原理-Proxy、Reflect

参考文章: