Vue 2.0的响应式原理的一点理解

本文深入剖析Vue.js的响应式原理,介绍了数据劫持、观察者模式的应用,以及Watcher、Dep等核心组件的工作机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    一点无关的废话(为什么突然转到vue?因为项目需要,所以在学习的时候做一下成果总结。)

   Vue官方文档对响应式原理的实现给出了一个大体的介绍,通过对象的访问器属性监视数据,通过观察者模式实现Module和View的绑定监听。如果不熟悉观察者模式的可以移步观察者模式。可以大概的想一下实现方式:需要一个发布者类监听数据,一个订阅者类对数据变化做出反应,一个收集器类收集依赖,对模板的操作需要一系列的指令。这里,我主要想写一下三个类的关系。

    从初始化一个Vue实例开始,看下代码流程。

function Vue (options) {
  if ("development" !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}

    主要就是_init方法,这个是Vue原型上的方法,在initMixin函数中定义,源码如下:

function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    var vm = this;
    // a uid
    vm._uid = uid$3++;

    var startTag, endTag;
    /* istanbul ignore if */
    if ("development" !== 'production' && config.performance && mark) {
      startTag = "vue-perf-start:" + (vm._uid);
      endTag = "vue-perf-end:" + (vm._uid);
      mark(startTag);
    }

    // a flag to avoid this being observed
    vm._isVue = true;
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options);
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }
    /* istanbul ignore else */
    {
      initProxy(vm);
    }
    // expose real self
    vm._self = vm;
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');

    /* istanbul ignore if */
    if ("development" !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(("vue " + (vm._name) + " init"), startTag, endTag);
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}

    _init方法中靠后的方法是不是很眼熟(initLifecycle开始),看看官方给出的生命周期图示,结合源码,可以很清楚看出每个钩子函数的执行时间点。对于数据绑定,是在initState方法中完成的,下面看看initState方法中做了哪些事:

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
  if (opts.computed) { initComputed(vm, opts.computed); }
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

    opts取到的是我们在初始化一个Vue实例时传入的对象,对于某一个vue组件,vm.$options就是我们在script标签下export抛出的对象。方法中各种init实现的就是数据代理,简单来说就是让我们通过vm.xx间接访问vm.A.xx,A是opts的某个属性。以initData(vm)为例,先看下initData的源码:

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
    "development" !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    );
  }
  // proxy data on instance
  var keys = Object.keys(data);
  var props = vm.$options.props;
  var methods = vm.$options.methods;
  var i = keys.length;
  while (i--) {
    var key = keys[i];
    {
      if (methods && hasOwn(methods, key)) {
        warn(
          ("Method \"" + key + "\" has already been defined as a data property."),
          vm
        );
      }
    }
    if (props && hasOwn(props, key)) {
      "development" !== 'production' && warn(
        "The data property \"" + key + "\" is already declared as a prop. " +
        "Use prop default value instead.",
        vm
      );
    } else if (!isReserved(key)) {
      proxy(vm, "_data", key);
    }
  }
  // observe data
  observe(data, true /* asRootData */);
}

    说点无关的,在看到对data进行类型判断的时候,是不是想到了官方文档写的“组件中的data只能是方法”。回到正题,定义了keys保存data的属性,通过遍历。对每个key执行proxy(先会去判断这个属性是否在methods和props中存在了),isReserved(key)是判断该属性是否是预留属性(判断属性是否是以'_'或者'$'开头,可以自己看下源码)。然后看下proxy方法源码:

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

    首先说下三个参数,target是初始化Vue的一个实例vm,sourceKey是字符串“_data”,key是data下面的某个属性。通过Object.defineProperty劫持数据,设置vm.A的setter/getter。当我们访问vm.A时,获取的值实际时vm._data.A,设置vm.A=x,实际上是给vm._data.A赋值。

    然后再回到initData方法,在做完数据代理之后,调用了observe方法,然后看下相应源码:

function observe (value, asRootData) {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}

    先是对value(即传入的data对象)做了一系列的类型判断(是否是引用类型object,是否是VNode的实例,是否自身有'_ob_'属性等),最终要完成的就是初始化一个Observer实例。接下来要看的就是Observer类有什么作用:

var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

/**
 * Walk through each property and convert them into
 * getter/setters. This method should only be called when
 * value type is Object.
 */
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i]);
  }
};

/**
 * Observe a list of Array items.
 */
Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

    每个Observer实例有一个属性value保存传入的data对象,初始化了一个Dep实例,给value添加一个属性'_ob_'保存该Observer实例。然后判断value是否是一个数组,如果是,则对数组的每一项执行observe()方法,最终结果就是对每一项执行Observer的原型方法walk。如果value不是数组,则直接执行walk方法。而walk方法则是对data对象的每个属性执行defineReactive方法。所以接下来我们需要去看Dep类和defineReactive分别做了什么。显示Dep类:

