vue3-study

301 阅读5分钟

vue3光速学习

开始前的准备-安装

  • CDN
<script src="https://unpkg.com/vue@next"></script>
  • npm
# 最新稳定版
$ npm install vue@next
  • 命令行工具(CLI) 升级vue-cli v4.5
yarn global add @vue/cli
# OR
npm install -g @vue/cli
#然后
npm i -g @vue/cli@next
  • Vite 使用 Vite 可以快速构建 Vue 项目
#npm:
$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev

#yarn
$ yarn create vite-app <project-name>
$ cd <project-name>
$ yarn
$ yarn dev

开始

基础API

<body>
    <div id="app"></div>
    <script>
        console.log('Vue', Vue)
        const {
            createApp,
            reactive, // 创建响应式数据对象
            ref, // 创建一个响应式的数据对象
            toRefs, // 将响应式数据对象转换为单一响应式对象
            isRef, // 判断某值是否是引用类型
            computed, // 创建计算属性
            watch, // 创建watch监听
            watchEffect,
            // 生命周期钩子
            onBeforeMount,
            onMounted,
            onUpdated,
            onUnmounted,
        } = Vue
        const MyComponent = {
            template: `
            <div>
                <div>count is {{ state.count }} </div>
                <div>plusOne is {{ state.plusOne }}</div>
                <div>Date is {{ time }}</div>
                <div>count is {{ count }} </div>
                <button @click="increment">count++</button>
            </div>
            `,
            
            setup(props, context) {
                console.log('setup....',)
                console.log('props',props)
                console.log('context',context)

                // reactive state
                const state = reactive({
                    count: 0,
                    plusOne: computed(() => state.count + 1)
                })

                // 定义创建响应式数据
                const time = ref(new Date())
                // 设置定时器为了测试数据响应
                setInterval(() => time.value = new Date(), 1000)

                // 判断某值是否是响应式类型
                console.log('time is ref:', isRef(time))
                console.log('time', time)
                console.log('time.value', time.value)

                // method
                const increment = () => {
                    console.log('increment....')
                    state.count++
                }

                // 定义监听
                watch(() => state.count * 2, val => {
                    console.log(`count * 2 is ${val}`)
                })

                // 副作用函数
                watchEffect(() => {
                    console.log('数值被修改了..',state.count)
                })

                // lifecycle
                onBeforeMount(() => {
                    console.log('onBeforeMount....')
                })
                onMounted(() => {
                    console.log(`onMounted ...!`)
                })
                onUpdated(() => {
                    console.log(`onUpdated ...!`)
                })
                onUnmounted(() => {
                    console.log(`onUnmounted ...!`)
                })
                // expose bindings on render context
                return {
                    state,
                    ... toRefs(state),
                    time,
                    increment
                }
            }
        }
        createApp(MyComponent).mount('#app')
    </script>
</body>

setup 使用composition API的入口

setup函数会在beforeCreate之后created之前执行

reactive

reactive() 函数接受一个普通对象返回一个响应式数据对象

ref与isRef

  • ref是将给定的值(确切的说是基本数据类型)创建一个响应式的数据对象
  • isRef 其实就是判断一下是不是ref生成的响应式数据对象

toRefs

toRefs 可以将reactive 创建出的对象展开为基础类型

watch定义监听器

watch(() => state.count * 2, val => {
    console.log(`count * 2 is ${val}`)
})

watchEffect 副作用函数

响应式对象修改会触发这个函数

 watchEffect(() => {
    console.log('数值被修改了..',state.count)
})

computed 计算属性

const state = reactive({
    count: 0,
    plusOne: computed(() => state.count + 1)
})

新特性

Composition API

与原来的 OptionsAPI 相比,Composition API 可以让我们将相同功能的代码组织在一起,而不需要散落到 OptionsAPI 的各个角落,为vue应用提供更好的逻辑复用和代码组织。

