miniprogram-computed之watch源码解析

前言

在上一章介绍了computed的基本使用与源码解析,本章继续学习关于watch的基本使用与源码解析。

基本使用

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
        }
    },
    watch: {
        'a, b.**' (...newVals) {
            // [2, { c:1, d: [2, 3] }]
            console.log(newVals)
        },
        total (newVal) {
            // 3
            console.log(newVal)
        }
    },
    attached() {
        this.setData({
            a: 2
        })
    }
})

watch源码解析

上一章介绍了源码中会监听_computedWatchInit属性,在created和attached生命周期中做一些初始化操作。

在created钩子中,会声明computedWatchInfo监听对象,用于保存一下依赖信息。

// 初始化当前组件的监听对象
const computedWatchInfo = {
    // 保存包装后的计算函数
    computedUpdaters: [],
    // 根据computed属性值保存计算过程中依赖的data中的key及value
    computedRelatedPathValues: {},
    // 保存了watch属性到value值的映射
    watchCurVal: {},
    // 用于标识attached钩子中,初始化执行了哪些computed属性
    _triggerFromComputedAttached: {}
}
// 保存到根实例_computedWatchInfo属性中
if (!this._computedWatchInfo) this._computedWatchInfo = {}
// computedWatchDefId 持续自增的数字
this._computedWatchInfo[computedWatchDefId] = computedWatchInfo
关键属性介绍

watchCurVal

保存了watch属性到value值的映射

如上文基本使用中的事例,监听了a属性以及b属性所有子数据的变化,则初始化执行之后watchCurVal的值为

// created
computedWatchInfo.watchCurVal = {
    'a, b.**': [
        1,
        { c: 1, d: [2, 3] }
    ],
    'total': [null]
}

_triggerFromComputedAttached

用于标识attached钩子中,初始化执行了哪些computed属性。

如上文基本使用中的事例,初始化执行computed之后,_triggerFromComputedAttached的值为

// attached
computedWatchInfo._triggerFromComputedAttached = {
    'total': true
}
watch初始化

created钩子函数中除了初始化监听对象之外,还对用户传入的watch属性执行了初始化操作。

(1)首先调用parseMultiDataPaths方法,解析出path数组,以及对应的options,针对深度监听,对应的deepCmp属性为true。

deepCmp属性在下文中主要用来区分比较新旧数据是否发生变化的计算方法,为true则执行深比较,为false在执行浅比较。

(2)计算出当前监听属性的val值,类型为数组。

(3)保存watch属性与value值得映射。

Object.keys(watchDef).forEach((watchPath) => {
    // watchPath: 'a.**, b.c'
    // parseMultiDataPaths解析结果
    // [
    //   { path: [ 'a' ], options: { deepCmp: false } },
    //   { path: [ 'b' ], options: { deepCmp: true } }
    // ]
    const paths = dataPath.parseMultiDataPaths(watchPath)
    // record the original value of watch targets
    // 当前path 对应的value值
    const curVal = paths.map(({ path, options }) => {
        // 根据数组路径,从data中获取value
        const val = dataPath.getDataOnPath(this.data, path)
        // deepCmp为true 深度克隆
        return options.deepCmp ? deepClone(val) : val
    })
    // 将watchPath与value保存到watchCurVal中
    computedWatchInfo.watchCurVal[watchPath] = curVal
})
监听执行

(1)遍历watch属性,利用observers执行监听。

Object.keys(watchDef).forEach((watchPath) => {
    observersItems.push({
        fields: watchPath,
        observer(this: BehaviorExtend) {
            // 利用observers执行监听
        }
    })
})

(2)判断当前函数的执行是否是由computed属性,在attached钩子中初始化造成的,如果是,直接返回,不走第三步的新旧值判断。

computed在attached钩子中初始化时,往_triggerFromComputedAttached属性中保存了当前属性值,为将值置为true。

在computed执行setData放入data中时,如果watch中监听了该computed属性(如基本使用事例,监听了total属性),此时total的Watcher将会执行,当查询到_triggerFromComputedAttached.total为true时,知道此处是由computed初始化引起的,便中止执行。

const paths = dataPath.parseMultiDataPaths(watchPath)
// (issue #58) ignore watch func when trigger by computed attached
if (
  Object.keys(computedWatchInfo._triggerFromComputedAttached).length
) {
    const pathsMap: Record<string, boolean> = {}
    // { a: true, b: true }
    paths.forEach((path) => (pathsMap[path.path[0]] = true))
    for (const computedVal in computedWatchInfo._triggerFromComputedAttached) {
        if (computedWatchInfo._triggerFromComputedAttached.hasOwnProperty(computedVal)) {
            // 包含computed属性
            if (
                pathsMap[computedVal] &&
                computedWatchInfo._triggerFromComputedAttached[computedVal]
            ) {
                // 置为false
                computedWatchInfo._triggerFromComputedAttached[
                  computedVal
                ] = false
                // 拦截下方watch原函数执行
                return
            }
        }
    }
}

(3)判断新旧值是否不同,不同则执行watch原函数

取出保存在watchCurVal中的旧值之后,计算出新值,并按照deepCmp属性,来选择进行深比较还是浅比较,如果值不同,则执行原watcher函数,并传入新值。

深比较:以递归的方式遍历两个对象的所有属性是否相等,不管这两个对象是不是同一对象的引用。

浅比较:即引用比较 ===

// 取出缓存的旧值
const oldVal = computedWatchInfo.watchCurVal[watchPath]
// 获取新的值与options配置项
const originalCurValWithOptions = paths.map(({ path, options }) => {
    const val = dataPath.getDataOnPath(this.data, path)
    return { val, options }
})
// 获取新值
const curVal = originalCurValWithOptions.map(({ val, options }) =>
    options.deepCmp ? deepClone(val) : val,
)
// 保存新值
computedWatchInfo.watchCurVal[watchPath] = curVal
// 比较新旧值
let changed = false
for (let i = 0; i < curVal.length; i++) {
    const options = paths[i].options
    const deepCmp = options.deepCmp
    // deepCmp为true走深比较,否则走浅比较
    if (
        deepCmp
        // 深比较
        ? !deepEqual(oldVal[i], curVal[i])
        // 浅比较
        : !equal(oldVal[i], curVal[i])
    ) {
        changed = true
        break
    }
}

// changed为true 说明需要执行原函数
if (changed) {
    watchDef[watchPath].apply(
        this,
        // 传入新值
        originalCurValWithOptions.map(({ val }) => val)
    )
}

原文链接:https://juejin.cn/post/7217734962107072572 作者:class良木

(0)
上一篇 2023年4月3日 下午5:14
下一篇 2023年4月4日 上午10:05

相关推荐

发表回复

登录后才能评论