vue核心面试题(Watch中的deep:true 是如何实现的 )

概念:

这儿的watch就是用户写的watch方法,在写watch时有两个属性一个immediate是用来确定是否立即执行方法的,另一个属性就是下来要讨论的deep,当它为true就会实现深度监听,今天我们是需求知道它怎么实现的?

当用户指定了 watch 中的deep属性为 true 时,如果当前监控的值是数组类型。会对对象中的每 一项进行求值,此时会将当前 watcher 存入到对应属性的依赖中,这样数组中对象发生变化时也 会通知数据更新。

computed为什么没有deep:true?因为computed是用在模板里的,在模板中的数据会调一个方法JSON.strginify(),放一个对象,默认会对这个对象里的所有属性求值。

原理:

调用initWatch初始化watch时传入的是一个对象,在initWatch中会调用createWatcher方法,里面有一个核心方法vm.$watch(),会传入三个参数第一个就是我们要监听的属性,第二参就是要执行的函数,第三参就是watch传入的属性,在这个$watch中会创建一个watcher实例,在watcher中会对传入的属性进行判断,如果expOrFn是函数,就直接给了getter,如果不是会将它会将这个expOrFn转成函数,因为这是用户定义的watch不是计算属性所以会调用watcher中的get方法,在get中最先将watcher挂载在了全局上,这时候就会执行用户的方法,进行依赖收集(这儿说一下它是怎么进行的依赖收集,在调用render函数触发dom中响应式变量的get的时候,在oberver中就有一行代码是如果Dep.target有watcher了就执行dep.depend()这个就会把watcher中依赖的属性全部追加依赖)当收集的属性变化了,watcher就会更新。这篇文章的核心点来了:如果用户要监听的是个对象,内层的对象就不会被依赖收集,这时候有一个deep属性,要想让收集到内层的对象,就需要将deep设置成true,这时候就会执行traverse这个方法,这个方法里就是做了个数组递归,如果是数组的话,会根据数组的每一项索引取值,进行递归追加依赖,如果是对象会拿的key进行遍历取值,进行递归追加依赖,traverse就是deep:true实现的核心。这样就会把数组或者对象的没一个属性都进行依赖追加进行监听,只有依赖发生变化就会通知视图更新。

源码:

1.initWatch

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

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

3.$watch

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true // 表示用户自己写的watch,表示与默认的渲染及计算属性没关系
    // vm.$watch('msg',()=>{}) 这是msg就会传到expOrFn,用户传的函数就是这的cb
    const watcher = new Watcher(vm, expOrFn, cb, options) // 创建了一个watcher
    if (options.immediate) { // 如果用户传入的immediate是true,就会立即执行传入的方法
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

4.new Watcher中的部分代码

  constructor (
    vm: Component,
    expOrFn: string | Function, // 这就是用户写在watch中要监听的属性
    cb: Function, // 当前用户定义的函数
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn) // 如果expOrFn是个字符串,会将这个expOrFn转成函数
      // msg function(){return vm.msg}
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy // 这时候lazy是false会执行get方法
      ? undefined
      : this.get()
  }

5.get方法

  get () {
    pushTarget(this) // 取值之前将watcher放到全局上,这个做了一个Dep.target = this操作
    // Dep.target只是一个标记,存储当前的watcher实例
    let value
    const vm = this.vm
    try { // 默认将expOrFn进行取值,这时候就将watcher收集起来了,当收集的属性变化了,watcher就会更新
    //如果expOrFn的值是一个对象,内层的对象就不会被依赖收集,要想让收集到内层的对象,
    //就需求把这个对象循环一遍,在循环的过程中就会把里面的watcher也收集起来
      value = this.getter.call(vm, vm) // 执行用户传入的方法,进行依赖收集
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) { // 如果deep是true会执行traverse方法
        traverse(value)
      }
      popTarget() // 进来之后把当前的watcher删除,Dep.target标记为上一个watcher
      this.cleanupDeps()
    }
    return value
  }
export function pushTarget (target: ?Watcher) {
  targetStack.push(target) // 在触发get的时候,将当前watcher存入targetStack中
  Dep.target = target // Dep.target存储为当前watcher的实例
}

export function popTarget () {
  targetStack.pop() // 在取值后将当前watcher删除
  Dep.target = targetStack[targetStack.length - 1] // Dep.target存储为当前上一个watcher
}

在get方法中,进来执行了pushTarget将当前watcher放在了targetStack里,然后在最后又将当前watcher从targetStack移除,我认为这儿做了个操作就是为了更改Dep.target的标记的,就是标记了当前的watcher,在标记后取值的时候对当前watcher进行依赖追加,取值追加依赖后又把Dep.target的标记改为初始的,初始的就是null,targetStack初始空的数组,在每次执行get对targetStack追加后又移除,我认为这个targetStack里面最多就一个元素,用完又删了,没太明白这样写的用意,有哪个大佬看明白了,评论区讲一下?

6.traverse 

export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  // 如果是数组的话,会根据数组的没一项索引取值,进行递归
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen) // 循环数组
  } else {
    keys = Object.keys(val) // 如果是对象会拿的key进行遍历取值,只有一取值就会把当前watcher存起来
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>