Vue2 中的响应式原理

流程图

flowchart TB A0["响应式渲染"] A["初始化阶段"] -- "定义响应式数据(Observer)每个属性实例化 dep = new Dep() 等待消费订阅" --- B["实例化 watcher = new Watcher(fn, cb)"] B --> C["执行 watcher.get()"] C --> D["pushTarget(watcher)"] D --> E["执行初始化函数"] E --> F{"获取响应式数据"} F -- "是 this.x" --> G["触发响应式"] F -- 否 --> H["执行函数,不触发响应式"] G -- "每一个 data 响应式属性被定义,添加订阅者 dep = new Dep(), get 触发:
dep.depend()
Dep.target.addDep()
dep.addSub[watcher]" --- J["watcher 实例中得到 deps -> subs[watcher]"] H --> K["popTarget"] J --> K K --> M["cleanupDeps 交换并重置 deps"] M --> N["结束"] A1["更新阶段"] --> B1["data set: this.x = newValue"] B1 --> C1["触发响应式 dep.notify() 通知更新"] C1 -- "初始阶段实例化 watcher 时添加的订阅者 subs: watcher[] 执行 watcher.update" --- D1["执行重新渲染(微任务)"] D1 --> C A2["Computed 渲染"] --> B2["每个属性实例化 watcher = new Watcher(lazy = true)"] B2 -- 定义响应式 get --- C2["获取值, 触发 watcher.evaluate() 更新值, 添加订阅者 watcher.depend()"] C2 --> D2["等待下一次更新"] D2 --> C

思考

以 Vue computed 选项式解析,watch 解析差异不大

js 复制代码
// Test
const app = new Vue({
  data() {
    return {
      msg: '',
    };
  },
  computed: {
    c_msg({ msg }) {
      return msg + ' world';
    },
  },
  render() {
    this.msg = 'hello';
    return this.c_msg;
  },
});
app.$mount('#root');

data变化时,computed如何监听到变化,更新渲染

🕹 小提示

响应式数据 Vue2 使用 Object.defineProperty

当一个对象被代理时,解构也能监听变化,如下

js 复制代码
let target = { a: 1, b: 2 };
// defineProperty
let keys = Object.keys(target);
for (let i = 0; i < keys.length; i++) {
  let key = keys[i];
  let value = target[key];
  Object.defineProperty(target, key, {
    configurable: true,
    enumerable: true,
    get() {
      console.log('get');
      return value;
    },
    set(newValue) {
      value = newValue;
      target[key] = newValue;
    },
  });
}
// 解构时 targe.a
let { a } = target;
// 输出 get
// proxy
const targetProxy = new Proxy(target, {
  set(o, key, value, receiver) {
    Reflect.set(o, key, value, receiver);
    return true;
  },
  get(o, key, receiver) {
    console.log('proxy get');
    return Reflect.get(o, key, receiver);
  },
});
// 解构时 targetProxy.b
let { b } = targetProxy;
// 输出 proxy get

发布订阅者模式

Vue2.x 中关注 Dep.target 属性(当前运行的组件实例)

核心组件

  • 发布者(Publisher):负责维护订阅者列表,提供添加、删除订阅者的方法,并在状态改变时通知所有订阅者
  • 订阅者(Subscriber):定义更新接口,当收到发布者通知时执行相应的更新操作
  • 事件中心(Event Center):可选的中介者,管理发布者和订阅者之间的关系
classDiagram direction TB note "pushTarget(Watcher)" note "popTarget" class Observer { value: any dep: Dep walk() 监听基本类型值 observeArray() 监听数组 } class Watcher { dirty: boolean lazy: boolean value: any ... deps: [] depIds: Set newDeps: [] newDepIds: Set fn: 同步执行函数 cb: 微任务执行队列 get() addDep() cleanupDeps() update() depend() run() evaluate() } class Dep { static target: null uid: number subs: [] addSub() removeSub() depend() notify() }
  • Observer
    • 定义数据,监听触发变化,通知发布者
  • Watcher
    • 添加订阅者
  • Dep 发布者
    • 触发订阅者

代码实现如下

Watcher

js 复制代码
class Watcher {
  constructor(vm, fn, cb, options) {
    this.vm = vm;

    // lazy 属性用于 computed 使用
    if (options) {
      this.lazy = !!options.lazy;
    } else {
      this.lazy = false;
    }
    this.dirty = this.lazy;

    // 记录发布者
    this.deps = [];
    this.newDeps = [];
    this.depIds = new Set();
    this.newDepIds = new Set();

    // 初始执行函数
    this.fn = fn;
    this.value = this.lazy ? void 0 : this.get();
  }
  get() {
    pushTarget(this);
    let vm = this.vm;
    let value = this.fn.call(vm, vm);
    popTarget();
    this.cleanupDeps();
    return value;
  }
  addDep(dep) {
    let id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        // 添加订阅者
        dep.addSub(this);
      }
    }
  }
  cleanupDeps() {
    let i = this.deps.length;
    while (i--) {
      let dep = this.deps[i];
      console.log('remove sub', dep.id, !this.newDepIds.has(dep.id));
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this);
      }
    }
    let tmp = this.depIds;
    this.depIds = this.newDepIds;
    this.newDepIds = tmp;
    this.newDepIds.clear();
    tmp = this.deps;
    this.deps = this.newDeps;
    this.newDeps = tmp;
    this.newDeps.length = 0;
  }
  update() {
    if (this.lazy) {
      this.dirty = true;
    } else {
      this.run();
    }
  }
  run() {
    this.value = this.get();
  }
  evaluate() {
    this.value = this.get();
    this.dirty = false;
  }
  depend() {
    let i = this.deps.length;
    console.log('this.deps', this.deps);
    while (i--) {
      this.deps[i].depend();
    }
  }
  // teardown
}

