关注 前端瓶子君,回复“交流”

加入我们一起学习,天天进步

看到掘金的这篇很全,就整理到自己的公众号上来,自己可以随时复习。原文链接如下:

作者: 三藏会法术     

链接:https://juejin.cn/post/6858558735695937544#heading-153

Vue2与Vue3的全局配置API变化区别

createApp

Vue2.x创建实例并且挂载DOM上

import Vue from "vue";
import App from './App.vue'

new Vue({
  render: (h) => h(App)
}).$mount("#app");

Vue3新增api===>createApp创建实例

createApp 会产生一个 app 实例,该实例拥有全局的可配置上下文

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

createApp 做了什么

  • ensureRenderer方法追溯过去底添加patchclass+patchStyle等跟操作DOM相关的方法

    • ensureRenderer(创建虚拟DOM)一直追溯到createRenderer以及baseCreateRenderer,baseCreateRenderer方法涉及了虚拟DOM的创建更新DIFF算法

  • 之后就是检查时候又mount是否挂载在DOM上

  • app对象上的方法:config、use、mixin、component、directive、mount、unmount、provide/inject

component

Vue2.x【注册或获取全局组件。注册还会自动使用给定的 id 设置组件的名称】

// 注册组件,传入一个选项对象 (自动调用 Vue.extend) 

Vue.component('my-component', { /* ... */ }) 

// 获取注册的组件 (始终返回构造器) 
var MyComponent = Vue.component('my-component')

Vue3【注册或获取全局组件. 注册还会自动使用给定的 name组件 设置组件的名称】

基本vue2写法一致

import { createApp } from 'vue'

const app = createApp({})

// 注册组件,传入一个选项对象
app.component('my-component', {
  /* ... */
})

// 获取注册的组件 (始终返回构造器) 
const MyComponent = app.component('my-component', {})

config【app=createApp(App)】

devtools

配置是否允许 vue-devtools 检查代码。开发版本默认为 true,生产版本默认为 false。生产版本设为 true 可以启用检查。

- Vue.config.devtools = true
+ app.config.devtools = true    

errorHandler

- Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  // 只在 2.2.0+ 可用
}
+ app.config.errorHandler = (err, vm, info) => {
  // handle error
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  // 这里能发现错误
}

指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例。

错误追踪服务 Sentry 和 Bugsnag 都通过此选项提供了官方支持。

warnHandler

- Vue.config.warnHandler = function (msg, vm, trace) {
  // `trace` 是组件的继承关系追踪
}
+ app.config.warnHandler = function(msg, vm, trace) {
  // `trace` 是组件的继承关系追踪
}

为 Vue 的运行时警告赋予一个自定义处理函数。注意这只会在开发者环境下生效,在生产环境下它会被忽略。

globalProperties 【新增属性】

app.config.globalProperties.foo = 'bar'

app.component('child-component', {
  mounted() {
    console.log(this.foo) // 'bar'
  }
})

添加可在程序内的任何组件实例中访问的全局属性。当存在键冲突时,组件属性将优先替代掉Vue2.x的 Vue.prototype属性放到原型上的写法

isCustomElement 【新增属性】

替代掉Vue2.x的ignoredElements

- Vue.config.ignoredElements = [
  // 用一个 `RegExp` 忽略所有“ion-”开头的元素
  // 仅在 2.5+ 支持
  /^ion-/
]

// 一些组件以'ion-'开头将会被解析为自定义组件
+ app.config.isCustomElement = tag => tag.startsWith('ion-')

指定一个方法来识别在Vue之外定义的自定义组件(例如,使用Web Component API)。如果组件符合这个条件,它就不需要本地或全局注册,Vue也不会抛出关于Unknown custom element的警告

注意,这个函数中不需要匹配所有原生HTML和SVG标记—Vue解析器会自动执行此检查

optionMergeStrategies

const app = Vue.createApp({
  mounted() {
    console.log(this.$options.hello)
  }
})

app.config.optionMergeStrategies.hello = (parent, child, vm) => {
  return `Hello, ${child}`
}

app.mixin({
  hello: 'Vue'
})

// 'Hello, Vue

定义自定义选项的合并策略。合并策略接收在父实例options和子实例options和∗∗子实例∗∗options,分别作为第一个和第二个参数。上下文Vue实例作为第三个参数传递

【自定义选项合并策略】mixin

const app = Vue.createApp({
  custom: 'hello!'
})

app.config.optionMergeStrategies.custom = (toVal, fromVal) => {
  console.log(fromVal, toVal)
  // => "goodbye!", undefined
  // => "hello!", "goodbye!"
  return fromVal || toVal
}

app.mixin({
  custom: 'goodbye!',
  created() {
    console.log(this.$options.custom) // => "hello!"
  }
})
  • optionMergeStrategies先获取到子实例的$options的mixin而没有父实例【custom第一次改变从undefined到goodbye--->打印"goodbye!", undefined】

  • 父实例的options替换掉子实例的options替换掉子实例的options【custom第二次从goodbye到hello!--->打印了"hello", "goodbye!"】

  • 最后在打印app.config.optionMergeStrategies.custom返回的父实例的$options

