vue源码系列4之computed与watch的区别


前言

有过面试的人应该知道,vue 的经典问法:computed与watch的区别:)这时候你就应该找一下官网:

  1. 计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。
  2. 当需要在数据变化时执行异步或开销较大的操作时。
  3. 巴拉巴拉…但是却不知道为啥: )

提示:以下是本篇文章正文内容,下面案例可供参考

一、官网例子

在我们index.html的加上我们的官网案例修改一下

 <div id="root">
        <p>{{ fullName }}</p>
        <p>
            Ask a yes/no question:
            <input v-model="question">
        </p>
        <p>答案{{ answer }}</p>
    </div>
    <script>
        new Vue({
            el: '#root',
            data: {
                firstName: 'Foo',
                lastName: 'Bar',
                question: '',
                answer: 'I cannot give you an answer until you ask a question!'
            },
            computed: {
                fullName: function() {
                        return this.firstName + ' ' + this.lastName
                    }
                    // fullName: {
                    //     get: function() {
                    //         return this.firstName + ' ' + this.lastName
                    //     },
                    //     // setter
                    //     set: function(newValue) {
                    //         var names = newValue.split(' ')
                    //         this.firstName = names[0]
                    //         this.lastName = names[names.length - 1]
                    //     }
                    // }
            },
            watch: {
                question: function(newQuestion, oldQuestion) {
                    this.answer = 'Waiting for you to stop typing...'
                    this.getAnswer()
                }
            },
            methods: {
                getAnswer() {
                    var vm = this
                    setTimeout(() => {
                        vm.answer = 233
                    }, 5000)
                }
            }
        })

好了现在进入源码:其实就是三种watcher 的区别,渲染watcher(上一篇),计算watcher,用户watcher。

二、watch

1.还是initSates

src\core\instance\state.js

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)//进入
  }
}

2.进入initWatch

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]//拿到方法 如上面的question
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)//不是数组直接使用watcher 
    }
  }
}

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

2.vm.$watch(“question”, question(…){…}, options) 核心

Vue.prototype.$watch = function(
        expOrFn: string | Function,//'question'
        cb: any,//question(){}
        options ? : Object
    ): Function {
        const vm: Component = this
       options.user = true,//用户watcher
        const watcher = new Watcher(vm, expOrFn, cb, options)//回到了Watcher
        if (options.immediate) {
            const info = `callback for immediate watcher "${watcher.expression}"`
            pushTarget()
            invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)//立马执行,// 以当前值立即执行一次回调函数
            popTarget()
        }
        return function unwatchFn() {
            watcher.teardown()
        }
    }

注意到了吗,new Watcher(vm, expOrFn, cb, options)与渲染watcher 的区别

new Watcher(vm, updateComponent, noop, {
        before() {
            if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, 'beforeUpdate')
            }
        }
    }
  1. 渲染watcher 第二个函数是updateComponent函数,第二个 callback函数是空。
  2. 用户watcher 第一个是属性名,第二个函数回调question(){}

3. 再进Watcher

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  user: boolean;
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    vm._watchers.push(this)
    // options
    if (options) {
      this.user = !!options.user
    }
    this.cb = cb
    // parse expression for getter
    
      this.getter = parsePath(expOrFn)//得是对象路径形式,如user.name 返回一个闭包
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)//执行那个闭包
    } 
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
}

4. parsePath(expOrFn)

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')//比如watch "user.nam2" segments=['user','name']
  return function (obj) {//obj =vm
    for (let i = 0; i < segments.length; i++) { //
      if (!obj) return
      obj = obj[segments[i]]//vm.user ==>obj = user
      //第二次循环 user.name  obj = name 其实就是读取这个watch 的对象 触发dep.depend
    }
    return obj
  }
}

dep 收集的是这个user -watcher 当触发更新的时候,执行的是cb 罢了

