vue3.0教程(2)

848 阅读12分钟

PS:自己记录用 看过官网的可忽略哦

渲染函数

Vue 推荐使用模板来创建HTML。

在需要 JavaScript 的完全编程的能力时,可以用渲染函数,它比模板更接近编译器

h() 参数

h() 函数是一个用于创建 vnode 的实用程序。

全名 createVNode()

三个参数:标签名&组件名、属性(attribute、prop 和事件)、子节点(包含slot)

如果没有 prop,那么通常可以将 children 作为第二个参数传入。如果会产生歧义,可以将 null 作为第二个参数传入,将 children 作为第三个参数传入

// @returns {VNode}
h(
  // {String | Object | Function} tag
  // 一个 HTML 标签名、一个组件、一个异步组件、或
  // 一个函数式组件。
  //
  // 必需的。
  'div',

  // {Object} props
  // 与 attribute、prop 和事件相对应的对象。
  // 我们会在模板中使用。
  //
  // 可选的。
  {},

  // {String | Array | Object} children
  // 子 VNodes, 使用 `h()` 构建,
  // 或使用字符串获取 "文本 Vnode" 或者
  // 有插槽的对象。
  //
  // 可选的。
  [
    'Some text comes first.',
    h('h1', 'A headline'),
    h(MyComponent, {
      someProp: 'foobar'
    })
  ]
)

示例:通过 level prop 动态生成标题 (heading) 的组件

const { createApp } = Vue

const app = createApp({})

