好记性不如烂笔头

Basic

通过 createApp 创建应用实例

在 vue2.x 中,使用的是new Vue(),返回的是根组件实例,而createApp返回的是应用实例,想要得到根组件实例,还需要使用mount方法,个人认为这个改动是在语法层面上区分创建组件和创建应用

ES6 Proxy

vue2.x 使用 Object.defineProperty来实现数据相应,vue3 中使用 new Proxy()为数据架设一层代理,来实现数据响应

计算属性 computed

  • 计算属性会缓存结果,只要依赖值不变,多次访问该计算属性也不会执行代码,而是直接返回缓存结果,减小性能开销
  • 计算属性拥有gettersetter

侦听器 watch

  • watch 与 computed 几乎相同,唯一不同的是,watch 允许在其中执行异步操作

class 与 style 的绑定

class

  • <div :class="{active: isActive}"></div>
    
    <div :class="classes"></div>
    data(){
    	return {
    		classes: {
    			avtive: true,
    			container: true
    		}
    	}
    }
    
    <div :class="[activeClass, containerClass]"></div>
    data(){
    	return {
    		activeClass: 'active',
    		containerClass: 'container'
    	}
    }
    
  • 也可以直接在v-bind:class中使用三运运算符来切换 class

  • 在自定义组件上使用 class

    • 若组件内只有一个根元素,那么这些 class 会自动合并在根元素上

    • 若有多个根元素,需要定义哪些根元素接收 class,使用$attrs

    • <div id="app">
        <my-component class="baz"></my-component>
      </div>
      
      const app = Vue.createApp({})
      
      app.component('my-component', {
        template: `
          <p :class="$attrs.class">Hi!</p>
          <span>This is a child component</span>
        `
      })
      
  • 继承的 Attribute 可以使用$arrts来定义 Attribute 具体出现在哪个元素上

    • app.component('date-picker', {
        inheritAttrs: false,
        template: `
          <div class="date-picker">
            <input type="datetime-local" v-bind="$attrs" />
          </div>
        `
      })
      
      <!--这样一来所有写在 date-picker 上的 attribute 将会绑定在 input 元素上-->
      

style

  • 对象语法

    • <div :style="{color: 'red', fontSize: '14px', borderRadius: borderRadius}"></div>
      
      data(){
      	return {
      		borderRadius: '4px'
      	}
      }
      
  • 也可以直接绑定到一个样式对象

    • <div :style="styleObject"></div>
      
      data(){
      	return {
      		styleObject: {
      			color: 'red',
      			fontSize: '14px',
      			borderRadius: '4px'
      		}	
      	}
      }
      
  • 数组语法

    • 可以使用数组语法包含多个样式对象

    • <div :style="[borderStyle, textStyle]"></div>
      
      data(){
      	return {
      		borderStyle: {
      			border: '1px solid red',
      			borderRadius: '4px',
      		},
      		textStyle: {
      			fontSize: '14px',
      			color: 'gray'
      		}
      	}
      }
      

v-for

  • v-if的优先级高于v-for,当两个指令同时作用于一个元素身上时,你需要你知道你在干什么

  • 当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。

  • 用比较简单的描述是,如果 v-for渲染出来的列表没有绑定key,那么在v-for绑定值变化的时候,Vue 是基于绑定值的索引来更新 DOM 树的

  • 查看这个例子

    • 我们将三个 input 元素的值改为 test1、test2、test3
    • 然后删除第二个元素,按照直觉来说,剩下的应该只有 test1 和 test3,但结果是 test1 和 test2,why?
    • 很简单,我们改变了v-for绑定值的 index,之前是0,1,2,删除后是0, 1,相当于删除了索引为 2 的元素,那么在这个 list 对应的 DOM 树中,也会去删除索引为 2 的节点,所以反应到页面上是 test1 和 test2
    • 这就是文档中所说的依赖子组件状态或临时DOM状态,在这种情况下我们需要绑定key值来明确地告诉 Vue 我们想要删除的节点到底是哪一个,而不是让索引做主
  • 为了让 Vue 可以监听到数组的变化,Vue 改写了以下数组方法:

    • push
    • pop
    • shift
    • unshift
    • splice
    • sort
    • reverse
  • 代替变更方法更好的办法是直接替换数组