<body>
    <div id="app"></div>
    <script>
        const {
            createApp,
            reactive,
            computed,
            watchEffect,
            ref,
            toRefs,
            onMounted,
            onUnmounted,
            watch
        } = Vue

        console.log('Vue',Vue)
        const MyComponent = {
            template: `
                <button @click="click">
                {{ state.message }}
                </button>

                <p> counter: {{counter}} </p>
                <p> doubleCounter: {{doubleCounter}} </p>
                <p ref="desc"></p>
            `,
            setup() {
                const state = reactive({
                    message:'Hello Vue 3!!',
                })
                watchEffect(() => {
                    console.log('state change ', state.message)
                })
                function click() {
                    state.message = state.message.split('').reverse().join('')
                }

                let timer
                const counterState = reactive({
                    counter:1,
                    doubleCounter: computed(() => counterState.counter * 2)
                })
                onMounted(()=>{
                    timer = setInterval(() => {
                        counterState.counter++
                    }, 1000);
                })
                onUnmounted(() => {
                    clearInterval(timer)
                })
                const desc = ref(null)
                watch(()=>counterState.counter, (val,oldVal)=>{
                    desc.value.textContent = `counter change from ${oldVal} to ${val}`
                })

                return {
                    state,
                    click,
                    desc,
                    ...toRefs(counterState)
                }
            }
        }
        createApp(MyComponent).mount('#app')
    </script>
</body>

Teleport

传送门组件提供一种简洁的方式可以指定它里面内容的父元素。

<template>
  <button @click="modalOpen = true">
    弹出一个全屏模态窗口
  </button>
  <teleport to="body">
    <div v-if="modalOpen" class="modal">
      <div>
        这是一个模态窗口!
        我的父元素是"body"!
        <button @click="modalOpen = false">Close</button>
      </div>
    </div>
  </teleport>
</template>

image.png

Fragments

vue3中组件可以拥有多个根

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

Emits Component Option

vue3中组件发送的自定义事件需要定义在emits选项中:

好处:

  • 原生事件会触发两次,比如click
  • 更好的指示组件工作方式
  • 对象形式事件校验
<template>
  <div @click="$emit('my-click')">
    <h3>自定义事件</h3>
  </div>
</template>
<script>
export default {
  emits: ['my-click']
}
</script>

自定义渲染器 custom renderer

这个 API 可以用来自定义渲染逻辑

案例:把数据渲染到canvas上

  • 创建CanvasApp.vue。创建一个组件描述要渲染的数据,因为我们只是想把它携带的数据绘制到canvas上,不需要单独声明该组件。
<template>
  <piechart 
  @click="handleClick" 
  :data="state.data" 
  :x="200" 
  :y="200" 
  :r="200">
  </piechart>
</template>
<script>
import { reactive, ref } from "vue";
export default {
  setup() {
    const state = reactive({
      data: [
        { name: "大专", count: 200, color: "brown" },
        { name: "本科", count: 300, color: "yellow" },
        { name: "硕士", count: 100, color: "pink" },
        { name: "博士", count: 50, color: "skyblue" }
      ]
    });
    function handleClick() {
      state.data.push({ name: "其他", count: 30, color: "orange" });
    }
    return {
      state,
      handleClick
    };
  }
};
</script>

index.html 里面添加一个 div#demo

  • 创建自定义渲染器,main.js