app.component('anchored-heading', {
  template: `
    <h1 v-if="level === 1">
      <slot></slot>
    </h1>
    <h2 v-else-if="level === 2">
      <slot></slot>
    </h2>
    <h3 v-else-if="level === 3">
      <slot></slot>
    </h3>
    <h4 v-else-if="level === 4">
      <slot></slot>
    </h4>
    <h5 v-else-if="level === 5">
      <slot></slot>
    </h5>
    <h6 v-else-if="level === 6">
      <slot></slot>
    </h6>
  `,
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

使用 render 函数重写上面的例子

const { createApp, h } = Vue

const app = createApp({})

app.component('anchored-heading', {
  render() {
    return h(
      'h' + this.level, // tag name
      {}, // props/attributes
      this.$slots.default() // array of children
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

完整实现

const { createApp, h } = Vue

const app = createApp({})

/** 递归地从子节点获取文本 */
function getChildrenTextContent(children) {
  return children
    .map(node => {
      return typeof node.children === 'string'
        ? node.children
        : Array.isArray(node.children)
        ? getChildrenTextContent(node.children)
        : ''
    })
    .join('')
}

app.component('anchored-heading', {
  render() {
    // 从 children 的文本内容中创建短横线分隔 (kebab-case) id。
    const headingId = getChildrenTextContent(this.$slots.default())
      .toLowerCase()
      .replace(/\W+/g, '-') // 用短横线替换非单词字符
      .replace(/(^-|-$)/g, '') // 删除前后短横线

    return h('h' + this.level, [
      h(
        'a',
        {
          name: headingId,
          href: '#' + headingId
        },
        this.$slots.default()
      )
    ])
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

注意事项

VNodes 必须唯一

下面的渲染函数是不合法的:

render() {
  const myParagraphVNode = h('p', 'hi')
  return h('div', [
    // 错误 - 重复的 Vnode!
    myParagraphVNode, myParagraphVNode
  ])
}

重复很多次的元素/组件,可以使用工厂函数来实现

render() {
  return h('div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}

创建组件 VNode

要为某个组件创建一个 VNode,传递给 h 的第一个参数应该是组件本身

render() {
  return h(ButtonCounter)
}

如果我们需要通过名称来解析一个组件,那么我们可以调用 resolveComponent

resolveComponent 是模板内部用来解析组件名称的同一个函数

render() {
  const ButtonCounter = resolveComponent('ButtonCounter')
  return h(ButtonCounter)
}

render 函数通常只需要对全局注册的组件使用 resolveComponent。而对于局部注册的却可以跳过

// 我们可以简化为
components: {
  ButtonCounter
},
render() {
  return h(resolveComponent('ButtonCounter'))
}

可以直接使用它,而不是通过名称注册一个组件,然后再查找:

render() {
  return h(ButtonCounter)
}

使用 JavaScript 代替模板功能

v-if 和 v-for

可以在渲染函数中用 JavaScript 的 if/else 和 map() 来重写:

props: ['items'],
render() {
  if (this.items.length) {
    return h('ul', this.items.map((item) => {
      return h('li', item.name)
    }))
  } else {
    return h('p', 'No items found.')
  }
}

v-model

v-model 指令扩展为 modelValue 和 onUpdate:modelValue

props: ['modelValue'],
emits: ['update:modelValue'],
render() {
  return h(SomeComponent, {
    modelValue: this.modelValue,
    'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
  })
}

v-on

为事件处理程序提供一个正确的 prop 名称,例如,要处理 click 事件,prop 名称应该是 onClick

render() {
  return h('div', {
    onClick: $event => console.log('clicked', $event.target)
  })
}

事件修饰符

对于 .passive 、.capture 和 .once 事件修饰符,可以使用驼峰写法将他们拼接在事件名后面:

render() {
  return h('input', {
    onClickCapture: this.doThisInCapturingMode,
    onKeyupOnce: this.doThisOnce,
    onMouseoverOnceCapture: this.doThisOnceInCapturingMode
  })
}
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
修饰符处理函数中的等价操作
.stopevent.stopPropagation()
.preventevent.preventDefault()
.selfif (event.target !== event.currentTarget) return
按键: .enter.13if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码
修饰键: .ctrl.alt.shift.metaif (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKeyshiftKey, 或 metaKey)
render() {
  return h('input', {
    onKeyUp: event => {
      // 如果触发事件的元素不是事件绑定的元素
      // 则返回
      if (event.target !== event.currentTarget) return
      // 如果向上键不是回车键,则终止
      // 没有同时按下按键 (13) 和 shift 键
      if (!event.shiftKey || event.keyCode !== 13) return
      // 停止事件传播
      event.stopPropagation()
      // 阻止该元素默认的 keyup 事件
      event.preventDefault()
      // ...
    }
  })
}

插槽

通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:

render() {
  // `<div><slot></slot></div>`
  return h('div', {}, this.$slots.default())
}
props: ['message'],
render() {
  // `<div><slot :text="message"></slot></div>`
  return h('div', {}, this.$slots.default({
    text: this.message
  }))
}

要使用渲染函数将插槽传递给子组件,请执行以下操作:

slots{ name: props => VNode | Array<VNode> } 的形式传递给子对象

const { h, resolveComponent } = Vue

render() {
  // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
  return h('div', [
    h(
      resolveComponent('child'),
      {},
      // 将 `slots` 以 { name: props => VNode | Array<VNode> } 的形式传递给子对象。
      {
        default: (props) => Vue.h('span', props.text)
      }
    )
  ])
}

任何响应式数据都应该在插槽函数内访问,以确保它被注册为子组件的依赖关系,而不是父组件。

相反,对 resolveComponent 的调用应该在插槽函数之外进行,否则它们会相对于错误的组件进行解析。

// `<MyButton><MyIcon :name="icon" />{{ text }}</MyButton>`
render() {
  // 应该是在插槽函数外面调用 resolveComponent。
  const Button = resolveComponent('MyButton')
  const Icon = resolveComponent('MyIcon')

  return h(
    Button,
    null,
    {
      // 使用箭头函数保存 `this` 的值
      default: (props) => {
        // 响应式 property 应该在插槽函数内部读取,
        // 这样它们就会成为 children 渲染的依赖。
        return [
          h(Icon, { name: this.icon }),
          this.text
        ]
      }
    }
  )
}

如果一个组件从它的父组件中接收到插槽,它们可以直接传递给子组件

render() {
  return h(Panel, null, this.$slots)
}
render() {
  return h(
    Panel,
    null,
    {
      // 如果我们想传递一个槽函数,我们可以通过
      header: this.$slots.header,

      // 如果我们需要以某种方式对插槽进行操作,
      // 那么我们需要用一个新的函数来包裹它
      default: (props) => {
        const children = this.$slots.default ? this.$slots.default(props) : []

        return children.concat(h('div', 'Extra child'))
      }
    }
  )
}

<component> 和 is

在底层实现里,模板使用 resolveDynamicComponent 来实现 is attribute。如果我们在 render 函数中需要 is 提供的所有灵活性,我们可以使用同样的函数:

const { h, resolveDynamicComponent } = Vue

// ...

// `<component :is="name"></component>`
render() {
  const Component = resolveDynamicComponent(this.name)
  return h(Component)
}

isresolveDynamicComponent 支持传递一个组件名称、一个 HTML 元素名称或一个组件选项对象

如果我们只需要支持组件名称,那么可以使用 resolveComponent 来代替。

如果 VNode 始终是一个 HTML 元素,那么我们可以直接把它的名字传递给 h

// `<component :is="bold ? 'strong' : 'em'"></component>`
render() {
  return h(this.bold ? 'strong' : 'em')
}

与 <template> 标签一样,<component> 标签仅在模板中作为语法占位符需要,当迁移到 render 函数时,应被丢弃

自定义指令:withDirectives

可以使用 withDirectives 将自定义指令应用于 VNode

resolveDirective 是模板内部用来解析指令名称的同一个函数。只有当你还没有直接访问指令的定义对象时,才需要这样做

const { h, resolveDirective, withDirectives } = Vue

// ...

// <div v-pin:top.animate="200"></div>
render () {
  const pin = resolveDirective('pin')

  return withDirectives(h('div'), [
    [pin, 200, 'top', { animate: true }]
  ])
}

内置组件

如 <keep-alive><transition><transition-group> 和 <teleport> 等内置组件默认并没有被全局注册,这也意味着我们无法通过 resolveComponent 或 resolveDynamicComponent 访问它们

在模板中这些组件会被特殊处理,即在它们被用到的时候自动导入。

当我们编写自己的 render 函数时,需要自行导入它们:

const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue
// ...
render () {
  return h(Transition, { mode: 'out-in' }, /* ... */)
}

渲染函数的返回值

render 函数返回的是单个根 VNode。但其实也有别的选项

返回一个字符串时会创建一个文本 VNode,而不被包裹任何元素

render() {
  return 'Hello world!'
}

也可以返回一个子元素数组,而不把它们包裹在一个根结点里。这会创建一个片段 (fragment)

// 相当于模板 `Hello<br>world!`
render() {
  return [
    'Hello',
    h('br'),
    'world!'
  ]
}

组件不需要渲染,这时可以返回 null。这样在 DOM 中会渲染一个注释节点

JSX

如果需要写很多渲染函数,会很痛苦,有一个 Babel 插件 github.com/vuejs/jsx-n… ,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。 参考:github.com/vuejs/jsx-n…

// 实现<anchored-heading :level="1"> <span>Hello</span> world! </anchored-heading>
import AnchoredHeading from './AnchoredHeading.vue'

const app = createApp({
  render() {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

app.mount('#demo')

函数式组件

函数式组件是自身没有任何状态的组件的另一种形式,在渲染过程中不会创建组件实例,并跳过常规的组件生命周期。

使用的是一个简单函数,而不是一个选项对象,来创建函数式组件

该函数实际上就是该组件的 render 函数。而因为函数式组件里没有 this 引用,Vue 会把 props 当作第一个参数传入

第二个参数 context 包含三个 property:attrsemit 和 slots

函数式组件可以像普通组件一样被注册和消费。如果你将一个函数作为第一个参数传入 h,它将会被当作一个函数式组件来对待

const FunctionalComponent = (props, context) => {
  // ...
}

可以自定义props 和 emits

FunctionalComponent.props = ['value']
FunctionalComponent.emits = ['click']

插件

插件是自包含的代码,通常向 Vue 添加全局级功能。它可以是公开 install() 方法的 object,也可以是 function

插件的功能范围没有严格的限制——一般有下面几种:

  1. 添加全局方法或者 property。如:vue-custom-element
  2. 添加全局资源:指令/过滤器/过渡等。如:vue-touch
  3. 通过全局 mixin 来添加一些组件选项。(如vue-router)
  4. 添加全局实例方法,通过把它们添加到 config.globalProperties 上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router

编写插件

插件被添加到应用程序中时,如果它是一个对象,就会调用 install 方法。

如果它是一个 function,则函数本身将被调用。在这两种情况下——它都会收到两个参数:由 Vue 的 createApp 生成的 app 对象和用户传入的选项

// plugins/i18n.js
export default {
  install: (app, options) => {
    app.config.globalProperties.$translate = (key) => {
      return key.split('.')
        .reduce((o, i) => { if (o) return o[i] }, options)
    }

    app.provide('i18n', options)

    app.directive('my-directive', {
      mounted (el, binding, vnode, oldVnode) {
        // some logic ...
      }
      ...
    })

    app.mixin({
      created() {
        // some logic ...
      }
      ...
    })
  }
}

使用插件

在使用 createApp() 初始化 Vue 应用程序后,你可以通过调用 use() 方法将插件添加到你的应用程序中。

第一个参数是要安装的插件

第二个参数是可选的,并且取决于每个特定的插件

import { createApp } from 'vue'
import Root from './App.vue'
import i18nPlugin from './plugins/i18n'

const app = createApp(Root)
const i18nStrings = {
  greetings: {
    hi: 'Hallo!'
  }
}

app.use(i18nPlugin, i18nStrings)
app.mount('#app')

深入响应性原理

Vue 最独特的特性之一,是其非侵入性的响应性系统

数据模型是被代理的 JavaScript 对象。而当你修改它们时,视图会进行更新。这让状态管理非常简单直观

响应性是一种允许我们以声明式的方式去适应变化的编程范例。

JavaScript 如何实现

  1. 当一个值被读取时进行追踪,例如 val1 + val2 会同时读取 val1 和 val2
  2. 当某个值改变时进行检测,例如,当我们赋值 val1 = 3
  3. 重新运行代码来读取原始值,例如,再次运行 sum = val1 + val2 来更新 sum 的值。

响应性现在可以在一个独立包中使用。将 $data 包裹在一个代理中的函数被称为 reactive。我们可以自己直接调用这个函数,允许我们在不需要使用组件的情况下将一个对象包裹在一个响应式代理中

const proxy = reactive({
  val1: 2,
  val2: 3
})

响应性基础

声明响应式状态reactive(针对引用数据类型)

为 JavaScript 对象创建响应式状态,可以使用 reactive 方法

reactive 相当于 Vue 2.x 中的 Vue.observable() API,为避免与 RxJS 中的 observables 混淆因此对其重命名

该 API 返回一个响应式的对象状态

该响应式转换是“深度转换”——它会影响传递对象的所有嵌套 property

import { reactive } from 'vue'

// 响应式状态
const state = reactive({
  count: 0
})

创建独立的响应式值作为 refs(针对基础数据类型)

有一个独立的原始值 (例如,一个字符串),让它变成响应式的:

  1. 可以创建一个拥有相同字符串 property 的对象,并将其传递给 reactive。 2.ref
import { ref } from 'vue'

const count = ref(0)

ref 会返回一个可变的响应式对象,该对象只包含一个名为 value 的 property:

import { ref } from 'vue'

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

count.value++
console.log(count.value) // 1

响应式状态解构

使用大型响应式对象的一些 property 时,可能很想使用 ES6 解构来获取我们想要的 property:

import { reactive } from 'vue'

const book = reactive({
  author: 'Vue Team',
  year: '2020',
  title: 'Vue 3 Guide',
  description: 'You are reading this book right now ;)',
  price: 'free'
})

let { author, title } = book

使用解构的两个 property 的响应性都会丢失。对于这种情况,我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联:

import { reactive, toRefs } from 'vue'

const book = reactive({
  author: 'Vue Team',
  year: '2020',
  title: 'Vue 3 Guide',
  description: 'You are reading this book right now ;)',
  price: 'free'
})

let { author, title } = toRefs(book)

title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref
console.log(book.title) // 'Vue 3 Detailed Guide'

响应式计算和侦听

computed

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

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

watchEffect

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

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

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

停止侦听

当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。

在一些情况下,也可以显式调用返回值以停止侦听:

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

// later
stop()

清除副作用

侦听副作用传入的函数可以接收一个 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()
  })
})

通过传入一个函数去注册失效回调,而不是从回调返回它,是因为返回值对于异步错误处理很重要

在执行数据请求时,副作用函数往往是一个异步函数:

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

const data = ref(null)
watchEffect(async onInvalidate => {
  onInvalidate(() => {
    /* ... */
  }) // 我们在Promise解析之前注册清除函数
  data.value = await fetchData(props.id)
})

侦听器调试

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

  • onTrack 将在响应式 property 或 ref 作为依赖项被追踪时被调用。
  • onTrigger 将在依赖项变更导致副作用被触发时被调用。
watchEffect(
  () => {
    /* 副作用 */
  },
  {
    onTrigger(e) {
      debugger
    }
  }
)

watch

watch API 完全等同于组件侦听器 property。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调

与 watchEffect 比较,watch 允许我们:

  • 懒执行副作用;
  • 更具体地说明什么状态应该触发侦听器重新运行;
  • 访问侦听状态变化前后的值。

侦听单个数据源

侦听器数据源可以是返回值的 getter 函数(ref的属性),也可以直接是 ref

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

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

侦听多个数据源

使用数组同时侦听多个源

const firstName = ref('')
const lastName = ref('')

watch([firstName, lastName], (newValues, prevValues) => {
  console.log(newValues, prevValues)
})

firstName.value = 'John' // logs: ["John", ""] ["", ""]
lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]

如果你在同一个方法里同时改变这些被侦听的来源,侦听器仍只会执行一次:(多个同步更改只会触发一次侦听器

setup() {
  const firstName = ref('')
  const lastName = ref('')

  watch([firstName, lastName], (newValues, prevValues) => {
    console.log(newValues, prevValues)
  })

  const changeValues = () => {
    firstName.value = 'John'
    lastName.value = 'Smith'
    // 打印 ["John", "Smith"] ["", ""]
  }

  return { changeValues }
}

通过更改设置 flush: 'sync',我们可以为每个更改都强制触发侦听器

这通常是不推荐的。或者,可以用 nextTick 等待侦听器在下一步改变之前运行

const changeValues = async () => {
  firstName.value = 'John' // 打印 ["John", ""] ["", ""]
  await nextTick()
  lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
}

侦听响应式对象

使用侦听器来比较一个数组或对象的值

const numbers = reactive([1, 2, 3, 4])

watch(
  () => [...numbers],
  (numbers, prevNumbers) => {
    console.log(numbers, prevNumbers)
  }
)

numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]

尝试检查深度嵌套对象或数组中的 property 变化时,仍然需要 deep 选项设置为 true

const state = reactive({ 
  id: 1,
  attributes: { 
    name: '',
  }
})

watch(
  () => state,
  (state, prevState) => {
    console.log('not deep', state.attributes.name, prevState.attributes.name)
  }
)

watch(
  () => state,
  (state, prevState) => {
    console.log('deep', state.attributes.name, prevState.attributes.name)
  },
  { deep: true }
)

state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"

Vue 2 中的更改检测警告

由于 JavaScript 的限制,有些 Vue 无法检测的更改类型

对于对象

Vue 无法检测到 property 的添加或删除。由于 Vue 在实例初始化期间执行 getter/setter 转换过程,因此必须在 data 对象中存在一个 property,以便 Vue 对其进行转换并使其具有响应式。

Vue 不允许动态添加根级别的响应式 property。

使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property:

Vue.set(vm.someObject, 'b', 2)

可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

this.$set(this.someObject, 'b', 2)

对于数组

Vue 不能检测以下数组的变动:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

Vue.nextTick(callback) 回调函数将在 DOM 更新完成后被调用

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function() {
    return {
      message: 'not updated'
    }
  },
  methods: {
    updateMessage: function() {
      this.message = 'updated'
      console.log(this.$el.textContent)   // => 'not updated'
      this.$nextTick(function() {
        console.log(this.$el.textContent) // => 'updated'
      })
    }
  }
})

因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:

methods: {
    updateMessage: async function () {
      this.message = 'updated'
      console.log(this.$el.textContent) // => 'not updated'
      await this.$nextTick()
      console.log(this.$el.textContent) // => 'updated'
    }
}