【Ts重构Vue】05-实现computed和watch功能

2,671 阅读1分钟

如何创建computed和watch?

在项目中computed和watch非常实用,它们是如何实现的呢?

我们的编码目标是下面的demo能够成功渲染,最终渲染结果<h1>未读消息:2</h1>

let v = new Vue({
  el: '#app',
  data () {
    return  {
      news: [1]
    }
  },
  computed: {
    newsCount() {
      return this.news.length
    },
  },
  render (h) {
    return h('h1',  '未读消息:' + this.newsCount)
  }
})

setTimeout(() => {
    v.news.push(2)
}, 1000)

Vue响应式原理

根据上图可以知道,Vue将数据构造为响应式的,如果需要监听数据则要新建Watch的实例,建立Dep和Watch之间联系。

实现watch

watch的常见用法如下:

watch: {
    news () {
        console.log('watch news!')
    }
}

watch功能依托Watch类实现,在Vue初始化时,为所有watch属性创建Watch实例。

function initWatch(vm: Vue) {
  const watch = vm.$options.watch

  for (let key in watch) {
    new Watch(vm._proxyThis, key, watch[key], { user: true })
  }
}

new Watch实例化过程中,会将key转为函数,执行该函数可以获取被监听的属性值,另外会将watch[key]函数保存在this.cb变量中。

this.getter = isFunction(key) ? key : parsePath(key) || noop
this.cb = cb

function parsePath(key: string): any {
  return function(vm: any) {
    return vm[key]
  }
}

接着直接执行上一步的函数this.getter,收集所有依赖。

private get(): any {
    let vm = this.vm
    pushTarget(this)
    let value = this.getter.call(vm, vm)
    popTarget()

    return value
}

当被监听属性发生变化时,会通知Watch实例进行更新,从而执行this.cb.call(vm, value, this.value)函数。

实现computed

computed的调用形式主要有以下两种:

computed: {
    newsCount() {
      return this.news.length
    },
    newsStr: {
        get () {
            return this.news.join(',')
        },
        set (val) {
            this.news = val.split(',')
        }
    }
}

computed属性可以定义getset函数,因此比较特殊:1.它的值依赖于其他数据属性;2.修改它也会驱动视图进行更新。

computed功能同样依赖Watch类实现,在Vue初始化时,为所有的computed属性创建watch实例:new Watch(vm._proxyThis, getter, noop, {lazy: true})

function initComputed(vm: Vue) {
  let proxyComputed: any
  const computed = vm.$options.computed

  if (!isPlainObject(computed)) return

  for (let key in computed) {
    let userDef = computed[key]
    let getter = isFunction(userDef) ? userDef : userDef.get

    vm._computedWatched[key] = new Watch(vm._proxyThis, getter, noop, {
      lazy: true
    })
  }

  vm.$options.computed = proxyComputed = observeComputed(
    computed,
    vm._computedWatched,
    vm._proxyThis
  )
  for (let key in computed) {
    proxyForVm(vm._proxyThis, proxyComputed, key)
  }
}

接着将computed属性本身设置为响应式,同时调用createComputedGetter对属性进行封装。

当修改computed属性时,computed触发闭包变量dep.notify通知渲染更新。

当修改news属性时,会触发Vue进行渲染更新,在重新获取computed属性值的时候,会执行createComputedGetter封装后的函数,其本质是执行上一步的getter函数,并将计算结果返回。

function observeComputed(obj: VueComputed, _computedWatched: any, proxyThis: any): Object {
  if (!isPlainObject(obj) || isProxy(obj)) return obj

  let proxyObj = createProxy(obj)

  for (let key in obj) {
    defineComputed(proxyObj, key, obj[key], _computedWatched[key], proxyThis)
  }

  return proxyObj
}

function defineComputed(
  obj: any,
  key: string,
  userDef: VueComputedMethod,
  watcher: any,
  proxyThis: any
): void {
  if (!isProxy(obj)) return

  let dep: Dep = new Dep()

  const handler: any = {}
  if (isFunction(userDef)) {
    handler.get = createComputedGetter(watcher)
    handler.set = noop
  } else if (isObject(userDef)) {
    handler.get = createComputedGetter(watcher)
    handler.set = userDef.set || noop
  }

  defineProxyObject(obj, key, {
    get(target, key) {
      Dep.Target && dep.depend()
      return handler.get.call(proxyThis)
    },
    set(target, key, newVal) {
      handler.set.call(proxyThis, newVal)
      dep.notify()
      return true
    }
  })
}

function createComputedGetter(watcher: Watch): Function {
  return function computedGetter() {
    if (watcher) {
      // 计算值
      watcher.evaluate()
      // 将computed-dep添加watch对象
      Dep.Target && watcher.depend()

      return watcher.value
    }
  }
}

分析computed肯定要提到其缓存特性,这又是如何实现的?

我们知道获取computed的属性值时,会执行createComputedGetter封装后的函数,通过给Watch类添加dirty属性控制是否重新计算computed的属性值。Watch类的其他函数中肯定需要配合修改,如evaluateupdate方法。

function createComputedGetter(watcher: Watch): Function {
  return function computedGetter() {
    if (watcher) {
      // 计算值
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 将computed-dep添加watch对象
      Dep.Target && watcher.depend()

      return watcher.value
    }
  }
}

Vue的数据处理流程

Vue在实例化过程中,会对传入的数据进行初始化处理。

首先肯定是为prop、data创建闭包变量dep,接着才是初始化computed和watch的属性,在后者中创建Watch实例监听属性的变化。

initProps(this)
initMethods(this)
initData(this)
initComputed(this)
initWatch(this)

总结

Vue的响应式渲染依赖Dep和Watch,computed和watch功能也依赖它们,另外,Vue还封装了方法$watch对属性进行监听。为了支持上述功能,Watch和Dep添加了一些配置项,在理解源码时,可以进行一定忽略。

Dep和Watch设计的相当巧妙,我们自己编程能不能想到这样的方式?推荐学习下设计模式,或许能有所帮助。

系列文章

【Ts重构Vue】00-Ts重构Vue前言

【Ts重构Vue】01-如何创建虚拟节点

【Ts重构Vue】02-数据如何驱动视图变化

【Ts重构Vue】03-如何给真实DOM设置样式

【Ts重构Vue】04-异步渲染

【Ts重构Vue】05-实现computed和watch功能