likes
comments
collection
share

Vue.js设计与实现学习总结(第四章7)侦听器

作者站长头像
站长
· 阅读数 11

前置知识点:

执行调度 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 得到新值.