本文将介绍Vue中的watch实现原理解析:watch侦听器用于监听响应式数据变化,当数据改变时执行回调函数。
参考视频内容,点这里查看

1 基本概念

1) 什么是watch

watch叫侦听器, 侦听某个响应式数据, 当数据改变时, 重新执行对应的回调

所以, watch可以建立数据->函数的对应关系

2) 基本使用

第一种: 接收引用了属性的副作用函数做为参数

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <script>
      const { reactive, watch } = Vue
      const state = reactive({ name: 'xiaoming', address: { city: '武汉' } })

      // watch侦听副作用函数(引用代理对象的属性)
      watch(
        () => state.name,
        () => {
          console.log('只有当侦听的某个属性改变时, 才执行')
        }
      )

      setTimeout(() => {
        // 只有当state.name属性改变时, 才执行
        state.name = 'xiaopang'
        // state.address.city = '北京' // 修改city不会触发更新
      }, 1000)
    </script>
  </body>
</html>

第二种: 接收响应式对象做为参数

  1. watch侦听响应式对象时, 默认是深度侦听
  2. watch对应的回调默认不执行, 只有当属性改变时执行
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <script>
      const { reactive, watch } = Vue
      const state = reactive({ name: 'xiaoming' })

      // watch接收响应式对象的情况
      watch(state, () => {
        console.log('该函数默认不执行, 只有状态更新时执行...')
      })

      setTimeout(() => {
        state.name = 'xiaopang'
      }, 1000)
    </script>
  </body>
</html>

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <script>
      const { reactive, watch } = Vue
      const state = reactive({ name: 'xiaoming', address: { city: '武汉' } })

      // watch接收响应式对象的情况
      watch(state, () => {
        console.log('该函数默认不执行, 只有状态更新时执行...')
      })

      setTimeout(() => {
        // 侦听对象时, 是深度侦听
        state.address.city = '北京'
      }, 1000)
    </script>
  </body>
</html>

2 实现侦听函数

watch的实现实际上跟effect非常类似

effect接受两个参数

  1. 属性关联的副作用函数(类似watch的第一个参数)
  2. 更新时执行的调度函数(类似watch的第二个参数)

因此, 我们考虑基于effect初步实现watch的功能

如果第一个参数就是一个副作用函数, watch跟effect的参数一致

function watch(source, cb) {
  effect(source, {
    scheduler() {
      cb()
    },
  })
}

3 实现侦听对象

1) 基本实现

如果第一个参数不是副作用函数, 可以将其包装成一个副作用函数

function watch(source, cb) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => source
  }
  effect(getter, {
    scheduler() {
      cb()
    },
  })
}

由于并没有触发代理对象的取值操作, 因此不会收集依赖

考虑实现一个函数, 遍历访问source中的每一个属性

function traverse(value) {
  for (const k in value) {
    // 将代理对象的每个属性访问一次
    value[k]
  }
}
function watch(source, cb) {
  let getter
  if (typeof source == 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  effect(getter, {
    scheduler() {
      cb()
    },
  })
}

2) 支持嵌套

如果代理对象中存在嵌套情况

  1. 在reactive中要递归为嵌套的对象创建代理
  2. 在traverse中要递归访问嵌套的属性

示例

get(target, key) {
  if (key == '__v_isReactive') return true // 新增

  // console.log(`自定义访问${key}`)
  // 收集依赖
  track(target, key)

  if (typeof target[key] == 'object') {
    // 递归处理对象类型
    return reactive(target[key])
  }

  return target[key]
},
function traverse(value) {
  for (const k in value) {
    if (typeof value[k] == 'object') {
      traverse(value[k])
    } else {
      // 将代理对象的每个属性访问一次
      value[k]
    }
  }
}

3) 解决循环引用问题

如果按上述写法, 会出现递归循环引用, 陷入死循环

什么是循环引用

如果一个对象的某个属性引用自身, 在递归时会死循环

问题示例

const state = reactive({ name: 'xiaoming', address: { city: '武汉' } })
state.test = state

// watch接收响应式对象的情况
watch(state, () => {
  console.log('该函数默认不执行, 只有状态更新时执行...')
})

setTimeout(() => {
  // 侦听对象时, 是深度侦听
  state.address.city = '北京'
}, 1000)

