• 作者:老汪软件技巧
  • 发表时间:2024-10-04 00:01
  • 浏览量:

Vue3响应式数据设计相关文章:

# Vue3响应式数据设计(一)4个步骤写出一个响应式数据系统

# Vue3 响应式数据设计(二)从4个方面思考完善响应式式数据设计

# Vue3 响应式数据设计(三)computed 实现

在上一篇中我们实现了一个computed ,在本篇我将实现一个精简版的watch。当然如果你对watch 不够了解的话可以先看看这篇文章# Vue3 watch 的六大特点

首先来说说什么是watch,watch的本质其实就是监听响应式数据的变化,变化了之后执行一个回调函数。实际上watch的实现本质就是利用了effect 和options.sheduler选项,如下面代码所示:

effect(() => {
  console.log(proxy.name)
}, {
  scheduler () {
    // 当proxy.name变化时,会执行scheduler 调度执行
  }
})

watch 的基本实现:

function watch(source, cb) {
  effect(() => source.name, {
    scheduler () {
      cb()
    }
  })
}
watch(proxy, () => {
  console.log('数据变化了')
})
proxy.name = 'hello'

解决watch 硬编码问题

以上代码执行后,会打印出数据变化了。但是我们的实现中硬编码了source.name的读取操作。也就是说我们实现的watch只能监听source.name的变化。为了让watch 监听具有通用性,我们需要封装一个通用的读取操作:

function watch(source, cb) {
  effect(() => traverse(source), {
    scheduler () {
      // 当数据变化时调用回调函数。
      cb()
    }
  })
  function traverse(value, seen = new Set()) {
    // 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
    if(typeof value !== 'object' || value === null || seen.has(value)) return
    // 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
    seen.add(value)
    for (const k in value) {
      traverse(value[key], seen)
    }
    return value
  }
}

上面的代码中我们封装了traverse 函数用来递归读取对象上的属性,这样我们就能监听到任意对象类型的数据变化了。

watch 的getter 形式

watch 除了可以监听响应式数据,还可以监听一个getter 函数:

watch(() => proxy.name, () => {
  console.log('proxy.name的值变化了')
})
proxy.name = 'hello'

目前我们实现的watch 函数还不能实现监听getter 函数,需要改造下。

vue响应式数据的理解__响应式表单

function watch(source, cb) {
  // 定义getter
  let getter
  // 如果source 是函数,说明用户传的getter,直接把source赋值给getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  effect(() => getter(), {
    scheduler () {
      // 当数据变化时调用回调函数。
      cb()
    }
  })
  function traverse(value, seen = new Set()) {
    // 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
    if(typeof value !== 'object' || value === null || seen.has(value)) return
    // 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
    seen.add(value)
    for (const k in value) {
      traverse(value[key], seen)
    }
    return value
  }
}
watch(() => proxy.name, () => {
  console.log('proxy.name的值变化了')
})

在上面的的代码中,我们实现了getter 函数的监听。

newVal和oldVal 参数补充

我们的watch 实现已经越来越完善,但是如果去看看Vue的watch ,会发现我们的实现回调函数缺少了新值和旧值。接下来我们就来补充这个功能。

function watch(source, cb) {
  // 定义getter
  let getter
  // 如果source 是函数,说明用户传的getter,直接把source赋值给getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let newVal, oldVal
  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler () {
      newVal = effectFn()
      // 当数据变化时调用回调函数。将新值旧值传到回调函数里面
      cb(newVal, oldVal)
      // 更新旧值,避免下次拿到的旧值是错的。
      oldVal = newVal
    }
  })
  // 手动执行副作用函数拿到的就是旧值
  oldVal = effectFn()
  function traverse(value, seen = new Set()) {
    // 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
    if(typeof value !== 'object' || value === null || seen.has(value)) return
    // 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
    seen.add(value)
    for (const k in value) {
      traverse(value[key], seen)
    }
    return value
  }
}

