Vue中的watch

简答的不介绍,文档都有https://cn.vuejs.org/v2/guide/computed.html

N年没用watch,突然看到同事用上了一脸懵,理论上computed+Vuex的getters还不能满足,上watch也不能100%解决。
但是用上了发现这货还挺坑的,细细说来

watch的简单例子

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue-watch</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
</head>

<body>
  <div id="watch-example">
    <p>
      Ask a yes/no question:
      <input v-model="question">
    </p>
    <p>{{ answer }}</p>

    <div style="margin-top: 20px">
      {{ fullName }}
    </div>
  </div>
  <script>
    var watchExampleVM = new Vue({
      el: '#watch-example',
      data: {
        question: '',
        answer: 'I cannot give you an answer until you ask a question!',
        firstName: '',
        lastName: '',
        fullName: '全名'
      },
      watch: {
        // 如果 `question` 发生改变,这个函数就会运行
        question: function (newQuestion, oldQuestion) {
          this.answer = 'Waiting for you to stop typing...'
          this.debouncedGetAnswer()
        },
        firstName: {
          handler(newVal, oldVal) {
            console.log(`firstName update ${oldVal} -> ${newVal}`)
            this.fullName = this.firstName + this.lastName
          },
          // immediate: true // 这里如果不添加immediate属性,则不马上执行handler
        },
        lastName: (newVal, oldVal) => {
          console.log(`lastName update ${oldVal} -> ${newVal}`)
          this.fullName = this.firstName + this.lastName
        }
      },
      created: function () {
        // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
        // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
        // AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
        // `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
        // 请参考:https://lodash.com/docs#debounce
        this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)

        this.firstName = 'Manfred'
      },
      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>
</body>

</html>
  • 首先是immediate这个属性,我们知道watch只是在Vue里面注册了监听器,我们可以通过Vue实例的_watchers属性,如watchExampleVM._watchers来获取watchers队列。
    监听器注册后是否需要马上执行一遍,默认是不执行,immediate=false,当然也可以声明执行。常见的watch的这个属性在页面渲染的时候就需要显示,那么就一定是immediate: true
  • 其次是deep这个属性,如果是对象嵌套的很深,那么deep就可以往下遍历对象的每一层属性,并监听属性,这样做开销会非常大。如下面的例子
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue-watch</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>

<body>
  <div id="root">
    <p>obj.a.b.c.d: {{obj.a.b.c.d}}</p>
    <p>obj.a.b.c.d: <input type="text" v-model="obj.a.b.c.d"></p>
  </div>
  <script>
    var watchExampleVM = new Vue({
      el: '#root',
      data: {
        obj: {
          a: {
            b: {
              c: {
                d: 123
              }
            }
          }
        }
      },
      watch: {
        obj: {
          handler(newName, oldName) {
            console.log('obj.a changed', newName, oldName);
          },
          // deep: true,
        },
      },
      mounted () {
        this.obj.a.b.c.d = 456 // deep不设置为true不生效
      }
    })
  </script>
</body>

</html>

如果要简单处理,我们可以这么做,这样虽然也是一层层遍历,但是只监听了最后一层的属性变化

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue-watch</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>

<body>
  <div id="root">
    <p>obj.a.b.c.d: {{obj.a.b.c.d}}</p>
    <p>obj.a.b.c.d: <input type="text" v-model="obj.a.b.c.d"></p>
  </div>
  <script>
    var watchExampleVM = new Vue({
      el: '#root',
      data: {
        obj: {
          a: {
            b: {
              c: {
                d: 123
              }
            }
          }
        }
      },
      watch: {
        'obj.a.b.c.d': {
          handler(newName, oldName) {
            console.log('obj.a changed', newName, oldName);
          }
        },
      },
      mounted () {
        this.obj.a.b.c.d = 456
      }
    })
  </script>
</body>

</html>

例子非常简单,watch舰艇question,如果变化了就调用接口。但是这个时候问题来了,如果此时你要取消这个watch怎么办?遇到的场景是,某某组件框架有bug。框架watch了一个props属性,如果这个属性变化了,就重置组件。但是你不能去改框架底层代码。所以,只能修改实例。

watch动态注入(add watch dynamically)

watch可以在代码声明解析,也支持实例创建。

如上面例子

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue-watch</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>

<body>
  <div id="root">
    <p>obj.a.b.c.d: {{obj.a.b.c.d}}</p>
    <p>obj.a.b.c.d: <input type="text" v-model="obj.a.b.c.d"></p>
  </div>
  <script>
    var watchExampleVM = new Vue({
      el: '#root',
      data: {
        obj: {
          a: {
            b: {
              c: {
                d: 123
              }
            }
          }
        }
      },
      mounted () {
        // 这里注册watch
        const objWatch = this.$watch('obj.a.b.c.d', (newName, oldName) => {
          console.log('obj.a changed', newName, oldName);
        })
        this.obj.a.b.c.d = 456

        setTimeout(() => {
          objWatch()
          console.log('虽然我改变了值,但是watch被取消了,不执行了')
          this.obj.a.b.c.d = 789
        }, 2000)
      }
    })
  </script>
</body>

</html>

vm.$watch注册与代码写死是一个道理,同时值得一提的是这里会返回一个watch以便取消。动态注入的方式很容易取消watch,那么代码里写的watch要怎么取消呢?

取消实例的watch

主要是文档也翻不到,网上也搜不到对应答案,脑瓜嗡嗡作响。最终,还是觉得「Talk is cheap. Show me the code」真理大法好。

直接看源代码https://github.com/vuejs/vue/blob/2.6/src/core/observer/watcher.js#L234

简单说就是被存到vm._watchers后,找到这个watcher,watcher的teardown方法,就是取消watch的方法了。我可以愉快修改别人框架的Vue实例了

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue-watch</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>

<body>
  <div id="root">
    <p>obj.a.b.c.d: {{obj.a.b.c.d}}</p>
    <p>obj.a.b.c.d: <input type="text" v-model="obj.a.b.c.d"></p>
  </div>
  <script>
    var watchExampleVM = new Vue({
      el: '#root',
      data: {
        obj: {
          a: {
            b: {
              c: {
                d: 123
              }
            }
          }
        }
      },
      watch: {
        'obj.a.b.c.d': {
          handler(newName, oldName) {
            console.log('obj.a changed', newName, oldName);
          }
        },
      },
      mounted () {
        this.obj.a.b.c.d = 456

        setTimeout(() => {
          // delete watch by use watcher.teardown()
          watchExampleVM._watchers.find(item => item.expression === 'obj.a.b.c.d').teardown()
          console.log('虽然我改变了值,但是watch被取消了,不执行了')
          this.obj.a.b.c.d = 789
        }, 2000)
      }
    })
  </script>
</body>

</html>
Last Updated:
<manfred>峯</hu>
欢迎关注微信公众号 【Big前端】无广告,无软文,就是这么傲娇。直推一线大厂高质量内容,不局限于前端·后台·运维相关,还包括房价🏠、信用卡💳等内容也可内推一线大厂腾讯阿里字节,对腾讯字节比较熟悉,简历可以发给我,我会给你介绍一线大厂的情况,让你更加了解一线大厂