Skip to content

Commit

Permalink
fix: 计算属性优化
Browse files Browse the repository at this point in the history
  • Loading branch information
missannil committed Feb 5, 2024
1 parent c85f35a commit 2479d08
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 83 deletions.
3 changes: 3 additions & 0 deletions jest/computed/arrMap/arrMap.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"component": true
}
18 changes: 18 additions & 0 deletions jest/computed/arrMap/arrMap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { load, render } from "miniprogram-simulate";
import path from "path";
import { user } from "../../common";

const id = load(path.resolve(__dirname, "arrMap")); // 此处必须传入绝对路径
const comp = render(id, { requiredUser: user }); // 渲染成自定义组件树实例

const parent = document.createElement("parent-wrapper"); // 创建挂载(父)节点

comp.attach(parent); // attach 到父亲节点上,此时会触发自定义组件的 attached 钩子

test("调用数组的方法时 ", async () => {
expect(comp.data.objFunAdd).toBe(2);

expect(comp.data.listMap).toBe(2);

expect(comp.data._compA_listMap).toStrictEqual([3, 4, 5]);
});
57 changes: 57 additions & 0 deletions jest/computed/arrMap/arrMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { observable } from "mobx";
import { DefineComponent, RootComponent, SubComponent } from "../../../src";
import { type CompDoc } from "../../common";

const store = observable({
name: "zhao",
list: [] as number[],
changedList(list: number[]) {
this.list = list;
},
});

const subA = SubComponent<Root, CompDoc>()({
data: {
compA_num: 0,
},
computed: {
_compA_listMap() {
return this.data.storeList.map((item) => item + 2);
},
},
});

type Root = typeof rootComponent;

const rootComponent = RootComponent()({
store: {
storeList: () => store.list,
},
data: {
objFun: {
a: 1,
add(num: number) {
return num + 1;
},
},
},
computed: {
objFunAdd() {
return this.data.objFun.add(this.data.objFun.a);
},
listMap() {
return this.data.storeList[0] + 1;
},
},
lifetimes: {
attached() {
store.changedList([1, 2, 3]);
},
},
});

DefineComponent({
name: "computed",
rootComponent,
subComponents: [subA],
});
Empty file.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { deepClone } from "../../../../utils/deepClone";
import type { Instance } from "..";
import { deepProxy } from "./data-tracer";
import { deepProxy, getProxyOriginalValue } from "./data-tracer";
import { getPathsValue } from "./getPathsValue";

