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

相关推荐
合作小小程序员小小店33 分钟前
web渗透PHP反序列化漏洞
前端·网络协议·web安全·网络安全·安全威胁分析
再学一点就睡6 小时前
初探 React Router:为手写路由筑牢基础
前端·react.js
悟空聊架构7 小时前
5 分钟上手!Burp 插件「瞎越」一键批量挖垂直越权
前端
麦麦大数据7 小时前
F010 Vue+Flask豆瓣图书推荐大数据可视化平台系统源码
vue.js·mysql·机器学习·flask·echarts·推荐算法·图书
炒毛豆7 小时前
vue3+antd实现华为云OBS文件拖拽上传详解
开发语言·前端·javascript
Pu_Nine_97 小时前
Axios 实例配置指南
前端·笔记·typescript·axios
红尘客栈27 小时前
Shell 编程入门指南:从基础到实战2
前端·chrome
前端大卫8 小时前
Vue 和 React 受控组件的区别!
前端
Hy行者勇哥9 小时前
前端代码结构详解
前端
练习时长一年9 小时前
Spring代理的特点
java·前端·spring