Vue之watch源碼解讀


Vue之watch源碼解讀


回顧 watch 的用法

watch 是 Vue 中的一個監聽數據變化的一個方法,我們在閱讀源碼之前先來回顧一下 watch 的用法

監聽基本數據類型

<div>
  {{ name }}
  <button @click="changeName">改變name</button>
</div>
export default {
  data() {
    return {
      name: 'maoxiaoxing',
    }
  },
  watch: {
    name(val, oldval) {
      console.log(val, oldval)
    }
  },
  methods: {
    changeName() {
      this.name = this.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'
    }
  }
}

watch 可以接收兩個參數,一個是變化之后的數據,一個是變化之前的數據,你可以基於這兩個值處理一些邏輯

監聽對象

<div>
  {{ obj.name }}
  <button @click="changeName">改變name</button>
</div>
export default {
  data() {
    return {
      obj: {
        name: 'maoxiaoxing',
      }
    }
  },
  watch: {
    obj: {
      handler(val, oldval) {
        console.log(val, oldval)
      },
      deep: true,
      immediate: true,
    }
  },
  methods: {
    changeName() {
      this.obj.name = this.obj.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'
    }
  },
  created() {
    console.log('created')
  }
}

在監聽對象變化的時候,加上 deep 這個屬性即可深度監聽對象數據;如果你想在頁面進來時就執行 watch 方法,加上 immediate 即可。值得注意的是,設置了 immediate 屬性的 watch 的執行順序是在 created 生命周期之前的

watch 接收參數為數組

我在看 Vue 源碼的時候,發現了一個比較有意思的地方,如果說 watch 監聽的屬性不去設置一個方法而是接收一個數組的話,可以向當前監聽的屬性傳遞多個方法

export default {
  data() {
    return {
      name: 'jack',
    }
  },
  watch: {
    name: [
      { handler: function() {console.log(1)}, immediate: true },
      function(val) {console.log(val, 2)}
    ]
  },
  methods: {
    changeName() {
      this.name = this.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'
    }
  }
}

數組中可以接收不同形式的參數,可以是方法,也可以是一個對象,具體的書寫方式和普通的 watch 沒什么不同。可以接收數據為參數這一點在官方文檔沒有找到,至於為什么可以這樣寫,下面的源碼講解會提及。

初始化 watch

initState

// src\core\instance\state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) // 如果有 props ,初始化 props
  if (opts.methods) initMethods(vm, opts.methods) // 如果有 methods ,初始化 methods 里面的方法
  if (opts.data) { // 如果有 data 的話,初始化,data;否則響應一個空對象
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed) // 如果有 computed ,初始化 computed
  if (opts.watch && opts.watch !== nativeWatch) { // 如果有 watch ,初始化 watch
    initWatch(vm, opts.watch)
  }
}

首先在 initState 初始化 watch,如果有 watch 這個屬性的話,就將 watch 傳入 initWatch 方法中處理

initWatch

// src\core\instance\state.js
function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

這個函數主要就是初始化 watch,我們可以看到 initWatch 會遍歷 watch,然后判斷每一個值是否是數組,如果是數組的就遍歷這個數組,創建多個回調函數,這塊也就解釋了上邊 watch 監聽的數據可以接收數組為參數原因;如果不是數組的話,就直接創建回調函數。從這里我們也能看到,我們學習源碼的好處,通過學習源碼,我們能學習到一些在官方文檔中沒有提到的寫法。

createWatcher

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

createWatcher 中會判斷 handler 是否是對象,如果是對象將 handler 掛載到 options 這個屬性,再將對象的 handler 屬性提取出來;如果 handler 是一個字符串的話,會從 Vue 實例找到這個方法賦值給 handler。從這里我們也能看出來,watch 還可以支持字符串的寫法。執行 Vue 實例上的 $watch 方法。

$watch

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  // 獲取 Vue 實例 this
  const vm: Component = this
  if (isPlainObject(cb)) {
    // 判斷如果 cb 是對象執行 createWatcher
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  // 標記為用戶 watcher
  options.user = true
  // 創建用戶 watcher 對象
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 判斷 immediate 如果為 true
  if (options.immediate) {
    // 立即執行一次 cb 回調,並且把當前值傳入
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  // 返回取消監聽的方法
  return function unwatchFn () {
    watcher.teardown()
  }
}

$watch 函數是 Vue 的一個實例方法,也就是我們可以使用 Vue.$watch 去調用,這里不再過多贅述,官方文檔中講的很詳細。$watch 會創建一個 Watcher 對象,這塊也是涉及響應式原理,在 watch 中改變的數據可以進行數據的響應式變化。同時也會判斷是否有 immediate 這個屬性,如果有的話,就直接調用回調。

Watcher

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    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 // 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 = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // expOrFn 是字符串的時候,例如 watch: { 'person.name': function... }
      // parsePath('person.name') 返回一個函數獲取 person.name 的值
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && 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()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  /*獲得getter的值並且重新進行依賴收集*/
  get () {
     /*將自身watcher觀察者實例設置給Dep.target,用以依賴收集。*/
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      /*
      執行了getter操作,看似執行了渲染操作,其實是執行了依賴收集。
      在將Dep.target設置為自身觀察者實例以后,執行getter操作。
      譬如說現在的的data中可能有a、b、c三個數據,getter渲染需要依賴a跟c,
      那么在執行getter的時候就會觸發a跟c兩個數據的getter函數,
      在getter函數中即可判斷Dep.target是否存在然后完成依賴收集,
      將該觀察者對象放入閉包中的Dep的subs中去。
    */
      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
      /*如果存在deep,則觸發每個深層對象的依賴,追蹤其變化*/
      if (this.deep) {
        /*遞歸每一個對象或者數組,觸發它們的getter,使得對象或數組的每一個成員都被依賴收集,形成一個“深(deep)”依賴關系*/
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  ... 其他方法
}

上面的 Watcher 我省略了一些其他方法,只保留了 get 函數,我們能在 get 函數中看到如果有 deep 屬性的話,就會遞歸處理對象中的每一個屬性,以達到深度監聽的效果。這里關於 watch 的使用和原理講解就完結了,我們通過閱讀源碼,不僅能夠了解 Vue 框架內部是怎樣實現的,同時也能看到一些官方文檔沒有提及的用法,對我們是很有幫助的。


免責聲明!

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



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