var uid = 0;

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
var Dep = function Dep () {
  this.id = uid++;
  this.subs = [];
};

Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};

Dep.prototype.removeSub = function removeSub (sub) {
  remove(this.subs, sub);
};

Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};

Dep.prototype.notify = function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null;
var targetStack = [];

function pushTarget (_target) {
  if (Dep.target) { targetStack.push(Dep.target); }
  Dep.target = _target;
}

function popTarget () {
  Dep.target = targetStack.pop();
}

    先不对Dep做说明,再看下defineReactive方法的源码:

function defineReactive (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  var childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if ("development" !== 'production' && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}

    可以看到,defineReactive方法实现了给每个data的属性初始化一个Dep实例,然后再给这些属性设置getter/setter。

  • getter
  1. 获取属性的值value
  2. 判断Dep.target,即该属性是否被用到(回顾一下Dep类相关代码,发现Dep.target是在pushTarget方法中赋值的,而pushTarget方法是在Watcher类的方法中调用的,后面再详细说)。如果没有,直接返回值value。
  3. 执行dep.depend()方法,回顾一下源码,发现又涉及到了Dep.target,而且还有addDep方法,都与Watcher有关。后面再说。
  • setter
  1. 获取属性值value,判断值是否有变化,赋值
  2. 主要关注点在dep.notify()方法,回到Dep类源码,发现又涉及到Watcher类。

    现在就看看Watcher源码,解开上面提到的谜团:

var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
  this.vm = vm;
  if (isRenderWatcher) {
    vm._watcher = this;
  }
  vm._watchers.push(this);
  // options
  if (options) {
    this.deep = !!options.deep;
    this.user = !!options.user;
    this.computed = !!options.computed;
    this.sync = !!options.sync;
    this.before = options.before;
  } else {
    this.deep = this.user = this.computed = this.sync = false;
  }
  this.cb = cb;
  this.id = ++uid$1; // uid for batching
  this.active = true;
  this.dirty = this.computed; // for computed watchers
  this.deps = [];
  this.newDeps = [];
  this.depIds = new _Set();
  this.newDepIds = new _Set();
  this.expression = expOrFn.toString();
  // parse expression for getter
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = function () {};
      "development" !== 'production' && warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      );
    }
  }
  if (this.computed) {
    this.value = undefined;
    this.dep = new Dep();
  } else {
    this.value = this.get();
  }
};

Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};

/**
 * Add a dependency to this directive.
 */
Watcher.prototype.addDep = function addDep (dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
  }
};

    Watcher原型方法代码比较多,这里只展示上面提到的两个方法。现在遇到一个问题,Watcher的实例什么时候初始化的,实例是什么。

    Vue的双向数据绑定是采用的数据劫持和观察者模式,数据劫持前面提到了,是通过Object.defineProperty实现的。前面说到的Observer类很明显是用来监视数据的,也就是发布者,Dep类存储了一些与Watcher有关的东西,起到了一个收集器的作用,那么Watcher就是订阅者了。我们在页面中写的双括号"{{}}"或者一些vue指令,在相关代码的解析下,分别就初始化了一个Watcher的实例。例如页面中:

<span class="text">{{item.name}}</span>

    对应的就会给item.name初始化一个Watcher的实例。初始化的过程中就会调用get方法,将Dep.target指向自己,并在try语句中触发了属性的getter,最终的结果就是将自身保存在属性对应的Dep实例的subs数组中,完成依赖收集。

    最后给出一张图总结:

    

  1. 钩子函数beforeCreate和created之间会调用initState,初始化methods,data,props等
  2. 在初始化data时,会完成数据代理,vm.xx = vm._data.xx = vm.data.xx,之后会调用observe方法,初始化一个Observer实例
  3. 初始化Observer实例会对data的每个属性设置setter/getter,同时每个属性都会初始化一个Dep实例,用来收集订阅者。
  4. 模板编译中会对指令/{{}}数据绑定初始化Watcher实例,初始化时会调用get方法,触发对应属性的getter将自身添加到对应属性的对应的Dep实例dep的subs数组中。
  5. 属性的值变化的时候,触发setter,调用属性对应的Dep实例dep的notify()方法,notify方法会去遍历subs数组中存放的Watcher实例,通知每个实例调用update对数据变化做出反应。

    

    能力有限,对于文中不正确的地方希望您可以给出宝贵的意见。最后感谢黄轶老师的文章,可移步老师的文章点击打开链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值