YIngChenIt/Blogs

【源码】watch 源码解析

Opened this issue · 0 comments

【源码】watch 源码解析

前言

watchcomputed一直是我们开发和面试时候经常用的一个点,接下来我们通过源码的解析深入的了解watch的内部机制

源码解析

我们知道Vue加载的时候会调用initState来初始化state

// vue/src/core/instance/init.js
  Vue.prototype._init = function (options?: Object) {
      ...
      initState(vm) // 初始化state
      ...
  }

其实我们的watch的初始化在initState内部实现

// vue/src/core/instance/state.js
export function initState (vm: Component) {
  ... 初始化props、methods、data、computed等
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

我们接下来看下initWatch是什么名堂

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) { // 如果是数组,遍历调用createWatcher
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

我们会发现一个很有趣的事情,这里做了一个是否是数组的判断,这是因为mixin机制可以让watch是一个数组的形式

上述代码就是遍历数组或者对象,然后调用createWatcher方法

我们接下来看下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)
}

那么问题来了,为什么createWatcher方法中还要进行一层类型判断呢?

那是因为我们有3种watch的用法

watch: {
    name: {
        handler() {}
    },
    name() {},
    name: 'getName',
}

如果是对象的话获取对象的handler方法,如果是字符串则去实例上取这个方法

最后我们来看最关键的一个方法vm.$watch(代码做了删减)

Vue.prototype.$watch = function(
    // expOrFn 是 监听的 key,cb 是监听回调,opts 是所有选项
    expOrFn, cb, opts
){    
    // expOrFn 是 监听的 key,cb 是监听的回调,opts 是 监听的所有选项
    var watcher = new Watcher(this, expOrFn, cb, opts);    

    // 设定了立即执行,所以马上执行回调
    if (opts.immediate) {
        cb.call(this, watcher.value);
    }
};

首先我们可以发现一点,当我们使用watch的时候,如果设置immediate为true,会立刻将handler执行

然后我们会发现watch的核心, new Watcher(), 没错,watch就是通过Vue的发布订阅机制来实现的一个功能点,在源码中给
watch中的属性新建一个观察者watcher,然后就可以实现属性变化的时候,执行cd也就是watch中的handler了,如果对这一块不了解的同学可以看下我的关于Vue中MVVM原理的文章手写mvvm 之 实现数据双向绑定

最后还有一个小知识点,watch是如何实现深度监听的,也就是我们使用的时候设置的deep(代码有删减)

Watcher.prototype.get = function() {
  Dep.target= this
  var value = this.getter(this.vm)    
  if (this.deep)  traverse(value)
  Dep.target= null
  return value
};

我们可以发现,当设置deep为true的时候会走traverse方法

function _traverse (val, seen) {
  var i, keys;
  var isA = Array.isArray(val);
  if (isA) { // 如果是数组
    i = val.length;
    while (i--) { _traverse(val[i], seen); }
  } else { // 如果是对象
    keys = Object.keys(val);
    i = keys.length;
    while (i--) { _traverse(val[keys[i]], seen); }
  }
}

我们可以发现就是一直递归往下查找,这里有一个比较有趣的事情是它通过val[i]val[keys[i]] 变相的获取值,因为data里面的数据是具有响应式能力的,每一次获取都会触发getter,然后往对应的dep中添加watcher, 然后就实现深度监听的能力了

总结

  • watch 内部通过为设置的属性生成一个watcher的方式实现数据劫持

  • 深度监听的原理是通过不断的递归,变相的调用data的getter,然后往对应的dep里面添加watcher