前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >石桥码农:20 vue计算属性和侦听器

石桥码农:20 vue计算属性和侦听器

作者头像
LIYI
发布2020-02-13 11:58:24
6240
发布2020-02-13 11:58:24
举报
文章被收录于专栏:艺述论专栏艺述论专栏
代码语言:javascript
复制
watch的实现原理是什么?

计算属性

在template里的插值表达式,如果太长,会让模板代码变得难于维护;如果有多处用到了同样的插值表达式,也不便于复用和修改。例如,这样的一个插值表达式:

代码语言:javascript
复制
<div>{{ message.split('').reverse().join('') }}</div>

基于这个需求,vue提供了计算属性。

示例:

代码语言:javascript
复制
<div>{{ reversedMessage }}</div>
...
data: ()=>({
    message:'hello vue'
  }),
computed:{
    reversedMessage:function(){
      return this.message.split('').reverse().join('')
    }
}

这段代码运行之后出现了一段警告:

代码语言:javascript
复制
[Vue warn]: Computed property "reversedMessage" was assigned to but it has no setter.

报错原因:你没有给计算属性设置set函数,却修改了计算属性的值。解决方法:给计算属性加上set函数。修改示例:

代码语言:javascript
复制
computed:{
    // reversedMessage:function(){
    //   return this.message.split('').reverse().join('')
    // }
    reversedMessage:{
      get(){
        return this.message.split('').reverse().join('')
      },
      set(val){}
    }
},

运行效果:

计算属性在computed中定义,以Function的形式定义,不保存状态,持有对data变量的引用,当相关的data变量变化时,计算属性亦随之变化。

注意,计算属性的Function不能使用箭头函数,因为箭头函数没有this。使用箭头函数定义计算属性,就不能引用data变量了。

有两个问题:

  1. 为什么data变量变化时,计算属性也会随之变化,这个自动渲染的机理是怎么实现的?
  2. get的本质是什么,为什么在getter内可以访问this?

第2个问题,get语法将对象属性绑定到查询该属性时将被调用的函数,所以本质上get属性是一个函数,只是它在调用时,不必加(),并且还有以下两点优势:

  • 如果属性值的计算是昂贵的,getter可以智能化缓存该值
  • 如果不被使用,就不会有计算成本

这两个优势是js es6本身带来的福利。

替代计算属性的计算方法

计算属性还有一个优点,就是它是基于它们的响应式依赖进行缓存的。例如reversedMessage依赖于message,只要message不变化,reversedMessage会一直缓存,多次访问 reversedMessage 计算属性会立即返回之前的计算结果。

计算属性reversedMessage也可以使用计算方法取代,示例:

代码语言:javascript
复制
methods: {
    reversedMessage: function () {
      return this.message.split('').reverse().join('')
    }
}

运行效果是一样的。

计算方法的特点是没有缓存,每次视图渲染时方法总会被调用。如果数据量大,计算开销很大,性能是一个问题。但如果计算开销不大,没有缓存反而是一个优点。

有一个问题:

计算方法依赖的数据变化了,视图会自动渲染吗?

答案竟然是会。

测试示例:

代码语言:javascript
复制
<div>{{ reversedMessage2() }}</div>
...
methods: {
    reversedMessage2: function () {
      return this.message.split('').reverse().join('')
    }
},
created(){
    setInterval(()=>{
      this.message += new Date().getTime()%100
    },1000)
}

运行效果:

为什么计算方法也是响应式的?

可能的解释是:在第一次模板渲染时,即使插值是js表达式,抑或是函数,当data变量的set属性被访问时,插值的依赖已经被收集了,这样如果依赖项更新了,插值自然也会更新。

