該文章重點來梳理一些重要但隱晦不經人注意的知識點!vue
watchEffect 的特徵在 watch 保持一致,因此這裏僅僅從 watchEffect 出發點梳理便可node
watchEffect 組件初始化的時候會執行一次,組件卸載的時候會執行一次react
watchEffect 能夠返回一個句柄 stop
,再次調用將能夠進行註銷 watchEffectios
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
複製代碼
有時反作用函數會執行一些異步的反作用,這些響應須要在其失效時清除 (即完成以前狀態已改變了) 。 watchEffect 函數能夠接收一個 onInvalidate 函數做入參,用來註冊清理失效時的回調。當如下狀況發生時,這個失效回調會被觸發:git
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
複製代碼
重點:es6
Vue 爲何採用經過傳入一個函數去註冊反作用清除回調,而不是從回調返回它(react useEffect)?github
Vue 的回答是由於返回值對於異步錯誤處理很重要。express
咱們分別來看看 Vue 和 React 的區別:axios
Vuec#
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 的實行時機:
flush: 'post'
那將會在 onBeforeMount、 onBeforeUpdate以後注意:Vue 的響應性系統會緩存反作用函數,並異步地刷新它們,這樣能夠避免同一個「tick」 中多個狀態改變致使的沒必要要的重複調用。相似於 React 的 setState
只有 reactive
或者 readonly
建立出來的對象使用 isProxy
斷定才爲 true
。
注意:使用原生的 new Proxy
建立出來的對象,斷定爲 false
只有源通過被 reactive
被包裹過的才爲 true
只有源通過被 readonly
被包裹過的才爲 true
默認狀況下,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 只有在組件渲染(rendered)完成後以後纔會進行注入
$ref 不該該在 template 和 computed 中去使用,好比:
// 不容許, 掛載後纔會注入 $ref
<template>
<data :data="$ref.child"></data>
</template>
// 不容許
export default {
computed: {
getChild() {
return this.$ref.child;
}
}
}
複製代碼
escape hatch 應急緊急方案
頂層錯誤捕獲
app.config.errorHandler = (err, vm, info) => {
console.log(err)
}
複製代碼
頂層警告捕獲
app.config.warnHandler = function(msg, vm, trace) {
console.log(msg)
}
複製代碼
全局配置項,相似 Vue2 的 Vue.prototype.$http = $axios;
用法
app.config.globalProperties.foo = 'bar'
複製代碼
這個 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)
}
}
複製代碼
這個 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 函數
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')
複製代碼
app.directive('focus', {
mounted(el) {
el.focus()
}
})
複製代碼
dir
就是:
{
mounted(el) {
el.focus()
}
}
複製代碼
如何製做插件和使用插件?
請看如下案例:
// 自定義 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'
]
})
複製代碼