Code前端首页关于Code前端联系我们

Vue

terry 2年前 (2023-09-08) 阅读数 142 #Vue
时钟源码解读
export default {
  data() {
    return {
      obj: {
        name: 'maoxiaoxing',
      }
    }
  },
  watch: {
    obj: {
      handler(val, oldval) {
        console.log(val, oldval)
      },
      deep: true,
      immediate: true,
    }
  },
  methods: {
    changeName() {
      this.obj.name = this.obj.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'
    }
  },
  created() {
    console.log('created')
  }
}
 
手表使用概览

watch是Vue中观察数据变化的方法。在看源码之前,我们先回顾一下如何使用时钟

监听基本数据类型

<div>
  {{ name }}
  <button @click="changeName">改变name</button>
</div>
 
export default {
  data() {
    return {
      name: 'maoxiaoxing',
    }
  },
  watch: {
    name(val, oldval) {
      console.log(val, oldval)
    }
  },
  methods: {
    changeName() {
      this.name = this.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'
    }
  }
}
 

手表可以接收两个参数,一个是变化后的,一个是变化前的,它可以根据这两个值进行一些逻辑处理

学生

<div>
  {{ obj.name }}
  <button @click="changeName">改变name</button>
</div>
 
export default {
  data() {
    return {
      obj: {
        name: 'maoxiaoxing',
      }
    }
  },
  watch: {
    obj: {
      handler(val, oldval) {
        console.log(val, oldval)
      },
      deep: true,
      immediate: true,
    }
  },
  methods: {
    changeName() {
      this.obj.name = this.obj.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'
    }
  },
  created() {
    console.log('created')
  }
}
 

监控对象变化时,包含deep属性,用于深度监控对象数据;如果你希望页面进来时执行watch方法,请立即添加。值得注意的是设置了immediate属性的watch的执行顺序是在创建的生命周期之前

时钟接收参数

作为数组

我在看Vue源码的时候,发现了一个比较有趣的地方。如果watch监听的属性没有设置方法而是接收一个数组,可以将多个方法传递给当前监听的属性

export default {
  data() {
    return {
      name: 'jack',
    }
  },
  watch: {
    name: [
      { handler: function() {console.log(1)}, immediate: true },
      function(val) {console.log(val, 2)}
    ]
  },
  methods: {
    changeName() {
      this.name = this.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'
    }
  }
}
 

数组可以接受不同形式的参数,可以是方法,也可以是对象。具体的书写方法与传统的时钟没有什么不同。官方文档并没有说明可以将数据作为参数接收。为什么可以这样写,下面源码解释中会讲到。

初始化手表

初始化状态

// src\core\instance\state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) // 如果有 props ,初始化 props
  if (opts.methods) initMethods(vm, opts.methods) // 如果有 methods ,初始化 methods 里面的方法
  if (opts.data) { // 如果有 data 的话,初始化,data;否则响应一个空对象
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed) // 如果有 computed ,初始化 computed
  if (opts.watch && opts.watch !== nativeWatch) { // 如果有 watch ,初始化 watch
    initWatch(vm, opts.watch)
  }
}
 

首先在initState状态下初始化watch,如果有watch属性,则将watch传给initWatch方法处理

initWatch

// src\core\instance\state.js
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)
    }
  }
}
 

该函数主要用于初始化时钟。我们可以看到initWatch遍历了时钟,然后判断每个值是否是一个数组。如果它是一个数组,它将循环遍历该数组并创建多个回调函数。本节也解释了上述内容。时钟监控的数据可以接收一个数组作为参数;如果不是数组,则直接创建回调函数。从这里我们也可以看出学习源码的好处。通过学习源码,你可以学到一些官方文档中没有提到的写作风格。

创建

观察者

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 判断 handler 是否是一个对象。如果是对象,则将处理程序附加到 options 属性,然后展开对象的处理程序属性;如果处理程序是字符串,则在 Vue 实例中找到此方法并将其分配给处理程序。从这里我们还可以看到,时钟也支持字符串写入。在 Vue 实例上执行 $watch 方法。

$参见

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  // 获取 Vue 实例 this
  const vm: Component = this
  if (isPlainObject(cb)) {
    // 判断如果 cb 是对象执行 createWatcher
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  // 标记为用户 watcher
  options.user = true
  // 创建用户 watcher 对象
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 判断 immediate 如果为 true
  if (options.immediate) {
    // 立即执行一次 cb 回调,并且把当前值传入
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  // 返回取消监听的方法
  return function unwatchFn () {
    watcher.teardown()
  }
}
 

watch函数是Vue的实例方法,即我们可以使用它,函数Vue.watch是Vue的实例方法,即我们可以使用它,文档解释得很详细。 $watch 创建一个 Watcher 对象,其中包含了 React 的原理。手表中修改的数据可以通过响应式的方式进行修改。不过,它也会判断是否有immediate属性,如果有,则直接调用回调。

观察者

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    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()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // expOrFn 是字符串的时候,例如 watch: { 'person.name': function... }
      // parsePath('person.name') 返回一个函数获取 person.name 的值
      this.getter = parsePath(expOrFn)
      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
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  /*获得getter的值并且重新进行依赖收集*/
  get () {
     /*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      /*
      执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。
      在将Dep.target设置为自身观察者实例以后,执行getter操作。
      譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c,
      那么在执行getter的时候就会触发a跟c两个数据的getter函数,
      在getter函数中即可判断Dep.target是否存在然后完成依赖收集,
      将该观察者对象放入闭包中的Dep的subs中去。
    */
      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
      /*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/
      if (this.deep) {
        /*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  ... 其他方法
}
 

对于上面的Watcher,我省略了其他一些方法,只保留了get函数。在get函数中我们可以看到,如果存在深度属性,则递归处理该对象的所有属性,从而达到深度监控的效果。 。关于手表的用途和原理的讲解就到此结束。通过阅读源码,我们不仅可以了解Vue框架的内部实现,还可以看到一些官方文档中没有提到的用途,这对我们来说非常有用。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

上一篇:Vue 响应性 下一篇:Vue3 项目工程

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门