5

【源码系列#04】Vue3侦听器原理(Watch) - 柏成

 8 months ago
source link: https://www.cnblogs.com/burc/p/17916475.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【源码系列#04】Vue3侦听器原理(Watch) - 柏成 - 博客园

专栏分享:vue2源码专栏vue3源码专栏vue router源码专栏玩具项目专栏,硬核💪推荐🙌
欢迎各位ITer关注点赞收藏🌸🌸🌸

侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数

javascript
const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newValue, oldValue) => {
  console.log(`x is ${newValue}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (newValue, oldValue) => {
    console.log(`sum of x + y is: ${newValue}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组

第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。

第三个可选的参数是一个对象,支持以下这些选项:

  • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined。
  • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器
  • flush:调整回调函数的刷新时机。参考回调的刷新时机watchEffect()
  • onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器
  • @issue1 深度递归循环时考虑对象中有循环引用的问题

  • @issue2 兼容数据源为响应式对象和getter函数的情况

  • @issue3 immediate回调执行时机

  • @issue4 onCleanup该回调函数会在副作用下一次重新执行前调用

javascript
/**
 * @desc 递归循环读取数据
 * @issue1 考虑对象中有循环引用的问题
 */
function traversal(value, set = new Set()) {
  // 第一步递归要有终结条件,不是对象就不在递归了
  if (!isObject(value)) return value

  // @issue1 处理循环引用
  if (set.has(value)) {
    return value
  }
  set.add(value)

  for (let key in value) {
    traversal(value[key], set)
  }
  return value
}

/**
 * @desc watch
 * @issue2 兼容数据源为响应式对象和getter函数的情况
 * @issue3 immediate 立即执行
 * @issue4 onCleanup:用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求
 */
// source 是用户传入的对象, cb 就是对应的回调
export function watch(source, cb, { immediate } = {} as any) {
  let getter

  // @issue2
  // 是响应式数据
  if (isReactive(source)) {
    // 递归循环,只要循环就会访问对象上的每一个属性,访问属性的时候会收集effect
    getter = () => traversal(source)
  } else if (isRef(source)) {
    getter = () => source.value
  } else if (isFunction(source)) {
    getter = source
  }else {
    return
  }

  // 保存用户的函数
  let cleanup
  const onCleanup = fn => {
    cleanup = fn
  }

  let oldValue
  const scheduler = () => {
    // @issue4 下一次watch开始触发上一次watch的清理
    if (cleanup) cleanup()
    const newValue = effect.run()
    cb(newValue, oldValue, onCleanup)
    oldValue = newValue
  }

  // 在effect中访问属性就会依赖收集
  const effect = new ReactiveEffect(getter, scheduler) // 监控自己构造的函数,变化后重新执行scheduler

  // @issue3
  if (immediate) {
    // 需要立即执行,则立刻执行任务
    scheduler()
  }

  // 运行getter,让getter中的每一个响应式变量都收集这个effect
  oldValue = effect.run()
}

对象中存在循环引用的情况

javascript
const person = reactive({
  name: '柏成',
  age: 25,
  address: {
    province: '山东省',
    city: '济南市',
  }
})
person.self = person

watch(
  person,
  (newValue, oldValue) => {
    console.log('person', newValue, oldValue)
  }, {
    immediate: true
  },
)
  1. 数据源为 ref 的情况,和 immediate 回调执行时机
javascript
const x = ref(1)

watch(
  x,
  (newValue, oldValue) => {
    console.log('x', newValue, oldValue)
  }, {
    immediate: true
  },
)

setTimeout(() => {
  x.value = 2
}, 100)
  1. 兼容数据源为 响应式对象getter函数 的情况,和 immediate 回调执行时机
javascript
const person = reactive({
  name: '柏成',
  age: 25,
  address: {
    province: '山东省',
    city: '济南市',
  }
})

// person.address 对象本身及其内部每一个属性 都收集了effect。traversal递归遍历
watch(
  person.address,
  (newValue, oldValue) => {
    console.log('person.address', newValue, oldValue)
  }, {
    immediate: true
  },
)

// 注意!我们在 watch 源码内部满足了 isFunction 条件
// 此时只有 address 对象本身收集了effect,仅当 address 对象整体被替换时,才会触发回调;
// 其内部属性发生变化并不会触发回调
watch(
  () => person.address,
  (newValue, oldValue) => {
    console.log('person.address', newValue, oldValue)
  }, {
    immediate: true
  },
)

// person.address.city 收集了 effect
watch(
  () => person.address.city,
  (newValue, oldValue) => {
    console.log('person.address.city', newValue, oldValue)
  }, {
    immediate: true
  },
)

setTimeout(() => {
  person.address.city = '青岛市'
}, 100)

onCleanup

watch回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数(即我们的onCleanup)。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。

javascript
const person = reactive({
  name: '柏成',
  age: 25
})

let timer = 3000
function getData(timer) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(timer)
    }, timer)
  })
}

// 1. 第一次调用watch的时候注入一个取消的回调
// 2. 第二次调用watch的时候会执行上一次注入的回调
// 3. 第三次调用watch会执行第二次注入的回调
// 后面的watch触发会将上次watch中的 clear 置为true
watch(
  () => person.age,
  async (newValue, oldValue, onCleanup) => {
    let clear = false
    onCleanup(() => {
      clear = true
    })

    timer -= 1000
    let res = await getData(timer) // 第一次执行2s后渲染2000, 第二次执行1s后渲染1000, 最终应该是1000
    if (!clear) {
      document.body.innerHTML = res
    }
  },
)

person.age = 26
setTimeout(() => {
  person.age = 27
}, 0)

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK