Vue 3.0 新特性与使用 三

1,541 阅读7分钟

该文章重点来梳理一些重要但隐晦不经人注意的知识点!

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 必须要在 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'
    ]
})

系列