update () {
    else if (this.sync) {
      this.run()
    } 
  }

  run () {
    if (this.active) {
      const value = this.get() //再次get()执行那个闭包 得到的是新值
      if ( ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  1. 首先必须是一个响应式数据,
  2. new 一个user-watcher 回调放在了第三个参数上,此时会访问到该响应式数据的dep,并把该watcher 加入该响应式数据的dep,当该响应式数据更新的时候会触发更新,watcher.update拿到新值进行回调的执行。

三、computed


class Watcher{
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    this._watchers.push(this)   
    if(options) {
      this.lazy = !!options.lazy  // 表示是computed
    }    
    this.dirty = this.lazy  // dirty为标记位,表示是否对computed计算   
    this.getter = expOrFn  // computed的回调函数
    this.value = undefined
  }
}

function initComputed(vm, computed) {
  const watchers = vm._computedWatchers = Object.create(null) // 创建一个纯净对象
  
  for(const key in computed) {
    const getter = computed[key]  // computed每项对应的回调函数
    
    watchers[key] = new Watcher(vm, getter, noop, {lazy: true})  // 实例化computed-watcher
    
    for(const key in computed) {
    	const getter = computed[key]  // // computed每项对应的回调函数
    	...
    	if (!(key in vm)) {
      	defineComputed(vm, key, getter)//把computed 的key 成为一个响应式数据,但也没订阅的时候 作为一个dep 而它的回调是computed每项对应的回调函数
    	}
    }
  }
}

function defineComputed(target, key) {
  ...
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: createComputedGetter(key),
    set: noop
  })
}


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

class Watcher {
  ...
  
  evaluate () {
    this.value = this.get()  //  计算属性求值 此时会触发computed内可以访问到的响应式数据的get 把当前watcher 加入响应式数据的dep
    this.dirty = false  // 表示计算属性已经计算,不需要再计算
  }
  
  depend () {
    let i = this.deps.length  // deps内是计算属性内能访问到的响应式数据的dep的数组集合
    while (i--) {
      this.deps[i].depend()  // 让每个dep收集当前的render-watcher
    }
  }
}

更新:

 update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } 
  }

简单点说就是:用一个c=a+b 的计算属性举例。也可以是c{get()}

  1. 拿到computed每项对应的回调函数 也就是c(){return a+b}
  2. new 一个 Watcher,c(){}回调在第二个参数,并且带一个lazy computed 的标记位,
  3. 把 computed属性c进行响应式,
  4. 并在computed对象属性c的get 做处理,当有人访问这个计算属性,执行计算的回调函数,
  5. 因为计算属性里面有响应式数据,触发该响应式数据,把该计算watcher 收集到该响应式数据的dep,进行计算后得到第一次的值,并把dirty 位进行标记,还记得newDeps,也会收集那个计算属性里面的响应式数据dep。(前两篇忘说了,watcher 与dep 是双向收集的)并让这些dep 去收集该计算watcher 的订阅者,比如{{c}},渲染watcher 肯定也订阅了
  6. 当有人访问计算watcher 的时候由于dirty 位 已经计算过,直接返回缓存。
  7. 当计算属性其中的响应式数据更新了,订阅了该计算watcher,触发计算watcher 的update,标记为为true,表明需要下次计算需要重新计算。
  8. 计算属性其中的响应式数据更新,这个响应试数据的dep由于第5步,也会去触发render (如果视图用上了),更新。

简化:先new 一个计算watcher,把计算属性进行响应式,当有人访问这个计算属性,比如渲染watcher,触发了get,进行计算,由于访问里面的响应式数据,比如说a,a的dep就收集了该watcher,也收集了这个渲染watcher,计算到结果后把dirty为变true
当有人访问计算属性,由于dirty为直接返回缓存,当a改变,先通知计算watcher把dirty改变,再通知渲染watcher,同时触发computed 的get,dirty为改了,重新求值。


总结

区别就是vue 中有三种watcher : render-watcher,计算watcher,user-watcher.

有空再画个图

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值