快速起步:vue-next 新特性 && CreateApp 结合源码的思维导图

413 阅读2分钟

快速起步 vue-next

vue3中文文档

vue3英文文档

vue-next-dev

安装和使用

CDN

<script src="https://unpkg.com/vue@next"></script>

对于生产环境,我们推荐链接到一个明确的版本号和构建文件,以避免新版本造成的不可预期的破坏

npm 安装

# 最新稳定版
$ npm install vue@next

命令行工具(CLI)

对于 Vue 3,你应该使用 npm 上可用的 Vue CLI v4.5 作为 @vue/cli

yarn global add @vue/cli
# OR
npm install -g @vue/cli

#升级最新版本

vue upgrade --next

Vite 构建工具

Vite是一个 web 开发构建工具,由于其原生 ES 模块导入方式,可以实现闪电般的冷服务器启动。

npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

npm run dev

vue-next 新特性

Composition API

Composition API 英文官方介绍

Composition API 中文官方介绍

一个大型组件的示例,其中逻辑关注点是按颜色分组。 这种碎片化使得理解和维护复杂组件变得困难

Vue 选项 API: 按选项类型分组的代码

如果我们能够将与同一个逻辑关注点相关的代码配置在一起会更好。而这正是组合式 API 使我们能够做到的

setup 选项

vue3中我们在setup()选项中使用组合式API.

新的 setup 组件选项在创建组件之前执行,一旦 props 被解析,并充当合成 API 的入口点。

:::warning 由于在执行 setup 时尚未创建组件实例,因此在 setup 选项中没有 this。这意味着,除了 props 之外,你将无法访问组件中声明的任何属性——本地状态计算属性方法。 :::

setup()函数接受两个参数:
  • props 属性对象;Proxy对象;
    • 不能使用es6解构它,否则将失去响应式;
    • 如需解构:可通过toRefs,在setup内部解构;
      • const {msg} =toRefs(props);
  • context 组件上下文普通对象;暴露3个组件的property
    • ctx.attrs:(ps非响应式对象) 访问组件所有特性
    • ctx.slots:(ps非响应式对象) 访问插槽内容,插槽在vue3中函数化;
      • 即ctx.slots.xxx();的方式来访问插槽内容;
    • ctx.emit:(ps触发事件方法) 向外部派发自定义事件;
setup()函数中的this指向:
  • setup中的this就是它执行时的上下文;
    • 如果是esm方式打包的,this为undefined;
    • 如果是单文件的方式运行的,this为window;
    • 源码中,setup()在解析其他组件选项之前被调用;this不再指向当前活跃实例的引用;
  • 结论:forget about this;vue3的setup中无需考虑this即可;

setup()中,三种方式实现数据响应式:

  • ref() 返回响应式对象;
  • reactive();将一个对象响应化;
  • toRefs(); 将一个响应式对象ref处理;
<script>
  import { ref } from 'vue'
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
   // 当setup中定义的响应式数据和data()选项中定义的重复时,vue3优先获取data中的数据;
  setup(props,ctx){
    
    const counter = ref(0)  // ref 返回响应式对象 {value:0}
    
    console.log(counter.value);  // 0 
    
    return {counter }
  },
  data() {
    return {
      counter: 1
    }
  }
}
</script>

使用ref响应式变量(创建一个响应式引用)

console.log(counter) // { value: 0 }

如上:为什么使用包装器对象,来包裹一个基本数据类型值?

答:因为 1、在 JavaScript 中,Number 或 String 等基本类型是通过值传递的,而不是通过引用传递的: 在任何值周围都有一个包装器对象,这样我们就可以在整个应用程序中安全地传递它,而不必担心在某个地方失去它的响应性。 按引用传递与按值传递

生命周期钩子注册内部setup

  • composition API生命周期钩子名称为: option API 名称前缀加 on

  • 你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。

下表包含如何在 setup () 内部调用生命周期钩子:

选项 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

<script>
  import { getCurrentInstance, onMounted, ref,watch,computed  } from 'vue'
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
   // 当setup中定义的响应式数据和data()选项中定义的重复时,vue3优先获取data中的数据;
  setup(props,ctx){
    
    const instance= getCurrentInstance();  // 获组件实例

    const counter = ref(0)  // ref 返回响应式对象 {value:0}
    const twiceTheCounter = computed(() => counter.value * 2)
    console.log(counter.value);  // 0 

     onMounted(()=>{
       // 在组件挂载的时候,通过组件实例的上下文获取setup外部属性;
        console.log(instance.ctx.counter);  // 1;
     });
    console.log(twiceTheCounter);
    watch(()=>{return counter.value}, (newValue, oldValue) => {
        console.log('The new counter value is: ' + counter.value)
      })
    return {counter,twiceTheCounter }
  },
  data() {
    return {
      counter2: 1
    }
  }
}
</script>

setup()执行时间很早,甚至早于created; 因此在setup()中访问外部属性,需要在onMounted钩子中进行访问才有效;

wantch && computed 响应式更改;

watch()函数侦听器,接受3个参数

  • 一个响应式引用或我们想要侦听的 getter 函数 ()=>{}
  • 一个回调
  • 可选的配置选项

项目示例地址



