我想用大白話講清楚watch和computed

背景

一直以來我對vue中的watchcomputed都只知其一;不知其二的,知道一點(例如:watchcomputed的本質都是new Watcher,computed有緩存,只有調用的時候纔會執行,也只有當依賴的數據變化了,纔會再次觸發...),而後就沒有而後了。javascript

也看了不少大佬寫的文章,一大段一大段的源碼列出來,着實讓我這個菜雞看的頭大,天然也就不想看了。最近,我又開始學習vue源碼,才真正理解了它們的實現原理。vue

data() {
  return {
    msg: 'hello guys',
    info: {age:'18'},
    name: 'FinGet'
  }
}

watcher

watcher 是什麼?偵聽器?它就是個類class!java

class Watcher{
  constructor(vm,exprOrFn,callback,options,isRenderWatcher){
  }
}
  • vm vue實例
  • exprOrFn 多是字符串或者回調函數(有點懵就日後看,如今它不重要)
  • options 各類配置項(配置啥,日後看)
  • isRenderWatcher 是不是渲染Wathcer

initState

Vue 初始化中 會執行一個 initState方法,其中有你們最熟悉的initData,就是Object.defineProperty數據劫持。緩存

export function initState(vm) {
  const opts = vm.$options;
  // vue 的數據來源 屬性 方法 數據 計算屬性 watch
  if(opts.props) {
    initProps(vm);
  }
  if(opts.methods) {
    initMethod(vm);
  }
  if(opts.data) {
    initData(vm);
  }
  if(opts.computed){
    initComputed(vm);
  }
  if(opts.watch) {
    initWatch(vm, opts.watch);
  }
}

在數據劫持中,Watcher的好基友Dep出現了,Dep就是爲了把Watcher存起來。函數

function defineReactive(data, key, val) {
  let dep = new Dep(); 
  Object.defineProperty(data, key, {
    get(){
      if(Dep.target) {
        dep.depend(); // 收集依賴
      }
      return val;
    },
    set(newVal) {
      if(newVal === val) return;
      val = newVal;
      dep.notify(); // 通知執行
    }
  })
}
initData的時候, Dep.target啥也不是,因此收集了個寂寞。 target是綁在Dep這個類上的(靜態屬性),不是實例上的。

可是當$mount以後,就不同了。至於$mount中執行的什麼compilegeneraterenderpatchdiff都不是本文關注的,不重要,繞過!學習

你只須要知道一件事:會執行下面的代碼this

new Watcher(vm, updateComponent, () => {}, {}, true); // true 表示他是一個渲染watcher

updateComponent就是更新哈,不計較具體執行,它如今就是個會更新頁面的回調函數,它會被存在Watchergetter中。它對應的就是最開始那個exprOrFn參數。lua

嘿嘿嘿,這個時候就不同了:spa

  1. 渲染頁面就是調用了你定義的數據(別槓,定義了沒調用),就會走get
  2. new Watcher 就會調用一個方法把這個實例放到Dep.target上。
pushTarget(watcher) {
  Dep.target = watcher;
}

這兩件事正好湊到一塊兒,那麼 dep.depend()就幹活了。prototype

因此到這裏能夠明白一件事,全部的 data中定義的數據,只要被調用,它都會收集一個渲染 watcher,也就是數據改變,執行 set中的 dep.notify就會執行渲染 watcher

下圖就是定義了msginfoname三個數據,它們都有個渲染Watcher

眼尖的小夥伴應該看到了msg中還有兩個watcher,一個是用戶定義的watch,另外一個也是用戶定義的watch。啊,固然不是啦,vue是作了去重的,不會有重複的watcher,正如你所料,另外一個是computed watcher

用戶watch

咱們通常是這樣使用watch的:

watch: {
  msg(newVal, oldVal){
    console.log('my watch',newVal, oldVal)
  }
  // or
  msg: {
    handler(newVal, oldVal) {
      console.log('my watch',newVal, oldVal)
    },
    immediate: true
  }
}

這裏會執行一個initWatch,一頓操做以後,就是提取出exprOrFn(這個時候它就是個字符串了)、handleroptions,這就和Watcher莫名的契合了,而後就瓜熟蒂落的調用了vm.$watch方法。

Vue.prototype.$watch = function(exprOrFn, cb, options = {}) {
    options.user = true; // 標記爲用戶watcher
    // 核心就是建立個watcher
    const watcher = new Watcher(this, exprOrFn, cb, options);
    if(options.immediate){
      cb.call(vm,watcher.value)
    }
 }

來吧,避免不了看看這段代碼(原本粘貼了好長一段,但說了大白話,我就把和這段關係不大的給刪減了):