无论如何this.options.custom最后会返回合并策略的return的值【使用场景利用父子组件的options.custom最后会返回合并策略的return的值【使用场景利用父子组件的options.custom最后会返回合并策略的return的值【使用场景利用父子组件的options,然后返回计算等操作得到所需要的值】optionMergeStrategies合并$options变化

performance

- Vue.config.performance=true;
+ app.config.performance=true;

设置为 true 以在浏览器开发工具的性能/时间线面板中启用对组件初始化、编译、渲染和打补丁的性能追踪。只适用于开发模式和支持 performance.mark API 的浏览器上。

directive

注册或获取全局指令。

import { createApp } from 'vue'
const app = createApp({})

// 注册
app.directive('my-directive', {
  // 指令的生命周期
  // 在绑定元素的父组件被挂载之前调用
  beforeMount(el, binding, vnode) {},
  // 在挂载绑定元素的父组件时调用
  mounted(el, binding, vnode) {},
  // 在更新包含组件的VNode之前调用
  beforeUpdate(el, binding, vnode, prevNode) {},
  // 组件的VNode及其子组件的VNode更新之后调用
  updated(el, binding, vnode, prevNode) {},
  // 在卸载绑定元素的父组件之前调用
  beforeUnmount(el, binding, vnode) {},
  // 在卸载绑定元素的父组件时调用
  unmounted(el, binding, vnode) {}
})

// 注册 (指令函数)
app.directive('my-directive', (el, binding, vnode, prevNode) => {
  // 这里将会被 `mounted` 和 `updated` 调用
})

// getter,返回已注册的指令
const myDirective = app.directive('my-directive')
  • el

指令绑定到的元素。这可以用来直接操作DOM。

  • binding【包含下列属性的对象】

    • instance:使用指令的组件的实例

    • value:指令的绑定值,例如:v-my-directive="1 + 1"中,绑定值为 2

    • oldValue:指令绑定的前一个值,仅在 beforeUpdate 和 updated 钩子中可用。无论值是否改变都可用

    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"

    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }

    • dir:一个对象,在注册指令时作为参数传递; 举个例子,看下面指令

app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

dir就是下面的对象

{
  mounted(el) {
    el.focus()
  }
}
  • vnode

编译生成的虚拟节点

  • prevNode

前一个虚拟节点,仅在beforeUpdate和updated钩子中可用

tips:除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行

mixin【基本Vue2.x一致】

全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。

mount【类似Vue2.x】

在所提供的DOM元素上挂载应用程序实例的根组件

import { createApp } from 'vue'

const app = createApp({})
// 做一些准备
app.mount('#my-app')

provide/inject【Vue2.x一致】

该选项与inject一起使用,允许一个祖先组件作为其所有后代的依赖注入器,无论组件层次结构有多深,只要它们位于同一父链中就可以。

provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的环境下可工作。

如果在组件中两者都只能在当前活动组件实例的 setup() 中调用,详细请看依赖注入部分

import { createApp } from 'vue'

const app = createApp({
  provide: {
    user: 'John Doe'
  }
})

app.component('user-card', {
  inject: ['user'],
  template: `
    <div>
      {{ user }}
    </div>
  `
})

unmount【新增属性】

在所提供的DOM元素上卸载应用程序实例的根组件

import { createApp } from 'vue'

const app = createApp({})
// 做一些必要的准备
app.mount('#my-app')

// 应用程序将在挂载后5秒被卸载
setTimeout(() => app.unmount('#my-app'), 5000)

use【Vue2.x一致】

安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。

当 install 方法被同一个插件多次调用,插件将只会被安装一次。

setup

setup 函数是一个新的组件选项。作为在组件内使用 Composition API 的入口点, 注意 setup 返回的 ref 在模板中会自动解开,不需要写 .value【setup 内部需要.value】

调用时机

  • 创建组件实例,然后初始化 props ,紧接着就调用setup 函数。从生命周期钩子的视角来看,它会在 beforeCreate 钩子之前被调用

  • 如果 setup 返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文

参数

props 作为其第一个参数

注意 props 对象是响应式的,watchEffect 或 watch 会观察和响应 props 的更新不要解构 props 对象,那样会使其失去响应性。

export default {
  props: {
    name: String,
  },
  setup(props) {
    console.log(props.name)
     watchEffect(() => {
      console.log(`name is: ` + props.name)
    })
  },
}

第二个参数提供了一个上下文对象【从原来 2.x 中 this 选择性地暴露了一些 property(attrs/emit/slots)】

attrs 和 slots 都是内部组件实例上对应项的代理,可以确保在更新后仍然是最新值。所以可以解构,无需担心后面访问到过期的值

为什么props作为第一个参数?

  • 组件使用 props 的场景更多,有时候甚至只使用 props

  • 将 props 独立出来作为第一个参数,可以让 TypeScript 对 props 单独做类型推导,不会和上下文中的其他属性相混淆。这也使得 setup 、 render 和其他使用了 TSX 的函数式组件的签名保持一致

this 在 setup() 中不可用。由于 setup() 在解析 2.x 选项前被调用,setup() 中的 this 将与 2.x 选项中的 this 完全不同。同时在 setup() 和 2.x 选项中使用 this 时将造成混乱

setup(props, { attrs }) {
  // 一个可能之后回调用的签名
  function onClick() {
    console.log(attrs.foo) // 一定是最新的引用,没有丢失响应性
  }
}

响应式系统 API

reactive

接收一个普通对象然后返回该普通对象的响应式代理【等同于 2.x 的 Vue.observable()】

Proxy对象是目标对象的一个代理器,任何对目标对象的操作(实例化,添加/删除/修改属性等等),都必须通过该代理器。因此我们可以把来自外界的所有操作进行拦截和过滤或者修改等操作

响应式转换是“深层的”:会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象

reactive 类的 api 主要提供了将复杂类型的数据处理成响应式数据的能力,其实这个复杂类型是要在object array map set weakmap weakset 这五种之中【如下源码,他会判断是否是五类以及是否被冻结】

因为是组合函数【对象】,所以必须始终保持对这个所返回对象的引用以保持响应性【不能解构该对象或者展开】例如 const { x, y } = useMousePosition()或者return { ...useMousePosition() }

function useMousePosition() {
    const pos = reactive({
        x: 0,
        y: 0,
      })
    return pos
}

toRefs API 用来提供解决此约束的办法——它将响应式对象的每个 property 都转成了相应的 ref【把对象转成了ref】。

 function useMousePosition() {
    const pos = reactive({
        x: 0,
        y: 0,
      })
    return toRefs(pos)
}

// x & y 现在是 ref 形式了!
const { x, y } = useMousePosition()

ref

接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value

const count = ref(0)
console.log(count.value) // 0

如果传入 ref 的是一个对象,将调用 reactive 方法进行深层响应转换

陷阱

setup 中return返回会自动解套【在模板中不需要.value】

ref 作为 reactive 对象的 property 被访问或修改时,也将自动解套 .value

const count = ref(0)
/*当做reactive的对象属性----解套*/
const state = reactive({
  count,
})
/* 不需要.value*/
console.log(state.count) // 0

/*修改reactive的值*/
state.count = 1
/*修改了ref的值*/
console.log(count.value) // 1

注意如果将一个新的 ref 分配给现有的 ref, 将替换旧的 ref

/*创建一个新的ref*/
const otherCount = ref(2)

/*赋值给reactive的旧的ref,旧的会被替换掉*/
state.count = otherCount
/*修改reactive会修改otherCount*/
console.log(state.count) // 2
/*修改reactive会count没有被修改 */
console.log(count.value) // 1

嵌套在 reactive Object 中时,ref 才会解套。从 Array 或者 Map 等原生集合类中访问 ref 时,不会自动解套【自由数据类型是Object才会解套,array  map  set   weakmap  weakset集合类 访问 ref 时,不会自动解套】

const arr = reactive([ref(0)])
// 这里需要 .value
console.log(arr[0].value)

const map = reactive(new Map([['foo', ref(0)]]))
// 这里需要 .value
console.log(map.get('foo').value)

心智负担上 ref vs reactive

  • 在普通 JavaScript 中区别声明基础类型变量与对象变量时一样区别使用 ref 和 reactive

  • 所有的地方都用 reactive,然后记得在组合函数返回响应式对象时使用 toRefs。这降低了一些关于 ref 的心智负担

readonly

传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理。一个只读的代理是“深层的”,对象内部任何嵌套的属性也都是只读的【返回一个永远不会变的只读代理】【场景可以参数比对等】

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 依赖追踪
  console.log(copy.count)
})

// original 上的修改会触发 copy 上的侦听
original.count++

// 无法修改 copy 并会被警告
copy.count++ // warning!

reactive响应式系统工具集

isProxy

检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

isReactive

检查一个对象是否是由 reactive 创建的响应式代理

import { reactive, isReactive } from 'vue'
const state = reactive({
      name: 'John'
    })
console.log(isReactive(state)) // -> true

如果这个代理是由 readonly 创建的,但是又被 reactive 创建的另一个代理包裹了一层,那么同样也会返回 true

import { reactive, isReactive, readonly } from 'vue'
const state = reactive({
      name: 'John'
    })
// 用readonly创建一个只读响应式对象plain
const plain = readonly({
    name: 'Mary'
})
//readonly创建的,所以isReactive为false
console.log(isReactive(plain)) // -> false  

// reactive创建的响应式代理对象包裹一层readonly,isReactive也是true,isReadonly也是true
const stateCopy = readonly(state)
console.log(isReactive(stateCopy)) // -> true

isReadonly

检查一个对象是否是由 readonly 创建的只读代理

reactive高级响应式系统API

toRaw

返回由 reactive 或 readonly 方法转换成响应式代理的普通对象。这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发更改。不建议一直持有原始对象的引用【不建议赋值给任何变量】。请谨慎使用

toRaw之后的对象是没有被代理/跟踪的的普通对象

const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true
console.log(toRaw(reactiveFoo) !== reactiveFoo) // true

markRaw

显式标记一个对象为“永远不会转为响应式代理”,函数返回这个对象本身。被 markRaw 标记了,即使在响应式对象中作属性,也依然不是响应式的

const foo = markRaw({
    name: 'Mary'
})
console.log(isReactive(reactive(foo))) // false

markRaw 注意点

  • markRaw和 shallowXXX 一族的 API允许选择性的覆盖reactive或者readonly 默认创建的 "深层的" 特性【响应式】/或者使用无代理的普通对象

  • 设计这种「浅层读取」有很多原因

    • 一些值的实际上的用法非常简单,并没有必要转为响应式【例如三方库的实例/省市区json/Vue组件对象】

    • 当渲染一个元素数量庞大,但是数据是不可变的,跳过 Proxy 的转换可以带来性能提升

  • 这些 API 被认为是高级的,是因为这种特性仅停留在根级别,所以如果你将一个嵌套的,没有 markRaw 的对象设置为 reactive 对象的属性,在重新访问时,你又会得到一个 Proxy 的版本,在使用中最终会导致标识混淆的严重问题:执行某个操作同时依赖于某个对象的原始版本和代理版本(标识混淆在一般使用当中应该是非常罕见的,但是要想完全避免这样的问题,必须要对整个响应式系统的工作原理有一个相当清晰的认知)。

const foo = markRaw({
  nested: {},
})

const bar = reactive({
  // 尽管 `foo` 己经被标记为 raw 了, 但 foo.nested 并没有
  nested: foo.nested,
})

console.log(foo.nested === bar.nested) // false
  • foo.nested没有被标记为(永远不会转为响应式代理),导致最后的值一个reactive

shallowReactive

只为某个对象的私有(第一层)属性创建浅层的响应式代理,不会对“属性的属性”做深层次、递归地响应式代理,而只是保留原样【第一层是响应式代理,深层次只保留原样(不具备响应式代理)】

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2,
  },
})

// 变更 state 的自有属性是响应式的【第一层次响应式】
state.foo++
// ...但不会深层代理【深层次不是响应式】(渲染性能)
isReactive(state.nested) // false
state.nested.bar++ // 非响应式

shallowReadonly

类似于shallowReactive,区别是:

  • 第一层将会是响应式代理【第一层修改属性会失败】,属性为响应式

  • 深层次的对象属性可以修改,属性不是响应式

const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2,
  },
})

// 变更 state 的自有属性会失败
state.foo++
// ...但是嵌套的对象是可以变更的
isReadonly(state.nested) // false
state.nested.bar++ // 嵌套属性依然可修改

ref 响应式系统工具集

customRef

用于自定义一个 ref,可以显式地控制依赖追踪和触发响应,接受一个工厂函数,两个参数分别是用于追踪的 track 与用于触发响应的 trigger,并返回一个一个带有 get 和 set 属性的对象【实际上就是手动 track追踪 和 trigger触发响应】

  • 以下代码可以使得v-model防抖

function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
          /*初始化手动追踪依赖讲究什么时候去触发依赖收集*/
        track()
        return value
      },
      set(newValue) {
          /*修改数据的时候会把上一次的定时器清除【防抖】*/
        clearTimeout(timeout)
        timeout = setTimeout(() => {
            /*把新设置的数据给到ref数据源*/
          value = newValue
            /*再有依赖追踪的前提下触发响应式*/
          trigger()
        }, delay)
      },
    }
  })
}

setup() {
    return {
        /*暴露返回的数据加防抖*/
      text: useDebouncedRef('hello'),
    }
  }

shallowRef

创建一个 ref ,将会追踪它的 .value 更改操作,但是并不会对变更后的 .value 做响应式代理转换(即变更不会调用 reactive)

前面我们说过如果传入 ref 的是一个对象,将调用 reactive 方法进行深层响应转换,通过shallowRef创建的ref,将不会调用reactive【对象不会是响应式的】

const refOne = shallowRef({});
refOne.value = { id: 1 };
refOne.id == 20;
console.log(isReactive(refOne.value),refOne.value);//false  { id: 1 }

triggerRef 【与shallowRef配合】

手动执行与shallowRef相关的任何效果

const shallow = shallowRef({
  greet: 'Hello, world'
})

// 第一次运行打印 "Hello, world" 
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 这不会触发效果,因为ref是shallow
shallow.value.greet = 'Hello, universe'

// 打印 "Hello, universe"
triggerRef(shallow)

Computed and watch【监控变化】

computed

  • 传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象【默认传入的是get函数的对象】

  • 传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态

const count = ref(1)
/*不支持修改【只读的】 */
const plusOne = computed(() => count.value + 1)
plusOne.value++ // 错误!

/*【可更改的】 */
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  },
})

plusOne.value = 1
console.log(count.value) // 0

watchEffect

立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数

computed与watchEffect区别:

  • computed 计算属性可通过setup return,再模板中使用,watchEffect不能;

  • computed可以使用多个,并且对多个属性进行不同的响应计算,watchEffect会存在副作用

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> 打印出 0

setTimeout(() => {
  count.value++
  // -> 打印出 1
}, 100)

当在组件的setup()函数或生命周期钩子期间调用watchEffect时,监视程序会链接到组件的生命周期,并在卸载组件时自动停止,一般情况下watchEffect返回可以stop 操作,停止监听程序

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

// 停止监听程序
stop()

副作用(函数式编程)

一个带有副作用的函数不仅只是简单的返回一个值,还干了一些其他的事情,比如:

  • 修改一个变量

  • 直接修改数据结构

  • 设置一个对象的成员

  • 抛出一个异常或以一个错误终止

  • 打印到终端或读取用户的输入

  • 读取或写入一个文件

  • 在屏幕上绘画

如果一个函数内外有依赖于外部变量或者环境时,常常我们称之为其有副作用,如果我们仅通过函数签名不打开内部代码检查并不能知道该函数在干什么,作为一个独立函数我们期望有明确的输入和输出,副作用是bug的发源地,作为程序员开发者应尽量少的开发有副作用的函数或方法,副作用也使得方法通用性下降不适合扩展和可重用性

清除副作用

在一些时候监听函数将执行异步副作用【一个响应式依赖被修改了,会做其他事情】,这些响应需要在其失效时清除(例如在效果完成前状态改变)。effect函数接收一个onInvalidate 函数作入参, 用来注册清理失效时的回调。这个 invalidation函数 在什么时候会被调用:

  • 监听函数重新被执行的时候【响应式依赖的数据被修改】

  • 监听停止的时候(如果watchEffect在setup()或者生命周期函数中被使用的时候组件会被卸载)【停止观察】

watchEffect(onInvalidate => {
  /*这是个异步操作*/
  const token = performAsyncOperation(id.value)//id依赖
  onInvalidate(() => {
    // id被修改了或者监听停止了会触发token.cancel()事件【这块区域的代码】.
    // 这里是异步事件的话,前面的peding的异步操作无效【这里的异步事件只执行一次】
     token.cancel()/*异步操作*/
    console.log('onInvalidate')
  })
})
  
  从上面看:我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它(如 React `useEffect` 中的方式),是因为返回值对于异步错误处理很重要
  
  ````js
  const data = ref(null)
  watchEffect(async onInvalidate => {
    onInvalidate(() => {...}) // 我们在Promise的resolves之前注册清理函数(cleanup function)
    data.value = await fetchData(props.id)
  })

我们知道异步函数都会隐式地返回一个 Promise,但是清理副作用的函数必须要在 Promise 被 resolve 之前被注册。另外,Vue 依赖这个返回的 Promise 来自动处理 Promise 链上的潜在错误

副作用刷新时机

Vue 的响应式系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个 tick 中多个状态改变导致的不必要的重复调用。在核心的具体实现中, 组件的更新函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时, 会在所有的组件更新后执行

<template>
  <div>{{ count }}</div>
</template>

<script>
  export default {
    setup() {
      const count = ref(0)

      watchEffect(() => {
        console.log(count.value)
      })

      return {
        count,
      }
    },
  }
</script>
  • count 会在初始运行时同步打印出来

  • 更改 count 时,将在组件更新后执行副作用

初始化运行是在组件 mounted 之前执行的【你希望在编写副作用函数时访问 DOM(或模板 ref),请在 onMounted 钩子中进行】

onMounted(() => {
  watchEffect(() => {
    // 在这里可以访问到 DOM 或者 template refs
  })
})

如果副作用需要同步或在组件更新之前重新运行,我们可以传递一个拥有 flush 属性的对象作为选项(默认为 'post')

// 同步运行
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'sync',
  }
)

// 组件更新前执行
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'pre',
  }
)

侦听器调试【响应式调试用的】

onTrack 和 onTrigger 选项可用于调试一个侦听器的行为。

  • 当一个 reactive 对象属性或一个 ref 作为依赖被追踪时,将调用 onTrack【调用次数为被追踪的数量】

  • 依赖项变更会导致重新追踪依赖,从而onTrack被调用【调用次数为被追踪的数量】

  • 的数量】 依赖项变更会导致重新追踪依赖,从而onTrack被调用【调用次数为被追踪的数量】 依赖项变更导致副作用被触发时,将调用 onTrigger

这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。建议在以下回调中编写 debugger 语句来检查依赖关系:【onTrack 和 onTrigger 仅在开发模式下生效】

watchEffect(
  () => {
    /* 副作用的内容 */
  },
  {
    onTrigger(e) {
      /*副作用依赖修改*/
      debugger
    },
    onTrack(e) {
      /*副作用依赖修改*/
      debugger
    },
  }
)

watch

watch API 完全等效于 2.x watch 中相应的选项。watch 需要侦听特定的数据源,并在回调函数中执行副作用【默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调】

watch允许我们:

  • 懒执行副作用

  • 更明确哪些状态的改变会触发侦听器重新运行副作用

  • 访问侦听状态变化前后的值

侦听单个数据源

侦听器的数据源可以是一个拥有返回值的 getter 函数,也可以是 ref:

// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

侦听多个数据源

watcher 也可以使用数组来同时侦听多个源

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

与 watchEffect 共享的行为

