miniprogram-computed之computed源码解析

224 阅读5分钟

前言

用惯了vue的computed属性,在小程序开发中难免也会想着使用,而官方正好提供了miniprogram-computed来在小程序中使用computed属性。本着精进技术的原则,一起来瞅瞅官方的源码吧!

基本使用

// js代码
import { behavior as computedBehavior } from 'miniprogram-computed'

Component({
    behaviors: [ computedBehavior ],
    data: {
        a: 1,
        b: {
            c: 1,
            d: [2, 3]
        }
    },
    computed: {
        total(data) {
            return data.a + data.b.c + data.b.d[0]
        }
    }
})

// 页面中使用
// <view>{{ total }}</view>

源码解析

1.初始化computedWatchInfo对象

在created生命周期中初始化了computedWatchInfo对象,并保存到页面根实例上。

computedWatchInfo对象维护了当前组件实例的computed计算信息。

computedUpdaters数组保存了包装后的计算函数,每当data值变化后,就执行其中保存的包装函数,并根据computedRelatedPathValues中收集的依赖关系,来判断是否要执行原始计算函数。

computedRelatedPathValues对象保存了computed属性值与计算函数运算过程中所依赖的key与keyValue的映射关系,用于后期data值变化后,通过查找依赖值是否变化,来决定是否进行重新计算。

// 初始化当前组件的监听对象
const computedWatchInfo = {
    // 保存包装后的计算函数
    computedUpdaters: [],
    // 根据computed属性值保存计算过程中依赖的data中的key及value
    computedRelatedPathValues: {},
    watchCurVal: {},
    _triggerFromComputedAttached: {}
}
// 保存到根实例_computedWatchInfo属性中
if (!this._computedWatchInfo) this._computedWatchInfo = {}
// computedWatchDefId 持续自增的数字
this._computedWatchInfo[computedWatchDefId] = computedWatchInfo
2.遍历computed对象将key及computedValue设置到data中

在attached生命周期中,遍历所有的computed属性及对应的计算函数,执行计算函数并传入代理后的data对象,将key及计算后的value值通过setData设置到data中。

(1)在attached生命周期中,遍历computed,拿到所有的计算函数,进行computed值的初始化,并对每个计算函数所依赖的data属性值进行依赖关联关系收集,并将收集后的关系表保存到监听对象computedRelatedPathValues对象中。

(2)dataTracer.create(this.data, relatedPathValuesOnDef) 这个方法的执行比较重要,它即通过Proxy对data对象进行了代理,也在getter方法中,收集了计算函数运行时,所依赖的data数值,并建立了依赖关联关系表,用于后期判断是否需要重新执行计算函数。

(3)将依赖关联关系表存到了computedRelatedPathValues属性上。

// 计算方法 function
const updateMethod = computedDef[targetField]
// 依赖关联关系表
const relatedPathValuesOnDef = []
// 返回计算后的val
const val = updateMethod(
    // 传入用户定义的data属性对象
    // 返回了代理的data对象
    dataTracer.create(this.data, relatedPathValuesOnDef)
)
const pathValues = relatedPathValuesOnDef.map(({ path }) => ({
    path,
    // 根据数组路径,从data中获取value
    value: dataPath.getDataOnPath(this.data, path)
}))
// 将computed属性与计算值设置到data中
this.setData({
    // 定义的computed属性   返回真实的的val
    [targetField]: dataTracer.unwrap(val)
})
computedWatchInfo._triggerFromComputedAttached[targetField] = true
// 保存每个计算函数的依赖关联关系表
computedWatchInfo.computedRelatedPathValues[targetField] = pathValues

(4)设置包装函数并保存到监听对象computedUpdaters属性中,函数内部会取到各自计算函数所依赖的关联关系表,比较旧的关联关系表中,是否有value值发生了变化,如果有,则说明需要重新计算,此时则调用原始的计算函数获取新的computed值,并设置新的依赖关联关系表。

