前置知识点:

执行调度 https://segmentfault.com/a/11...
计算属性 https://segmentfault.com/a/11...

注释

在上一篇介绍了计算属性的实现原理, 这篇是 Vue 中时常和计算属性做比拟的侦听器的原理实现的简介.
所谓的侦听器watch实质上就是观测响应式数据是否发生变化, 当数据发生变化时告诉并执行相应的回调函数:

watch (obj, () => {    console.log('数据变动了')})// 批改数据导致响应式数据变动obj.foo++

实质上是利用了副作用函数effect以及调度选项option.scheduler:

effect(() => {  console.log(obj.foo)},// options{  scheduler () {    // obj.foo 变动时, 执行 scheduler 调度函数  }})

如果副作用函数存在scheduler选项, 当响应式数据发生变化时会触发scheduler调度函数执行, 而不是间接触发副作用函数利用这点能够实现最简略的watch函数:

function watch (source, cb) {  effect(    // 触发读取操作, 从而建立联系    () => source.foo,    {      scheduler () {        // obj.foo 变动时, 执行 scheduler 调度函数        cb()      }    }  )}

然而这个太根本了, 还只能监听obj.foo这个属性的变动, 因而须要封装一个通用的读取操作, 使watch具备通用性:

function watch (source, cb) {  effect(    // 触发读取操作, 从而建立联系    // 调用函数递归读取将每一个数据都建立联系    () => traverse(source),    {       scheduler () {        // obj.foo 变动时, 执行 scheduler 调度函数        cb()      }    }  )}function traverse (value, seen = new Set()) {  // 如果该值是原始数据类型, 或者已被读取过去就什么都不做  if (typeof value !== 'object' || value === null || seen.has(value)) return  // 将数据增加到 seen 中, 代表遍历过了 防止引起死循环  seen.add(value)  // 假如 value就是就是一个对象, 临时不思考数组等状况  for (const k in value) {    // 递归调用 traverse    traverse(value[k], seen)  }  return value}

这样就能够读取对象上的任意属性, 从而当任意属性发生变化时都能触发回调函数执行, watch不仅仅能够观测响应函数还能够承受getter函数:

watch (  // getter 函数  () => obj.foo,  // 回调函数  () => console.log('obj.foo 的值扭转了'))

getter函数的外部能够指定改watch依赖哪些响应式数据, 只有当这些数据变动时才会触发回调函数执行:

function watch (source, cn) {  // 定义 getter  let getter  // 如果 source 是函数, 阐明用户 传递的是 getter  if (typeof source === 'function') {    getter = source  } else {    // 否则依照原来的调用 traverse 递归读取    getter = () => traverse(source)  }  effect(    // 执行 getter 获取值    () => getter(),    {      scheduler () {        // obj.foo 变动时, 执行 scheduler 调度函数        cb()      }    }  )}

这时性能曾经比价欠缺了, 不过目前还少了一点就是不可能失去变动前后的值, 然而在 Vue.js 中是能够的:

watch (  // getter 函数  () => obj.foo,  // 回调函数  (newVal, oldVal) => console.log(newVal, oldVal))

因而须要获取新值与旧值, 这时能够利用effectlazy选项:

对于 lazy 请参看: https://segmentfault.com/a/11...
function watch (source, cn) {  // 定义 getter  let getter  // 如果 source 是函数, 阐明用户 传递的是 getter  if (typeof source === 'function') {    getter = source  } else {    // 否则依照原来的调用 traverse 递归读取    getter = () => traverse(source)  }  let oldValue, newValue  // 应用 effect 注册副作用函数时开启 lazy 选项, 并把返回的值存储到 effectFn 中以便后续手动调用  const effectFn = effect(    // 执行 getter 获取值    () => getter(),    {      lazy: true,      scheduler () {        // 从新执行 effectFn 失去的是新值        newValue = effectFn()        // 将新旧值作为回调函数的参数        // obj.foo 变动时, 执行 scheduler 调度函数        cb(newValue, oldValue)        // 更新旧值, 不然下一次就会失去谬误的旧值        oldValue = newValue      }    }  )  // 手动调用副作用函数拿到的就是旧值  oldValue = effectFn()}

在代码的最上面, 手动调用 effectFn 函数返回的值得到的就是旧值也就是第一次执行失去的值, 当触发 scheduler 调度函数时会从新调用 effectFn 失去新值.