怎么用 Vue Composition 造轮子
最近项目临近尾声,终于有时间来对这段工作总结。其实之前使用的一直是 Vue 但是现在公司的主要业务使用是 React 为此还特意看了许多文章,加上实际上这两个框架有很多类似的地方,所以就有了这篇文章。
因为主要是分享经验所以下面的示例主要作为抛砖引玉的作用,在正式分享之前先科普两个小知识
- hooks 默认以 use 为前缀,这个是 React 的官方推荐做法,约束大于配置也方便 eslint 等解析工具识别,这里也遵循这个做法;
- watchEffect 与 useEffect 最大的区别就是,watchEffect 可以自动检测依赖,而 useEffect 需要你显示指定;
const count = ref(0);
const onClick = () => {
count.value++;
};
watchEffect(() => {
console.log(`被点击`);
});
例如上面这段代码,当 onClick
被触发的时候 watchEffect 不会重复执行,因为它内部有一个收集相关依赖的过程,只有依赖变化才会重新执行,这一点很重要,请记住。
示例
根据常见场景划分了三个类型,如果你有更好的例子欢迎补充
DOM
修改页面 title
import { ref, watchEffect } from 'vue';
const useTitle = (title) => {
const str = ref(title);
watchEffect(() => {
document.title = str.value;
});
};
调用useTitle
即可更新标题
监听页面大小变化
import { watchEffect, reactive, toRefs } from 'vue';
const useResize = () => {
const size = reactive({
width: window.innerWidth,
height: window.innerHeight,
});
const onChange = () => {
Object.assign(size, {
width: window.innerWidth,
height: window.innerHeight,
});
};
watchEffect((onInvalidate) => {
window.addEventListener('resize', onChange);
onInvalidate(() => {
window.removeEventListener('resize', onChange);
});
});
return toRefs(size);
};
这里调用 toRefs
的原因是为了解构的情况也可以使用,例如
const { width } = useResize();
监听网络是否断开
import { ref, watchEffect } from 'vue';
const useLineState = () => {
const line = ref(window.navigator.onLine);
const onLine = () => {
line.value = true;
};
const onOffline = () => {
line.value = false;
};
watchEffect((onInvalidate) => {
window.addEventListener('online', onLine);
window.addEventListener('offline', onOffline);
onInvalidate(() => {
window.removeEventListener('online', onLine);
window.removeEventListener('offline', onOffline);
});
});
return line;
};
跟上面的 useResize
类似,监听相关事件来决定 state
的相关状态
监听 dom 元素变化
import { watchEffect, reactive, toRefs } from 'vue';
const isObject = (obj) => typeof obj === 'object' && obj;
const isElement = (obj) => isObject(obj) && obj.nodeType === Node.ELEMENT_NODE;
const useResizeObserver = (dom) => {
if (!isElement(dom)) {
throw new Error(`DOM is not an element!`);
}
const size = reactive(dom.getBoundingClientRect());
watchEffect((onInvalidate) => {
const resizeObserver = new ResizeObserver(() => {
Object.assign(size, dom.getBoundingClientRect());
});
onInvalidate(() => {
resizeObserver.disconnect();
});
});
return toRefs(size);
};
这里监听元素使用了还在实验阶段的 ResizeObserver
,为了兼容性请使用 polyfill
封装请求
ajax 的请求很常见,不过在 vue2.x 中我们很容易写出下面的代码
mounted(() => {
fn().then().catch().finally();
});
这样的代码最大的问题就是不够清晰,因为变量在 data
中定义,而如果切换成下面这种形式,看起来就会直观许多。
更多的例子还可以结合 form
和 table
进行更加深度的 hooks 封装
const { data, loading, error } = useRequest(() => {
//...
});
if (loading) {
//...
}
if (error) {
// ...
}
import { watchEffect, reactive, toRefs } from 'vue';
const useRequest = (fn, { manual } = {}) => {
const obj = reactive({
loading: false,
data: undefined,
error: undefined,
});
const run = () => {
obj.loading = true;
Promise.resolve(fn())
.then((data) => {
obj.data = data;
})
.catch((err) => {
obj.error = err;
})
.finally(() => {
obj.loading = false;
});
};
watchEffect(() => {
if (!manual) {
run();
}
});
return {
...toRefs(obj),
run,
};
};
模拟生命周期
这里 Vue 官方的自带生命周期已经很齐全了,不过有的生命周期还是可以通过 watchEffect
做到模拟。
例如:mounted
和 beforeUnmount
,这里实现的原理主要就是利用 watchEffect
自动检测依赖,那如果不对响应式变量做收集的相关操作其实就是一个 mounted
import { watchEffect, nextTick } from 'vue';
const useMounted = (fn) => {
watchEffect(() => {
if (typeof fn !== 'function') {
return;
}
nextTick().then(() => {
fn();
});
});
};
const useBeforeUnmount = (fn) => {
watchEffect((onInvalidate) => {
onInvalidate(() => {
if (typeof fn !== 'function') {
return;
}
fn();
});
});
};
封装持久化
在一些登录页面中很容易看到记住相关账号和密码,传统的做法就是 login
之后存储相关的账号和密码,然后在页面渲染的时候查看相关 localStorage
有没有对应的数据。
这样写做大的问题还是分离,以及书写的代码量很多,下面就介绍一下如何结合 localStorage
做到一个支持对象的 hooks
import { ref, watchEffect } from 'vue';
const isObject = (obj) => typeof obj === 'object' && obj;
const useLocalStorageState = (key, defaultValue) => {
const value = ref(undefined);
try {
value.value = window.localStorage.getItem(key);
if (value.value === undefined) {
value.value = defaultValue;
}
value.value = JSON.parse(value.value);
} catch {}
watchEffect(() => {
const v = value.value;
window.localStorage.setItem(key, isObject(v) ? JSON.stringify(v) : v);
});
return value;
};
const name = useLocalStorageState('name', 'admin');
最后
以上代码全部上传到了codesandbox,如果对你有帮助可以star
一下