watch 和 watchEffect 在停止侦听, 清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入),副作用刷新时机 和 侦听器调试 等方面行为一致:

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar],onInvalidate) => {
  /* ... */
  onInvalidate(() => {...})
},
  {
    onTrigger(e) {
      /*副作用依赖修改*/
      debugger
    },
    onTrack(e) {
      /*副作用依赖修改*/
      debugger
    },
  })

生命周期钩子函数

与 2.x 版本生命周期相对应的组合式 API

  • beforeCreate -> 使用 setup()

  • created -> 使用 setup()

  • beforeMount -> onBeforeMount

  • mounted -> onMounted

  • beforeUpdate -> onBeforeUpdate

  • updated -> onUpdated

  • beforeDestroy -> onBeforeUnmount

  • destroyed -> onUnmounted

  • errorCaptured -> onErrorCaptured

import { onMounted, onUpdated, onUnmounted } from 'vue'
setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    onUnmounted(() => {
      console.log('unmounted!')
    })
  }

这些生命周期钩子注册函数只能在 setup() 期间同步使用, 因为它们依赖于内部的全局状态来定位当前组件实例(正在调用 setup() 的组件实例), 不在当前组件下调用这些函数会抛出一个错误。

组件实例上下文也是在生命周期钩子同步执行期间设置的,因此,在卸载组件时,在生命周期钩子内部同步创建的侦听器和计算状态也将自动删除。

新增的钩子函数

除了和 2.x 生命周期等效项之外,组合式 API 还提供了以下调试钩子函数:

  • onRenderTracked

  • onRenderTriggered

两个钩子函数都接收一个 DebuggerEvent,与 watchEffect 参数选项中的 onTrack 和 onTrigger 类似:

export default {
  onRenderTracked(e){
      debugger
    // 检查有响应和追踪的依赖性
 },
  onRenderTriggered(e) {
    debugger
    // 检查哪个依赖性导致组件重新渲染
  },
}

Vue提供的内置组件

component 与Vue2.x一致

渲染一个“元组件”为动态组件。依 is 的值,来决定哪个组件被渲染。

<!-- 动态组件由 vm 实例的 `componentId` property 控制 -->
<component :is="componentId"></component>

<!-- 也能够渲染注册过的组件或 prop 传入的组件 -->
<component :is="$options.components.child"></component>

transition 与 Vue2.x 【基本】 一致有差异

Props新增

  • persisted - boolean 如果为true,则表示这是一个转换,实际上不会插入/删除元素,而是切换显示/隐藏状态。transition 过渡挂钩被注入,但会被渲染器跳过。相反,自定义指令可以通过调用注入的钩子(例如v-show)来控制过渡

  • enter-class----->enter-from-class

  • ~~leave-class----->leave-from-class

事件

  • before-appear

transition-group 与 Vue2.x 一致

**slot 与 Vue2.x 一致 **

teleport 【新增组件】

Props

to - string 必填属性,必须是一个有效的query选择器,或者是元素(如果在浏览器环境中使用)。中的内容将会被放置到指定的目标元素中

<!-- 正确的 -->
<teleport to="#some-id" />
<teleport to=".some-class" />
 /*元素*/
<teleport to="[data-teleport]" />

<!-- 错误的 -->
<teleport to="h1" />
<teleport to="some-string" />

disabled - boolean 这是一个可选项 ,做一个是可以用来禁用的功能,这意味着它的插槽内容不会移动到任何地方,而是按没有teleport组件一般来呈现【默认为false】

<teleport to="#popup" :disabled="displayVideoInline">
  <h1>999999</h1>
</teleport>

注意,这将移动实际的DOM节点,而不是销毁和重新创建,并且还将保持任何组件实例是活动的。所有有状态HTML元素(比如一个正在播放的视频)将保持它们的状态。【控制displayVideoInline并不是销毁重建,它保持实例是存在的,不会被注销】

关于Teleport 其他内容

Vue 鼓励我们通过将UI和相关行为封装到组件中来构建UI。我们可以将它们彼此嵌套在一起,以构建构成应用程序UI的树

但是,有时组件模板的一部分逻辑上属于这个组件,而从技术角度来看,最好将这一部分模板移到DOM中的其他地方,放到Vue应用程序之外

一个常见的场景是创建一个包含全屏模态的组件。在大多数情况下,您希望模态的逻辑驻留在组件中,但是模态框的定位问题很快就很难通过CSS解决,或者需要更改组件的组成

考虑下面的HTML结构:

<body>
  <div style="position: relative;">
    <h3>Tooltips with Vue 3 Teleport</h3>
    <div>
      <modal-button></modal-button>
    </div>
  </div>
</body>

让我们看看 mode -button

该组件将有一个button元素来触发模态的打开,还有一个div元素,其类为.modal,它将包含模态的内容和一个自关闭按钮