<script>
  import { getCurrentInstance, onMounted, ref,watch,computed  } from 'vue'
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
   // 当setup中定义的响应式数据和data()选项中定义的重复时,vue3优先获取data中的数据;
   //  在源码中先从setupState中取值,再从data中取值;
  setup(props,ctx){
    
    const instance= getCurrentInstance();  // 获组件实例

    const counter = ref(0)  // ref 返回响应式对象 {value:0}
    const twiceTheCounter = computed(() => counter.value * 2)  // 输出只读属性的响应式引用;
    console.log(counter.value);  // 0 

     onMounted(()=>{
       // 在组件挂载的时候,通过组件实例的上下文获取setup外部属性;
        console.log(instance.ctx.counter);  // 1;
     });
    console.log(twiceTheCounter);
    watch(()=>{return counter.value}, (newValue, oldValue) => {
        console.log('The new counter value is: ' + counter.value)
      })
    return {counter,twiceTheCounter }
  },
  data() {
    return {
        // 与setup中变量相同时;counter从data中取值;
      counter2: 1
    }
  }
}
</script>


Provide/Inject

use 组合式函数;

使用组合函数组织代码; useCounter();

<script>
  import { getCurrentInstance, onMounted, ref,watch,computed  } from 'vue'
  // 也可单独封装到一个js文件中;
  function useCounter(){
     const counter = ref(0)  // ref 返回响应式对象 {value:0}
     const twiceTheCounter = computed(() => counter.value * 2)
     console.log(counter.value);  // 0 

     watch(()=>{return counter.value}, (newValue, oldValue) => {
        console.log('The new counter value is: ' + counter.value)
      })

      return {
         counter,twiceTheCounter
      }
  }

export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
   // 当setup中定义的响应式数据和data()选项中定义的重复时,vue3优先获取data中的数据;
   //  在源码中先从setupState中取值,再从data中取值;
  setup(props,ctx){
      const { counter,twiceTheCounter }=useCounter();
      const instance= getCurrentInstance();  // 获组件实例

    // const counter = ref(0)  // ref 返回响应式对象 {value:0}
    // const twiceTheCounter = computed(() => counter.value * 2)
    // console.log(counter.value);  // 0 
    
    // console.log(twiceTheCounter);
    // watch(()=>{return counter.value}, (newValue, oldValue) => {
    //     console.log('The new counter value is: ' + counter.value)
    //   })

       onMounted(()=>{
       // 在组件挂载的时候,通过组件实例的上下文获取setup外部属性;
        console.log(instance.ctx.counter);  // 1;
     });

    return {counter,twiceTheCounter }
  },
  data() {
    return {
        // 与setup中变量相同时;counter从data中取值;
      counter2: 1
    }
  }
};

</script>

Teleport

Teleport 英文官方文档

Teleport 中文官方文档

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

官方例子,简单易懂:

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
   }
 }
})

同一个节点挂载多个teleport

<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>

Fragments

Fragments 英文官方文档

Fragments 中文官方文档

Vue 3 现在正式支持了多根节点的组件

在 3.x 中,组件可以包含多个根节点!但是,这要求开发者显式定义 attribute 应该分布在哪里。

<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

有关 attribute 继承如何工作的详细信息 组件属性继承

Emits Componnet Option

组件自定义事件-英文官方文档

组件自定义事件-中文官方文档

触发的事件名需要完全匹配监听这个事件所用的名称。 即:如果用户自定义事件名为驼峰命名,则监听事件名必须为驼峰命名;kebab-case 命名同理;

官方举例,简单易懂:

this.$emit('myEvent')

<!-- 没有效果 -->
<my-component @my-event="doSomething"></my-component>

v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以@myEvent将会变成 @myevent——导致 myEvent 不可能被监听到。

总结:官方推荐使用:kebab-case 的事件名。

v-model参数

默认情况下,

  • 组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。
  • 我们可以通过向 v-model 传递参数来修改这些名称:

官方举例,简单易懂:

<my-component v-model:title="bookTitle"></my-component>

子组件将需要一个 title prop 并抛出事件 update:title :

app.component('my-component', {
  props: {
    title: String
  },
  emits: ['update:title'],
  template: `
    <input 
      type="text"
      :value="title"
      @input="$emit('update:title', $event.target.value)">
  `
})

vue3迁移指南

源码分析

响应式数据类型

vue3 只能对 ObjectArrayMapSetWeakMapWeakSet 几种数据类型的target实现数据的响应式;

源码为证:

// http://172.25.154.30:5000/packages/reactivity/src/reactive.ts

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION   // 返回集合类型;
    default:
      return TargetType.INVALID   // 否则返回 无效类型;
  }
}

vue3 对常用类型的封装

// packages/shared/src/index.ts

const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
 val: object,
 key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)

export const isArray = Array.isArray
export const isMap = (val: unknown): val is Map<any, any> =>
 toTypeString(val) === '[object Map]'
export const isSet = (val: unknown): val is Set<any> =>
 toTypeString(val) === '[object Set]'

export const isDate = (val: unknown): val is Date => val instanceof Date
export const isFunction = (val: unknown): val is Function =>
 typeof val === 'function'
export const isString = (val: unknown): val is string => typeof val === 'string'
export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
export const isObject = (val: unknown): val is Record<any, any> =>
 val !== null && typeof val === 'object'

export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
 return isObject(val) && isFunction(val.then) && isFunction(val.catch)
}

vue初始化源码分析思维导图

结合思维导图,具体源码后续拆开分析

思维导图地址

如何给vue-next提issue

vue-next: repro-new-issue

Vue RFCs

Vue RFCs request for comments