事件处理

  • 在内联语句中使用@eventName这样的方法来处理事件

    • <div @click="f"></div>
      
    • methods: {
          // e这个参数我们在绑定事件回调函数的时候并没有传啊?that's ok,vue 会帮我们传的
         	// 这样的写法,vue 会在调用回调函数时默认帮我们传一个 event 对象
      	f(e){
      		console.log(e)
      	}
      }
      
    • <div @click="f('message', $event)"></div>
      
    • methods: {
      	f(message,e){
              // 当我们需要其他参数时,并且还想接收到 event 对象
              // 我们需要在内联语句中显示的传递这个参数,使用 $event 这个变量,你问这个哪来的?去问 Evan You
              console.log(`message is ${message}`)
      		console.log(e)
      	}
      }
      
  • 一个事件可以绑定多个回调函数,使用逗号分割

    • <div @click="doOne($event), doTwo($event)"></div>
      
  • 事件修饰符

    • .stop阻止事件冒泡

    • .prevent阻止事件默认行为

    • .capture使用事件捕获模式

    • .self触发事件的元素只能是本身

    • .once事件只会触发一次

    • .passive文档解释我没太看懂,看了下 MDN,将 MDN 的解释照搬过来

      • passive: Boolean,设置为true时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。查看 使用 passive 改善的滚屏性能 了解更多.

    v-model

    • .lazy会将触发事件由input改变为change
    • .number将输入值转变为Number类型
    • .trim过滤输入值的首尾空白字符

Component

在自定义组件上使用 v-model

v-model本质上是一个语法糖,当我们剥开包装纸,其实是这样的

<custom-input
  v-model="searchText"
></custom-input>

<custom-input
  :model-value="searchText"
  @update:model-value="searchText = $event"
></custom-input>

为了让它正常工作,这个组件内的 <input> 必须:

  • 将其 value attribute 绑定到一个名叫 modelValue 的 prop 上
  • 在其 input 事件被触发时,将新的值通过自定义的 update:modelValue 事件抛出

当然,我们也可以自定这个值,并不一定是 modelValue

<template>
  <input
      type="text"
      :value="value"
      @input="$emit('update:xxx', $event.target.value)"
  >
</template>

<script setup>
// props 接收一个名为 xxx 的 property
defineProps({
  xxx: {
    type: String,
    required: true
  }
})
</script>

当然,我们也可以在内部的input上使用v-model,不过需要改变一下写法,这里直接照搬文档

app.component('custom-input', {
  props: ['xxx'],
  emits: ['update:xxx'],
  template: `
    <input v-model="value">
  `,
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) { 
        this.$emit('update:modelValue', value)
      }
    }
  }
})

使用 emits 定义组件发出的事件

app.component('custom-form', {
  emits: ['inFocus', 'submit']
})

当在 emits 选项中定义了原生事件 (如 click) 时,将使用组件中的事件替代原生事件侦听器。

  • 这句话的意思是,如果我们在一个自定义组件内使用emits定义了组件发出的事件,那么在该组件上定义相同的事件(也就是原生事件侦听器)将不会触发,而是改用组件内定义的事件触发

  • <script setup>
    import MyButton from './components/MyButton.vue'
    import {ref} from "vue";
    
    const f = () => {
      console.log('customize element tigger');
    }
    </script>
    
    <template>
      <MyButton label="click me" @click="f"></MyButton>
    </template>
    
  • <!-- MyButton.vue -->
    <template>
      <div>
        <button  @click="f">
          {{ label }}
        </button>
      </div>
    </template>
    
    <script setup>
    defineProps({
      label: {
        type: String,
        default: '',
        required: false
      }
    })
    
    const f = () => {
      console.log('component click tigger');
    }
    
    // 如果没有使用emits,那么我们点击这个按钮将会触发两次click
    defineEmits(['click'])
    </script>
    
  • 在 2.x 中,如果 MyButton 组件上定义了监听了 click 事件,但是组件内部没有对其处理的话,是触发不了这个事件的,必须使用.native才能触发

    • <!-- MyButton.vue -->
      
      <template>
      	<button>click me</button>
      </template>
      
    • <my-button @click=f></my-button>
      
    • methods:{
          // 这个f是不会被触发的,除非是组件内部对click做出反应抛出一个click事件,或者在组件上使用 .native 修饰符
          // .native 会将组件上的事件监听绑定到组件内部的根元素上
      	f(){
      		console.log('tigger')
      	}
      }
      

emits还可以验证抛出事件

要添加验证,请为事件分配一个函数,该函数接收传递给 $emit 调用的参数,并返回一个布尔值以指示事件是否有效。

  • emits: {
      // 没有验证
      click: null,
      // 验证 submit 事件
      submit: ({ email, password }) => {
        if (email && password) {
          return true
        } else {
          console.warn('Invalid submit event payload!')
          return false
        }
      }
    }
    

插槽

备用内容/默认内容:

  • 可以给插槽指定默认内容,当父组件在使用该组件时且并未提供任何插槽内容,默认内容将会被渲染

具名插槽:

  • slot标签添加一个name的 Attribute,然后在父组件中使用<template v-slot:slotName>指定使用哪一个插槽,slot没有name的会当作default
  • 插槽的名称也可以使用动态指令参数
  • 具名插槽还可以使用缩写,但只能在有其他参数时使用(有多个插槽且需要提供名称时),将v-sot替换为#,使用缩写是需要使用明确的插槽名