const app = Vue.createApp({});

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal!
    </button>

    <div v-if="modalOpen" class="modal">
      <div>
        I'm a modal! 
        <button @click="modalOpen = false">
          Close
        </button>
      </div>
    </div>
  `,
  data() {
    return { 
      modalOpen: false
    }
  }
})

当在初始HTML结构中使用这个组件时,我们可以看到一个问题——模态被呈现在深嵌套的div中,模态的绝对位置以父div相对位置作为参考。

Teleport提供了一种干净的方式,允许我们控制DOM中希望在哪个父节点下呈现HTML片段,而不必诉诸全局状态或将其拆分为两个组件。

让我们修改我们的modal-button来使用并告诉Vue "teleport this HTML to the "body"标签"。

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal! (With teleport!)
    </button>

    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          I'm a teleported modal! 
          (My parent is "body")
          <button @click="modalOpen = false">
            Close
          </button>
        </div>
      </div>
    </teleport>
  `,
  data() {
    return { 
      modalOpen: false
    }
  }
})

与Vue组件一起使用

如果包含一个Vue组件,它将仍然是的父组件的逻辑子组件

const app = Vue.createApp({
  template: `
    <h1>Root instance</h1>
    <parent-component />
  `
})

app.component('parent-component', {
  template: `
    <h2>This is a parent component</h2>
    <teleport to="#endofbody">
      <child-component name="John" />
    </teleport>
  `
})

app.component('child-component', {
  props: ['name'],
  template: `
    <div>Hello, {{ name }}</div>
  `
})

在这种情况下,即使在不同的地方呈现child-component,它仍将是parent-componen的子组件【而不是爷爷组件】,并将从其父组件接收一个name 的props

这也意味着来自父组件的注入如预期的那样工作,并且子组件将嵌套在Vue Devtools的父组件之下,而不是放在实际内容移动到的地方

对同一目标使用多次teleports

一个常见的用例场景是一个可重用的组件,该组件可能同时有多个活动实例。对于这种场景,多个组件可以将它们的内容挂载到相同的目标元素。这个顺序将是一个简单的附加—稍后的挂载将位于目标元素中较早的挂载之后。

<teleport to="#modals">
  <div>A</div>
</teleport>
<teleport to="#modals">
  <div>B</div>
</teleport>

<!-- result-->
<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

依赖注入Provide / Inject

provide 和 inject 提供依赖注入,功能类似 2.x 的 provide/inject。两者都只能在当前活动组件实例的 setup() 中调用。

例如,如果我们想在根组件上提供一个book name,并将其inject到子组件上

import { provide, inject } from 'vue'

const RootComponent = {
  setup() {
    provide('book', 'Vue 3 guide')
  }
}

const MyBook = {
  setup() {
    const book = inject(
      'book',
      'Eloquent Javascript' /* 选项的默认值,假如父组件不提供值就返回默认 */
    )
    return {
      book
    }
  }
}

inject 接受一个可选的的默认值作为第二个参数。如果未提供默认值,并且在 provide 上下文中未找到该属性,则 inject 返回 undefined。

如果我们需要提供或注入多个值,我们可以通过随后分别调用provide或inject来实现【多次调用】

import { provide, inject } from 'vue'

const RootComponent = {
  setup() {
    provide('book', 'Vue 3 guide')
    provide('year', '2020')
  }
}

const MyBook = {
  setup() {
    const book = inject(
      'book',
      'Eloquent Javascript' /* 选项的默认值,假如父组件不提供值就返回默认 */
    )
    const year = inject('year')
    return {
      book,
      year
    }
  }
}

注入的响应性

可以使用 ref 或 reactive 来保证 provided 和 injected 之间值的响应

import { ref, reactive } from 'vue'

// 提供者
setup() {
  const book = reactive({
    title: 'Vue 3 Guide',
    author: 'Vue Team'
  })
  const year = ref('2020')

 /*提供reactive响应式*/
  provide('book', book)
 /*提供ref响应式*/
  provide('year', year)
}

// 消费者
setup() {
  const book = inject('book')
  const year = inject('year')
 /*响应式*/
  return { book, year }
}

现在,当提供者组件上的book或year发生变化时,我们可以观察到它们在注入的组件上的变化。

警告:我们不建议改变一个被注入的反应性属性【子组件去修改数据流】,因为它会破坏Vue的单向数据流。相反,尝试在提供值【父组件去修改】的地方改变值,或者提供一个方法来改变值

import { ref, reactive } from 'vue'

// in provider
setup() {
  const book = reactive({
    title: 'Vue 3 Guide',
    author: 'Vue Team'
  })

  function changeBookName() {
    book.title = 'Vue 3 Advanced Guide'
  }

  provide('book', book)
  provide('changeBookName', changeBookName)
}

// in consumer
setup() {
  const book = inject('book')
  const changeBookName = inject('changeBookName')

  return { book, changeBookName }
}

指令

v-text 【Vue2.x一致】

v-html【Vue2.x一致】

v-show【Vue2.x一致】

v-if【Vue2.x一致】

v-else【Vue2.x一致】

v-else-if【Vue2.x一致】

v-for【Vue2.x一致】

v-on【Vue2.x一致】

v-bind 【Vue2.x 修饰符差异】

修饰符

.prop 去除.sync 去除.camel 将 kebab-case attribute 名转换为 camelCase