import { createApp, createRenderer } from 'vue'
import CanvasApp from './CanvasApp.vue'
const nodeOps = {
  insert: (child, parent, anchor) => {
    // 我们重写了insert逻辑,因为在我们canvasApp中不存在实际dom插入操作
    // 这里面只需要将元素之间的父子关系保存一下即可
    child.parent = parent;

    if (!parent.childs) { 
      parent.childs = [child]
    } else {
      parent.childs.push(child);
    }
    // 只有canvas有nodeType,这里就是开始绘制内容到canvas
    if (parent.nodeType == 1) {
      draw(child); 
      // 如果子元素上附加了事件,我们给canvas添加监听器
      if (child.onClick) {
        ctx.canvas.addEventListener('click', () => {
          child.onClick();
          setTimeout(() => {
            draw(child)
          }, 0);
        })
      }
    }
  },
  remove: child => {},
  createElement: (tag, isSVG, is) => {
    // 创建元素时由于没有需要创建的dom元素,只需返回当前元素数据对象
    return {tag}
  },
  createText: text => {},
  createComment: text => {},
  setText: (node, text) => {},
  setElementText: (el, text) => {},
  parentNode: node => {},
  nextSibling: node => {},
  querySelector: selector => {},
  setScopeId(el, id) {},
  cloneNode(el) {},
  insertStaticContent(content, parent, anchor, isSVG) {},
  patchProp(el, key, prevValue, nextValue) {
    el[key] = nextValue;
  },
};

// 创建一个渲染器
let renderer = createRenderer(nodeOps);

// 保存画布和其上下文
let ctx;
let canvas;

// 扩展mount,首先创建一个画布元素
function createCanvasApp(App) {
  const app = renderer.createApp(App);
  const mount = app.mount
  app.mount = function (selector) {
    canvas = document.createElement('canvas');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    document.querySelector(selector).appendChild(canvas);
    ctx = canvas.getContext('2d');
    mount(canvas);
  }
  return app
}

createCanvasApp(CanvasApp).mount('#demo')
  • 编写绘制逻辑
const draw = (el,noClear) => {
    if (!noClear) {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
    }
    if (el.tag == 'piechart') {
      let { data, r, x, y } = el;
      let total = data.reduce((memo, current) => memo + current.count, 0);
      let start = 0,
          end = 0;
      data.forEach(item => {
        end += item.count / total * 360;
        drawPieChart(start, end, item.color, x, y, r);
        drawPieChartText(item.name, (start + end) / 2, x, y, r);
        start = end;
      });
    }
    el.childs && el.childs.forEach(child => draw(child,true));
  }
  
  const d2a = (n) => {
    return n * Math.PI / 180;
  }
  const drawPieChart = (start, end, color, cx, cy, r) => {
    let x = cx + Math.cos(d2a(start)) * r;
    let y = cy + Math.sin(d2a(start)) * r;
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.lineTo(x, y);
    ctx.arc(cx, cy, r, d2a(start), d2a(end), false);
    ctx.fillStyle = color;
    ctx.fill();
    ctx.stroke();
    ctx.closePath();
  }
  const drawPieChartText = (val, position, cx, cy, r) => {
    ctx.beginPath();
    let x = cx + Math.cos(d2a(position)) * r/1.25 - 20;
    let y = cy + Math.sin(d2a(position)) * r/1.25;
    ctx.fillStyle = '#000';
    ctx.font = '20px 微软雅黑';
    ctx.fillText(val,x,y);
    ctx.closePath();
  }

非兼容的变更

Global API

Global API 改为应用程序实例调用

vue2.X许多全局API和配置可以全局改变Vue的行为,比如:

//全局组件
Vue.component('button-counter', {
  data: () => ({
    count: 0
  }),
  template: '<button @click="count++">Clicked {{ count }} times.</button>'
})
//类似的全局指令声明方式
Vue.directive('focus', {
  inserted: el => el.focus()
})

导致的一些问题:

  • vue2 ,没有“app”概念,w我们定义的应用只是通过new Vue()创建的根Vue实例。从同一个 Vue 构造函数创建的每个根实例共享相同的全局配置。测试期间,全局配置很容易意外污染其他测试用例,导致测试变得困难
  • 全局配置也导致没有办法在单页面创建不同全局配置的多个app实例 vue3为了避免这些问题,使用createApp返回一个应用实例,由它暴露一系列全局API
import { createApp } from 'vue'
const app = createApp({})
    .component('comp', { render: () => h('div', 'i am comp') })
    .mount('#app')

vue2全局API及其相应的实例API列表:

2.x 全局 API3.x 实例 API (app)
Vue.configapp.config
Vue.config.productionTip移除 (见下方)
Vue.config.ignoredElementsapp.config.compilerOptions.isCustomElement (见下方)
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use (见下方)
Vue.prototypeapp.config.globalProperties (见下方)
Vue.extend移除 (见下方)
全局和内部 API 重构为可 tree-shakable(摇树优化)

vue2中一些类似Vue.nextTick(),作为静态函数直接挂在构造函数上,如果我们没在应用中使用过它,就会形成'死代码'。这类global-api造成的dead code无法使用webpack的tree-shaking排除掉,都会被包含在最终的打包产物中。

vue3中做出相应变化,考虑到tree-shaking的支持,通过具名导出进行访问这些global-api

import { nextTick } from 'vue'
nextTick(()=>{
// something something DOM-related
})

受影响的API:

  • Vue.nextTick
  • Vue.observable (用 Vue.reactive 替换)
  • Vue.version
  • Vue.compile (仅完整构建版本)
  • Vue.set (仅兼容构建版本)
  • Vue.delete (仅兼容构建版本)

模板指令

model选项和v-bindsync 修饰符被移除,统一为v-model参数形式

vue2中.sync和v-model功能有重叠,容易混淆,vue3做了统一

  • prop:value -> modelValue
  • 事件:input -> update:modelValue
  • v-bind 的 .sync 修饰符和组件的 model 选项已移除,可在 v-model 上加一个参数代替
<div @click="$emit('update:modelValue',modelValue + 1)">
    update:modelValue:{{modelValue}}
</div>
<div @click="$emit('update:counter',counter + 1)">
    update:counter:{{counter}}
</div>
props:{
    modelValue:{
        type:Number,
        default:0
    },
    counter:{
        type:Number,
        default:0
    }
}

<VmodelTest v-model = "count"></VmodelTest>
<VmodelTest v-model:counter = "count"></VmodelTest>
<template v-for> 和非 v-for 节点上的 key 用法已更改
  • vue2中template标签不能拥有key,不过可以为其每个子节点分别设置key
<template v-for="item in list">
  <div :key="'heading-' + item.id">...</div>
  <span :key="'content-' + item.id">...</span>
</template>

vue3中,key则应该设置在template标签上。

<template v-for="item in list" :key="item.id">
  <div>...</div>
  <span>...</span>
</template>

当使用 <template v-for> 时如果存在使用 v-if 的子节点,则 key 应改为设置在 <template> 标签上。

<!-- Vue 2.x -->
<template v-for="item in list">
  <div v-if="item.isVisible" :key="item.id">...</div>
  <span v-else :key="item.id">...</span>
</template>

<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
  <div v-if="item.isVisible">...</div>
  <span v-else>...</span>
</template>

组件

只能使用普通函数创建函数式组件
  • 性能提升在vue3中可忽略不计,所以vue3中推荐使用状态组件
  • 函数式组件只能由接收 props 和 context (即:slotsattrsemit) 的普通函数创建
  • SFC中<template>不能添加functional特性声明函数式组件
  • { functional: true } 选项已从通过函数创建的组件中移除
import { h } from 'vue'
const Heading = (props, context) => {
  return h(`h${props.level}`, context.attrs, context.slots)
}
Heading.props = ['level']
export default Heading
异步组件使用变化

异步组件要求使用defineAsyncComponent方法创建

定义一个异步组件:

import { defineAsyncComponent } from "vue"

//不带选项的异步组件
const asyncComp = defineAsyncComponent(()=>import('./other.vue'))

