# 一、Vue2 解析

本次分析的Vue2版本为2.6。

# 1. 响应式原理

  1. new Vue 开始,经过了一系列调用 init (opens new window)initState (opens new window)initData (opens new window)observe (opens new window)new Obsever (opens new window)defineReactive (opens new window),来看下defineReactive的实现。
function defineReactive(obj, key, val) {
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      const value = val;
      // 如果存在依赖此数据的Watcher,则进行依赖搜集
      if (Dep.target) {
        dep.depend();
      }
      return value;
    },
    set(newVal) {
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      val = newVal;
      // 数据更新的时候进行派发更新
      dep.notify();
    },
  });
}
  1. 通过 get (opens new window)set (opens new window) 监听 Data 中的数据变化,同时为每一个属性创建 Dep (opens new window) 用来搜集使用该 DataWatcher,来看下Dep的实现。
class Dep {
  static target;
  // subs 存放的 Watcher 对象集合
  subs;

  constructor() {
    this.subs = [];
  }
  addSub(sub: Watcher) {
    this.subs.push(sub);
  }
  depend() {
    if (Dep.target) {
      // 依赖收集,会调用上面的 addSub 方法
      Dep.target.addDep(this);
    }
  }
  notify() {
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}
  1. 编译模板,创建 Watcher (opens new window),并将 Dep.target 标识为当前 Watcher,来看下Watcher的实现。
class Watcher {
  constructor(vm, expOrFn) {
    // expOrFn 就是 vm._render
    this.getter = expOrFn;
    this.value = this.get();
  }
  get() {
    Dep.target = this;
    // 重新触发_render函数,生成VDom、更新Dom
    const value = this.getter.call(this.vm, this.vm);
    return value;
  }
  addDep(dep) {
    // 收集当前的Watcher为依赖
    dep.addSub(this);
  }
  update() {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  run() {
    const value = this.get();
  }
}
  1. 编译模板时,如果使用到了 Data 中的数据,就会触发 Dataget 方法,然后调用 Dep.addSubWatcher 搜集起来。
  2. 数据更新时,会触发 Dataset 方法,然后调用 dep.notify (opens new window),进而调用 watcher.update (opens new window) 方法,将所有使用到这个 DataWatcher 加入一个异步队列 (opens new window)
  3. 最终执行 _render (opens new window) 方法完成页面更新

流程图如下:

# 2. watch原理

computedwatch内部都是利用了watcheruser watcher的过程如下:

  1. Vue 在 initWatch (opens new window) 过程中,创建Watcher,并设置标志位 user (opens new window)true,并判断用户是否设置了immediatetrue,如果是,立即执行回调;
  2. watch的对象 update (opens new window) 时,判断是否设置了synctrue,如果是,不加入异步队列,直接更新;
  3. Watcher更新时判断标志位user是否为true,如果是,则执行用户传入的 cb (opens new window),把newValoldVal传入。

# 3. computed原理

看这个例子:

computed: {
  name() {
    return `My name is ${this.user.name}`;
  }
}

computed watcher的过程如下:

  1. Vue 在 initComputed (opens new window) 过程中,创建标志位lazytrueWatcher
  2. 因为初始化的时候dirty=lazy=true,会调用 watcher.evaluate (opens new window) 方法进行一次求值 this.getter.call(vm, vm) (opens new window) ,此时会访问this.user.name,所以会触发其依赖收集。这时候Dep.target的值为computed watcher,依赖收集完后,this.user.namedep中就有了computed watcher
  3. 然后在watcher.evaluate中将dirty设置为false
  4. 如果Dep.target存在,则调用 watcher.depend (opens new window) 进行一次render watcher的收集;
// 创建computed的getter的工厂函数
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
  1. name 值改变时,会触发set,然后通知 computed watcher,执行 update (opens new window) 方法,并将dirty设置为true
  2. 再次访问computed属性时,如果dirtyfalse,则不会执行 watcher.evaluate (opens new window) 方法,直接返回之前缓存的值,如果dirtytrue,则重新计算。

# 二、Vue3 解析

Vue3 的代码在 vue-next (opens new window) 仓库中,本次分析的版本是3.2。响应式部分在reactivity文件夹中,并且可独立引用。

Vue3 的响应式多了一个副作用函数,即effect函数,指的是响应式数据在发生变更的时候,要执行的函数。

# 1. 响应式原理