代码语言:javascript
复制
Object.defineProperty(obj, key, {
    get: function () {
      // 添加订阅者 watcher 到主题对象 Dep
      if (Dep.target) dep.addSub(Dep.target);
      return val
},

这或许也是第1小节中第一个问题的答案,为什么data变量变化时,计算属性也会随之变化,因为插值的依赖在第一次编译时就计算好了。

在编译时,可以将计算方法、计算属性都看作是一个特殊的js表达式。

计算属性实现的原理

回到刚才的问题,计算属性如何与属性建立依赖关系?属性发生变化又如何通知到计算属性重新计算?

在src/core/instance/state.js文件内有一个函数initComputed,是处理计算属性的:

代码语言:javascript
复制

// 初始化计算属性
function initComputed (vm: Component, computed: Object) {
  ...
  for (const key in computed) {
    ...
    defineComputed(vm, key, userDef)
    ...
  }
}

辗转调用数次后,执行指针到了:

代码语言:javascript
复制
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

其中if (Dep.target) { watcher.depend()}是关键,当Dep.target有值,且处于收集时,为当前的节点收集这个依赖。

而Dep.target此时是什么呢?

关键在于src/core/observer/watcher.js文件中的get:

代码语言:javascript
复制
get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 这里的getter是指向计算属性,而计算属性又会指向data变量的getter
    value = this.getter.call(vm, vm)
  } catch (e) {
    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
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

先是通过pushTarget(this)推入,此时Dep.target有值了,最后是popTarget(),Dep.target为空了。在此期间很关键的关于this.getter.call的调用,会指向开发者定义的计算属性(reversedMessage),而计算属性又会指向data变量的getter:

代码语言:javascript
复制
// src/observer/index.js
Object.defineProperty(obj, key, {
  get: function reactiveGetter () {
    if (Dep.target) {
      // 建立依赖关系
      dep.depend()
      ...
    }
    return value
  },
  set: function reactiveSetter (newVal) {
    ...
    // 依赖发生变化,通知到计算属性重新计算
    dep.notify()
  }
})

当全局变量Dep.target非空的,建立依赖关系;当值发生变化时,广播通知视图节点更新。

关于计算属性reversedMessage,建立响应式的流程可以概括为:

代码语言:javascript
复制
1. 首先,data 属性初始化,建立getter/setter
2. computed 计算属性初始化,提供的函数将用作属性 vm.reversedMessage 的 getter
3. 当首次获取 reversedMessage 计算属性的值时,Dep 开始依赖收集
4. 在 message getter 被调用时,如果 Dep 处于依赖收集状态,则判定 message 为 reversedMessage 的依赖,并建立依赖关系
5. 当 message 发生变化时,根据依赖关系,触发 reverseMessage 的重新计算,这是通过dep.notify()达成的

以上就是计算属性响应式机制的实现原理。vue在处理插值中的js表达式与计算方法时,响应式的实现原理与之是类似的。

由于计算属性与data对象的属性一样,都要被defineProperty重定义在vm上,所以计算属性对象computed中的名称与data对象中的不能重复。

methods中的函数名称同理,亦不能与计算属性有重名。

侦听属性

侦听属性是有一些数据需要随着其它数据变动而变动时使用的。示例:

代码语言:javascript
复制
<div id="demo">{{ fullName }}</div>
watch: {
    message: function (val) {
      this.fullName = val + ' ' + new Date().getTime()
    },
},

当message变化时,主动改变fullName的值。

事实上侦听属性在开发中并不被经常使用。类似于钩子逻辑的做法会让业务逻辑变得头绪繁乱而复杂。如果需要计算的值完全是同步的,侦听属性完全可以由计算属性或计算方法代替;如果包括异步代码,可以考虑使用事件机制代替。

把侦听属性当作事件监听用

以下示例改自于官方文档,当数据变化时触发一个函数的执行:

代码语言:javascript
复制
<p>
  Ask a yes/no question:
  <input v-model="question" />
</p>
<p>{{ answer }}</p>
watch: {
    question: function (newQuestion, oldQuestion){
      this.getAnswer()
    }
},
getAnswer: function () {
  if (!/[??]/.test(this.question)) {
    this.answer = 'Questions usually contain a question mark. ;-)'
    return
  }
  if (this.answer == 'Thinking...') return
  this.answer = 'Thinking...'
  var vm = this
  axios.get('https://yesno.wtf/api')
    .then(function (response) {
      vm.answer = response.data.answer
    })
    .catch(function (error) {
      vm.answer = 'Error! Could not reach the API. ' + error
    })
}

当question变化时,自动调用getAnswer函数,请求一个接口。由于接口请求是异步的,这里要适当做一个滤重。

运行效果:

在运行时如果提示缺少axios:

代码语言:javascript
复制
[Vue warn]: Error in callback for watcher "question": "ReferenceError: axios is not defined"

此时需要安装网络类库:

代码语言:javascript
复制
yarn add axios

然后在script添加引用:

代码语言:javascript
复制
import axios from 'axios'

handler方法和immediate属性:监听属性时要立即执行函数怎么做?

watch 有一个特点是,最初绑定的时候是不会执行的,要等到被监视的属性改变时才执行方法。如果想要一开始就执行,应该怎么做?

修改示例:

代码语言:javascript
复制
question: {
  handler (newQuestion, oldQuestion) {
    this.getAnswer()
  },
  // 代表了立即执行handler方法
  immediate: true
}

想监听子属性、子子属性的变化应该怎么做?

如果被监听的数据属性是obj,而obj又有一个子属性a,正常情况下当a发生变化时,obj是不被视为有变化的,watch机制不会被触发。除非将watch的默认选项deep,由false改为true:

代码语言:javascript
复制
<div>
      <p>obj.a: {{obj.a}}</p>
      <p>obj.a: <input type="text" v-model="obj.a"></p>
</div>
new Vue({
  data: {
    obj: {
      a: 123
    }
  },
  watch: {
    obj: {
      handler(newName, oldName) {
         console.log('obj.a changed');
      },
      deep:true,
      immediate: true
    }
  } 
})

watch的注销

通过声明创建的watch,不用手动注销,它会随着组件的销毁而自动注销。

但是通过vm.$watch手动创建的监听,却需要手动注销:

代码语言:javascript
复制
const unWatch = vm.$watch('text', (newVal, oldVal) => {
  console.log(`${newVal} : ${oldVal}`);
})
...
unWatch(); // 手动注销watch

vm.$watch返回一个unWatch方法,在destroyed周期函数中调用即可。

watch的实现原理是什么?

代码语言:javascript
复制
createWatcher (vm, key, handler) {
  let options
  ...
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  vm.$watch(key, handler, options)
}

如上所示,通过代码声明创建的watch,最终调用的还是vm.$watch方法。面在vue源码中,关于这个watch方法的实现,代码大概是这样的:

代码语言:javascript
复制
Vue.prototype.$watch=function(expOrFn,cb,options){
	const vm = this
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    //immediate=true时 马上执行一次
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    //返回取消监听的匿名函数
    return function unwatchFn () {
      watcher.teardown()
    }
}

基本思想就是创建一个Watcher对象,注册好依赖,等data属性更新的时候,会触发Watcher对象的update方法。这与data属性的响应式实现的原理是类似的。

源码

https://git.code.tencent.com/shiqiaomarong/vue-go-rapiddev-example/tags/v20200116

参考链接

  • https://www.jianshu.com/p/318a91bce00e
  • https://blog.csdn.net/zhaohanqq/article/details/84527836
  • https://segmentfault.com/a/1190000010408657
  • https://blog.csdn.net/weixin_41646716/article/details/90242968
  • https://juejin.im/post/5af908ea5188254265399009
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-01-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 艺述论 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 计算属性
    • 替代计算属性的计算方法
      • 计算属性实现的原理
      • 侦听属性
        • 把侦听属性当作事件监听用
          • handler方法和immediate属性:监听属性时要立即执行函数怎么做?
            • 想监听子属性、子子属性的变化应该怎么做?
              • watch的注销
              • watch的实现原理是什么?
              • 源码
              • 参考链接
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档