Vue利用广度优先搜索实现watch

 更新时间:2023年08月04日 10:04:36   作者:前端胖头鱼  
这篇文章主要为大家学习介绍了Vue如何利用广度优先搜索实现watch(有意思),文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

前言

通过前面几篇文章,我们对Vue3中的响应式设计有了初步的了解。

这一篇我们试着实现一个watch

1. 两种watch的基本用法

1.1 通过函数回调监听数据

最基本的用法是给watch指定一个回调函数并返回你想要监听的响应式数据

const state1 = reactive({
  name: '前端胖头鱼',
  age: 100
})
watch(() => state1.age, () => {
  console.log('state1的age发生变化了', state1.age)
})
state1.age = 200
setTimeout(() => {
  state1.age = 300
}, 500)

1.2 直接监听一个对象

还可以直接监听一个响应式对象来观测它的变化。

const state1 = reactive({
  name: '前端胖头鱼',
  age: 100,
  children: {
    name: '胖小鱼',
    age: 10
  }
})
watch(state1, () => {
  console.log('state1发生变化了', state1)
})
state1.age = 200
setTimeout(() => {
  state1.children.age = 100
}, 500)

2. 实现watch最核心的点

其实watch的底层实现非常简单,和computed一样都需要借助任务调度

简单来说就是感知数据的变化,数据发生了变化就执行对应的回调,那么怎么感知呢?

const state = reactive({
  name: '前端胖头鱼'
})
useEffect(() => {
  // 原本state发生变化之后,应该执行这里
  console.log(state.name)
}, {
  // 但是指定scheduler之后,会执行这里
  scheduler () {
    console.log('state变化了')
  }
})
state.name = '胖小鱼'

聪明的你肯定也猜到了,scheduler不就是天然感知数据的变化的工具吗?

没错,watch的实现少不了它,来吧,搞起!!!

3. 支持两种使用方式

3.1 支持回调函数形式

const watch = (source, cb) => {
  effect(source, {
    scheduler () {
      cb()
    },
  })
}
// 测试一波
const state = reactive({
  name: '前端胖头鱼',
})
watch(() => state.name, () => {
  console.log('state.name发生了变化', state.name)
})
state.name = '胖小鱼'

3.2 支持直接传递响应式对象

不错哦!第一种方式已经初步实现了,接下来搞第二种。

第二种直接传入响应式对象的方式和第一种传入回调函数并指向响应式数据的区别是什么?

在于我们需要手动遍历这个响应式对象使得它的任意属性发生变化我们都能感知到。

3.3 广度优先搜索遍历深层嵌套的属性

此时就到了这篇文章装逼(额~~)的点了。如果想访问一个深层嵌套对象的所有属性,最常见的做法就是递归。

如果你想在面试的过程中秀一波,我觉得使用广度优先搜索是个不错的主意(狗头脸),代码也非常简单,就不详细解释了。

如果您对广度优先搜索和深度优先搜索感兴趣欢迎在评论区留言,我会单独写一篇文章来讲它。

 const bfs = (obj, callback) => {
  const queue = [ obj ]
  while (queue.length) {
    const top = queue.shift()
    if (top && typeof top === 'object') {
      for (let key in top) {
        // 读取操作出发getter,完成依赖搜集
        queue.push(top[ key ])
      }
    } else {
      callback && callback(top)
    }
  }
}
const obj = {
  name: '前端胖头鱼',
  age: 100,
  obj2: {
    name: '胖小鱼',
    age: 10,
    obj3: {
      name: '胖小小鱼',
      age: 1,
    }
  },
}
bfs(obj, (value) => {
  console.log(value)
})

我们已经能够读取深层嵌套对象的任意属性了,接下来继续完善watch方法

const watch = (source, cb) => {
  let getter
  // 处理传回调的方式
  if (typeof source === "function") {
    getter = source
  } else {
    // 封装成读取source对象的函数,触发任意一个属性的getter,进而搜集依赖
    getter = () => bfs(source)
  }
  const effectFn = effect(getter, {
    scheduler() {
      cb()
    }
  })
}
// 测试一波
const state = reactive({
  name: "前端胖头鱼",
  age: 100,
  obj2: {
    name: "胖小鱼",
    age: 10,
  },
})
watch(state, () => {
  console.log("state发生变化了");
});

看来还是有不少坑啊!虽然我们实现了n层嵌套对象属性的读取(理论上所有的属性改变都应该触发回调),但是state.obj2.name = 'yyyy'却没有被感知到,为什么呢?

3.4 浅响应与深响应

回顾一下reactive函数,你会发现,当value本身也是一个对象的时候,我们并不会使value也变成一个响应式数据。

所以哪怕我们通过bfs方法遍历了该对象的所有属性,也仅仅是第一层的key具有了响应式效果而已。

// 统一对外暴露响应式函数
function reactive(state) {
  return new Proxy(state, {
    get(target, key) {
      const value = target[key]
      // 搜集key的依赖
      // 如果value本身是一个对象,对象下的属性将不具有响应式
      track(target, key) 
      return value;
    },
    set(target, key, newValue) {
      // console.log(`set ${key}: ${newValue}`)
      // 设置属性值
      target[key] = newValue
      trigger(target, key)
    },
  })
}

解决办法也很简单,是对象的情况下再给他reactive一次就好了。

