說說 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 函數就是就是輸出虛擬節點的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM