vue-helper (vue2)
merge-data
合并 vue 数据对象 VNodeData (点此处学习深入 vue 数据对象)
import { dashToHump } from "../string-helper";
import { wrapInArray } from "../array-helper";
const pattern = {
styleList: /;(?![^(]*\))/g,
styleProp: /:(.*)/,
};
function parseStyle(style) {
const styleMap = {};
for (const s of style.split(pattern.styleList)) {
let [key, val] = s.split(pattern.styleProp);
key = key.trim();
if (!key) continue;
if (typeof val === "string") val = val.trim();
styleMap[dashToHump(key)] = val;
}
return styleMap;
}
export function mergeStyles(target, source) {
if (!target) return source;
if (!source) return target;
target = wrapInArray(typeof target === "string" ? parseStyle(target) : target);
return target.concat(typeof source === "string" ? parseStyle(source) : source);
}
export function mergeClasses(target, source) {
if (!source) return target;
if (!target) return source;
return target ? wrapInArray(target).concat(source) : source;
}
export function mergeListeners(...args) {
const dest = {};
let i = args.length;
while (i--) {
const arg = args[i];
if (!arg) continue;
for (const event of Object.keys(arg)) {
if (!arg[event]) continue;
if (dest[event]) {
dest[event] = wrapInArray(dest[event]).concat(arg[event]);
} else {
dest[event] = arg[event];
}
}
}
return dest;
}
export function mergeData(...args) {
const mergeTarget = {};
let i = args.length;
while (i--) {
const arg = args[i];
if (!arg) continue;
for (const prop of Object.keys(arg)) {
switch (prop) {
case "class":
case "directives":
if (!arg[prop]) break;
mergeTarget[prop] = mergeClasses(mergeTarget[prop], arg[prop]);
break;
case "style":
if (!arg[prop]) break;
mergeTarget[prop] = mergeStyles(mergeTarget[prop], arg[prop]);
break;
case "staticClass":
if (!arg[prop]) break;
if (mergeTarget[prop] === undefined) mergeTarget[prop] = "";
else if (mergeTarget[prop]) mergeTarget[prop] += " ";
mergeTarget[prop] += arg[prop].trim();
break;
case "on":
case "nativeOn":
if (!arg[prop]) break;
mergeTarget[prop] = mergeListeners(mergeTarget[prop], arg[prop]);
break;
case "attrs":
case "props":
case "domProps":
case "scopedSlots":
case "staticStyle":
case "hook":
case "transition":
if (!arg[prop]) break;
if (!mergeTarget[prop]) mergeTarget[prop] = {};
mergeTarget[prop] = { ...arg[prop], ...mergeTarget[prop] };
break;
default:
if (arg[prop] === undefined || mergeTarget[prop] !== undefined) break;
mergeTarget[prop] = arg[prop];
}
}
}
return mergeTarget;
}
用法
在你想二次封装一个组件,想给内部标签一些默认的 HTML attribute 或者 props, 但又不想丢失组件原有的功能或者特性。你可以用它
在函数式组件中尤其好用
const VVBtn = {
functional: true,
render(h, { data }) {
const $data = mergeData(data, {
staticClass: "vv-btn",
style: "",
props: {},
directives: [],
});
return <Btn {...$data} />;
},
};
composition-api-wrap
这个是用来在 vue2 的项目中引入了 composition-api,但是在 setup 中不能访问$router,$route,$store等
import get from "lodash.get";
import { computed, getCurrentInstance as getVM } from "@vue/composition-api";
export function getCurrentInstance() {
const vm = getVM();
if (!vm) return;
return vm.proxy;
}
export const wrapProperty =
(property, makeComputed = true) =>
() => {
const vm = getCurrentInstance();
if (!vm) throw new Error("This must be called within a setup function.");
return makeComputed ? computed(() => get(vm, property)) : get(vm, property);
};
export const useRouter = wrapProperty("$router", false);
export const useI18n = wrapProperty("$i18n", false);
export const t = (...args) => {
const i18n = useI18n() ?? I18n;
return i18n.t.call(i18n, ...args);
};
export const useRoute = wrapProperty("$route");
export const useStore = wrapProperty("$store");
export const useRouteQuery = () => {
const route = useRoute();
return computed(() => route.value.query);
};
export const useRouteParams = () => {
const route = useRoute();
return computed(() => route.value.params);
};
export const useContext = () => {
const vm = getCurrentInstance();
if (!vm) throw new Error("This must be called within a setup function.");
return {
app: vm,
store: vm.$store,
router: vm.$router,
route: computed(() => vm.$route),
query: computed(() => vm.$route.query),
params: computed(() => vm.$route.params),
};
};
用法
const routeParams = useRouteParams();
const id = computed(() => routeParams.value.id);
// useI18n 因为vue-i18n实现问题, 不能使用像const {t} = useI18n()
// 但你可以这样使用
const i18n = useI18n();
i18n.t("");
// 或者
t("");
getSlot
因为 vue2 作用域插槽 scopedSlots 与具名插槽 slots 是不同的实现 有了这个你在封装组件时就不需要区分是作用域插槽还是具名插槽
export function getSlot(vm, name = "default", data, optional = false) {
if (vm.$scopedSlots[name]) {
return vm.$scopedSlots[name](isFunction(data) ? data() : data);
} else if (vm.$slots[name] && (!data || optional)) {
return vm.$slots[name];
}
return undefined;
}
用法
const Demo = {
name: "Demo",
render() {
return getSlot(this, "default", { a: 5 });
},
};
<template>
<demo>test 01</demo>
<demo>
<template #default="{ a }"> test 02 a: {{ a }}</template>
</demo>
</template>
RenderToComp
众所周知, render 函数是可以通过混入(mixin)或者扩展(extends,Vue.extend)来组合渲染的内容 例如
const DemoA = Vue.extends({
render() {
return [this.renderBtnOne(), this.renderBtnTwo(), this.renderAppendBtns()]
},
methods: {
renderBtnOne() {
return <button>功能1</button>
},
renderBtnTwo() {
return <button>功能2</button>
}
,
renderAppendBtns() {
}
}
})
const DemoB = DemoA.extends({
methods: {
renderBtnTwo() {
return null
},
renderAppendBtns() {
return <button>功能3</button>
}
}
})
但很显然模板语法是做不到上面这样的, 只能写 v-if, 我看到那 v-if 我就来气 既然在模板里不行, 那在模板里塞个 render 不就行了吗 然后就有了
export const RenderToComp = {
props: {
render: Function,
slim: Boolean,
tag: { type: String, default: "span" },
},
functional: true,
render(h, { props }) {
const { render, slim, tag } = props;
const vNode = render?.call({ $createElement: h }, h);
if (!vNode) return null;
if (slim) return vNode;
return <tag>{vNode}</tag>;
},
};
多提一嘴:setup 里没法用 jsx, 你可以写 h()
用法
上面那个例子
- DemoA
<template>
<div>
<RenderToComp :render="renderBtns" />
</div>
</template>
<script>
export default Vue.extends({
methods: {
renderBtns() {
return [this.renderBtnOne(), this.renderBtnTwo(), this.renderAppendBtns()]
},
renderBtnOne() {
return <button>功能1</button>
},
renderBtnTwo() {
return <button>功能2</button>
}
,
renderAppendBtns() {
}
}
})
</script>
// DemoB
const DemoB = DemoA.extends({
methods: {
renderBtnTwo() {
return null;
},
renderAppendBtns() {
return <button>功能3</button>;
},
},
});
WatchItem
这个还没有具体实践过,慎用! 现在想 watch 数组里的各项, 但又不想直接 watch 这个数组。 且数组增加时要能给加上的数组项加上监听
这个组件可以用来监听数组的各项(太 hack 了哈哈)
这个是利用了 vue 是已组件为细粒度的特性(说的不对的话,欢迎指正)
const WatchItem = {
props: {
item: null,
watch: Function,
},
setup(props, { emit }) {
const stop = watch(() => props.item, props.watch);
emit("stop-init", { stop, item: props.item });
},
};
用法
<template>
<WatchItem v-for="item of items" :key="item.id" :watch="watchItem" @stop-init="" />
</template>
<script>
export default {
data: () => ({ stopMap: {} }),
methods: {
watchItem(val, oldVal) {
console.log(val, oldVal);
},
initStopMap({ stop, item }) {
this.stopMap[item.id] = stop
},
},
};
</script>
array-helper
文件路径: src/common/utils/helper/array-helper.js
map
在有些时候, 我们对一个数组进行处理的时候, 不仅要过滤,还得生成一个新数组。
大家可能会这么写:.filter(...).map(...), 我就看这个不爽,明明我一遍循环就能解决的问题,非得搞两次 然后就有了这个
const mapSkip = Symbol("skip");
function map(iterable, mapper) {
const result = [];
for (let i = 0; i < iterable.length; i++) {
const item = iterable[i];
const element = mapper(item, i, iterable);
if (element === mapSkip) continue;
result.push(element);
}
return result;
}
用法
这里也是利用的 symbol 唯一值的特性, 你可以这么用
const arr = Array.from({ length: 10 }, (_, index) => index + 1);
// before
const arr1 = arr.filter((i) => i > 5).map((i) => i * 10);
//after
const arr2 = map(arr, (i) =>
i > 5 ? mapSkip : (i * 10)
)
;
listToMap
function listToMap<T = any, K = any, R = any>(
list: T[],
getKey: K | ((T) => K) = "id",
getValue: (T) => T | R = (item) => item,
map: Map<K, T | R> | any = {}
): { [K]: T | R } | Map<K, T | R> {
return list.reduce((acc, cur) => {
const key = isFunction(getKey) ? getKey(cur) : cur[getKey];
const value = getValue(cur);
Reflect.set(acc, key, value);
return acc;
}, map);
}
看名字就知道, 这是个可以把数组转换为映射的方法
用法
const attributes = [
{ id: "id1", value: 5 },
{ id: "id2", value: 3 },
];
const items = ["id1", "id2"];
// before 有些同学会这么写
const newItems1 = items.map((item) => {
const findItem = attributes.find((attribute) => attribute.id === item);
if (!findItem) return;
return findItem.value;
});
// after
// 有了`listToMap`, 时间复杂度O(n^2)=>O(n)
const attributesMap = listToMap(attributes, "id");
const newItems2 = items.map((item) => {
const attribute = attributesMap[item];
if (!attribute) return;
return attribute.value;
});
这种利用空间换时间的思路也可以用在其他地方
此外, 这个映射的格式可以是Object,也可以是Map、WeakMap(键名,键值都得是对象),甚至是Object.create(null)
promise-helper
文件路径: src/common/utils/helper/promise-helper.js
pMap
github 地址: github.com/sindresorhu…
p-map 适用于使用不同的输入多次运行 promise-returning 或 async 函数的场景。
它与 Promise.all 方法的区别是,你可以控制并发,也可以决定是否在出现错误时停止迭代。
const pMapSkip = Symbol("skip");
async function pMap<T = Promise | any, R = any>(
iterable: Iterable<T>,
mapper: (item, index) => R,
{
concurrency = Number.POSITIVE_INFINITY,
stopOnError = true,
}: {
concurrency?: number; //—— 并发数,默认值 Infinity,最小值为 1;
stopOnError?: boolean; //出现异常时,是否终止,默认值为 true。
} = {}
): Promise<R> {
return new Promise((resolve, reject) => {
const result = [];
const errors = [];
const skippedIndexes = [];
const iterator = iterable[Symbol.iterator]();
let isRejected = false;
let isIterableDone = false;
let resolvingCount = 0;
let currentIndex = 0;
const next = () => {
if (isRejected) return;
const nextItem = iterator.next();
const index = currentIndex;
currentIndex++;
if (nextItem.done) {
isIterableDone = true;
if (resolvingCount === 0) {
if (stopOnError && errors.length > 0) {
reject(errors);
} else {
skippedIndexes.sort((a, b) => b - a);
for (const skippedIndex of skippedIndexes) {
result.splice(skippedIndex, 1);
}
resolve(result);
}
}
return;
}
resolvingCount++;
(async () => {
try {
const element = await nextItem.value;
if (isRejected) return;
const value = await mapper(element, index);
if (value === pMapSkip) {
skippedIndexes.push(index);
} else {
result[index] = value;
}
resolvingCount--;
next();
} catch (e) {
if (stopOnError) {
isRejected = true;
reject(e);
} else {
errors.push(e);
resolvingCount--;
next();
}
}
})();
};
for (let index = 0; index < concurrency; index++) {
next();
if (isIterableDone) break;
}
});
}
用法:
它与上述的 map 一样,提供了一个 pMapSkip, 可以直接移除对应索引值
const inputs = [200, 100, pMapSkip];
const mapper = (value) => delay(value, { value });
async function main() {
console.time("start");
const result = await pMap(inputs, mapper, { concurrency: 1 });
console.dir(result); // 输出结果:[ 200, 100 ]
console.timeEnd("start"); //start: 368.708ms
}
main();
而当把 concurrency 属性的值更改为 2 之后,再次执行以上代码。那么命令行将会输出以下信息:
[ 200, 100 ]
start: 210.322ms
pAll
该模块提供的功能,与 Promise.all API 类似,主要的区别是该模块允许你限制任务的并发数。 如果数组里有函数且 runInFunction 为 true, 可以自动帮你执行
export async function pAll(
iterable,
{ runInFunction = true, concurrency = Number.POSITIVE_INFINITY, stopOnError = true } = {}
) {
return pMap(
iterable,
(element) => {
if (!runInFunction) return element;
return isFunction(element) ? element() : element;
},
{ concurrency, stopOnError }
);
}
用法
const inputs = [
() => delay(200, { value: 1 }),
async () => {
await delay(100);
return 2;
},
async () => 8,
];
async function main() {
console.time("start");
const result = await pAll(inputs, { concurrency: 1 });
console.dir(result); // 输出结果:[ 1, 2, 8 ]
console.timeEnd("start");
}
main();
awaitTo
try-catch 包裹器(异步)
function to(
promise: Promise,
{
notifyError = true,
errorExt = {},
notifyOptions = {},
initialValue = undefined,
loading = false,
}: ?{
notifyError?: boolean | string;
notifyOptions?: any;
errorExt?: any;
initialValue?: any;
loading?: boolean;
} = {}
): Promise<[Error, any]> {
loading && $loading.show();
return promise
.then((data) => [null, data])
.catch((err) => {
if (errorExt) Object.assign(err, errorExt);
if (notifyError) {
notifyErrorByHttpCode(isString(notifyError) ? notifyError : err, notifyOptions);
console.warn(err);
}
return [err, initialValue];
})
.finally(() => {
loading && $loading.hide();
});
}
用法
const [err, { data }] = await awaitTo(request.get(``));
cached
函数返回值缓存是优化一个函数的常用手段。(同步, 异步皆可) 我们可以将函数、输入参数、返回值全部保存起来,当下次以同样的参数调用这个函数时,直接使用存储的结果作为返回(不需要重新计算)。
这种方法是有代价的,我们实际是在用内存空间换取运行时间。此方法中使用了 LRUCache(Least Recently Used, 最近最少使用)来优化存储空间
注意: 如果是请求, 请谨慎使用, 如果前端的数据和后端数据不一致,请求还一直拿缓存的值就玩大发了 例如像类目列表, 品牌列表等很久才会更新, 请求频率高且数据量大的才考虑用(ps: 整个协商缓存多好, 还得前端在这想办法)
const generateKey = (arg) => {
if (isString(arg)) return arg;
else if (Array.isArray(arg))
return arg.reduce((acc, cur) => (acc ? `${acc},${generateKey(cur)}` : generateKey(cur)), "");
else if (isRealObject(arg)) {
const keys = Object.keys(arg).sort();
const res = keys.reduce((acc, cur) => {
const value = arg[cur];
return acc ? `${acc},${cur}=${generateKey(value)}` : `${cur}=${generateKey(value)}`;
}, "");
return `{${res}}`;
}
return String(arg);
};
export function cached<T>(func: T, capacity: number = 100): T {
const cache = new LRUCache(capacity);
return async function(...args) {
const key = generateKey(args);
const target = cache.get(key);
if (target) {
if (!target?.then) return target;
const [err, data] = await to(target);
if (!err) return data;
}
const result = func(...args);
cache.put(key, result);
return result;
};
}
用法
const getData = () => request.get(``);
const cacheGetData = cached(getData);
cacheGetData();
//cacheGetData 与 detData 行为一致