// 设置包装函数
const updateValueAndRelatedPaths = () => {
// 取到旧的依赖关联关系
const oldPathValues = computedWatchInfo.computedRelatedPathValues[targetField]
// 默认不需要更新
let needUpdate = false
// 查找旧的依赖关系中,是否有值发生了变化
for (let i = 0; i < oldPathValues.length; i++) {
    const { path, value: oldVal } = oldPathValues[i]
    const curVal = dataPath.getDataOnPath(this.data, path)
    // 新值与旧值不相等 说明需要更新
    if (!equal(oldVal, curVal)) {
        needUpdate = true
        break
    }
}
// 不需要更新就暂停
if (!needUpdate) return false
// 这里说明需要更新
// 新的依赖关联关系
const relatedPathValues = []
// 取到新值
const val = updateMethod(
    // 传入代理对象
    dataTracer.create(this.data, relatedPathValues),
)
// 更新computed key value
this.setData({
    [targetField]: dataTracer.unwrap(val),
})
// 再次拿到新的依赖关联关系
const pathValues = relatedPathValues.map(({ path }) => ({
    path,
    // 区别就是从原始对象上拿到了value值
    value: dataPath.getDataOnPath(this.data, path),
}))
// 再次保存新的依赖关联关系
computedWatchInfo.computedRelatedPathValues[targetField] = pathValues
return true
}
// 更新方法保存在这里
computedWatchInfo.computedUpdaters.push(
  updateValueAndRelatedPaths,
)
3.设置observers监听

(1)往observersItems数组中加入监听属性_computedWatchInit,分别在created和attached生命周期中触发上述第一步及第二步的执行。

attached(this: BehaviorExtend) {
    this.setData({
        _computedWatchInit: ComputedWatchInitStatus.ATTACHED,
    })
}
created(this: BehaviorExtend) {
    this.setData({
        _computedWatchInit: ComputedWatchInitStatus.CREATED,
    })
}
// 执行初始化
observersItems.push({
    fields: '_computedWatchInit',
    observer(this: BehaviorExtend) {
        // 拿到当前的组件执行状态
        const status = this.data._computedWatchInit
        if (status === ComputedWatchInitStatus.CREATED) {
            // 执行第一步
        } else if (status === status === ComputedWatchInitStatus.ATTACHED) {
            // 执行第二步
        }
    }
})

(2)监听data中的所有属性,每当属性值变化时,找到对象的组件实例对应的监听对象,对监听对象的computedUpdaters数组(保存的包装后的计算函数)进行遍历执行。

// 监听所有属性
observersItems.push({
  fields: '**',
  observer(this: BehaviorExtend) {
    if (!this._computedWatchInfo) return
    // 取出当前组件的computed信息
    const computedWatchInfo = this._computedWatchInfo[computedWatchDefId]
    // 没有则返回
    if (!computedWatchInfo) return
    // 将所有需要更新的computed重新进行计算
    let changed: boolean
    do {
        changed = computedWatchInfo.computedUpdaters.some((func) =>
            func.call(this),
        )
    } while (changed)
  }
})

(3)将observersItems数组中保存的监听配置,放入到observers对象中去。

// 初始化observers
if (typeof defFields.observers !== 'object') {
    defFields.observers = {}
}
// 包装监听函数
if (Array.isArray(defFields.observers)) {
    defFields.observers.push(...observersItems)
} else {
    // 这边做了兼容 如果已经监听 就把两个监听函数合并起来
    observersItems.forEach((item) => {
      // defFields.observers[item.fields] = item.observer
      const f = defFields.observers[item.fields]
      if (!f) {
          defFields.observers[item.fields] = item.observer
      } else {
          defFields.observers[item.fields] = function () {
              item.observer.call(this)
              f.call(this)
          }
      }
    })
}

Vue与小程序中的computed使用区别

  • 1.Vue中computed计算函数中的this指向当前组件实例,而小程序为undefined,只能通过访问传入的代理data对象,来进行值的运算。
  • 2.Vue中的computed属性值只有在访问时,才进行值运算,而小程序中的computed无论页面中是否使用,都会进行首次值的运算。
  • 3.Vue中的computed属性值内部通过dirty值进行脏值检验,依赖属性发生变化后,会修改dirty值,当再次读取computed属性值时,由于dirty发生了变化,则会进行重新计算。而小程序中是通过监听所有的data属性值,当任意值发生变化后,遍历执行所有的包装计算方法,如果该方法所依赖的data属性值发生了变化,则重新计算,并通过setData进行赋值操作,触发页面更新。