Vue数据响应式原理执行过程

773 阅读4分钟

简介

Vue2 基于 Object.defineProperty(简称 defineProperty)  实现数据响应式,通过劫持对象属性的 getter 和 setter 来完成依赖收集与更新派发,要实现mvvm的双向绑定,就必须要实现以下几点: 1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者 2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数 3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图 4、mvvm入口函数,整合以上三者

代码

mvvm

原理图示

image.png

class MVVM {
  constructor(options) {
    this.$el = options.el
    this.$data = options.data
    this.proxyData(this.$data)
    observe(this.$data)
    if (this.$el) new Compiler(this.$el, this)
  }
//...
}

observe(this.$data)

数据劫持:进入vue时,对data的数据进行递归defineReactive,劫持监听所有属性,同时给每个属性new一个Dep类(依赖对象)(收集watcher,通知watcher去更新,但收集并不在这个阶段

function defineReactive(data, key, val) {
  const dep = new Dep()
  let childOb = observe(val)
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(val)) {
            dependArray(val)
          }
        }
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      if (val === newVal) {
        return
      }
      val = newVal
      childOb = observe(newVal)
      dep.notify()
    }
  })
}

new Compiler(this.$el, this)

依赖收集:解析模板指令,{{}},为每个指令new一个Watcher(观察者),并在对应data属性的dep中添加这个Watcher;发布通知:当属性值发生变化时,触发set方法,在里面Dep 会通知所有依赖它的 Watcher,Watcher 会触发 DOM 更新

对应关系

  • Dep与Watcher之间是多对多的关系
  • 一个data属性对应一个Dep,一个Dep对应n个Watcher(属性多次在模板中被使用时n>1:{{a}}/v-text='a')
  • 一个表达式对应一个Watcher, 一个Watcher对应n个Dep(多层表达式时n>1:a.b.c)
class Watcher {
  constructor(data, expression, cb) {
    this.data = data
    this.expression = expression
    this.cb = cb
    this.value = this.get()
  }

  get() {
    Dep.target = this;
    const value = parsePath(this.data, this.expression)// 这里会触发属性的getter,从而添加订阅者
    Dep.target = null
    return value
  }

  update() {
    const oldValue = this.value
    this.value = parsePath(this.data, this.expression)
    this.cb.call(this.data, this.value, oldValue)
  }
}

vue3响应式原理

初始化

  • 使用 new Proxy(target, handler) 代理目标对象。
  • 通过 handler 的 get 和 set 拦截器捕获属性的读写操作
const original = { count: 0 };
const observed = reactive(original);

// reactive()内部简化实现
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集逻辑
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      // 派发更新逻辑
      if (oldValue !== value) {
        trigger(target, key);
      }
      return result;
    }
  });
}

依赖搜集

  • 通过全局变量 activeEffect 记录当前执行的副作用函数。
  • 当副作用函数读取响应式属性时,触发 Proxy 的 get 拦截器。
  • 调用track方法,获取目标对象的依赖映射 -> 获取属性的依赖集合 -> 将当前副作用添加到对应属性依赖集合 -> 反向关联:让副作用知道自己依赖了哪些属性
function track(target, key) {
  if (activeEffect) {
    // 获取目标对象的依赖映射
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()));
    }
    // 获取属性的依赖集合
    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, (dep = new Set()));
    }
    // 将当前副作用函数添加到依赖集合中
    dep.add(activeEffect);
    // 反向关联:让副作用函数知道自己依赖了哪些属性
    activeEffect.deps.push(dep);
  }
}

activeEffect(reactiveEffect)配置项

effect.fn = fn; // 存储原始副作用函数
effect.deps = []; // 存储依赖集合
effect.active = true; // 活跃状态标记
effect.options = options; // 存储配置选项

通知所有依赖

当响应式对象的属性被修改时,触发 Proxy 的 set 拦截器, 调用trigger执行所有依赖(副作用函数,其中包括渲染函数)

function trigger(target, key) {
  // 获取目标对象的依赖映射
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  // 获取属性的依赖集合
  const dep = depsMap.get(key);
  if (dep) {
    // 执行所有依赖(副作用函数)
    dep.forEach(effect => effect());
  }
}

关键数据结构

  • targetMap(WeakMap):存储所有响应式对象的依赖关系

    targetMap = {
      target(obj1) => Map {
        'key1' => Set([effect1, effect2]),
        'key2' => Set([effect3])
      },
      target(obj2) => ...
    }
    

响应式对比缺点

  1. 无法检测属性添加 / 删除:Vue2 无法自动响应对象属性的添加或删除,需要使用 Vue.set() 或 this.$set()
  2. 数组方法限制:Vue2 部分数组方法(如直接通过索引修改元素、修改数组长度)无法触发响应式更新www.cnblogs.com/karthuslori…
  3. 性能开销大:Vue2 对于大型对象,递归转换所有属性会导致初始渲染性能下降。
  4. 依赖收集效率
  • Vue2:采用 “全量收集” 策略,无论属性是否被使用,初始化时都会绑定依赖,可能产生无效依赖。
  • Vue3:采用 “懒收集” 策略,仅当属性被访问时才收集依赖,避免无效依赖记录,内存占用更低。WeakMap 存储:响应式对象作为 key,垃圾回收时自动释放内存。
  1. 更新性能
  • Vue2:属性更新时需通知所有依赖的 Watcher,可能触发不必要的组件更新。
  • Vue3:依赖收集更精准,更新时仅触发相关组件重新渲染,减少无效更新。