该文章重点来梳理一些重要但隐晦不经人注意的知识点!
watchEffect && watch
watchEffect 的特征在 watch 保持一致,所以这里仅仅从 watchEffect 出发点梳理即可
watchEffect 组件初始化的时候会执行一次,组件卸载的时候会执行一次
watchEffect 可以返回一个句柄 stop
,再次调用将可以进行注销 watchEffect
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。 watchEffect 函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:
- 副作用即将重新执行时
- 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
重点:
Vue 为什么采用通过传入一个函数去注册副作用清除回调,而不是从回调返回它(react useEffect)?
Vue 的回答是因为返回值对于异步错误处理很重要。
我们分别来看看 Vue 和 React 的区别:
Vue
setup() {
const count = ref(0);
function getData() {
return new Promise((resolve, reject) => {
resolve(100);
})
}
const data = ref(null)
watchEffect(async onInvalidate => {
onInvalidate(() => {
console.log('onInvalidate is triggered');
}) // 我们在Promise解析之前注册清除函数
const data = await getData();
})
return {count};
}
React
function App() {
const [count, setCount] = useState(0);
function getData() {
return new Promise((resolve, reject) => {
resolve(100);
})
}
useEffect(()=> {
const getDataAsync = async () => {
const data = await getData();
}
getDataAsync();
return () => {
console.log('onInvalidate is triggered');
}
}, [count]);
return <div></div>
}
通过上面 Vue 和 React 可以知道在清除副作用的写法上的差异,Vue 通过 onInvalidate
来处理,而 React 是通过 return
一个函数来处理。
对于 Vue 来说,Vue 认为处理异步的错误也是很重要的,为什么这么说呢,按照 Vue 的写法,watchEffect
传入了一个 async
异步函数,了解过 ES6 的 async/await
内部实现的原理可以知道,async/await
实际上会隐式的返回一个 Promise
,我们看看文档片段:
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
// spawn 的实现
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
意味着 watchEffect
可以链式处理一些内部 Promise
的机制,比如:await
的返回的 Promise
如果触发了 reject
,Vue 依赖这个返回的 Promise
来自动处理 Promise
链上的潜在错误,这就是为什么 Vue 说返回值对于异步错误处理很重要。
还有一点就是清理函数 onInvalidate
必须要在 Promise
被 resolve
之前被注册。
相比较于 React 的写法,因为 React 清理副作用的方法是采用 return
一个回调出来,按照这种机制,如果我们在 useEffect
函数中传入 async/await
函数,我们根据对 async/await
的原理实现,可以知道隐式返回一个 Promise
回来,这就和 uesEffect
按照返回一个回调来处理清除副作用回调的方式就产生了冲突。并且和 Vue 不同的是 React 的并没有处理 useEffect
中的异步错误,所以在 React 中是不允许在 useEffect
中传入异步回调的。
watchEffect 的实行时机:
- 会在初始运行时同步执行(onBeforeMount之前)
- 更改观察的 state 时,将在组件更新(onBeforeUpdate)前执行副作用
- 如果增加了
flush: 'post'
那将会在 onBeforeMount、 onBeforeUpdate之后
注意:Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。类似于 React 的 setState
isProxy
只有 reactive
或者 readonly
创建出来的对象使用 isProxy
判定才为 true
。
注意:使用原生的 new Proxy
创建出来的对象,判定为 false
isReactive
只有源经过被 reactive
被包裹过的才为 true
isReadonly
只有源经过被 readonly
被包裹过的才为 true
provide/inject
默认情况下,provide 提供的数据不是响应式的,但我们如果需要,可以使用 computed
进行处理后再提供出去。
Vue2:
app.component('todo-list', {
//...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length);
}
}
})
Vue3:
import { provide, readonly, reactive, ref } from 'vue';
setup() {
const location = ref('North Ploe');
const geolocation = reactive({
longitude: 90,
latitude: 135
});
const updateLocation = () => {
location.value = 'South Pole';
}
// 这里最好使用 readonly 包装后在提供出去,防止 child 直接对其修改
provide('location', readonly(location));
provide('geolocation', readonly(geolocation));
provide('updateLocation', updateLocation);
}
$ref
$ref 只有在组件渲染(rendered)完成后之后才会进行注入
$ref 不应该在 template 和 computed 中去使用,比如:
// 不允许, 挂载后才会注入 $ref
<template>
<data :data="$ref.child"></data>
</template>
// 不允许
export default {
computed: {
getChild() {
return this.$ref.child;
}
}
}
escape hatch 应急紧急方案
Application Config
errorHandler
顶层错误捕获
app.config.errorHandler = (err, vm, info) => {
console.log(err)
}
warnHandler
顶层警告捕获
app.config.warnHandler = function(msg, vm, trace) {
console.log(msg)
}
globalProperties
全局配置项,类似 Vue2 的 Vue.prototype.$http = $axios;
用法
app.config.globalProperties.foo = 'bar'
isCustomElement
这个 Api 的作用在于能够把第三方或者自定义而没有在 Vue 中注册标签使用时,忽略警告。
<template>
<haha-Hello>123</haha-Hello>
</template>
export default {
name: 'hello'
}
正常情况下是会报警告的,但这个 Api 就能配置忽略这个警告,标识这是我自定义的组件。
用法:
app.config.isCustomElement = tag => tag.startsWith('haha-')
注意:目前这个 Api 是有问题的,请看 girhub issues
这里提供了一些解决方案,Vue 作者尤雨溪也说明了,这个 Api 目前有点问题:
As pointed out, Vue 3 requires configuring custom elements via compiler options if pre-compiling templates.
如前所述,如果是预编译模板,则Vue 3需要通过编译器选项配置自定义元素。
This seems to be now a Vue CLI specific configuration problem so I'm closing it. But feel free to continue the discussion.
现在这似乎是Vue CLI特定的配置问题,因此我将其关闭。但是请随时继续讨论。
从中提到了,预编译模板(template)使用自定义标签,需要通过编译器选项配置自定义元素,从 girhub issues 中可以看到一个答案,在 vite 上的解决方案:
vite.config.js:
vueCompilerOptions: {
isCustomElement: tag => {
return /^x-/.test(tag)
}
}
具体可以看 Vite 的 Api:github vite Api 中的 config 在配置项:config.ts 就可以找到 Vue 编译选项配置字段:vueCompilerOptions
这样配置后就可以忽略上诉例子的警告了:
vueCompilerOptions: {
isCustomElement: tag => {
return /^haha-/.test(tag)
}
}
optionMergeStrategies
这个 Api 是只针对于 options Api 的,作用是对 mixin
的合并更改策略。
const app = Vue.createApp({
custom: 'hello!'
})
app.config.optionMergeStrategies.custom = (parent, child) => {
console.log(child, parent)
// => "goodbye!", undefined
// => "hello", "goodbye!"
return child || parent
}
app.mixin({
custom: 'goodbye!',
created() {
console.log(this.$options.custom) // => "hello!"
}
})
这里可以看到,在 created
输出的时候,输出的是 hello,就是因为设置了合并策略,当组件和 mixin
存在相同属性的时候,会使用 child
的值,当不存在自定义属性重复的时候,当前组件输出的就是 child
因为这时候 parent
为 undefined
www.zhihu.com/question/40… 什么时候执行 render 函数
Directive
Vue2:
<div id="hook-arguments-example" v-demo:[foo].a.b="message"></div>
Vue.directive('demo', {
bind: function (el, binding, vnode) {
var s = JSON.stringify
el.innerHTML =
'name: ' + s(binding.name) + '<br>' +
'value: ' + s(binding.value) + '<br>' +
'expression: ' + s(binding.expression) + '<br>' +
'argument: ' + s(binding.arg) + '<br>' +
'modifiers: ' + s(binding.modifiers) + '<br>' +
'vnode keys: ' + Object.keys(vnode).join(', ')
}
})
new Vue({
el: '#hook-arguments-example',
data: {
foo: 'HaHa'
message: { color: 'white', text: 'hello!' }
}
})
/*
* name: "demo"
* value: { color: 'white', text: 'hello!' }
* expression: "message"
* argument: "HaHa"
* modifiers: {a: true, b: true}
* name: "tag, data, children, text, elm, ns, context, fnContext, fnOptions, fnScopeId, key, componentOptions, componentInstance, parent, raw, isStatic, isRootInsert, isComment, isCloned, isOnce, asyncFactory, asyncMeta, isAsyncPlaceholder"
**/
Vue3:
Vue3.x 和 Vue2.x 的指令在生命周期上有这明显差别,但使用是差不多的
import { createApp } from 'vue'
const app = createApp({})
// register
app.directive('my-directive', {
// called before bound element's attributes or event listeners are applied
created() {},
// called before bound element's parent component is mounted
beforeMount() {},
// called when bound element's parent component is mounted
mounted() {},
// called before the containing component's VNode is updated
beforeUpdate() {},
// called after the containing component's VNode and the VNodes of its children // have updated
updated() {},
// called before the bound element's parent component is unmounted
beforeUnmount() {},
// called when the bound element's parent component is unmounted
unmounted() {}
})
// register (function directive)
app.directive('my-directive', () => {
// this will be called as `mounted` and `updated`
})
// getter, return the directive definition if registered
const myDirective = app.directive('my-directive')
- instance: 使用指令的组件实例。
- value: 传递给指令的值。例如,在v-my-directive =“ 1 + 1”中,该值为2。
- oldValue: 旧的值,仅在 beforeUpdate 和更新时可用。值是否已更改都可用。
- arg: 参数传递给指令(如果有)。例如,在 v-my-directive:foo 中,arg 为“ foo”。
- modifiers: 包含修饰符(如果有)的对象。例如,在v-my-directive.foo.bar 中,修饰符对象为 {foo:true,bar:true}。
- dir: 一个对象,在注册指令时作为参数传递。例如,在指令中
app.directive('focus', {
mounted(el) {
el.focus()
}
})
dir
就是:
{
mounted(el) {
el.focus()
}
}
use 和 plug
如何制作插件和使用插件?
请看以下案例:
// 自定义 plug 插件
// myUi.js
import MyButton from './MyButton.vue';
import MyInput from './MyInput.vue';
const componentPool = [
MyButton,
MyInput
];
export default {
install () {
if (options.components) {
option.components.map((compName) => {
componentPool.map((comp) => {
if (compName === comp.name) {
app.component(comp.name, comp);
}
})
})
} else {
componentPool.map(comp => {
app.component(comp.name, comp);
})
}
}
}
myUi 该插件,简单的实现了一下按需加载 UI 的方案
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import MyUI from './libs/MyUI';
const app = createApp(App);
app.use(MyUI, {
components: [
'MyButton',
'MyInput'
]
})