说说 Vue 依赖收集

前置说明

vue 版本 2.6.2,测试用的代码

<!DOCTYPE html>
<html>
<head>
  <title>vue test</title>
</head>
<body>
<div id="app">
  {{message}}

  <button-counter :title="tt"></button-counter>
</div>

  <!-- Vue.js v2.6.11 -->
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    Vue.component('button-counter', {
      props: ['title'],
      data: function () {
        return {
          count: 0
        }
      },
      template: '<button v-on:click="count++">{{title}}: You clicked me {{ count }} times.</button>'
    });
    var app = new Vue({
      el: '#app',
      data: {
        message: 'TEST',
        tt: 'on'
      },
      mounted() {
        window.addEventListener('test', (e) => {
          this.message = e.detail;
        }, false);
      },
    })

    console.log(app);
    // var event = new CustomEvent('test', { 'detail': 5 }); window.dispatchEvent(event);
  </script>
</body>
</html>

简要概括

在拦截器(Object.defineProperty)里,在它的闭包中会有一个观察者(Dep)对象,这个对象用来存放被观察者(watcher)的实例。

并且拦截器注册 get 方法,该方法用来进行「依赖收集」。其实「依赖收集」的过程就是把 Watcher 实例存放到对应的 Dep 对象中去。

get 方法可以让当前的 Watcher 对象(Dep.target)存放到它的 subs 中(addSub)方法,在数据变化时,set 会调用 Dep 对象的 notify 方法通知它内部所有的 Wathcer 对象进行视图更新。

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

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        //... 省略部分代码
      }
      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;
      }
      //... 省略部分代码
      val = newVal;
      dep.notify();
    }
  });
}

分析

在初始化过程中(beforeCreate 和 created 之间) Object.defineProperty 劫持了数据

劫持的过程中定义了观察者 dep,其结构非常简单:

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

然后在挂载过程中(beforeMount 和 mounted 之间) ,拦截器(Object.defineProperty) 触发了 get ,get 函数里 dep.depend(); 就做了观察者 dep 关联被观察者 watcher 的动作。

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

watcher 的结构如下

var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
  this.vm = vm;
  // ... 省略部分
  this.cb = cb;
  this.id = ++uid$2; // uid for batching
  // ... 省略部分
  this.expression = expOrFn.toString();
};

所以最后结构观察者和被观察者就是这样的结构,完成了依赖收集。最终就是我们熟知的触发流程,点击上图代码的按钮时,拦截器的 set 触发了 dep.notify() 通知了所有被观察者 Wacher,而一番排队操作后需而触发 watcher 里的表达式,就去重新渲染这个组件。

Dep {
  id: 9,
  subs: [
    0: Watcher {
      ...
      expression: "function () { vm._update(vm._render(), hydrating); }"
      ...
    }
  ]
}

以上有个关键的一步,为什么 Dep.target 为什么会指向这个 Watcher 对象?

Dep.target 为什么会指向这个 Watcher 对象?

在 callHook(vm, 'beforeMount') 后,进入 mount 阶段,此时初始化 Watcher


function noop (a, b, c) {}

var updateComponent;
// 省略if (config.performance && mark)判断
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 */);

我们知道 computed 属性会被标记为 lazy 直到取值时才触发 this.cb,那么一般情况下就调用 this.get。

var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
  this.vm = vm;
  // ... 省略部分
  vm._watchers.push(this);
  if (options) {
    this.lazy = !!options.lazy;
    // ... 省略部分
  }
  this.cb = cb;
  this.id = ++uid$2; // uid for batching
  // ... 省略部分
  this.expression = expOrFn.toString();
  this.value = this.lazy
      ? undefined
      : this.get();
};

而就是在 Watcher.prototype.get,注意 pushTarget,此时就和 Dep 发布者产生了联系,Dep 的 target 被设置为了这个 wacher,并且在每次监测对象被 get 时,就会往自身的 Dep 里推入这个 wacher。

Watcher.prototype.get = function get() {
  pushTarget(this);
  var value;
  var vm = this.vm;
  //...
  value = this.getter.call(vm, vm);
  //...
  popTarget();
  this.cleanupDeps();
  
  return value;
};

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

到此就完成了依赖收集。

那么再来阐述下被观察者怎么开始更新视图的

多数情况下,被观察者 Watcher 的结构里都有表达式 expression 属性,它的内容是 vm._update(vm._render(), hydrating),渲染的过程就是触发了此函数。

那么首先需要调用 vm._render() 方法,此方法要返回一个 VNode

Vue.prototype._render = function () {
  // ...
  var vm = this;
  var ref = vm.$options;
  var render = ref.render;
  vnode = render.call(vm._renderProxy, vm.$createElement);
  // ...
  return vnode
}

// 而render方法其实就是用于输出一个虚拟节点
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[(message + 1 > 1)?_c('div',[_v(_s(message + 1))]):_e(),_v(" "),_c('button',{on:{"click":function($event){message += 1}}},[_v("阿道夫")])])}
})

然后结果交给 vm._update

Vue.prototype._update = function(vnode, hydrating) {
  var vm = this;
  var prevEl = vm.$el;
  var prevVnode = vm._vnode;
  // ...
  vm._vnode = vnode;
  
  // ...
  vm.$el = vm.__patch__(prevVnode, vnode);
  
  
  // ...
};

结论是 mount 阶段 初始化 Watcher,然后在 wathcer初始化后调用 get,get里 pushTarget(this),并且执行自身的getter也就是表达式,表达式的内容就是 vm._update(vm._render(), hydrating) 故而就开始执行 render函数,render 函数就是就是输出虚拟节点的。

猜你喜欢

转载自www.cnblogs.com/everlose/p/12564451.html