vue 响应式浅读

209 阅读3分钟

响应式 = 数据劫持 + 发布订阅者模式

数据劫持:

vue2.x版本中使用Object.defineProperty + 重写数组方法

语法:

Object.defineProperty(obj, prop, descriptor)

  • obj 要定义属性的对象
  • prop 要定义或修改的属性的名称
  • descriptor 要定义或修改的属性描述符 descriptor的属性:
  • configurable:决定描述符属性是否可改、属性是否可删除;
  • enumerable:是否可枚举;
  • value:属性对应的值;
  • writable:属性对应的值是否可以改变;
  • get/set函数:数据劫持的关键,属性被读取、赋值时执行相应的操作。

数组: 仅当使用赋值操作时,如 arr = []会触发set,vue2.x通过重写Array.prototype的方法进行监听; 但是对于直接操作具体索引的操作还是无法监听,如arr[1]= 2;需要改成arr.splice(1,1,2)达到响应式的效果;或者通过Vue.set(arr,1,2),向对象添加属性亦可使用set方法,Vue.set(student, 'age', 18); Vue.set的原理是对新增内容进行判断,若内容不是响应式的,则再进行一次依赖收集(defineReactive、notify)。

vue3.x版本中使用Proxy

  • 使用proxy代理做数据劫持,则不存在上述关于新增属性、数组操作的问题;
  • Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。 语法:
const p = new Proxy(target, handler)
  • target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

具体用法参考:

developer.mozilla.org/zh-CN/docs/…

发布订阅者模式:

image.png

简易代码实现:

  // 循环修改为每个属性添加get set
  for (let key in data) {
    defineReactive(data, key);
  }
}

const defineReactive = function(obj, key) {
  // 局部变量dep,用于get set内部调用
  const dep = new Dep();
  // 获取当前值
  let val = obj[key];
  Object.defineProperty(obj, key, {
    // 设置当前描述属性为可被循环
    enumerable: true,
    // 设置当前描述属性可被修改
    configurable: true,
    get() {
      console.log('in get');
      // 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
      dep.depend();
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      // 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
      // 这里每个需要更新通过什么断定?dep.subs
      dep.notify();
    }
  });
}

const observe = function(data) {
  return new Observer(data);
}

const Vue = function(options) {
  const self = this;
  // 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
  if (options && typeof options.data === 'function') {
    this._data = options.data.apply(this);
  }
  // 挂载函数
  this.mount = function() {
    new Watcher(self, self.render);
  }
  // 渲染函数
  this.render = function() {
    with(self) {
      _data.text;
    }
  }
  // 监听this._data
  observe(this._data);  
}

const Watcher = function(vm, fn) {
  const self = this;
  this.vm = vm;
  // 将当前Dep.target指向自己
  Dep.target = this;
  // 向Dep方法添加当前Wathcer
  this.addDep = function(dep) {
    dep.addSub(self);
  }
  // 更新方法,用于触发vm._render
  this.update = function() {
    console.log('in watcher update');
    fn();
  }
  // 这里会首次调用vm._render,从而触发text的get
  // 从而将当前的Wathcer与Dep关联起来
  this.value = fn();
  // 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
  // 造成代码死循环
  Dep.target = null;
}

const Dep = function() {
  const self = this;
  // 收集目标
  this.target = null;
  // 存储收集器中需要通知的Watcher
  this.subs = [];
  // 当有目标时,绑定Dep与Wathcer的关系
  this.depend = function() {
    if (Dep.target) {
      // 这里其实可以直接写self.addSub(Dep.target),
      // 没有这么写因为想还原源码的过程。
      Dep.target.addDep(self);
    }
  }
  // 为当前收集器添加Watcher
  this.addSub = function(watcher) {
    self.subs.push(watcher);
  }
  // 通知收集器中所的所有Wathcer,调用其update方法
  this.notify = function() {
    for (let i = 0; i < self.subs.length; i += 1) {
      self.subs[i].update();
    }
  }
}

const vue = new Vue({
  data() {
    return {
      text: 'hello world'
    };
  }
})

vue.mount(); // in get
vue._data.text = '123'; // in watcher update /n in get

来源:juejin.cn/post/684490…

  • Observer负责将数据转换成getter/setter形式;
  • Dep负责管理数据的依赖列表;是一个发布订阅模式,上游对接Observer,下游对接Watcher
  • Watcher是实际上的数据依赖,负责将数据的变化转发到外界(渲染、回调);首先将data传入Observer转成getter/setter形式;当Watcher实例读取数据时,会触发getter,被收集到Dep仓库中;当数据更新时,触发setter,通知Dep仓库中的所有Watcher实例更新,Watcher实例负责通知外界