v-model【Vue2.x一致】

v-slot【Vue2.x一致】

v-cloak【Vue2.x一致】

v-once 【Vue2.x一致】

v-pre【Vue2.x一致】

v-is【新增】

注意:本节只影响在页面的HTML中直接编写Vue模板的情况

全局API

createApp

返回一个应用程序实例,提供了一个应用程序上下文。应用程序实例挂载的整个组件树共享相同的上下文。

const app = Vue.createApp({})

参数:该函数接收一个根组件选项对象作为第一个参数

const app = Vue.createApp({
  data() {
    return {
      ...
    }
  },
  methods: {...},
  computed: {...}
  setup(){...}
  ...
})

使用第二个参数,我们可以将根组件props 传递给应用

<div id="app">
  <!-- 这里将会显示 'Evan' -->
  {{ username }}
</div>

const app = Vue.createApp(
  {
    props: ['username']
  },
  { username: 'Evan' }
)

h

返回“虚拟节点”,通常缩写为VNode:一个简单的对象,它包含描述Vue应该在页面上渲染何种类型的节点的信息,包括对任何子节点的描述。你可以手动阅读render functions

render() {
  return Vue.h('h1', {}, 'Some title')
}

参数:接受三个参数tag, props and children

tag:

类型:String | Object | Function | null 详情:一个HTML标签名,一个组件,一个异步组件或null。使用null将渲染成注释。此参数是必需的

props

类型:Object 详情:模板中使用的attributes、props 和events 对应的对象。可选

children

类型: String | Array | Object

详情:Children VNodes,使用h()构建,或使用字符串来获取“text VNodes”或带有槽的对象。可选

const aaa = {
  props: {
    someProp: String
  },
  setup(props) {
    console.log(props, "dsadasdasddasds");
  },
  render() {
    return h(
      "h2",
        // {Object}props
        //与props,attributes和events相对应的对象
        //我们将在template中使用。
        // 可选的。
        {style: {"font-size": "20px",
          color: "#136"}},
          [this.someProp,this.$slots.default()]);
        }
};
app.component("anchored-heading", {
  render() {
    return h(
        /*
  // {String | Object | Function | null}标签
        // HTML标记名称,组件,异步组件或null。
        //使用null将渲染注释。
        //必填
        */
      "h" + this.level, // tag name
        // {Object}props
        //与props,attributes和events相对应的对象
        //我们将在template中使用。
        // 可选的。
      {}, 
        // {String | Array | Object} children
        //使用`h()`构建的子级VNode,
        //或使用字符串获取“文本VNodes”或
        //具有插槽的对象。
        // 可选的。
      [
        "Some text comes first.",
        h("h1", "A headline"),
        h(aaa, {
          someProp: "foobar"
        })
      ]  );},
});
 Vue.h(
        'a',
        {
          name: headingId,
          href: '#' + headingId
        },
        this.$slots.default()
      )
    ])

defineAsyncComponent 【异步组件】

创建只在必要时加载的异步组件

参数

对于基本用法,defineAsyncComponent可以接受返回Promise的工厂函数。当您从serve检索到组件定义时,应该调用Promise的解析回调。您还可以调用reject(reason)来指示加载失败。

import { defineAsyncComponent } from 'vue'


const AsyncComp = defineAsyncComponent(() =>
   /*或者*/
  import('./components/AsyncComponent.vue')
   /*或者*/
  new Promise((resolve, reject) => {
  /*可以reject*/
      resolve({
        template: '<div>I am async!</div>'
      })
    })
)

app.component('async-component', AsyncComp)

在使用本地注册时,还可以直接提供返回Promise的函数

import { createApp, defineAsyncComponent } from 'vue'

createApp({
  // ...
  components: {
    AsyncComponent: defineAsyncComponent(() =>
      import('./components/AsyncComponent.vue')
    )
  }
})

resolveDynamicComponent【解析活动的组件active】

resolveDynamicComponent只能在render或setup函数中使用。允许使用与component:is=""相同的机制来解析组件。返回解析的组件或一个新创建的VNode以组件名称作为节点标记的。如果没有找到组件,会发出警告

resolveDirective

resolveDirective只能在render或setup函数中使用。允许通过名称解析指令,如果它在当前应用程序实例中可用。返回一个Directive或 当没有找到的时候,返回undefined。

app.directive('highlight', {})
render(){
    const highlightDirective = resolveDirective('highlight')
}

withDirectives

警告withDirectives只能在render或setup函数中使用。允许应用指令到VNode。返回一个带有应用指令的VNode。

const bar = resolveDirective('bar')

return withDirectives(h('div'), [
  [bar, this.y]
])

nextTick

将回调延迟到下一个DOM更新周期之后执行。在更改了一些数据以等待DOM更新之后立即使用它

setup() {
    const message = ref('Hello!')
    const changeMessage = async newMessage => {
      message.value = newMessage
      /*等待DOM更新*/
      await nextTick()
      console.log('Now DOM is updated')
    }
  }

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端算法源码编程群,每日一刷(工作日),每题瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

Logo

前往低代码交流专区

更多推荐