Dep

js 复制代码
let uid = 0;
class Dep {
  constructor() {
    this.id = uid++;
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  removeSub(sub) {
    this.subs.splice(this.subs.indexOf(sub), 1);
  }
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }
  notify() {
    console.log('subs', this.subs);
    for (let i = 0; i < this.subs.length; i++) {
      this.subs[i].update();
    }
  }
}
Dep.target = null;
let stack = [];
function pushTarget(target) {
  stack.push(target);
  Dep.target = target;
}
function popTarget() {
  stack.pop();
  Dep.target = stack[stack.length - 1];
}

定义响应式数据

响应式数据定义,并且如何发布消息,订阅者接收,然后执行

js 复制代码
const NOOP = function () {};
const computedWatcherOptions = { lazy: true };
class Vue {
  constructor(options) {
    this.$options = options;
    this.init();
  }
  init() {
    let vm = this;
    let options = this.$options;

    // data
    if (options.data) {
      let data = (vm._data = options.data.call(vm, vm));
      let keys = Object.keys(data);
      let i = keys.length;
      // 借助实例 vm._data 属性来监听实例 data 属性变化
      while (i--) {
        let key = keys[i];
        Object.defineProperty(vm, key, {
          configurable: true,
          enumerable: true,
          get() {
            return vm._data[key];
          },
          set(value) {
            vm._data[key] = value;
          },
        });
      }
      observe(data);
    }

    // computed
    if (options.computed) {
      let computed = options.computed;
      const watchers = (vm._computeWatchers = Object.create(null));
      for (let key in computed) {
        let getter = computed[key];
        watchers[key] = new Watcher(vm, getter, NOOP, computedWatcherOptions);
        Object.defineProperty(vm, key, {
          configurable: true,
          enumerable: true,
          get() {
            let watcher = vm._computeWatchers[key];
            console.log(watcher);
            if (watcher) {
              if (watcher.dirty) {
                watcher.evaluate();
              }
              if (Dep.target) {
                watcher.depend();
              }
            }
            return watcher.value;
          },
          set: NOOP,
        });
      }
    }
  }
  $mount() {
    let vm = this;
    let options = this.$options;
    // 初始渲染函数
    new Watcher(vm, options.render, NOOP);
  }
}
function observe(value) {
  let ob = new Observer(value);
  return ob;
}
class Observer {
  constructor(value) {
    this.value = value;
    // 普通对象
    this.walk(value);
  }
  walk(obj) {
    let keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive$$1(obj, keys[i]);
    }
  }
}
function defineReactive$$1(obj, key) {
  let dep = new Dep();
  let val = obj[key];
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: true,
    get() {
      // 获取值,发布消息
      console.log('Dep.target', Dep.target);
      if (Dep.target) {
        dep.depend();
      }
      return val;
    },
    set(newValue) {
      if (val === newValue) return;
      val = newValue;
      // 值变化了,通知更新
      dep.notify();
    },
  });
}

参考链接

完整代码参考

END

相关推荐
極光未晚1 分钟前
Vue 项目 webpack 打包体积分析:从 “盲猜优化” 到 “精准瘦身”
前端·vue.js·性能优化
刘小筛8 分钟前
Ant Design Vue (2x) 按钮(button)单击后离开,按钮状态无变化
前端
mogullzr11 分钟前
4.1.ByteOJ用户模块——登录注册功能(RSA + TimeStamp加密过)
前端·后端
鹏多多.12 分钟前
flutter-使用AnimatedDefaultTextStyle实现文本动画
android·前端·css·flutter·ios·html5·web
chenburong20211 小时前
uniapp-vue2导航栏全局自动下拉变色
vue.js·uni-app
卑微前端在线挨打1 小时前
2025数字马力一面面经(社)
前端
OpenTiny社区1 小时前
一文解读“Performance面板”前端性能优化工具基础用法!
前端·性能优化·opentiny
拾光拾趣录2 小时前
🔥FormData+Ajax组合拳,居然现在还用这种原始方式?💥
前端·面试
不会笑的卡哇伊2 小时前
新手必看!帮你踩坑h5的微信生态~
前端·javascript
bysking2 小时前
【28 - 记住上一个页面tab】实现一个记住用户上次点击的tab,上次搜索过的数据 bysking
前端·javascript