Vue 3.0 新特性與使用 三

該文章重點來梳理一些重要但隱晦不經人注意的知識點!vue

watchEffect && watch

watchEffect 的特徵在 watch 保持一致,因此這裏僅僅從 watchEffect 出發點梳理便可node

watchEffect 組件初始化的時候會執行一次,組件卸載的時候會執行一次react

watchEffect 能夠返回一個句柄 stop,再次調用將能夠進行註銷 watchEffectios

const stop = watchEffect(() => {
  /* ... */
})

// later
stop()
複製代碼

有時反作用函數會執行一些異步的反作用,這些響應須要在其失效時清除 (即完成以前狀態已改變了) 。 watchEffect 函數能夠接收一個 onInvalidate 函數做入參,用來註冊清理失效時的回調。當如下狀況發生時,這個失效回調會被觸發:git

  • 反作用即將從新執行時
  • 偵聽器被中止 (若是在 setup() 或生命週期鉤子函數中使用了 watchEffect,則在組件卸載時)
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 必需要在 Promiseresolve 以前被註冊。

相比較於 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'
    ]
})
複製代碼

系列

相關文章
相關標籤/搜索