作用域插槽

  • 让父组件能够访问子组件通过插槽暴露的数据
  • 子组件:<slot :dataName=data></slot>,父组件通过v-slot:default="slotProps"来访问,slotProps是一个对象,里面包含了子组件插槽所暴露的数据项,default表示这是默认插槽,参考上面的具名插槽
  • 当然,我们还可以使用ES6的解构语法:v-slot:default={dataName}

Provide / Inject

相当于“长距离 props”,子组件并不需要知道 inject 的 property 来自哪里,父组件也不需要知道哪些子组件使用了它 provide 的 property。

provide 要访问组件实例的 property 需要将 provide 写为一个返回对象的函数

provide 传递的内容并不是响应式的,我们可以使用 computed 来做到响应式

Composition API

这个算是Vue3 的最大更新了,这个 composition api 让我感觉 Vue3 现在确实是一个 framework,而不是一个 library,composition api 允许我们分离逻辑代码,使得单文件组件中的逻辑更加清晰有条理,在以往的 options api 中,如果我们有一个较重的组件,里面的data可能包含十多个 property,methods中的方法可能每一个都包含N行代码,阅读十分困难,而且逻辑关联不清楚。

Setup

setup函数接收两个参数:

  • props
  • context

Props

与 options api 的 props 一样,它是响应式的。

export default {
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
  }
}

但是,因为 props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。

如果要解构 prop,需要使用toRefs函数来完成

import { toRefs } from 'vue'

setup(props) {
  const { title } = toRefs(props)

  console.log(title.value)
}

如果title是可选的prop(required: false),那么传入的props中可能没有这个titile,这种情况toRefs不会创建一个ref,需要使用toRef替代它

import { toRef } from 'vue'
setup(props) {
  const title = toRef(props, 'title')
  console.log(title.value)
}

Context

传递给 setup 函数的第二个参数是 contextcontext 是一个普通 JavaScript 对象,暴露了其它可能在 setup 中有用的值:

export default {
  setup(props, context) {
    // Attribute (非响应式对象,等同于 $attrs)
    console.log(context.attrs)

    // 插槽 (非响应式对象,等同于 $slots)
    console.log(context.slots)

    // 触发事件 (方法,等同于 $emit)
    console.log(context.emit)

    // 暴露公共 property (函数)
    console.log(context.expose)
  }
}

context是一个普通的 JavaScript 对象, 可以使用解构

Provide / Inject

在 composition api 中,provide 需要从 vue 中 import,其次 provide 变为一个函数,接收连个参数:key,value

inject 相同,接收两个参数:需要 inject 的 property 的 key,默认值(可选)

为了使 provide 和 inject 的值具有响应性,需要在 provide 时使用refreactive

对于响应式的 property,尽量在 provide 处进行修改,跟 props 的单向数据流相似。如果需要在 inject 处更新 property,可以通过 provide 暴露一个更新方法。

模板引用

<template> 
  <div ref="root">This is a root element</div>
</template>

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // DOM 元素将在初始渲染后分配给 ref
        console.log(root.value) // <div>This is a root element</div>
      })

      return {
        root
      }
    }
  }
</script>

watch()watchEffect()在 DOM 挂载或更新之前运行,所以在这里面运行时,模板引用还未更新,可以使用选项{flush: 'post'},这将在 DOM 更新后运行,确保模板引用和 DOM 保持同步。

Mixin

mixin 对象的所有选项将被“混合”入组件本身

const myMixin = {
  created() {
    this.hello()
  },
  methods: {
    hello() {
      console.log('hello from mixin!')
    }
  }
}

// 定义一个使用此 mixin 对象的应用
const app = Vue.createApp({
  mixins: [myMixin]
})

app.mount('#mixins-basic') // => "hello from mixin!"
  • 当组件和 mixin 对象拥有同名选项时,这些选项将会合并
    • data 返回对象中的 property 重名,将会以组件自身数据优先合并
    • 钩子函数将会合并为一个数组,并依次调用,mixin的钩子先于组件钩子调用
    • 其他值为对象的选项重名时,将会合并,对象中的 property 重名时,以组件自身的 property 优先

mixin 也可以全局注册,但会影响每一个创建的组件,谨慎使用

Teleport

允许我们控制在 DOM 中哪个父节点下渲染 HTML,一个例子时 toast 组件的 content 部分,我们希望他是基于body定位的,在vue2 中我是通过模板引用拿到元素,然后将其appendbody中,现在 teleport 可以直接让我们将 content 部分传送到body标签下:<teleport to="body">组件内容</teleport>