1. Vue的响应式原理

发布于 2022年 01月 16日 23:31

Vue是一款渐进式的JavaScript框架,所谓的渐进式框架就是把框架分层,从内到外包括: 视图层渲染、组件机制、路由机制、状态管理以及构建工具。Vue最独特的特性之一就是响应式系统,它赋予了框架重新渲染页面的能力,其重要的能力就是检测变化侦测。

什么是变化侦测

Vue2.0引入虚拟Dom节点,一个状态绑定的依赖是当前的Vue组件;这样当状态发生改变后会通知组件,组件内部使用虚拟dom进行对比。这样可以大大降低依赖数目,从而降低依赖追踪所消耗的内存。

如何追踪变化

在vue源码初始化data时,对data进行遍历,并转成getter/setter形式

...
// observe data
observe(data, true /* asRootData */);

observe函数具体为

@description 对vue进行侦测
@return 为value返回一个Observer实例
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
}

上面的__ob__属性会在后面的数组变化侦测用到。Observer构造函数为

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

在构造函数中创建依赖dep以及定义__ob__属性为当前Observer实例(后续数组侦测会用到),通过walk函数对value进行遍历

Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
      defineReactive$$1(obj, keys[i]);
    }
};

因es6的proxy支持还不理想,目前为止defineReactive?1是根据Object.defineProperty侦测到对象的变化,但是ES6之前Javascript没提供元编程的能力,故无法侦测一个对象新增一个属性或者删除一个属性。当从obj的key中读取数据时,get函数触发,当往data的key中设置数据时,set函数触发

如何收集依赖

getter中收集依赖,setter中触发依赖

function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();
    ...
    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
        }
        ...
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        // 触发依赖
        dep.notify();
      }
    });
}

依赖收集的代码

var Dep = function Dep () {
    this.id = uid++;
    this.subs = [];
  };

  Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub);
  };
  // watcher中添加dep实例
  Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  };
  Dep.prototype.notify = function notify () {
    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 only one watcher
  // can be evaluated at a time.
  Dep.target = null;
  var targetStack = [];

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

  function popTarget () {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
  }

依赖是谁

依赖是上面代码的Dep.target, 也是状态变化后需要通知的对象,通过一个Watcher类来集中处理通知,这个Watcher就是个中介,数据变化时,它再通知其他地方,属于中介者模式;

// exporFn是侦测的属性, cb为变化后的回调处理
 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.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy 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);
        this.value = this.lazy
        ? undefined
        : this.get();
  };

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  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;
    // 防止重复添加,在dep中添加Watcher实例
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  };
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {// 跟计算属性是否需要重新计算相关
      this.dirty = true;
    } else if (this.sync) {
        /*同步则执行run直接渲染视图*/
      this.run();
    } else {
        /*异步推送到观察者队列中,下一个tick时调用。*/
      queueWatcher(this);
    }
  };
  /**
   * Depend on all deps collected by this watcher.
   */
  Watcher.prototype.depend = function depend () {
    var i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  };
};

页面渲染时的调用

new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */);

updateComponent函数是用来获取虚拟dom并渲染到页面的函数。示意图为

推荐文章