Vue.js实现watch属性的示例详解

目录
  • 1.写在前面
  • 2.watch的实现原理
  • 3.立即执行的watch与回调执行时机
    • 立即执行的回调函数
    • 回调函数的执行时机
  • 4.过期的副作用函数和cleanup
  • 5.写在最后

1.写在前面

在上篇文章中,我们讨论了compted的实现原理,就是利用effect和options参数进行封装。同样的,watch也是基于此进行封装的,当然watch还可以传递第三参数去清理过期的副作用函数。不仅可以利用副作用函数的调度性,去实现回调函数的立即执行,也可以控制回调函数的执行时机。

2.watch的实现原理

watch本质就是去观测一个响应式数据,当数据变化时通知并执行相应的回调函数。watch的实现本质和computed类似,基于effect函数和options.scheduler选项。

const data = {
  name:"pingping",
  age:18,
  flag:true
};

const state = new Proxy(data,{
  /*...*/
})

watch(state,()=>{
  console.log("数据变化了呀...");
});

//在响应数据的age值被修改了,就会导致回调函数执行
state.age++;

watch函数的实现代码如下所示,副作用函数存在scheduler选项中,当响应数据发生变化时,会触发scheduler调度函数执行,而非直接触发副作用函数执行。

//watch 函数接收source响应数据和回调函数cb
function watch(source, cb){
  effect(
    //触发读取操作,建立联系
    ()=>source.age,
    {
      scheduler(){
        // 当数据发生变化,调用回调函数cb
        cb();
      }
    }
  )
}

上面代码片段中,我们在使用watch对数据进行监听时,只能对soure.age的变化进行观测,不具有通用性,对此需要进行进一步封装。

function watch(source, cb){
  effect(
    //调用traverse函数递归读取操作,建立联系
    ()=>traverse(source),
    {
      scheduler(){
        // 当数据发生变化,调用回调函数cb
        cb();
      }
    }
  )
}
const isObject = (value:any) => typeof value === "object" && value !== null;

function traverse(value, seen = new Set()){
  //如果读取的数据是原始值,或已经读取过响应数据,则什么也不做
  if(!isObject(value) || seen.has(value)) return;
  //将数据添加到seen中,表示遍历读取过数据,避免循环引用导致死循环
  seen.add(value);
  //对数据对象进行遍历递归读取,用于依赖收集
  for(const k in value){
    traverse(value[k],seen);
  }
  return value;
}

在上面代码中,单独封装了一个递归函数traverse可以对响应数据进行遍历递归读取操作,这样就可以读取到对象的上所有属性,从而监听任意属性值发生变化时都能够触发回调函数。

事实上,使用watch进行数据观测时,不仅可以观测响应数据,还可以观测getter函数。那么,我们只需要先对输入的被观测数据判断数据类型是否为function,如果是则赋值给getter,否则还是监听响应式数据。

function watch(source,cb){
    let getter;
    if(typeof source === "function"){ // 如果是函数表示是getter,可以直接赋值
       getter = source;
    }else{
       getter = () => traverse(source)// 包装成effect对应的effectFn, 函数内部进行遍历达到依赖收集的目的
    }
    let oldValue, newValue;
    const effectFn = effect(
      ()=>getter(),
      {
        //开启lazy选项,将返回值存储到effectFn中以便于之后手动调用
        lazy: true,
        scheduler(){
          newValue = effectFn(); // 值变化时再次运行effect函数,获取新值
          cb(newValue,oldValue);
          //更新旧值,不然下次得到的是错误的旧值
          oldValue = newValue;
        }
      }
    )
    //手动调用副作用函数,拿到的值是旧值
    oldValue = effectFn();
}

其实,上面代码中充分了利用了lazy选项的特性,利用其创建一个懒执行的effect。通过手动执行effectFn函数得到的返回值是旧值,当数据变化并触发scheduler调度器执行时,会重新执行effectFn函数并且得到新值。

这样,我们获取到了数据变化前后的新值和旧值,可以将其作为参数传递给回调函数cb,在变化执行副作用函数后需要将新值赋值给oldValue,方便后续执行计算,否则后续变更会获取到错误的旧值。

写个demo使用下:

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

state.age++

3.立即执行的watch与回调执行时机

watch本质上对effect的二次封装,其具有两个特性:立即执行的回调函数、回调函数的执行时机。

立即执行的回调函数

