vue组件内外数据保持同步 & 组件数据加载解析顺序

背景与简介

一个公共的组件,比如BaseTable,需要同步组件内外部数据的同步,组件内部数据修改,组件外数据同步修改,组件外部数据修改,组件内部数据同步修改;同时,当组件内外部数据修改,需要执行某些数据的计算或赋值,比如动态计算树形表格的索引treeIndex。还要避免数据同步来回更新造成的死循环。

组件数据加载解析顺序

实例初始化完成、props 解析 -> beforeCreate -> 响应式数据、计算属性、方法和侦听器设置完成 -> created -> beforeMount -> mounted

  • 组件内外部使用push,$set,不修改原数组对象,会使得watch中的newValue等于oldValue,JSON.stringify(newValue) === JSON.stringify(oldValue)
  • watch加了immediate: true,先执行响应式数据初始化,立即触发watch后,走到created生命周期。
  • 侦听器依次设置时,immediate: true的立即执行,执行后再继续设置其他的侦听器,也就是说,当immediate立即执行的监听,当某些数据变更后,如果侦听器还没有设置,就不会执行
  • 没有immediate: true,的watch,触发时机是晚于created、mounted的,当数据再次发生变化后,beforeUpdate之前执行;
  • watch加了immediate: true,可以监听到响应式数据初始化后的变化。

组件内外执行顺序

1、加载渲染过程

父组件 beforeCreate -> created -> beforeMount
子组件 beforeCreate -> created -> beforeMount -> mounted
父组件 mounted

2、更新过程

父组件beforeUpdate
子组件beforeUpdate
子组件updated
父组件updated

3、销毁过程

父组件beforeDestory
子组件beforeDestory
子组件destoryed
父组件destoryed

代码实现

通过watch对组件内外部数据进行监听,实现数据同步

组件外状态数据 dataSource
组件内状态数据 list

  @Watch('dataSource', { deep: true, immediate: true })
  onDataSourceChange(newValue, oldValue) {
    console.log('%c ‍♂️ 张浩雨: onDataSourceChange -> newValue, oldValue ', 'font-size:16px;background-color:#bbf443;color:black;', newValue, oldValue)
    if (JSON.stringify(newValue) !== JSON.stringify(this.list)) { //* 组件外数据修改,内外数据不相同,组件内数据修改,内外数据相同
      //* 组件外数据不等于组件内的数据时,执行某些字段的额外操作
      this.baseConfig.showTree && this.setTreeIndex(newValue);
    }
    this.list = newValue; //* 组件外部数据同步到组件内的数据
    // this.listCopy = this.list;
  }
  @Watch('list', { deep: true })
  onListChange(newValue) {
    console.log('%c  张浩雨: onListChange -> newValue ', 'font-size:16px;background-color:#248c78;color:white;', newValue)
    //todo 实时更新查询表单数据
    this.baseConfig.showTree && this.setTreeIndex(newValue);
    if (!newValue.length) this.selectedRecords = []; //* 展示“暂无数据”图片时,销毁了table组件,需要手动置空selectedRecords
    this.$emit('update:dataSource', newValue)
  }

代码详解

1、组件内外数据,初始化时解析顺序

父组件 beforeCreate -> 响应式数据设置dataSource = [] -> created -> beforeMount
子组件 beforeCreate -> 响应式数据设置list = [],侦听器设置完成(dataSource的监听器immediate为true立即执行,监听到undefined 到 [] 的变化,运行代码this.list = this.dataSource,将组件外部数据同步到组件内的数据 ,没有immediate的监听由于在下方,还未设置,不会触发它的监听)-> created -> beforeMount -> mounted
父组件 mounted

注意:两个监听的顺序会导致,初始化后,没有immediate的监听可能会(不会)被触发执行;
组件内list没有immediate的监听,在另一个监听的上方时,初始化时,执行immediate的监听,运行代码this.list = this.dataSource,后续会触发list的监听;
当组件内list没有immediate的监听,在另一个监听的下方时,初始化时,执行immediate的监听,运行代码this.list = this.dataSource,组件内list没有immediate的监听还没有设置,后续不会触发list的监听

2、组件外部数据update

this.dataSource = [] -> dataSource的监听器监听到数据变更 -> 组件外数据不等于组件内的数据时,执行某些字段的额外操作,并且this.list = newValue,组件外部数据同步到组件内的数据 -> list的监听器监听到数据变更,this. e m i t ( ′ u p d a t e : d a t a S o u r c e ′ , n e w V a l u e ) ,将组件内部数据同步到组件外部,且不再触发 d a t a S o u r c e 的 w a t c h t h i s . d a t a S o u r c e = [ ] , emit('update:dataSource', newValue),将组件内部数据同步到组件外部,且不再触发dataSource的watch this.dataSource = [{}] , emit(update:dataSource,newValue),将组件内部数据同步到组件外部,且不再触发dataSourcewatchthis.dataSource=[]set、push -> 同上

3、组件内部数据update

