theydy / notebook

记录读书笔记 + 知识整理,vuepress 迁移中 https://theydy.github.io/notebook/
0 stars 0 forks source link

响应式原理 #39

Open theydy opened 3 years ago

theydy commented 3 years ago

数据的响应式起点在于 _init 函数中的 initState 中,在这个方法里按 props、methods、data、computed、watch 顺序初始化数据,并将他们转换为响应式对象。

  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);
    }
  }

这里只看 props 和 data 的响应式过程,因为最终调用的响应式方法其实都一样只是步骤有所不同,所以先列出 props 和 data 初始化的步骤,最后分析具体的响应式方法实现。

initProps

initProps 主要逻辑如下,遍历组件的 props 配置,获取 prop 值,执行 defineReactive 转为响应式对象,再将 _props 代理到 vm 实例上。这里最主要的是 defineReactive

    var props = vm._props = {};
    var loop = function ( key ) {
      keys.push(key);
      var value = validateProp(key, propsOptions, propsData, vm);

      {

        defineReactive(props, key, value, function () {
          // ...
        });
      }

      if (!(key in vm)) {
        proxy(vm, "_props", key);
      }
    };

    for (var key in propsOptions) loop( key );

initData

initData 主要逻辑如下,先取到 data 的配置,因为一般 data 会写成一个函数,所以 getData 其实就是执行这个函数获取最终的返回值做为 data 值。

然后将 _data 代理到 vm 实例上,最后调用 observe 转为响应式对象。这里最主要的是 observe

    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};

    var keys = Object.keys(data);
    var i = keys.length;

    while (i--) {
      var key = keys[i];
      // ...
      proxy(vm, "_data", key);
    }
    // observe data
    observe(data, true /* asRootData */);

响应式关键方法

由上可知,initPropsinitData 中响应式的关键方法分别是 defineReactiveobserve

其实在 defineReactive 会引出 Dep 类,Dep 类又会引出 Watcher 类;observe 会引出了 Observer 类。

实现数据响应式的关键:

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
  }

从代码可以看出,observe 函数的主要作用是对于一个引用类型对象新建一个 Observerob = new Observer(value);,并且这个 ob 对象最后会保存在 value.__ob__ 下。

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 (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
  }

defineReactive 函数里真正调用 Object.defineProperty 劫持了 getter / setter 。getter 中通过 dep.depend() 收集依赖,setter 中通过 dep.notify() 触发更新。

这个函数中需要额外注意的地方有三点:

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);
    }
  };

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

  Observer.prototype.observeArray = function observeArray (items) {
    for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  };

  function protoAugment (target, src) {
    target.__proto__ = src;
  }

  function copyAugment (target, src, keys) {
    for (var i = 0, l = keys.length; i < l; i++) {
      var key = keys[i];
      def(target, key, src[key]);
    }
  }

Observer 的实例会保存在 value.__ob__ 上,接着对于数组和对象类型会执行不同的操作

Dep

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

  Dep.target = null; // 这是一个 Watcher 对象

  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 () {
    var subs = this.subs.slice();
    if (!config.async) {
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  };

Dep 是收集依赖的框,Dep.target 指向当前的 watcherDep.subs 存放依赖的所有 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.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);
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
    this.value = this.lazy
      ? undefined
      : 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
  };

  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.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };

  Watcher.prototype.run = function run () {
    if (this.active) {
      var value = this.get();
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        var oldValue = this.value;
        this.value = value;
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue);
          } catch (e) {
            handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
          }
        } else {
          this.cb.call(this.vm, value, oldValue);
        }
      }
    }
  };

  Watcher.prototype.depend = function depend () {
    var i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  };

  // ...省略 evaluate、cleanupDeps 和 teardown 的代码

老实说 Watcher 其实在响应式的流程中不太明显,不认真看都看不出来有用到。

Watcher 根据传的参数不同可以分为 render watcher、computed watcher、watch watcher,这里暂时当成只有 render watcher 即可。

响应式的收集 / 更新过程如下:

收集:访问某个响应式数据 → 触发 getter 收集依赖

更新:修改某个响应式数据 → 触发 setter 更新依赖

这里的依赖其实就是指的 watcher ,而「访问响应式数据」这个操作最明显的显然是在 template 中了吧,template$mount 过程中会被编译为 render 函数,之后执行 render,这里就触发了收集依赖的过程了,不过实际上 render 函数不是直接被调用,而是做为 Watcher 中的 getter 函数被调用的。


    var updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };

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

这就是 render watcher 构建入口,结合 Watcher 的构造函数看一下,删掉无关的代码,其实 render watcher 我认为在新建实例的过程中最重要的就是把 updateComponent 赋值给 getter,接着执行 get 函数取了一次值,而 get 函数的作用主要就是 pushTarget(this) 把当前的 watcher 赋值给 Dep.target,然后执行 getter 这里也就是 updateComponent ,然后pushTarget()Dep.target 置为空。

这样一套下来,只要 render 函数中访问到的响应式数据显然收集到的就是这个 render watcher 了。

响应式数据更新时,实际上会遍历 Depsubs 执行 subs[i].update(); 也就是 watcherupdate 方法,为了防止同一个 watcher 重复更新,会先收集到一个队列中最后在 nextTick 中遍历执行 watcher.run ,对于 render watcher 可以认为在 run 方法中又执行了一次 updateComponent 也就完成了视图的更新。

  var Watcher = function Watcher (
    vm, // vm
    expOrFn, // updateComponent
    cb, // noop
    options, // { before }
    isRenderWatcher // true
  ) {
    this.vm = vm;
    // ... 
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    }
    // ...
    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) {
      // ...
    } finally {
      // ...
      popTarget();
    }
    return value
  };

  Watcher.prototype.update = function update () {
    // ...
    queueWatcher(this); // 在 nextTick 中遍历需要更新的 watcher 执行 watcher.run()
  };

  Watcher.prototype.run = function run () {
    if (this.active) {
      var value = this.get();
      // ...
    }
  };