Vue2中的侦听器

366 阅读3分钟

问题导读

最近在使用ElementUI做项目的时候,使用到Tree组件,但是因为需求中需要定制化的东西比较多,原生的Tree组件并不能很好的完成项目需求,于是就想着看看ElementUI的代码实现逻辑,然后针对可定制化的部分,进行调整。

其中,再看到Tree组件的watch部分的时候,第一眼竟然充满了疑惑,我承认估计是JavaScript部分知识的薄弱,导致有些东西,无法第一时间明白其中的逻辑,还好,在手动测试后,明白了其中的细节,遂成此篇文章,以记录整个思路。

我们先来看看ElementUI中Tree组件的watch选项。

watch: {
      defaultCheckedKeys(newVal) {
        this.store.setDefaultCheckedKey(newVal);
      },

      defaultExpandedKeys(newVal) {
        this.store.defaultExpandedKeys = newVal;
        this.store.setDefaultExpandedKeys(newVal);
      },

      data(newVal) {
        this.store.setData(newVal);
      },

      checkboxItems(val) {
        Array.prototype.forEach.call(val, (checkbox) => {
          checkbox.setAttribute('tabindex', -1);
        });
      },

      checkStrictly(newVal) {
        this.store.checkStrictly = newVal;
      }
},

说实话,我第一次看这段代码的时候,是没看明白的,我想,所谓的侦听器,不应该侦听具体的属性嘛,为啥直接是一个handler呢。key上哪去了。

于是我就是直接在console中,动手撸了一下代码

a = { b(c) {console.log(c)}}

随着结果的输出,我一下子就清楚了,不信你看 image.png

说实话,这块内容对我来说,是暴击呀,真的是为自己的JS买单了。

但是事情还远远没有结束,因为我们还没看watch的内容呢

基本概念

Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch 回调。

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

接下来看一个简单的例子:

<div id="watch-example"> 
    <p> Ask a yes/no question: 
        <input v-model="question"> 
    </p> 
    <p>{{ answer }}</p> 
</div>
<script> 
    var watchExampleVM = new Vue({ 
        el: '#watch-example', 
        data: { question: '', answer: 'I cannot give you an answer until you ask a question!' }, 
        watch: {   
            // 如果 `question` 发生改变,这个函数就会运行 
            question: function (newQuestion, oldQuestion) { 
                this.answer = 'Waiting for you to stop typing...' 
                this.debouncedGetAnswer() 
             } 
         }, 
         created: function () {  
             // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。   
             // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率  
             // AJAX 请求直到用户输入完毕才会发出。想要了解更多关于 
             // `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识, 
             // 请参考:https://lodash.com/docs#debounce 
             this.debouncedGetAnswer = _.debounce(this.getAnswer, 500) },
         methods: { 
             getAnswer: function () { 
                 if (this.question.indexOf('?') === -1) { 
                     this.answer = 'Questions usually contain a question mark. ;-)' 
                     return 
                 } 
                 this.answer = 'Thinking...' 
                 var vm = this 
                 axios.get('https://yesno.wtf/api') 
                 .then(function (response) { 
                     vm.answer = _.capitalize(response.data.answer) 
                 })
                 .catch(function (error) { 
                     vm.answer = 'Error! Could not reach the API. ' + error 
                 }) 
            } 
       } 
  }) 
</script>

上面这段代码就是文档中的例子,这个例子很好的解释了watch的功能和应用场景。非常好,大家可以去Vue官方文档上去看,去体验watch的真实案例。

而接下来我要说的并不是仅仅上述的内容

vue中的watch

不知道大家有没有看过vue2的源码,我最近在看这块内容,因为我好奇,就比如这watch选项。

当然了,如何看源码我就不解释了,不同的人有不同的看法,我就送initState函数说起

export function initState (vm: Component) {
  vm._watchers = []
  const 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)
  }
}

这里根据我们在vue实例中定义的不同选项,进行统一的初始化,比如,props, data, methods, computed,还有watch选项。

这篇文章我就仅仅学习watch所以,就看最后一部分,如果你在Vue实例中定义了watch选项,那么这里就会创建watch侦听器,如何创建的呢?

这里提一句,在判断opts.watch的时候,跟nativeWatch做了一个对比,这是因为,Firefox也有一个watch函数在Object.prototype.

// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch

继续看watch.


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

我们一般在vue实例上定义watch选项都是使用Object.在Object里,使用需要侦听的data对象或者props对象里面的属性作为key,定义一个function来作为对应Key发生变化时的handler.

所以在initWatch函数里,我们遍历定义的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的功能可以用最后一行代码体现

vm.$watch( expOrFn, callback, [options] )

观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。

vm.$watch 返回一个取消观察函数,用来停止触发回调:

var unwatch = vm.$watch('a', cb) 
// 之后取消观察 
unwatch()

为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。

vm.$watch('someObject', callback, { 
    deep: true 
}) 
vm.someObject.nestedValue = 123 
// callback is fired

在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调:

vm.$watch('a', callback, { 
    immediate: true 
}) 
// 立即以 `a` 的当前值触发回调

注意在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的 property。

// 这会导致报错 
var unwatch = vm.$watch( 'value', function () { 
    doSomething() 
    unwatch() 
}, { immediate: true })

如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:

var unwatch = vm.$watch( 'value', function () { 
    doSomething() 
    if (unwatch) { 
        unwatch() 
    } 
}, { immediate: true })

回顾

现在再来看看导读里面的问题,你看看ElementUI中的代码

watch: {
      defaultCheckedKeys(newVal) {
        this.store.setDefaultCheckedKey(newVal);
      },

      defaultExpandedKeys(newVal) {
        this.store.defaultExpandedKeys = newVal;
        this.store.setDefaultExpandedKeys(newVal);
      },

      data(newVal) {
        this.store.setData(newVal);
      },

      checkboxItems(val) {
        Array.prototype.forEach.call(val, (checkbox) => {
          checkbox.setAttribute('tabindex', -1);
        });
      },

      checkStrictly(newVal) {
        this.store.checkStrictly = newVal;
      }
},

首先这些函数的名会最为key来进行侦听,为什么可以呢,因为这些key在data,或者props选项中都是有定义的。完全可以这些写。而这些函数的参数,也就是回调函数的参数,新值和旧值。