// 统一对外暴露响应式函数
function reactive(state) {
  return new Proxy(state, {
    get(target, key) {
      const value = target[key]
      // 搜集key的依赖
      // 如果value本身是一个对象,对象下的属性将不具有响应式
      track(target, key) 
      // 如果是对象,再使其也变成一个响应式数据
      if (typeof value === "object" && value !== null) {
        return reactive(value);
      }
      return value;
    },
    set(target, key, newValue) {
      // console.log(`set ${key}: ${newValue}`)
      // 设置属性值
      target[key] = newValue
      trigger(target, key)
    },
  })
}

最后再回到前面的例子,你会发现我们成功了!!!

4. watch的新值和旧值

到目前为止,我们实现了watch最基本的功能,感知其数据的变化并执行对应的回调。

接下来我们再实现一个基础功能:在回调函数中获取新值与旧值。

watch(state, (newVal, oldVal) => {
  // xxx
})

新值和旧值主要在于获取时机不一样,获取方式确实一模一样的,执行effectFn即可

const watch = (source, cb) => {
  let getter
  let oldValue
  let newValue
  // 处理传回调的方式
  if (typeof source === "function") {
    getter = source
  } else {
    getter = () => bfs(source)
  }
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      // 变化后获取新值
      newValue = effectFn()
      cb(newValue, oldValue)
      // 执行回调后将新值设置为旧值
      oldValue = newValue
    }
  })
  // 第一次执行获取值
  oldValue = effectFn()
}

测试一波

const state = reactive({
  name: "前端胖头鱼",
  age: 100,
  obj2: {
    name: "胖小鱼",
    age: 10,
  },
})
watch(() => state.name, (newValue, oldValue) => {
  console.log("state.name", { newValue, oldValue })
})
state.name = '111'

5. 支持立即调用时机

最后再实现立即调用时机immediatewatch就大功告成啦!

const watch = (source, cb, options = {}) => {
  let getter
  let oldValue
  let newValue
  // 处理传回调的方式
  if (typeof source === "function") {
    getter = source
  } else {
    getter = () => bfs(source)
  }
  const job = () => {
    // 变化后获取新值
    newValue = effectFn()
    cb(newValue, oldValue)
    // 执行回调后将新值设置为旧值
    oldValue = newValue
  }
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      job()
    }
  })
  // 如果指定了立即执行,便执行第一次
  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}
watch(() => state.name, (newValue, oldValue) => {
  console.log("state.name", { newValue, oldValue });
}, { immediate: true });

通过判断immediate是否为true来决定是否一开始就执行cb回调,且第一次回调的旧值oldValue应该为undefined

以上就是Vue利用广度优先搜索实现watch的详细内容,更多关于Vue watch的资料请关注脚本之家其它相关文章!

相关文章

  • Vue项目使用svg图标实践

    Vue项目使用svg图标实践

    这篇文章主要介绍了Vue项目使用svg图标实践,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-09-09
  • vue中input框的禁用和可输入问题

    vue中input框的禁用和可输入问题

    这篇文章主要介绍了vue input框的禁用和可输入问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04
  • vite5+vue3+ import.meta.glob动态导入vue组件图文教程

    vite5+vue3+ import.meta.glob动态导入vue组件图文教程

    import.meta.glob是Vite提供的一个特殊功能,它允许你在模块范围内动态地导入多个模块,这篇文章主要给大家介绍了关于vite5+vue3+ import.meta.glob动态导入vue组件的相关资料,需要的朋友可以参考下
    2024-07-07
  • vue中简单弹框dialog的实现方法

    vue中简单弹框dialog的实现方法

    下面小编就为大家分享一篇vue中简单弹框dialog的实现方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-02-02
  • 基于mpvue的小程序项目搭建的步骤

    基于mpvue的小程序项目搭建的步骤

    mpvue 是美团开源的一套语法与vue.js一致的、快速开发小程序的前端框架,这篇文章主要介绍了基于mpvue的小程序项目搭建的步骤,非常具有实用价值,需要的朋友可以参考下
    2018-05-05
  • vue最简单的前后端交互示例详解

    vue最简单的前后端交互示例详解

    这篇文章主要介绍了vue最简单的前后端交互示例详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-10-10
  • vue3中使用ref语法糖的示例代码

    vue3中使用ref语法糖的示例代码

    Vue3提了一个Ref Sugar的RFC,即ref语法糖,目前还处理实验性的(Experimental)阶段,今天通过本文给大家介绍vue3中使用ref语法糖的相关知识,感兴趣的朋友跟随小编一起看看吧
    2022-09-09
  • Element-UI控件Tree实现数据树形结构的方法

    Element-UI控件Tree实现数据树形结构的方法

    这篇文章主要介绍了Element-UI控件Tree实现数据树形结构,本期介绍添加、修改等功能也比较简单,可以通过element-ui的$prompt弹框控件来实现,需要的朋友可以参考下
    2024-01-01
  • vue 通过绑定事件获取当前行的id操作

    vue 通过绑定事件获取当前行的id操作

    这篇文章主要介绍了vue 通过绑定事件获取当前行的id操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-07-07
  • Vue移动端用淘宝弹性布局lib-flexible插件做适配的方法

    Vue移动端用淘宝弹性布局lib-flexible插件做适配的方法

    这篇文章主要介绍了Vue移动端用淘宝弹性布局lib-flexible插件做适配的操作方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-05-05

最新评论