# 高级数据更新方法

组件框架支持几种更新数据的方式。

# 使用 setData 更新数据

setData 是最经典的更新数据方法。在调用 setData 时,组件框架会做两件事情:

  1. 立刻将变更的数据改到组件数据(即 this.data)上;
  2. 将新的数据应用到 WXML 模板上、更新界面。

setData 对 WXML 模板的更新是 同步 的(除了在数据监听器内被调用时)。换而言之,调用完 setData 后,可以视为 WXML 立刻被更新完毕了,可以立刻调用 组件树访问获取界面上的节点信息 等相关方法。

除了最普通的用法外,setData 还可以使用数据路径来更新 data 中的某个子数据字段,例如:

export default Component()
  .data(() => ({
    obj: {
      foo: [1, 2],
    },
  }))
  .init(function ({ setData, lifetime }) {
    lifetime('attached', () => {
      // 将 data.obj.a[0] 设置为 3
      setData({
        'obj.foo[0]': 3,
      })
    })
  })
  .register()

setData 的第二个参数(可选)是一个回调函数,它在这组数据完全应用到渲染引擎后回调。这个回调函数的回调时机偏晚,滥用可能会影响性能,请谨慎使用。

this.setData({}, () => {
  // 完全应用到渲染引擎后回调
})

# 高级路径更新

如果想仅更新对象内的一个字段,可以使用高级路径更新的方式。这样虽然接口复杂一些,但可以拥有更好的性能。

可以使用 replaceDataOnPath 来更新对象内的数据字段,例如:

export default Component()
  .data(() => ({
    obj: {
      foo: [1, 2],
    },
  }))
  .init(function ({ lifetime }) {
    lifetime('attached', () => {
      // 将 data.obj.a[0] 设置为 3
      // (等价于 setData 'obj.foo[0]')
      this.replaceDataOnPath(['obj', 'foo', 0], 3)
      // 将更新应用到模板上
      this.applyDataUpdates()
    })
  })
  .register()

spliceArrayDataOnPath 仅 glass-easel 组件框架支持。

如果字段是数组类型的,还可以使用 spliceArrayDataOnPath 来对数组项进行插入和删除,例如:

export default Component()
  .data(() => ({
    obj: {
      foo: [1, 2, 3, 4],
    },
  }))
  .init(function ({ lifetime }) {
    lifetime('attached', () => {
      // 类似于数组的 splice 方法
      // 可以在一个位置上移除若干项,再插入若干项
      this.spliceArrayDataOnPath(['obj', 'foo'], 1, 2, [5, 6, 7])
      // 得到的 obj.foo 是 [1, 5, 6, 7, 4]
      // 将更新应用到模板上
      this.applyDataUpdates()
    })
  })
  .register()

调用 replaceDataOnPathspliceArrayDataOnPath 后,并不会立即将更新内容应用到 WXML 模板上,还需要调用 applyDataUpdates。如果需要连续调用多个 replaceDataOnPathspliceArrayDataOnPath,在末尾调用一次 applyDataUpdates 即可。

# 组合更新

groupUpdates 仅 glass-easel 组件框架支持。

连续调用多个 replaceDataOnPathspliceArrayDataOnPath 后,可能会遗忘调用 applyDataUpdates

可以考虑改用 groupUpdates 将它们组合起来,例如:

export default Component()
  .data(() => ({
    obj: {
      foo: [1, 2],
    },
  }))
  .init(function ({ lifetime }) {
    lifetime('attached', () => {
      this.groupUpdates(() => {
        this.replaceDataOnPath(['obj', 'foo', 0], 3)
        this.replaceDataOnPath(['obj', 'foo', 1], 4)
      })
    })
  })
  .register()

groupUpdates 回调函数返回后,会自动将更新应用到 WXML 模板上,不再需要调用 applyDataUpdates

# 使用 updateData 更新

updateData 仅 glass-easel 组件框架支持。

updateData 调用语法类似于 setData,但它不会立刻将更新应用到 WXML 模板上,需要额外调用 applyDataUpdatesgroupUpdates

如果需要连续多次调用 setData,推荐改用 updateData,并结合 groupUpdates 来将它们组合起来。这种方式性能显著优于多次调用 setData。例如:

export default Component()
  .data(() => ({
    a: 1,
    b: 2,
  }))
  .init(function ({ lifetime }) {
    lifetime('attached', () => {
      // 用 groupUpdates 将多个 updateData 组合起来
      this.groupUpdates(() => {
        this.updateData({ a: 3 })
        this.updateData({ b: 4 })
      })
      // 这样做的性能优于连续多次调用 setData
    })
  })
  .register()

此外,在 数据监听器 内使用 setData 时,推荐使用 updateData 代替:因为数据监听内的 setData 相当于 updateData

# 使用 groupSetData 连续更新

有时候,连续两个 setData 不能被替换为 groupUpdates + updateData。典型的情况是,第一个 setData 更新 <scroll-view> 内部的节点列表,第二个 setData 需要更新滚动位置。这种时候必须用两个 setData 来分别更新。

但这样可能导致第一次 setData 更新后、第二次 setData 更新前的界面中间状态展示给用户。此时可以用 groupSetData 将两个 setData 组合起来,确保界面中间状态不要展示给用户。用法例如:

this.groupSetData(() => {
  this.setData({ list })
  this.setData({ scrollPosition })
})

# 递归更新问题

调用 setDataapplyDataUpdates,或当 groupUpdates 回调函数执行完毕的时刻,新的组件数据应用到 WXML 模板上。这是个同步的过程。

在这个同步过程中,更新执行到一半的时候,可能先暂停父组件的更新、进入到子组件内部、让子组件先执行完更新,再回到父组件继续更新。如果此时,子组件又通过某些方法再次触发了父组件的更新,会导致父组件同时进行两个更新,会导致 WXML 模版混乱。

最常见的一种情况是:

  1. 父组件更新了子组件的某个属性;
  2. 子组件触发了 数据监听器属性 observer
  3. 在监听器回调里面触发事件;
  4. 父组件又同步调用了一次 setData(或其他数据更新方法)。

避免这种问题的方法是:适当使用 wx.nextTick 避免递归更新,可参考 在独立任务中触发事件 的做法。