//带选项的异步组件
//loader选项是以前的component
// 如error loading 组件
const asyncModalWithOptions = defineAsyncComponent({
  loader: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

渲染函数

渲染函数 API 更改
$scopedSlots 移除,所有插槽都通过 $slots 作为函数暴露
  • 不再传入 h 函数,需要我们手动导入
import { h } from "vue";

render() {
// 获取插槽
// 2.x this.$scopedSlots.content()
// this.$slots.default()
// console.log(this.$slots.content())
const emit = this.$emit
return h("div", [
  h(
    "div",{
       onClick:this.onClick 
    },
    `i am renderTest, ${this.counter}`,
    this.$slots.default(),
    this.$slots.content()
  )
]);
}
<RenderTest v-model:counter="count">
    <template v-slot:default>
      <p>默认插槽</p>
    </template>
    <template v-slot:content>
      <p>content 具名插槽</p>
    </template>
</RenderTest>

自定义元素

自定义组件白名单

vue3中在模板编译期间检测自定义元素,如果添加一些vue之外的自定义元素,需要在编译器选项中设置 isCustomElement选项

  • 使用了 vue-loader,则应通过 vue-loader 的 compilerOptions 选项传递
// webpack 中的配置
rules: [
  {
    test: /.vue$/,
    use: 'vue-loader',
    options: {
      compilerOptions: {
        isCustomElement: tag => tag === 'plastic-button'
      }
    }
  }
  // ...
]
  • vite 在vite.config.js中配置 vueCompilerOptions

  • 如果使用动态模板编译,通过 app.config.compilerOptions.isCustomElement 传递:

const app = Vue.createApp({})
app.config.compilerOptions.isCustomElement = tag => tag === 'plastic-button'

is 属性仅限于用在component标签

vue3中设置动态组件时,is属性仅能用于component标签上

<component is='comp'></component>

在 DOM 模板中使用时,模板受原生 HTML 解析规则的约束。一些 HTML 元素,例如 <ul><ol><table> 和 <select> 对它们内部可以出现的元素有限制,以及一些像 <li><tr>、和 <option> 只能出现在特定的其他元素中。

2.X 在原生标签上使用 is attribute 来绕过这些限制:

<table>
  <tr is="blog-post-row"></tr>
</table>

3.x 随着 is 的行为发生变化,现在将元素解析为 Vue 组件需要添加一个 vue: 前缀:(仅限in-dom模板)

<table>
  <tr is="vue:blog-post-row"></tr>
</table>

其他

组件data选项总是声明为函数

2.x中可通过object 或者是 function 定义 data 选项,在 3.x 中,data 选项只接受返回 object 的 function

//2.x
const app = new Vue({
    data: {
      apiKey: 'a1b2c3'
    }
  })
  
//3.x
 createApp({
    data() {
      return {
        apiKey: 'a1b2c3'
      }
    }
  }).mount('#app')
自定义指令Api和组件保持一致
  • 指令的钩子函数已经被重命名,以更好地与组件的生命周期保持一致。 2.x
**bind** - 指令绑定到元素后调用。只调用一次。
**inserted** - 元素插入父 DOM 后调用。
**update** - 当元素更新,但子元素尚未更新时,将调用此钩子。
**componentUpdated** - 一旦组件和子级被更新,就会调用这个钩子。
**unbind** - 一旦指令被移除,就会调用这个钩子。也只调用一次。

<p v-highlight="'yellow'">以亮黄色高亮显示此文本</p>
Vue.directive('highlight', {
  bind(el, binding, vnode) {
    el.style.background = binding.value
  }
})

3.x

  • created - 新增!在元素的 attribute 或事件监听器被应用之前调用。
  • bind → beforeMount
  • inserted → mounted
  • beforeUpdate:新增!在元素本身被更新之前调用,与组件的生命周期钩子十分相似。
  • update → 移除!该钩子与 updated 有太多相似之处,因此它是多余的。请改用 updated
  • componentUpdated → updated
  • beforeUnmount:新增!与组件的生命周期钩子类似,它将在元素被卸载之前调用。
  • unbind -> unmounted
const app = Vue.createApp({})

app.directive('highlight', {
  created(el, binding, vnode, prevVnode) {}, // 新增
  beforeMount(el, binding, vnode) {
    el.style.background = binding.value
  },
  mounted() {},
  beforeUpdate() {}, // 新增
  updated() {},
  beforeUnmount() {}, // 新增
  unmounted() {}
})
transition(过渡)类名变更
  • v-enter 修改为 v-enter-from
  • v-leave 修改为 v-leave-from

image.png

watch 变化

.分割的表达式不再被watch和watch支持,可以使用计算函数作为watch支持,可以使用计算函数作为watch参数实现

this.$watch(() => this.foo.bar, (v1, v2) => { 
    console.log(this.foo.bar) 
})
已挂载的应用不会取代它所挂载的元素

在 Vue 2.x 中,当挂载一个具有 template 的应用时,被渲染的内容会替换我们要挂载的目标元素。在 Vue 3.x 中,被渲染的应用会作为子元素插入,从而替换目标元素的 innerHTML

//vue2.x
<body>
  <div id="rendered">Hello Vue!</div>
</body>

//vue3.x
<body>
  <div id="app" data-v-app="">
    <div id="rendered">Hello Vue!</div>
  </div>
</body>

被移除的API

keyCode 作为 v-on 修饰符被移除

不再支持使用数字 (即键码) 作为 v-on 修饰符

<!-- 键码版本 -->
<input v-on:keyup.13="submit" />

<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />

on$off 和 $once 实例方法移除

on$off 和 $once 可以使用其他三方库实现: 示例

// npm i mitt -S
// 创建emitter 

const emitter = mitt() 

// 发送事件 
emitter.emit('foo', 'foooooooo') 

// 监听事件
emitter.on('foo', msg => console.log(msg))

filters 过滤器移除

在 3.x 中,过滤器已移除,且不再支持。建议用方法调用或计算属性来替换它们

一个案例通关vue3 核心特性

TodoMVC

组件化实战

将上面todo案例组件化:

1. EditTodo组件:新增、编辑待办项(todo)

  • 非属性特性的展开 v-bind = '$attrs'
  • 因为要复用 , 使用全局注册
  • 自定义组件v-model的实现
    • 传参改变默认modelValue v-model:todo-title="newTodo"
<input
  type="text"
  :value="todoTitle"
  @input = "onInputChange"
  v-bind="$attrs"
/>

import EditTodo from './components/todos/EditTodo.vue'

createApp(App).component('EditTodo',EditTodo)

<EditTodo
  v-model:todo-title="newTodo"
  @keyup.enter="addTodo"
  autofocus
  placeholder="新增今日待办"
  autocomplete="off"
></EditTodo>

2. TodoItem组件:todo列表

  • 输入
    • todo
    • editedTodo v-model:edited-todo="editedTodo"(对传入的todo选择和编辑,要和外面交互)
  • 输出
    • editTodo:更新editedTodo的事件
    • removeTodo:todo删除事件
    • 内部状态 beforeEditCache(缓存编辑前的title)
  • 逻辑、交互
    • editTodo
      • editTodo为父组件状态,emit("update:edited-todo", todo);
    • removeTodo
      • 派发事件,父级去做删除 emit("remove-todo", todo);
      • setup(props, { emit }) {}
    • doneEdit
    • cancelTodo
  • emits: ["remove-todo", "update:edited-todo"]
  • v-todo-focus 局部指令
directives: {
    "todo-focus": (el, { value }) => {
      if (value) {
        el.focus();
      }
    },
  }

成功抽离

<ul>
  <TodoItem v-for="todo in filterdTodos" 
  :key="todo.id"
  :todo = "todo" 
  v-model:edited-todo="editedTodo"
  @remove-todo="removeTodo"
  ></TodoItem>
</ul>

3. filter

  • 输入
    • 过滤条件 items
    • 选中项 modelValue
  • 输出 'update:modelValue'

4. 进一步抽离

  • 与todos相关逻辑
    • 缓存和获取
    • todos、addTodo、removeTodo、watchEffect
  • filter相关