  1. reactive (opens new window) 函数对包裹的对象进行 proxy (opens new window) 代理,在 get (opens new window) 中利用 track (opens new window) 函数进行依赖收集,在 set (opens new window) 中利用 trigger (opens new window) 派发更新。
function reactive(obj: any) {
  const proxy = new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key);
      let res = Reflect.get(target, key);
      return res;
    },
    set: function (target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return res;
    }
  });
  return proxy;
}
  1. 依赖收集在 targetMap (opens new window) 中,其是一个WeakMapkey是响应式对象,valueMap类型的 depsMap (opens new window)depsMapkey是响应式对象的keyvalueeffect (opens new window) 函数。
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

function trigger(target: any, key: ObjKeyType) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }

  let deps = depsMap.get(key);
  if (deps) {
    deps.forEach((efn: EffectFn) => efn());
  }
}

function track(target: any, key: ObjKeyType) {
  if (effectStack.length === 0) return;

  let depMap = targetMap.get(target);
  if (!depMap) {
    targetMap.set(target, (depsMap = new Map()))
  }

  let deps = depMap.get(key);
  if (!deps) {
    depMap.set(key, (deps = new Set()));
  }

  // 添加栈顶副作用作为依赖
  deps.add(getCurrentEffect());
}
  1. 响应式Data更新的时候会触发 trigger (opens new window),然后从targetMap中取出对应的依赖进行更新。
  2. effect函数的创建时机包括 mountComponent (opens new window)computed (opens new window)watch (opens new window) 等。

流程图如下:

# 2. ref原理

ref (opens new window) 是一个语法糖,返回一个对象,其在get中调用trackset中调用trigger

function ref(value) {
  const res = {
    get value() {
      track(res, 'value');
      return value;
    },
    set value(newVal) {
      value = newVal;
      trigger(res, 'value');
    }
  };

  return res;
}

# 3. Vue3 的computed原理

  1. computed (opens new window) 内部用effect函数包裹传入的函数getter,并执行getter,拿到value
  2. 内部effect中调用了trigger,这样computed依赖的值变化的时候,会触发此effect函数执行,也就能够触发依赖computedeffect函数也得到执行;
  3. 构造一个对象,对象的get方法中调用了track,进行了一次依赖收集;
  4. 最后返回构造的对象
function computed(getter) {
  let value;
  let res;
  effect(() => {
    value = getter();
    trigger(res, 'value');
  })
  res = {
    get value() {
      track(this, 'value');
      return value;
    }
  }
  return res;
}

# 三、总结

对比 Vue2 和 Vue3 的响应式实现方式的不同,可以看出

  1. Vue2 使用Object.defineProperty进行数据劫持,Vue3 使用Proxy,后者优势在于可以劫持pushpop等方法,也因为在顶层对象直接劫持,可以提高性能。
  2. 依赖收集器的数据结构有变化,Vue2 的依赖收集在Dep.subs中,也就是一个类的数组中,Vue3 的依赖收集在targetMap中,其是一个WeakMap
  3. Vue3 的响应式结构更加简单,与其他部分耦合性小,并已经独立成包,即可以和其他框架进行结合。

# 四、相关资料

  1. 图解 Vue 响应式原理 (opens new window)
  2. 搞懂computed和watch原理,减少使用场景思考时间 (opens new window)
  3. Vue源码之computed和watch (opens new window)
  4. Vue.js 技术揭秘 (opens new window)
  5. vue3.0响应式函数原理 (opens new window)
  6. 手写Vue3 响应式(Reactivity)模块 (opens new window)
  7. vue3源码分析(三)—— 响应式系统(reactivity) (opens new window)
  8. Vue3 深度解析 (opens new window)