变更之前,this.list === this.datasource //* true
this.list = [{}, {}] -> list的监听器监听到数据变更 -> 始终执行某些字段的额外操作,并this. e m i t ( ′ u p d a t e : d a t a S o u r c e ′ , n e w V a l u e ) ,将组件内部数据同步到组件外部 − > d a t a S o u r c e 的监听器监听到数据变更 − > 组件内数据修改,组件内外数据相同,无需执行某些字段的额外操作, t h i s . l i s t = n e w V a l u e ,切不会触发 l i s t 的 w a t c h t h i s . d a t a S o u r c e = [ ] , emit('update:dataSource', newValue),将组件内部数据同步到组件外部 -> dataSource的监听器监听到数据变更-> 组件内数据修改,组件内外数据相同,无需执行某些字段的额外操作,this.list = newValue,切不会触发 list的watch this.dataSource = [] , emit(update:dataSource,newValue),将组件内部数据同步到组件外部>dataSource的监听器监听到数据变更>组件内数据修改,组件内外数据相同,无需执行某些字段的额外操作,this.list=newValue,切不会触发listwatchthis.dataSource=[]set、push -> 同上

为什么始终执行某些字段的额外操作,不判断组件内外数据是否一致,或者不判断新老数据是否一致?
解释说明:

  1. 组件内部数据$set,push等修改原数据,新老数据一致
  2. 组件外部数据修改,this.list = this.dataSource,组件内外数据一致,所以始终都执行了某些字段的额外操作

代码验证

  @Watch('queryForm', { deep: true, immediate: true })
  onQueryFormChange(newValue, oldValue) {
    // console.log('%c  张浩雨: onQueryFormChange -> newValue, oldValue ', 'font-size:16px;background-color:#2ad7fd;color:black;', newValue, oldValue, this.queryFormCurrent)
    if (!oldValue && this.searchFormConfig.needPaging) { //* created之前执行
      Object.assign(newValue, { pageIndex: 1, pageSize: 10 })
    }
    this.queryFormCurrent = newValue
  }
  @Watch('queryFormCurrent', { deep: true })
  onQueryFormCurrentChange(newValue, oldValue) {
    // console.log('%c  张浩雨: onQueryFormCurrentChange -> newValue, oldValue ', 'font-size:16px;background-color:#0636ba;color:white;', newValue, oldValue)
    //todo 实时更新查询表单数据
    this.$emit('update:queryForm', newValue)
  }

参考vue3.0官网生命周期

vue生命周期 时机 详情
beforeCreate 在组件实例初始化完成之后立即调用。 会在实例初始化完成、props 解析之后、data() 和 computed 等选项处理之前立即调用。注意,组合式 API 中的 setup() 钩子会在所有选项式 API 钩子之前调用,beforeCreate() 也不例外。
created 在组件实例处理完所有与状态相关的选项后调用。 当这个钩子被调用时,以下内容已经设置完成:响应式数据、计算属性、方法和侦听器。然而,此时挂载阶段还未开始,因此 $el 属性仍不可用。
beforeMount 在组件被挂载之前调用。 当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。
mounted 在组件被挂载之后调用。 组件在以下情况下被视为已挂载:所有同步子组件都已经被挂载。(不包含异步组件或 树内的组件)其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于确保 DOM 相关代码仅在客户端被调用。
beforeUpdate 在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。 这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。
updated 在组件因为一个响应式状态变更而更新其 DOM 树之后调用。 父组件的更新钩子将在其子组件的更新钩子之后调用。这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。不要在 updated 钩子中更改组件的状态,这可能会导致无限的更新循环!
beforeUnmount 在一个组件实例被卸载之前调用。 当这个钩子被调用时,组件实例依然还保有全部的功能。
unmounted 在一个组件实例被卸载之后调用。 一个组件在以下情况下被视为已卸载:其所有子组件都已经被卸载。所有相关的响应式作用 (渲染作用以及 setup() 时创建的计算属性和侦听器) 都已经停止。可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。

vue2.0的官网解释

vue生命周期 详情
beforeCreate 在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。
created 在实例创建完成后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且 $el property 目前尚不可用。
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。该钩子在服务器端渲染期间不被调用。mounted: 实例被挂载后调用,这时 el 被新创建的 vm. e l 替换了。如果根实例挂载到了一个文档内的元素上,当 m o u n t e d 被调用时 v m . el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm. el替换了。如果根实例挂载到了一个文档内的元素上,当mounted被调用时vm.el 也在文档内。注意 mounted 不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在 mounted 内部使用 vm.$nextTick:该钩子在服务器端渲染期间不被调用。
beforeUpdate 在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务器端进行。
updated 在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或 watcher 取而代之。注意,updated 不会保证所有的子组件也都被重新渲染完毕。如果你希望等到整个视图都渲染完毕,可以在 updated 里使用 vm.$nextTick:该钩子在服务器端渲染期间不被调用。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。该钩子在服务器端渲染期间不被调用。
destroyed 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。该钩子在服务器端渲染期间不被调用。
activated 被 keep-alive 缓存的组件激活时调用。
deactivated 被 keep-alive 缓存的组件失活时调用。

vue2.0官网图片

vue组件内外数据保持同步 & 组件数据加载解析顺序_第1张图片

你可能感兴趣的:(vue.js,前端,javascript)