以上问题可以简化为

const obj = {
  foo: 'foo',
}
// obj的一个属性引用obj本身, 出现循环引用问题
obj.bar = obj

function traverse(value) {
  for (const k in value) {
    if (typeof value[k] == 'object') {
      traverse(value[k])
    } else {
      // 将代理对象的每个属性访问一次
      value[k]
    }
  }
}

traverse(obj)

为了避免递归循环引用陷入死循环, 改造traverse方法

function traverse(value, seen = new Set()) {
  // 如果这个对象已经被遍历过了, 直接返回
  if (typeof value !== 'object' || seen.has(value)) return

  // 将遍历的对象记录下来, 再递归时判断
  seen.add(value)

  for (let key in value) {
    // 这里取值, 会触发proxy对象的getter操作
    traverse(value[key], seen)
  }
}

4 实现新旧值

我们知道, 在watch的回调中可以获取新值和旧值. 是如何实现的呢?

基本使用

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <script>
      const { reactive, watch } = Vue

      const state = reactive({ name: 'xiaoming', address: { city: '武汉' } })

      watch(
        () => state.name,
        (newValue, oldValue) => {
          console.log(newValue, oldValue)
        }
      )

      setTimeout(() => {
        state.name = 'xiaopang'
      }, 1000)
    </script>
  </body>
</html>

具体实现

function watch(source, cb) {
  let getter
  if (typeof source == 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let oldValue, newValue
  const _effect = effect(getter, {
    lazy: true, // 手动控制run的时机
    scheduler() {
      newValue = _effect.run()
      cb(newValue, oldValue)
      oldValue = newValue
    },
  })

  oldValue = _effect.run()
}

5 立即执行的回调

默认情况下, watch中的回调是不执行的. 但是可以通过传入参数让其立即执行

第一次立即执行回调时, 拿到的旧值是undefined

基本使用

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <script>
      const { reactive, watch } = Vue

      const state = reactive({ name: 'xiaoming', address: { city: '武汉' } })

      watch(
        () => state.name,
        (newValue, oldValue) => {
          console.log('设置immediate选项会立即执行回调')
          console.log(newValue, oldValue)
        },
        { immediate: true }
      )

      setTimeout(() => {
        state.name = 'xiaopang'
      }, 1000)
    </script>
  </body>
</html>

具体实现

立即执行的函数和更新时执行的函数本质上是没有区别的

因此, 我们可以将scheduler封装起来

function watch(source, cb, options = {}) {
  let getter
  if (typeof source == 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let oldValue, newValue
  const job = () => {
    newValue = _effect.run()
    cb(newValue, oldValue)
    oldValue = newValue
  }
  const _effect = effect(getter, {
    lazy: true,
    scheduler: job,
  })

  if (options.immediate) {
    job()
  } else {
    oldValue = _effect.run()
  }
}

6 watchEffect

watchEffect也可以用于侦听属性的改变.

基本用法

  1. 接受副作用函数作为参数
  2. 自动收集依赖
  3. 不关心新旧值
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <script>
      const { reactive, watchEffect } = Vue

      const state = reactive({ name: 'xiaoming' })

      watchEffect(() => {
        console.log(state.name)
      })

      setTimeout(() => {
        state.name = 'xiaopang'
      }, 1000)
    </script>
  </body>
</html>

具体实现

watchEffect跟effect是非常类似的.

由于只接受一个副作用函数作为参数. 注册和更新时都执行这个函数.

基本逻辑可以跟watch复用.

function doWatch(source, cb, options = {}) {
  let getter
  if (typeof source == 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let oldValue, newValue

  const job = () => {
    if (cb) {
      newValue = _effect.run()
      cb(newValue, oldValue)
      oldValue = newValue
    } else {
      _effect.run()
    }
  }
  const _effect = effect(getter, {
    lazy: true,
    scheduler: job,
  })

  if (options.immediate) {
    job()
  } else {
    oldValue = _effect.run()
  }
}
function watch(source, cb, options = {}) {
  doWatch(source, cb, options)
}
function watchEffect(source, options = {}) {
  doWatch(source, null, options)
}

:::
若有收获,就点个赞吧!
持续更新webgis开发相关技术/面试/就业内容
关注我学习webgis开发不迷路👇👇👇

更多推荐