class Watcher{
  constructor(vm,exprOrFn,callback,options,isRenderWatcher){
    this.vm = vm;
    this.callback = callback;
    this.options = options;
    if(options) {
      this.user = !!options.user;
    }
    this.id = id ++;
    if (typeof exprOrFn == 'function') {
      this.getter = exprOrFn; // 將內部傳過來的回調函數 放到getter屬性上
    } else {
      this.getter = parsePath(exprOrFn);
      if (!this.getter) {
        this.getter = (() => {});
      }
    }
    this.value = this.get();
  }
  get(){
    pushTarget(this); // 把當前watcher 存入dep中
    let result = this.getter.call(this.vm, this.vm); // 渲染watcher的執行 這裏會走到observe的get方法,而後存下這個watcher
    popTarget(); // 再置空 當執行到這一步的時候 因此的依賴收集都完成了,都是同一個watcher
    return result;
  }
}
// 這個就是拿來把msg的值取到,取到的就是oldVal
function parsePath(path) {
  if (!path) {
    return
  }
  var segments = path.split('.');
  return function(obj) {
    for (var i = 0; i < segments.length; i++) {
      if (!obj) { return }
      obj = obj[segments[i]];
    }
    return obj
  }
}

你們能夠看到,new Watcher會執行一下get方法,當是渲染Watcher就會渲染頁面,執行一次updateComponent,當它是用戶Watcher就是執行parsePath中的返回的方法,而後獲得一個值this.value也就是oldVal

嘿嘿嘿,既然取值了,那又走到了msgget裏面,這個時候dep.depend()又幹活了,用戶Watcher就存進去了。

msg改變的時候,這過程當中還有一些騷操做,不重要哈,最後會執行一個run方法,調用回調函數,把newValueoldValue傳進去:

run(){
    let oldValue = this.value;
    // 再執行一次就拿到了如今的值,會去重哈,watcher不會重複添加
    let newValue = this.get();
    this.value = newValue;
    if(this.user && oldValue != newValue) { 
      // 是用戶watcher, 就調用callback 也就是 handler
      this.callback(newValue, oldValue)
    }
  }

computed

computed: {
  c_msg() {
    return this.msg + 'computed'
  }
  // or
  c_msg: {
    get() {
      return this.msg + 'computed'
    },
    set() {}
  }
},

computed有什麼特色:

  1. 調用的時候纔會執行
  2. 有緩存
  3. 依賴改變時會從新計算

調用的時候執行,我怎麼知道它在調用?嘿嘿嘿,Object.defineProperty不就是幹這事的嘛,巧了不是。

依賴的數據改變時會從新計算,那就須要收集依賴了。仍是那個邏輯,調用了this.msg -> get -> dep.depend()

function initComputed(vm) {
  let computed = vm.$options.computed;
  const watchers = vm._computedWatchers = {};
  for(let key in computed) {
    const userDef = computed[key];
    // 獲取get方法
    const getter = typeof userDef === 'function' ? userDef : userDef.get;
    // 建立計算屬性watcher lazy就是第一次不調用
    watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true });
    defineComputed(vm, key, userDef)
  }
}
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: () => {},
  set: () => {}
}
function defineComputed(target, key, userDef) {
  if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = createComputedGetter(key)
  } else {
      sharedPropertyDefinition.get = createComputedGetter(userDef.get);
      sharedPropertyDefinition.set = userDef.set;
  }
  // 使用defineProperty定義 這樣才能作到使用才計算
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

下面這一段最重要,上面的看一眼就好,上面作的就是把get方法找出來,用Object.defineProperty綁定一下。

class Watcher{
  constructor(vm,exprOrFn,callback,options,isRenderWatcher){
      ... 
    this.dirty = this.lazy;
    // lazy 第一次不執行
    this.value = this.lazy ? undefined : this.get();
    ...
  }
  
  update(){
    if (this.lazy) {
      // 計算屬性 須要更新
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this); // 這就是個襯托 如今無論它
    }
  }
  evaluate() {
    this.value = this.get();
    this.dirty = false;
  }
}

緩存就在這裏,執行get方法會拿到一個返回值this.value就是緩存的值,在用戶Watcher中,它就是oldValue,寫到這裏的時候,對尤大神的佩服,又加深一層。🐂🍺plus!

function createComputedGetter(key) {
  return function computedGetter() {
    // this 指向vue 實例
    const watcher = this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) { // 若是dirty爲true
        watcher.evaluate();// 計算出新值,並將dirty 更新爲false
      }
      // 若是依賴的值不發生變化,則返回上次計算的結果
      return watcher.value
    }
  }
}

watcherupdate是何時調用的?也就是數據更新調用dep.notify()dirty就須要變成true,可是計算屬性仍是不能立刻計算,仍是須要在調用的時候才計算,因此在update的時候只是改了dirty的狀態!而後下次調用的時候就會從新計算。

class Dep {
  constructor() {
    this.id = id ++;
    this.subs = [];
  }
  addSub(watcher) {
    this.subs.push(watcher);
  }
  depend() {
    Dep.target.addDep(this);
  }
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

總結

  1. watchcomputed 本質都是Watcher,都被存放在Dep中,當數據改變時,就執行dep.notify把當前對應Dep實例中存的Watcherrun一下,這樣執行了渲染Watcher 頁面就刷新了;
  2. 每個數據都有本身的Dep,若是他在模版中被調用,那它必定有一個渲染Watcher
  3. initData時,是沒有 Watcher 能夠收集的;
  4. 發現沒有,渲染WatcherComputed 中,exprOrFn都是函數,用戶Watcher 中都是字符串。

文章中的代碼是簡略版的,還有不少細枝末節的東西沒說,不重要也只是針對本文不重要,你們能夠去閱讀源碼更深刻的理解。

相關文章
相關標籤/搜索