import { isEqual } from "./isEqual";
Expand All @@ -23,11 +22,13 @@ export function computedUpdater(this: Instance, isUpdated = false): boolean {
}
if (changed) {
const newDependences: ComputedDependence[] = [];
const newValue = itemCache.method.call({ data: deepProxy(this.data, newDependences) });
let newValue = itemCache.method.call({ data: deepProxy(this.data, newDependences) });

// 更新值不会立即再次进入**函数,而是当前**函数运行完毕后触发**函数,
newValue = getProxyOriginalValue(newValue);

this.setData({
[key]: deepClone(newValue),
[key]: newValue,
});

isUpdated = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,24 @@ export function deepProxy(
): object {
const handler = {
get<T extends object>(target: T, prop: keyof T & string) {
if (prop === "__original__") {
return target;
}
const val = target[prop];
// 依赖长度不为0时 去重

// val不为undefined(依赖还为初始化的其他计算属性)且非自身属性或函数不再返回代理。 (如 this.data.arr.slice(),slice不属于自身属性,小程序允许data子字段为函数的情况,也不代理)
if ((val !== undefined) && (!Object.prototype.hasOwnProperty.call(target, prop) || typeof val === "function")) {
// val有不是函数的情况么?
return (val as Function).bind(target);
}
// 依赖长度不为0时径依赖去重(只留最后一个路径),比如 return this.data.obj.user.name 去重得到的是最后1个路径 ['obj','user','name'] , 不去重则3个依赖路径了 ['obj'], ['obj','user'],['obj','user','name'],在这里去重效率高些,外部去重时还要再次遍历
if (basePath.length !== 0) {
// 依赖去重
const lastDependences = dependences[dependences.length - 1]; // 倒数第一个是当前依赖。2022 at(-1)
// 只留最后一个路径。比如 return this.data.obj.user.name 得到的是最后1个路径 ['obj','user','name'] 减少无效依赖,提高性能。 而非3个 ['obj'], ['obj','user'],['obj','user','name']
if (lastDependences.paths.toString() === basePath.toString()) {
// console.log('删除', lastDependences)
dependences.pop();
}
dependences.pop();
}
const curPath = basePath.concat(prop);

// console.log(prop, val,'in');

dependences.push({ paths: curPath, val: deepClone(val) });
dependences.push({ paths: curPath, val });

// 自身方法或原型属性不代理
if (!Object.prototype.hasOwnProperty.call(target, prop) || typeof val === "function") {
// 原型链上的属性不代理,函数加bind。如 this.data.arr.slice() this.data.arr.forEach(...)
return typeof val === "function" ? val.bind(target) : val;
}
// 非对象不代理
if (typeof val !== "object" || val === null) return val;

Expand All @@ -42,3 +38,11 @@ export function deepProxy(

return new Proxy(data, handler);
}

export function getProxyOriginalValue<T extends { __original__?: string }>(value: T): unknown {
if (typeof value !== "object" || value === null || !value.__original__) {
return value;
} else {
return deepClone(value.__original__);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Func } from "hry-types/src/Misc/Func";
import { computedUpdater } from "./computedUpdater";
import { getPathsValue } from "./getPathsValue";
import { getPropertiesValue } from "./getPropertiesValue";
import { initComputed } from "./initComputed";
import { initComputedAndGetCache } from "./initComputedAndGetCache";
import { isEqual } from "./isEqual";

import { deepClone } from "../../../../utils/deepClone";
Expand All @@ -25,25 +25,29 @@ function initWatchOldValue(data: FinalOptionsOfComponent["data"], watchConfig: o
return watchOldValue;
}
export function computedWatchHandle(options: FinalOptionsOfComponent) {
// 计算属性初始化
// 计算属性初始化和首次依赖收集
const computedConfig = options.computed;
const rawPropertiesValue = getPropertiesValue(options.properties);
if (computedConfig && !isEmptyObject(computedConfig)) {
// 此时store已经初始化数据到data了

const __computedInitCache__ = initComputed(options, computedConfig, { ...options.data, ...rawPropertiesValue });
const __computedInitCache__ = initComputedAndGetCache(options, computedConfig, {
...options.data,
...rawPropertiesValue,
});

// 缓存放入data中
options.data.__computedCache__ = __computedInitCache__;

// 计算属性更新函数放入methods中 要做冲突判断,避免用户定义了同名methods字段
options.methods.__computedUpdater__ = computedUpdater;
// // 把计算属性缓存从方法中带入到实例中,在created周期加入实例后删除
// methodOpt.__computedInitCache__ = () => computedCache;
}
const observersConfig = options.observers;
// 通过observers加入`**`字段来触发计算属性更新
const originalFunc = observersConfig["**"] as Func | undefined;

observersConfig["**"] = function(this: Instance): undefined {
// 任何setData都会触发计算属性更新(可能依赖数据并没变化)
this.__computedUpdater__?.();

originalFunc && originalFunc.call(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,31 @@
import type { Func } from "hry-types/src/Misc/Func";
import type { FinalOptionsOfComponent } from "..";

import { deepClone } from "../../../../utils/deepClone";
import type { ComputedDependence } from "./computedUpdater";
import { deepProxy } from "./data-tracer";
import { deepProxy, getProxyOriginalValue } from "./data-tracer";

type ItemCache = {
dependences: ComputedDependence[];
method: Func;
value: unknown;
};

export type ComputedCache = Record<string, ItemCache>;
type ComputedId = string;

export type ComputedCache = Record<ComputedId, ItemCache>;

/**
* 如果依赖列表某项的首个字段值为undefined并且字段为其他计算属性字段 返回false(即被依赖的计算字段写在了依赖他的计算字段后面), 否则返回ComputedInfo
* 如果依赖列表某项的首个字段值为undefined并且字段为其他计算属性字段 返回false(即被依赖的计算字段写在了依赖他的计算字段后面), 否则返回true表示依赖有效
*/
function isValidDependences(dependences: ComputedDependence[], computedKeys: string[]): boolean {
for (const { paths: path, val } of dependences) {
// 引用值为undefined时
// console.log(path[0]);
if ((val === undefined) && computedKeys.includes(path[0])) {
return false;
}
}

return true; // 依赖有效
}

export function getItemCache(
initAllData: object,
computedKeys: string[],
computedFunc: Func,
): false | ItemCache {
// 当前计算字段的依赖列表
const dependences: ComputedDependence[] = [];
const initValue = computedFunc.call({ data: deepProxy(initAllData, dependences) });

if (isValidDependences(dependences, computedKeys)) {
return {
// 去重 避免 如: ` age(){ return this.data.xxx.num + this.data.xxx.str + this.data.xxx} ` 造成的多个依赖相同情况。应就要一个 xxx
dependences: uniqueDependences(dependences),
method: computedFunc,
value: deepClone(initValue),
};
} else {
/**
* 情形1 依赖了其他未初始化的计算属性(不报错) 对应测试文件[NoOptionalProperties.ts](../../../jest/computed/NoOptionalProperties/NoOptionalProperties.test.ts)的copyPropUser字段
*/
return false;
}
// 依赖有效
return true;
}

/**
Expand All @@ -63,24 +39,17 @@ export function getItemCache(
*/
function uniqueDependences(dependences: ComputedDependence[]): ComputedDependence[] {
if (dependences.length === 1) return dependences;
// console.log(dependences);

for (let f = 0; f < dependences.length; f++) {
const firstPath = dependences[f].paths.join(".") + ".";
for (let i = f + 1; i < dependences.length; i++) {
const curPath = dependences[i].paths.join(".") + ".";
// console.log(firstPath,curPath)
if (firstPath.startsWith(curPath)) {
// console.log("删除:", curPath, f);

// 例如 path[0] = 'a.b.c.',curPath = 'a.b.'
dependences.splice(f, 1);

return uniqueDependences(dependences);
}
if (curPath.startsWith(firstPath)) {
// console.log("删除:", firstPath, i);

// 例如 curPath = 'a.b.' path[0] = 'a.b.c.',
dependences.splice(i, 1);

Expand All @@ -93,43 +62,55 @@ function uniqueDependences(dependences: ComputedDependence[]): ComputedDependenc
}

/**
* 把计算属性初始值加入到options.data中返回缓存
* 把计算属性初始值加入到options.data中并返回缓存(递归函数)
* @param options - 配置选项
* @param computedConfig - 计算字段配置
* @param initAllData - 初始化时全部的data包含(properties,data,和已经初始化后的computed字段)
* @param uninitedkeys - 未初始化的key (默认为所有computedConfig的key)
* @param computedCache - 返回的所有计算字段缓存(默认为空对象)
* @returns `computedCache` 计算字段缓存
*/
export function initComputed(
export function initComputedAndGetCache(
options: FinalOptionsOfComponent,
computedConfig: Record<string, Func>,
initAllData: object,
initAllData: Record<string, unknown>,
uninitedkeys: string[] = Object.keys(computedConfig),
computedCache: ComputedCache = {},
): ComputedCache {
// 收集依赖 uninitedkeys不受内部2处重新赋值的影响

for (const key of uninitedkeys) {
// uninitedkeys 受2处重新赋值的影响
const itemCache = getItemCache(initAllData, uninitedkeys, computedConfig[key]);
const computedFunc = computedConfig[key];
const dependences: ComputedDependence[] = [];
// 通过代理data获取计算字段的初始值和依赖
let initValue = computedFunc.call({ data: deepProxy(initAllData, dependences) });

// 去除当前的key 2
// 去除当前已初始的计算属性key
uninitedkeys = uninitedkeys.filter(ele => ele !== key);

if (itemCache === false) {
// 把当前依赖不正确的key放到后面去
uninitedkeys.push(key);
} else {
const dataOpt = options.data;
// 验证依赖是否有效
if (isValidDependences(dependences, uninitedkeys)) {
initValue = getProxyOriginalValue(initValue);

dataOpt[key] = itemCache.value;
// 把计算属性初始值加入到options.data中
options.data[key] = initValue;

// @ts-ignore 隐式索引
initAllData[key] = itemCache.value;
// 把计算属性初始值加入到initAllData中,后续其他计算属性依赖时会可能会用到
initAllData[key] = initValue;

computedCache[key] = itemCache;
computedCache[key] = {
dependences: uniqueDependences(dependences),
method: computedFunc,
value: initValue,
};
} else {
// 把当前依赖不正确的key放到后面去
uninitedkeys.push(key);
}
}
// 看uninitedkey(2处的新值)是否未空,空表示所有依赖收集完毕直接返回
// 看uninitedkey是否未空,空表示所有依赖收集完毕直接返回
if (uninitedkeys.length === 0) {
return computedCache;
}

// uninitedkey不为空,递归
return initComputed(options, computedConfig, initAllData, uninitedkeys, computedCache);
return initComputedAndGetCache(options, computedConfig, initAllData, uninitedkeys, computedCache);
}
2 changes: 1 addition & 1 deletion src/api/DefineComponent/assignOptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type { DefineComponentOption } from "..";
import { BStore } from "../../../behaviors/BStore";
import { computedWatchHandle } from "./computedWatchHandle";

import type { ComputedCache } from "./computedWatchHandle/initComputed";
import type { ComputedCache } from "./computedWatchHandle/initComputedAndGetCache";
import { initStore } from "./initStore";

type InstanceInnerFields = {
Expand Down

0 comments on commit 2479d08

Please sign in to comment.