立即执行的回调函数,默认情况下,一个watch的回调函数只会在响应数据发生变化时才执行,但是在Vue.js中可以通过options.immediate来指定回调是否立即执行。

当options.immediate存在且为true时,回调函数在该watch创建时立即执行一次。事实上,回调函数的立即执行和后续执行在本质上区别不大,对此可以将其调度器scheduler进行封装为通用函数,通过options.immediate的存在与否判断是在初始化还是变更时进行执行。

function watch(source, cb, options={}){
  let getter;
  if(type === "function"){
    getter = source;
  }else{
    getter = ()=>traverse(source);
  }

  let oldValue, newValue;

  // 提取调度函数为独立的函数
  const scheduler = ()=>{
    newValue = effectFn(); // 值变化时再次运行effect函数,获取新值
    cb(newValue,oldValue);
    //更新旧值,不然下次得到的是错误的旧值
    oldValue = newValue;
  }

  const effectFn = effect(
    ()=>getter(),
    {
      //开启lazy选项,将返回值存储到effectFn中以便于之后手动调用
      lazy: true,
      scheduler: scheduler
    }
  )
  if(options.immediate){
    //当immediate为true时,立即执行scheduler函数从而触发回调执行
    scheduler()
  }else{
    //手动调用副作用函数,拿到的值是旧值
    oldValue = effectFn();
  }
}

上面代码中,回调函数是立即执行的,在第一次回调函数执行时没有所谓的旧值,此时回调函数的oldValue值是undefined。

回调函数的执行时机

当然,除了上面的可以指定回调函数为立即执行外,还可以通过options参数来指定回调函数的执行时机。在Vue.js3中可以通过flush选项来指定调度函数的执行时机,当flush的值为"post"时,表示调度函数需要将副作用函数放在微任务队列中,等待DOM更新结束后执行。

function watch(source, cb, options={}){
  let getter;
  if(type === "function"){
    getter = source;
  }else{
    getter = ()=>traverse(source);
  }

  let oldValue, newValue;

  // 提取调度函数为独立的函数
  const obj = ()=>{
    newValue = effectFn(); // 值变化时再次运行effect函数,获取新值
    cb(newValue,oldValue);
    //更新旧值,不然下次得到的是错误的旧值
    oldValue = newValue;
  }

  const effectFn = effect(
    ()=>getter(),
    {
      //开启lazy选项,将返回值存储到effectFn中以便于之后手动调用
      lazy: true,
      scheduler(){
        if(options.flush === "post"){
          const p = Promise.resolve();
          p.then(obj);
        }else{
          obj();
        }
      }
    }
  )
  if(options.immediate){
    //当immediate为true时,立即执行scheduler函数从而触发回调执行
    scheduler()
  }else{
    //手动调用副作用函数,拿到的值是旧值
    oldValue = effectFn();
  }
}

其实就是根据options.flush是否等于"post",来实现是否需要将obj函数进行异步处理。

4.过期的副作用函数和cleanup

在讲到watch过期的副作用函数,就要提到在多进程或多线程编程中经常被提及的竞态问题。在下面代码片段中,使用watch观测state对象的变化,每次state对象发生变化时都会发送网络请求。

let finalData;

watch(state, async ()=>{
  //发送等待网络请求
  const res = await fetch("/user/info");
  finalData = res;
})

在上面代码看起来是没啥问题,但其实会发生竞态问题,在第一次修改state对象的字段值后,会导致回调执行,同时发送第一次请求A;在A请求返回结果之前,我们继续修改state的字段值,同时发送第二次请求B。但是请求A和请求B谁的结果先返回,我们并不知道?

将A的请求结果覆盖B的请求结果

在理论分析下,我们先后发送A、B请求,按道理应该是先返回A,再返回B请求的结果。这是因为请求A是副作用函数第一次执行产生的副作用,而请求B是副作用函数第二次执行产生的副作用。请求B在请求A后发生,请求A应当在这之前就过期了,返回的结果应该是无效的。

但是,在前面没有对watch的执行时机进行调度的情况下,就会发生请求A的值后返回覆盖B请求返回值的错误。

要想解决这种问题,我们只需要提供一个副作用过期的手段即可。事实上,watch函数的回调函数可以传入第三个参数onInvalidate函数,让其注册一个回调在当前副作用函数过期时执行:

function watch(source, cb, options={}){
  let getter;
  if(type === "function"){
    getter = source;
  }else{
    getter = ()=>traverse(source);
  }

  let oldValue, newValue;

  let cleanupFn;//用于存储用户注册的过期回调
  //定义onInvalidate函数
  const onInvalidate = (fn)=>{
    //将过期的回调函数存储到cleanupFn中
    fn();
  }

  // 提取调度函数为独立的函数
  const obj = ()=>{
    newValue = effectFn(); // 值变化时再次运行effect函数,获取新值

    //在调用回调函数cb之前,先调用过期的回调函数
    if(cleanupFn){
      cleanupFn();
    }
    cb(newValue,oldValue,onInvalidate);
    //更新旧值,不然下次得到的是错误的旧值
    oldValue = newValue;
  }

  const effectFn = effect(
    ()=>getter(),
    {
      //开启lazy选项,将返回值存储到effectFn中以便于之后手动调用
      lazy: true,
      scheduler(){
        if(options.flush === "post"){
          const p = Promise.resolve();
          p.then(obj);
        }else{
          obj();
        }
      }
    }
  )
  if(options.immediate){
    //当immediate为true时,立即执行scheduler函数从而触发回调执行
    scheduler()
  }else{
    //手动调用副作用函数,拿到的值是旧值
    oldValue = effectFn();
  }
}

在上面代码片段中,定义变量存储用户通过onInvalidate函数注册的回调函数,将过期的回调赋值给cleanupFn,在job函数中每次执行回调函数cb前都会检查是否存在过期回调。存在过期回调则执行cleanupFn函数清理,最后将onInvalidate返回给用户使用。

写个demo实践下:

watch(state, async (newValue, oldValue, onInvalidate)=>{
  let expired = false;
  onInvalidate(()=>{
    expired = true;
  })

  const res = await fetch("/user/info");

  if(!expired){
    finaleData = res;
  }
});

//第一次修改
state.age++;
setTimeout(()=>{
  state.age++;
},200)

示意图如下:

请求过期

5.写在最后

本文中,讨论了watch函数是如何利用副作用函数和options进行封装实现的,也通过调度函数去控制回调函数的立即执行和执行时机,还可以解决竞态问题。

以上就是Vue.js实现watch属性的示例详解的详细内容,更多关于Vue.js watch属性的资料请关注我们其它相关文章!

(0)