在这段代码中,最核心的是使用lazy选项创建了一个懒执行的effect。注意上面代码中最下面的部分,我们手动调用effectFn函数得到的返回值是旧值,即第一次执行得到的值。当变化发生并触发scheduler时,会重新调用effectFn函数得到新的值,这样我们就拿到了新值与旧值,接着将它们作为参数传递给cb 就可以了。最后一个非常重要的是,不要忘记用新值更新旧值oldVal = newVal,否则下一次变更发生时得到错误的旧值。

watch 执行时机

在Vue中的watch执行时机主要有3种情况,立即执行,组件更新前执行,组件更新后执行。我们先来看看立即执行如何实现。

function watch(source, cb, options = {}) {
  // 定义getter
  let getter
  // 如果source 是函数,说明用户传的getter,直接把source赋值给getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let newVal, oldVal
  const job = () => {
    newVal = effectFn()
    // 当数据变化时调用回调函数。将新值旧值传到回调函数里面
    cb(newVal, oldVal)
    // 更新旧值,避免下次拿到的旧值是错的。
    oldVal = newVal
  }
  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: job
  })
  // 手动执行副作用函数拿到的就是旧值
  if (options.immediate) {
    // 当immediate为true时,立即执行job,从而触发回调执行
    job()
  } else {
    oldVal = effectFn()
  }
  oldVal = effectFn()
  function traverse(value, seen = new Set()) {
    // 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
    if(typeof value !== 'object' || value === null || seen.has(value)) return
    // 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
    seen.add(value)
    for (const k in value) {
      traverse(value[key], seen)
    }
    return value
  }
}
watch(() => proxy.name, () => {
  console.log('proxy.name的值变化了')
}, {
  immediate: true
})

在上面的代码中我们主要添加了immediate参数,当immediate 为true 时会立即执行一次。我们运行上面的代码,虽然没有修改proxy属性的值,回调函数也执行了一次,实现了watch的立即执行。

在Vue中watch 除了立即执行,还可以通过flush参数来控是在组件更新前执行还是在更新后执行。值为post 时,在更新后执行 ,为prev在更新前执行。我们还没实现组件,所以这里只能模拟实现一个。

function watch(source, cb, options = {}) {
  // 定义getter
  let getter
  // 如果source 是函数,说明用户传的getter,直接把source赋值给getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let newVal, oldVal
  const job = () => {
    newVal = effectFn()
    // 当数据变化时调用回调函数。将新值旧值传到回调函数里面
    cb(newVal, oldVal)
    // 更新旧值,避免下次拿到的旧值是错的。
    oldVal = newVal
  }
  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: () => {
      if (options.flush === 'post') {
        const p = Promise.resolve()
        p.then(job)
      } else {
        job()
      }
    }
  })
  // 手动执行副作用函数拿到的就是旧值
  if (options.immediate) {
    // 当immediate为true时,立即执行job,从而触发回调执行
    job()
  } else {
    oldVal = effectFn()
  }
  oldVal = effectFn()
  function traverse(value, seen = new Set()) {
    // 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
    if(typeof value !== 'object' || value === null || seen.has(value)) return
    // 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
    seen.add(value)
    for (const k in value) {
      traverse(value[key], seen)
    }
    return value
  }
}
watch(() => proxy.name, () => {
  console.log('proxy.name的值变化了')
}, {
  immediate: true,
  flush: 'post'
})
proxy.name = 'hello'

在上面的代码中我们添加了flush 参数用来控制是在组件更新前执行回调还是在组件更新后执行回调。这里我们用Promise微任务来模拟组件的更新。

watch 常用的功能我们基本都已经实现,现在来看下我们响应式数据设计的完整代码:

响应式数据设计完整代码


