Vue源码窥探之 watch

博客主要介绍了在小程序中实现某功能的方法。包括功能的初始化、核心函数调用以监听值的变化,还提及常见配置项。阐述了相关方法的实现过程,如递归遍历收集依赖等。最后指出原使用方式的不足,并提出利用新思路改造以避免冗余。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

watch 的初始化是在 stateMixin 中进行的,在 instance/state

export function initState (vm: Component) {
  ...
  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)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

这里不管其 handler 是一个 Array 或者 Fun,如果是 Array 就遍历执行 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)
}

这里因为 watch 可以是个对象,也可以是个函数。
拿到其最终的回调函数,最后调用 vm.$watch 函数,该函数定义在 Vue 的原型中,在 stateMixin 的时候定义。

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
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

核心在于最终会调用 const watcher = new Watcher(vm, expOrFn, cb, options) ,这样就可以监听到值的变化了,需要注意的是,这是一个 user watcher,一旦我们 watch 的数据发生变化,就会执行 watcherrun 方法,从而执行我们的 cb 函数,如果设置了 immediate ,就会直接执行回调函数 cb,最终返回一个 unwatchFn,这个是移除这个 watcher,这个我们在业务中使用的到。

watch 中常见的配置项有 immediatedeepsync

  • immediate 的实现我们已经清楚。
  • deep ,我们知道之前我们实例化了一个 wacher ,最终会执行 get 方法
get () {
	...
    // dependencies for deep watching
    if (this.deep) {
       traverse(value)
    }

get 方法中,我们的是 this.deeptrue,最终会执行 traverse(value)

const seenObjects = new Set()

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)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

这里其值是 Array 或者 Object 都会递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher。

  • sync 的实现就比较简单了,因为我们知道,当响应式数据发生变化的时候,会触发 watcher.update 方法,但只是把这个 watcher 放入到一个队列中,在 nextTick 之后才会执行该 watcher 的回调函数,但我们通过设置 sync ,就可以立即执行回调函数。
update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

在小程序中实现 wacth 功能

新建一个 utils.js

function observe(obj, key, cb, deep, page) {
  
  let val = obj[key]
  if (val !== null && typeof val === 'object' && deep) {
    Object.keys(val).forEach(item => {
      observe(val, item, cb, deep, page)
    })
  }
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: true,
    get() {
      return val
    },
    set(value) {
      if (value === val) {
        return
      }
      cb.call(page, value, val)
      val = value
    }
  })
}

function setWatch(page) {
  let { data, watch } = page
  Object.keys(watch).forEach((item) => {
    let cb = watch[item].handler || watch[item]
    let deep = watch[item].deep
    observe(data, item, cb, deep, page)
  })
}

module.exports = {
	setWatch
}

然后在页面中引入即可

import { setWatch } from '../../utils.js'
Page({
	data: {
		name: 123
	},
	onLoad() {
		setWatch(this)
	},
	watch: {
		name(newVal, oldVal) {
			console.log(newVal)
		}
	}
})

方法是比较简单的,在 setWatch 函数中取到 handlerdeep ,然后作为参数最终执行 observe 函数,然后利用 Object.defineProperty 来进行数据响应式。
着重说一下,我们的使用,可以看到我们在使用的时候还需要手动在 onLoad 中注册一下,这是不太好用的。再者我们可能很多个页面都需要使用该功能,那写起来就会很冗余,我们可以利用 mixin 的思路来改造一下 Page

utils 中增加一个方法

function customPage(options) {
  const { onLoad } = options
  options.onLoad = function() {
    options.watch && setWatch(this)
    onLoad && onLoad()
  }
  return Page(options)
}

module.exports = {
	...
	customPage
}

这样我们在页面中使用的时候只引用一个 customPage 即可,不用关心在 onLoad 中注册了。

import { customPage } from '../../utils/util.js'

customPage({
  data: {
    name: 123
  },
  watch: {
    name(newVal) {
	  console.log(newVal)
	}
  }
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值