相关推荐

  • Vue.js计算属性computed与watch(5)

    在模板中绑定表达式是非常便利的,但是它们实际上只用于简单的操作.模板是为了描述视图的结构.在模板中放入太多的逻辑会让模板过重且难以维护.这就是为什么 Vue.js 将绑定表达式限制为一个表达式.如果需要多于一个表达式的逻辑,应当使用**计算属性**. Vue实例的computed的属性 <div class="test"> <p>原始的信息{{message}}</p> <p>计算后的信息{{ComputedMessage}}</p

  • Vue.js每天必学之计算属性computed与$watch

    在模板中绑定表达式是非常便利的,但是它们实际上只用于简单的操作.模板是为了描述视图的结构.在模板中放入太多的逻辑会让模板过重且难以维护.这就是为什么 Vue.js 将绑定表达式限制为一个表达式.如果需要多于一个表达式的逻辑,应当使用**计算属性**. 基础例子 <div id="example"> a={{ a }}, b={{ b }} </div> var vm = new Vue({ el: '#example', data: { a: 1 }, comp

  • Vue.js watch监视属性知识点总结

    这个属性用来监视某个数据的变化,并触发相应的回调函数执行 1.基本用法 (1)添加watch属性,值为一个对象.对象的属性名就是要监视的数据,属性值为回调函数,每当这个属性名对应的值发生变化,就会触发该回调函数执行 (2)回调函数有2个参数: newVal:数据发生改变后的值 oldVal:数据发生改变前的值 var vm = new Vue({ el:'#app', data: { name: '郭靖' }, watch: { name(newVal,oldVal){ console.log(

  • Vue.js实现watch属性的示例详解

    目录 1.写在前面 2.watch的实现原理 3.立即执行的watch与回调执行时机 立即执行的回调函数 回调函数的执行时机 4.过期的副作用函数和cleanup 5.写在最后 1.写在前面 在上篇文章中,我们讨论了compted的实现原理,就是利用effect和options参数进行封装.同样的,watch也是基于此进行封装的,当然watch还可以传递第三参数去清理过期的副作用函数.不仅可以利用副作用函数的调度性,去实现回调函数的立即执行,也可以控制回调函数的执行时机. 2.watch的实现原

  • Vue.js图片预览插件使用详解

    Vue.js 是什么 Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架.与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用.Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合.另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动. 如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带您了解其核心概念和一个示例工程. 如果你已经是有经验的前端开发者,想知道 Vue

  • vue electron实现无边框窗口示例详解

    目录 一.前言 二.实现方案 1.创建无边框窗口 2.创建windows窗口控件组件 三.后记 一.前言 无边框窗口是不带外壳(包括窗口边框.工具栏等),只含有网页内容的窗口.对于一个产品来讲,桌面应用带边框的很少,因为丑(我们的UI觉得--与我无关-.-).因此我们就来展开说下,在做无边框窗口时候需要注意的事项以及我踩过的坑. 二.实现方案 1.创建无边框窗口 要创建无边框窗口,只需在 BrowserWindow的 options 中将 frame 设置为 false: const { Bro

  • vue整合项目中百度API示例详解

    目录 官网介绍 申请密钥 官方示例 项目实战 创建地图 获取经纬度 创建Map实例 两个坐标点之间的距离 查询地点信息 Vue项目中整合百度API获取地理位置的方法 组件中使用 vue-baidu-map 百度地图官方vue组件 官网介绍 百度地图 JavaScript API 是一套由 JavaScript 语言编写的应用程序接口 可帮助您在网站中,构建功能丰富交互性强的地图应用 支持PC端和移动端,基于浏览器的地图应用开发,且支持HTML5特性的地图开发 官网传送门 百度地图JavaScri

  • vue组件生命周期钩子使用示例详解

    目录 组件生命周期图 组件生命周期钩子 1.beforeCreate 2.created 3.beforeMount 4.mounted 5.beforeUpdate 6.updated 7.activated 8.deactivated 9.beforeDestroy 10.destroyed 11.errorCaptured 组件生命周期图 组件生命周期钩子 所有的生命周期钩子自动绑定 一.组件的生命周期:一个组件从创建到销毁的整个过程 二.生命周期钩子:在一个组件生命周期中,会有很多特殊的

  • JS代码计算LocalStorage容量示例详解

    目录 LocalStorage 容量 计算总容量 已使用容量 剩余可用容量 LocalStorage 容量 localStorage的容量大家都知道是5M,但是却很少人知道怎么去验证,而且某些场景需要计算localStorage的剩余容量时,就需要我们掌握计算容量的技能了~~ 计算总容量 我们以10KB一个单位,也就是10240B,1024B就是10240个字节的大小,我们不断往localStorage中累加存入10KB,等到超出最大存储时,会报错,那个时候统计出所有累积的大小,就是总存储量了!

  • Xterm.js入门官方文档示例详解

    目录 前言 xterm.js是什么? 安装 初始化 使用插件 API文档模块 类 Terminal 构造函数 constructor 接口 插件 attach插件 前后端示例 结语 前言 入职的新公司所在的事业部专注于K12的编程教育.公司项目里有使用xterm.js这个库, 并基于master分支做出了一定的修改.为了尽快的熟悉业务以及公司的代码, 所以这里打算学习xterm.js的文档(粗略的翻译, 方便自己查阅, 凡是保留原文的地方, 是我目前还没有明白具体使用场景和用法的地方) xter

  • vue实现At人文本输入框示例详解

    目录 知识前置 需求分析 实现 创建能够输入文本的文本框 添加at功能 后记 知识前置 基于vue手把手教你实现一个拥有@人功能的文本编辑器(其实就是微信群聊的输入框) Selection 对象,表示用户选择的文本范围或插入符号的当前 developer.mozilla.org/zh-CN/docs/… contenteditable 是一个枚举属性,表示元素是否可被用户编辑. developer.mozilla.org/zh-CN/docs/… 需求分析 文本框能够输入文本(太简单了) 能够a

  • Vue.js框架路由使用方法实例详解

    Vue.js框架路由使用方法实例详解 html代码: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name='viewport' content='width=device-width,initial-

  • Vue.js进行查询操作的实例详解

    Vue.js进行查询操作的实例详解 实例代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script src="../lib/vue.min.js" type="text/javascript" ></script> <title>字符转换</title> </head>

随机推荐