const data = {
  name: 'jame',
  age: 30
}
const proxy = new Proxy(data, {
  get (target, key) {
    track(target, key)
    return target[key]
  },
  set (target, key, newVal) {
    target[key] = newVal
    trigger(target, key)
    return true
  }
})
let acctiveEffect = null
const effectStack = []
function effect(fn, options = {}) {
  function effectFn() {
    clearUp(effectFn)
    // 当effectFn执行时,将其设置为当前激活的副作用函数
    acctiveEffect = effectFn
    effectStack.push(effectFn)
    const res = fn()
    effectStack.pop()
    acctiveEffect = effectStack[effectStack.length - 1]
    return res
  }
  effectFn.options = options
  // 用来存储所有与该副作用相关的依赖集合
  effectFn.deps = []
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}
let bucket = new WeakMap()
/**
 * desc 读取属性值时和副作用函数建立联系
 * @param {代理的目标对象} target 
 * @param {属性,键} key 
 * @returns 
 */
function track(target, key) {
  if (!acctiveEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, deps = new Set())
  }
  // 把当前激活的副作用函数添加到依赖集合deps中
  deps.add(acctiveEffect)
  // deps 就是一个与当前副作用函数存在联系的集合
  // 将其添加到activeEffect.deps数组中
  acctiveEffect.deps.push(deps)
}
/**
 * desc 当修改属性值时触发副作用函数处理逻辑
 * @param {目标对象} target 
 * @param {键, 属性} key 
 * @returns 
 */
function trigger (target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  // 用一个新变量是为了避免无限循环
  const newEffects = new Set()
  effects && effects.forEach(effectFn => {
    if (effectFn !== acctiveEffect) {
      newEffects.add(effectFn)
    }
  })
  newEffects && newEffects.forEach(fn => {
    if (fn.options && fn.options.scheduler) {
      fn.options.scheduler(fn)
    } else {
      fn()
    }
  })
}
function clearUp (effectFn) {
  for (var i = 0; i < effectFn.deps.length; i++ ) {
    // deps 是依赖的集合
    const deps = effectFn.deps[i]
    // 将effectFn从依赖集合中移除
    deps.delete(effectFn)
  }
  // 最后需要重置effectFn.deps数组
  effectFn.deps.length = 0
}
const jobQueue = new Set()
const p = Promise.resolve()
let isFlushing = false
function flushJop () {
  if (isFlushing) {
    return
  }
  isFlushing = true
  p.then(() => {
    jobQueue.forEach(job => job())
  }).finally(() => {
    isFlushing = false
  })
}
function computed (getter) {
  // 用来保存上一次的值
  let value
  // 用来保存是否需要重新计算
  let dirty = true
  const effectFn = effect(getter, {
    lazy: true,
    scheduler () {
      if (!dirty) {
        dirty = true
        trigger(obj, 'value')
      }
    }
  })
  const obj = {
    get value() {
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      track(obj, 'value')
      return value
    }
  }
  return obj
}
function watch(source, cb, options = {}) {
  // 定义getter
  let getter
  // 如果source 是函数,说明用户传的getter,直接把source赋值给getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let newVal, oldVal
  const job = () => {
    newVal = effectFn()
    // 当数据变化时调用回调函数。将新值旧值传到回调函数里面
    cb(newVal, oldVal)
    // 更新旧值,避免下次拿到的旧值是错的。
    oldVal = newVal
  }
  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: () => {
      if (options.flush === 'post') {
        const p = Promise.resolve()
        p.then(job)
      } else {
        job()
      }
    }
  })
  // 手动执行副作用函数拿到的就是旧值
  if (options.immediate) {
    // 当immediate为true时,立即执行job,从而触发回调执行
    job()
  } else {
    oldVal = effectFn()
  }
  oldVal = effectFn()
  function traverse(value, seen = new Set()) {
    // 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
    if(typeof value !== 'object' || value === null || seen.has(value)) return
    // 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
    seen.add(value)
    for (const k in value) {
      traverse(value[key], seen)
    }
    return value
  }
}

总结: 本篇基于前面的响应式数据设计,实现了Vue中watch常用的使用场景。

Vue3响应式数据设计(四)watch实现就分享到这里了,感谢收看,一起学习一起进步。