Vue3(上)


1.Vue3 Start

他在介绍中表明的是:

  • 更小、更快、更易维护,一些命令的变化。
  • 3.0 新加入了 TypeScript 以及 PWA 的支持
  • 支持了 composiiton API
  • vdom的对比算法更新,只更新绑定了动态数据部分
  • 单独功能可以抽离 取代了mixin(尤雨溪作者本人指出minxin模块来源不清晰、命名问题、性能开销问题)

源码改动

它的源码是通过monorepo的形式管理源代码的

  • Mono单个
  • Repo:repository仓库
  • 主要是将许多项目的代码存储在同一个repository仓库中,,这样的话可以让多个包相互独立的同时,又在同一个仓库下管理

使用TypeScript进行代码重写

性能改动

  • 使用Proxy进行数据劫持,这也在之前做Vue2笔记的时候提及过,避免了很多索引类的bug
  • 删除了一些没必要的API,如$on、$off、$once等,删除了一些特性,如filter、内联模板等
  • 编译优化,生成block tree、Slot编译优化、diff算法优化

新的API

Options API -> Composition API

使用Hooks函数增加代码的复用性

  • 在Vue2的时候也是因为Options和mixins相互关联,多个mixins还有命名冲突问题

基础语法

基础语法和Vue2大致相同,除了

原来

const app = new Vue({})

变成了

const app = Vue.createApp({})

查看源码

(2021/11/6),在官网git clone项目,使用pnpm下载依赖,使用 yarn dev打包出文件进行调试,然后在项目下引用这个打包好的包(/dist/vue.global.js)即可

可以在webpack的设置里面

"scripts": {
    "dev": "node scripts/dev.js --sourcemap",
    //...
}

此时再次打完包之后会在dist下出现一个 vue.global.js.map 文件,此时调试代码的时候 进行 step into next function可以跳转到达文件夹而非打包文件下对应的方法

注意:需要在chrome的setting里设置 Enable JavaScript source map

阅读源码推荐的插件:vscode的拓展插件bookmarks,然后control + option + k 键即可标记当前函数,以后方便在拓展中直接找到(还能明明标记名称);重复 control + option + k键可以取消

2.回顾Vnode

VNode全称Virtual Node,也就是虚拟节点(VNode Tree组成虚拟DOM);在Vue中无论大大小小元素都可以在Vue中被VNode表示出来,

类似如下

const vnode = {
    type: "div",
    props: {
        class: "title"
    },
    children: "hello world"
}

它本质上是一个对象,作为一个对象而未转化为DOM,最大的好处在于多平台的适配

diff

参考:聊聊 Vue 的双端 diff 算法

旧的diff流程:

  • 使用key复用旧节点,通过移动节点代替创建。但是比如处理到第二个新的 vnode,发现它在旧的 vnode 数组中的下标为 4,说明本来就是在后面了,那就不需要移动了。反之,如果是 vnode 查找到的对应的旧的 vnode 在当前 index 之前才需要移动

    比如节点:ABCD -> DABC,移动的是ABC,移动3次

  • 新的 vnode 数组全部处理完后,旧的 vnode 数组可能还剩下一些不再需要的,那就删除掉

Vue2的diff流程:

双端 diff

  • 头和尾的指针向中间移动,直到 oldStartIdx <= oldEndIdx 并且 newStartIdx <= newEndIdx,说明就处理完了全部的节点。

    每次对比下两个头指针指向的节点、两个尾指针指向的节点,头和尾指向的节点,是不是 key是一样的,也就是可复用的。

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (oldStartVNode.key === newStartVNode.key) { // 头头
    patch(oldStartVNode, newStartVNode, container)
    oldStartVNode = oldChildren[++oldStartIdx]
    newStartVNode = newChildren[++newStartIdx]
  } else if (oldEndVNode.key === newEndVNode.key) {//尾尾
    patch(oldEndVNode, newEndVNode, container)
    oldEndVNode = oldChildren[--oldEndIdx]
    newEndVNode = newChildren[--newEndIdx]
  } else if (oldStartVNode.key === newEndVNode.key) {//头尾,需要移动
    patch(oldStartVNode, newEndVNode, container)
    insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)

    oldStartVNode = oldChildren[++oldStartIdx]
    newEndVNode = newChildren[--newEndIdx]
  } else if (oldEndVNode.key === newStartVNode.key) {//尾头,需要移动
    patch(oldEndVNode, newStartVNode, container)
    insert(oldEndVNode.el, container, oldStartVNode.el)

    oldEndVNode = oldChildren[--oldEndIdx]
    newStartVNode = newChildren[++newStartIdx]
  } else {

    // 头尾没有找到可复用的节点
    // 那就在旧节点数组中找,找到了就把它移动过来,并且原位置置为 undefined。没找到的话就插入一个新的节点。
  }
}

如果旧 vnode 的尾节点是新 vnode 的头结点,那就要把它移动到旧 vnode 的头结点的位置。

此时减少了节点移动次数

vue3的diff做了一点优化,它借鉴于Inferno、ivi框架

对于diff算法来说,如果列表中有key,则执行 patchKeyedChildren方法;没有 key,执行 patchUnkeyedChildren方法

patchUnkeyedChildren函数:取较短的节点进行遍历,一一对比(patch),如果不是之前的节点,就直接变,不会复用,少了就mount,多了就删除

patchKeyedChildren函数:从头开始while循环,让节点进行对比,节点类型不同直接break;

  1. 然后从头尾双端开始while,让头节点、尾节点进行对比,相同继续循环,不同直接跳出循环;(对应下面的1、2)

  2. 然后判断是否新增节点,在当前子序列适当位置插入,拿一个null和新节点进行patch,挂载一个新的节点;(对应下面的3)

  3. 然后再判断如有仍残留旧节点,则进行删除(unmount);(对应下面的4)

  4. 最后处理中间乱序的节点,新建一个map哈希表,以key和节点作为键值对存储,使用key进行匹配、对比,确定最长连续的子序列,然后对新节点数组进行移动/删除/增加(对应下面5)

详情可以看这个解析

尤大还很贴心,源码的注释比较明了

let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index

// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
  const n1 = c1[i]
  const n2 = (c2[i] = optimized
              ? cloneIfMounted(c2[i] as VNode)
              : normalizeVNode(c2[i]))
  if (isSameVNodeType(n1, n2)) {
    //...
    //patch n1,n2
  } else {
    break
  }
  i++
}

// 2. sync from end
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
  const n1 = c1[e1]
  const n2 = (c2[e2] = optimized
              ? cloneIfMounted(c2[e2] as VNode)
              : normalizeVNode(c2[e2]))
  if (isSameVNodeType(n1, n2)) {
    //...
    //patch n1,n2
  } else {
    break
  }
  e1--
  e2--
}

// 3. common sequence + mount
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i > e1) {
  if (i <= e2) {
    const nextPos = e2 + 1
    const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
    while (i <= e2) {
      patch(
        null,
        (c2[i] = optimized
         ? cloneIfMounted(c2[i] as VNode)
         : normalizeVNode(c2[i])),
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      i++
    }
  }
}

// 4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
else if (i > e2) {
  while (i <= e1) {
    unmount(c1[i], parentComponent, parentSuspense, true)
    i++
  }
}

// 5. unknown sequence
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
else {
  const s1 = i // prev starting index
  const s2 = i // next starting index

  // 5.1 build key:index map for newChildren
  const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
  for (i = s2; i <= e2; i++) {
    const nextChild = (c2[i] = optimized
                       ? cloneIfMounted(c2[i] as VNode)
                       : normalizeVNode(c2[i]))
    if (nextChild.key != null) {
      if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
        warn(
          `Duplicate keys found during update:`,
          JSON.stringify(nextChild.key),
          `Make sure keys are unique.`
        )
      }
      keyToNewIndexMap.set(nextChild.key, i)
    }
  }

  // 5.2 loop through old children left to be patched and try to patch
  // matching nodes & remove nodes that are no longer present
  let j
  let patched = 0
  const toBePatched = e2 - s2 + 1
  let moved = false
  // used to track whether any node has moved
  let maxNewIndexSoFar = 0
  // works as Map<newIndex, oldIndex>
  // Note that oldIndex is offset by +1
  // and oldIndex = 0 is a special value indicating the new node has
  // no corresponding old node.
  // used for determining longest stable subsequence
  const newIndexToOldIndexMap = new Array(toBePatched)
  for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

  for (i = s1; i <= e1; i++) {
    const prevChild = c1[i]
    if (patched >= toBePatched) {
      // all new children have been patched so this can only be a removal
      unmount(prevChild, parentComponent, parentSuspense, true)
      continue
    }
    let newIndex
    if (prevChild.key != null) {
      newIndex = keyToNewIndexMap.get(prevChild.key)
    } else {
      // key-less node, try to locate a key-less node of the same type
      for (j = s2; j <= e2; j++) {
        if (
          newIndexToOldIndexMap[j - s2] === 0 &&
          isSameVNodeType(prevChild, c2[j] as VNode)
        ) {
          newIndex = j
          break
        }
      }
    }
    if (newIndex === undefined) {
      unmount(prevChild, parentComponent, parentSuspense, true)
    } else {
      newIndexToOldIndexMap[newIndex - s2] = i + 1
      if (newIndex >= maxNewIndexSoFar) {
        maxNewIndexSoFar = newIndex
      } else {
        moved = true
      }
      patch(
        prevChild,
        c2[newIndex] as VNode,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      patched++
    }
  }

  // 5.3 move and mount
  // generate longest stable subsequence only when nodes have moved
  const increasingNewIndexSequence = moved
  ? getSequence(newIndexToOldIndexMap)
  : EMPTY_ARR
  j = increasingNewIndexSequence.length - 1
  // looping backwards so that we can use last patched node as anchor
  for (i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i
    const nextChild = c2[nextIndex] as VNode
    const anchor =
          nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
    if (newIndexToOldIndexMap[i] === 0) {
      // mount new
      patch(
        null,
        nextChild,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else if (moved) {
      // move if:
      // There is no stable subsequence (e.g. a reverse)
      // OR current node is not among the stable sequence
      if (j < 0 || i !== increasingNewIndexSequence[j]) {
        move(nextChild, container, anchor, MoveType.REORDER)
      } else {
        j--
      }
    }
  }
}

render的优化

1.对于不会改动的静态节点,会进行作用域的提升

2.只对动态的节点进行diff算法(将动态的节点的数据放入dynamicChildren,diff的时候执行patchBlockChildren函数)

<template>
    <div>Allen</div>
    <div>Allen</div>
    <div>{{message}}</div>
    <button @click="changeMessage">修改</button>
</template>
function anonymous{
    //静态节点
    const _hoisted_1 =/*#_PURE_*/createVNode( "div", null,"Allen",-1/* HOISTED */);
    const _hoisted_2 =/*#_PURE_*/createNode ("h2" ,null,"Allen" , -1/* HOISTED */);
    return function render(_ctx, _cache){
        with (_ctx) {
            const { createVNode: _createWNode, toDisplayString: _toDisplayString, Fragment:_Fragment, openBlock: _openBlock, createBlock:_createBlock} = _cache;
            return (_openBlock(), _createBlock(_Fragment,null,[
                _hoisted_1,
                _hoisted_2,
                _createVode("p",null,_toDisplayString(message)1,/* TEXT*/),
                _createVNode( "button", i oncClick: changeMessage }"修改" , 8 /* PROPS */[ "onClick"1)],64/*STABLE_FRAGMENT */))
        }
    }
}

3.API对于2.x的改动

全局API

调用 createApp 返回一个应用实例,一个 Vue 3 中的新概念。

许多原本 Vue.xxx的操作,现在是通过 const app = Vue.createApp({})创建实例,,然后变成了 app.xxx

2.x 全局 API 3.x 实例 API (app)
Vue.config app.config
Vue.config.productionTip 移除
Vue.config.ignoredElements app.config.compilerOptions.isCustomElement
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
Vue.prototype app.config.globalProperties
Vue.extend 移除

template

原来Vue2.x中只允许有一个根元素(一般为div)

Vue3.x以上,允许template有多个根元素,且允许不需要div标签进行包裹,从源码可以知道,如果有多个根元素,则他会在最外层给你多加一层 Fragment

option API - > Composition API

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

Vue2.x

app.$mount('#app')
<body>
  <div id="app">
    Some app content
  </div>
</body>
<body>
  <div id="rendered">Hello Vue!</div>
</body>

Vue3

app.mount('#app')
<body>
  <div id="app" data-v-app="">
    <div id="rendered">Hello Vue!</div>
  </div>
</body>

data选项

组件选项 data 的声明不再接收纯 JavaScript object,而是接收一个 function(只接收 funciton 形式了)。

子组件emit

Vue 3 现在提供一个 emits 选项,和现有的 props 选项类似。这个选项可以用来定义一个组件可以向其父组件触发的事件。

和 prop 类似,现在可以通过 emits 选项来定义组件可触发的事件:


该选项也可以接收一个对象,该对象允许开发者定义传入事件参数的验证器,和 props 定义里的验证器类似。

emit的对象写法,一般针对于将传递给父组件的参数进行验证,如果false,则vue会报一个警告(但是参数还是会传过去)

export default {
    emits: {
        add: null, //不需要传参数
        addn: (payload1, payload2) => { //有俩参数
            if (payload1 < 10) return false;
            return true;
        }
    }
}

keep-alive内置组件

可以传入数组类的值作为include、exclude属性

  • include - string | RegExp | Array。只有名称匹配的组件会被缓存。

  • exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。

  • max - number | string。最多可以缓存多少组件实例。

<keep-alive include="a,b">
    <component :is="componentId"></component>
</keep-alive>
<keep-alive :include="/a|b/">
    <component :is="componentId"></component>
</keep-alive>
<keep-alive :include="[a,b]">
    <component :is="componentId"></component>
</keep-alive>

异步组件

Vue3中使用异步组件可以导入内置的 defineAsyncComponent

它接受两种参数

  • 类型一工厂函数,该工厂函数需要返回一个Promise对象

    • import {defineAsyncComponent} from vue;
      // 此时vue打包时会将该异步组件单独打包到另外一个js文件里
      const AsyncComponent = defineAsyncComponent(() => import('./AsyncTest.vue'))
  • 接受一个对象类型,对异步函数进行配置

    • const AsyncComponent = defineAsyncComponent({
        loader: () => import('./AsyncTest.vue'),
        loadingComponent: Loading,  //加载时占位的组件
      })

组件的v-model

在 3.x 中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件:

<ChildComponent v-model="pageTitle" />

<!-- 是以下的简写: -->

<ChildComponent
  :modelValue="pageTitle"
  @update:modelValue="pageTitle = $event"
/>

子组件则相对处理


当然也可以使用computed,以计算属性关联数据的设置和获取,子组件直接通过v-model绑定computed,一种更优雅的方式



值得注意的是,如果v-model传进去的值是一个对象,而对象里面属性值发生改变的时候,子组件的computed并不能及时emit发送消息到父组件进行更改,而是直接修改props,从而让我们“看到”父组件的对象的属性值修改以后的样子,所以这种方法面对对象类型的props时,是无用的!!!!!!

组件v-model注意事项
<!-- 父组件传入的pageObj是一个对象 -->
<child-component v-model="pageObj" />


实际上相当于



而面对复杂数据类型v-model的解决方案

方案一:子组件使用ref包裹浅拷贝 + watch监听,向父组件发送事件

export default defineComponent({
  props: {
    modelValue: {
      type: Object,
      default: () => ({})
    }
  },
  setup(props, { emit }) {
    const FormField = ref({ ...props.modelValue })
    watch(
      FormField,
      (newValue) => {
        emit('update:modelValue', newValue)
      },
      {
        deep: true
      }
    )
    return {
      FormField
    }
  }
})

而此时父组件修改这个props时要这样修改才有效



方案二:子组件直接不使用v-model,直接用值来绑定就好了(更容易理解)

对input使用v-bind分批绑定,然后input里面的修改使用事件监听 + 发送事件到父组件那边

<!-- 子组件: -->

<el-input
  :modelValue=""
  @update:modelValue="事件处理"
/>
自定义v-model

如果想要在一个子组件上绑定多个v-model,则可以使用 v-model:自定义名称="xx"来定义其他绑定的属性名称

此时子组件通过 props: { 自定义名称: String, },来接收,通过 this.$emit('update:自定义名称', value)方法来发送监听事件参数

示例:

父组件

<child v-model="message" v-model:title="title" />

子组件



h()函数

Vue推荐绝大多数情况下使用template模板创建你的HTML,如果一些场景你真的需要JavaScript的完全编程能力,这时候你可以使用渲染函数,他比模板更接近编译器

可以理解为 template(经过compile) -> render -> vnode,此时我们直接写render,过程可以在Vue(中)的脚手架部分的runtime only看到

h函数

  • 用于创建一个vnode的函数
  • 其实原名为createVNode函数,但是为了简便在Vue简称为h函数

参数传入:

// @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'
        })
    ]
)

总结就是:1.标签 | 组件,2. 属性, 3. 子组件 | 子标签

注意:如果没有props(属性),可以将子组件作为第二个传入;而如果会产生歧义,则将null作为第二个参数传入

import { h } from "vue";
export default {
  render() {
    return h(
      "div",
      {
        class: "app",
      },
      "hello Render"
    );
  },
};

在setup中替代h函数

在setup函数的返回值替换成一个函数,函数返回值为h函数

setup() {
    const count = ref(0);
    return () => {
      return h("div", { class: "app" }, [
        h("h2", null, `当前计数${count.value}`),
        h(
          "button",
          {
            onClick: () => count.value++,
          },
          "+"
        ),
        h(
          "button",
          {
            onClick: () => count.value--,
          },
          "-"
        ),
        h(Child, null, ""),
      ]);
    };
  },

使用JSX

和h函数同理,只不过在render函数中写成jsx形式

export default {
  render() {
    return <h1>hello world</h1>;
  },
};

生命周期

beforeDestroy -> beforeUnmount

destroyed -> unmounted

setup函数执行 -> applyOptions调用(beforeCreate、Created生命周期函数调用)-> 剩下的生命周期转换成setup的API(onBeforeMountonMounted

对于beforeMount之后的生命周期函数,其中beforexxx的生命周期函数,都是立即调用,而xxx的生命周期函数,则放入队列,等到该周期完毕之后,再flushPostFlushCbs刷新队列,执行函数

所以setup执行时机比beforeCreate生命周期还早!

路由使用

若在搭建脚手架时没有安装,则需要手动安装vue路由管理

npm i vue-router@next

原本导出路由:

import Router from 'vue-router'
import Vue from 'vue'
Vue.use(Router);
const routes = [];
const router = new Router({
    routes
})
export default router;

现在同createApp,导出单个方法createRouter

而模式的选择也不是之前使用字符串的形式对mode属性赋值,而要导入 createWebHistory、createWebHashHistory

import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
const router = createRouter({
    routes,
    history: createWebHistory();
})
export default router;

然后在main.js中

//...
import router from './router'
const app = createApp(App)
app.use(router);
app.mount('#app');

router-viewrouter-link的使用同之前vue2.x一样

router-link

v-slot API (Vue Router3.1.0 新增)

router-link的插槽slot,同样可以使用作用域插槽,但是它的插槽自定义props对象其中有包含了一些属性

  • props: href 跳转的链接
  • props: route route对象
  • props: navigate导航函数,需要配合custom属性使用
  • props: isActive 是否当前处于活跃状态
<router-link to="/home" v-slot="props">
    <h2>{{ props.href }}</h2>
</router-link>
<router-view />

router-view

v-slot API (Vue Router3.1.0 新增)

<!-- 通过props.Component得到目前渲染出来的组件 -->
<!-- 此时通过component拿到对应的组件,然后就可以放入transition / keep-alive 中,使用动画 / 缓存了 -->
<router-view v-slot="props">
    <transition name="allen">
        <keep-alive>
            <component :is="props.Component"></component>
        </keep-alive>
    </transition>
</router-view>

动态添加路由

有时候一些应用场景需要我们去动态添加路由,而不是一开始将routes写死,我们可以使用 router.addRoute

addRoute(route: RouteConfig): () => void 添加一条新路由规则

addRoute(parentName: string, route: RouteConfig): () => void 添加一条新的路由规则记录作为现有路由的子路由

const routes = [];
const router = createRouter({
    routes,
})
if(管理员){
    router.addRoute({ path: "/order", component = () => import('../components/order') })
    // 添加二级路由
    router.addRoute('home', {
        path:'moment',
        component: () => import('../components/HomeMoment')
    })
}
export default router;

当然,除了动态添加路由,当然也有动态删除路由

  1. 方式一:添加一个name相同的路由
  2. 方式二:使用 removeRoute(路由名称)
  3. 方式三:addRoute返回一个函数,调用这个函数则会删除该路由

vuex

安装

npm i vuex@next

使用和之前类似,只是依然要导包

import { createStore } from 'vuex';
const store = createStore({
    state() {
        return {
            //...
        }
    }
});
export default store

然后在main.js中

//...
import store from './store'
const app = createApp(App)
app.use(store);
app.mount('#app');

vue中全局变量、函数

可以使用 app.config.globalProperties 进行定义(vue.prototype废除)

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

const app = createApp(App)
app.use(store)
app.use(router)
// 为了区分全局变量,在函数前面加一个$
app.config.globalProperties.$filter = {
  formatTime(time: string) {
    console.log('time', time);
  }
}
app.mount('#app')

在组件中使用

<strong>{{ $filter.formatTime('666') }}</strong>

4.Composition API

option API 模式的弊端

  • 当我们实现一个功能时,这个功能对应的代码逻辑会被拆分到各个属性当中
  • 当我们的组件变得很大很复杂的时候,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散
  • 尤其对那些开始没有编写这些组件的人来说,代码变得难以阅读

composition API使得逻辑代码重合部分可以拆分为一个函数(使用hook,和React一样,使用useXXX命名),然后导入使用

setup其实就是组件的一个选项,只不过这个选项可以强大到替代之前的所有选项

setup里面无法使用this,没有绑定!!

setup的参数

  • props
    • 还是需要写props的option进行声明
    • 父组件还是需要写components选项
  • context,它包含三个属性
    • attrs:所有非props的attribute,比如class、id之类的;
    • slots:父组件传过来的插槽
    • emit:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,无法通过this.$emit发出事件)

setup的返回值可以在模板template中被使用,也就是说我们可以通过setup的返回值来替代data选项

reactive

但是在setup里定义的变量非响应式的,因为他没有放在data,所以我们可以包裹一个 reactive,将其变身称为响应式的(此时包裹在reactive里面的数据会被Vue使用Proxy进行数据劫持)

事实上,我们编写的data选项,也是在内部就给了reactive函数将其变成响应式的

注意:reactive API对传入类型是有限制的,他要求我们必须传入一个对象或者数组类型



注意:reactive在v-model使用双向绑定的时候有可能会有些问题,所以即使数据是对象,此时也建议使用ref包裹

Ref API

官方推荐(尤雨溪)能用ref就用ref,而不是reactive,后期方便抽离

如果我们对reactive传入一个基本数据类型,就会报一个警告,提示我们使用ref

ref API会返回一个可变的响应式对象,该对象作为一个响应式引用维护它内部的值,这就是ref名称的来源

  • 它内部的值是在ref的value属性被维护的

但是,虽然是最为ref的value进行存储,但是我们并不需要在模板中使用 变量名.value 来获取存储的数据,在tempplate模板中使用ref对象,它会自动进行解包:

  1. 浅层解包,只能对未包裹外层(如果外层有包裹,必须被reactive对象包裹)的基本数据类型进行解包
  2. 但是并不代表在逻辑代码里,也就是setup里有自动解包的功能,所以在setup函数里还是得通过 变量名.value 来操作


Vue Reactivity Transfrom

该功能已经废弃,可以作为了解

已废弃的实验性功能

响应性语法糖曾经是一个实验性功能,且已被废弃,请阅读废弃原因

总结出来就是:

  • 丢失.value使得更难进行依赖追踪,心理开销(mental overhead)变得更加明显,特别是如果语法也在 SFC 之外使用,并且在SFC内外使用可能会出现不一致
  • 写法上容易和refs混淆,特别是外部函数期望使用$refs
  • 最重要的是,潜在的碎片化风险。尽管这显然是选择加入的,但一些用户对该提案表示强烈反对,原因是他们担心他们将不得不使用不同的代码库,其中一些人选择使用它,而另一些人则没有。这是一个合理的问题,因为反应性转换需要一种不同的心智模型来扭曲JavaScript语义(变量赋值能够触发反应效应)。

在未来的一个小版本更新中,它将会从 Vue core 中被移除。

  • 想要摆脱它的话,请查看这个命令行工具,它可以自动完成这一过程。
  • 如需继续使用,请通过 Vue Macros 插件。

ref vs. 响应式变量,有点Svelte的感觉在里面

自从引入组合式 API 的概念以来,一个主要的未解决的问题就是 ref 和响应式对象到底用哪个。响应式对象存在解构丢失响应性的问题,而 ref 需要到处使用 .value 则感觉很繁琐,并且在没有类型系统的帮助时很容易漏掉 .value

Vue 的响应性语法糖是一个编译时的转换步骤,让我们可以像这样书写代码:

vue



每一个会返回 ref 的响应式 API 都有一个相对应的、以 $ 为前缀的宏函数。包括以下这些 API:

toRefs

  • toRefs:将 reactive对象中所有属性都转换为ref,建立链接
  • toRef:对 reactive对象其中一个属性转换ref,建立链接
  • 注意,两者都是建立在 reactive API 之上的

正常情况下,对reactive包裹的对象做解构是无法得到响应式数据的,除非使用 toRefs 进行包裹,将里面结构的数据转换为 ref

如果是仅仅只需要解构reactive之后得到一个我们需要的属性,则使用 toRef就可以了(对比 toRefs,性能开销较小 )

// 得不到响应式的 ×
setup() {
    const info = reactive({count: 100})
    let { count } = info
    const increment = () => {
        count++;
    };
    return {
        count,
        increment,
    };
},
import { reactive, toRefs } from "vue";
//.....
//响应式的count
setup() {
    const info = reactive({ name: 'allen', count: 100 });
    //也可以
    //let { name, count } = toRefs(info);
    let count = toRef(info, "count");
    const increment = () => {
        count.value++;
    };
    return {
        count,
        increment,
    };
}

还有一个 unref API,用于判断当前是否为ref

实质上也不过就是 unref(val) =>

val = isRef(val) ? val.value : val;

options API和Composition API 中响应式数据的命名冲突

Vue3内部取值的时候,如果有 $符号,则 先找缓存,没有的话再找setup中保存的响应式数据,找不到再找data,找不到再找props,找不到再找ctx里面有没有值(ctx里面保存computed、methods的数据)

if (key[0] !== '$') {
    const n = accessCache![key]
    if (n !== undefined) {
        switch (n) {
            case AccessTypes.SETUP:
                return setupState[key]
            case AccessTypes.DATA:
                return data[key]
            case AccessTypes.CONTEXT:
                return ctx[key]
            case AccessTypes.PROPS:
                return props![key]
                // default: just fallthrough
        }
    } else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
        accessCache![key] = AccessTypes.SETUP
        return setupState[key]
    } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        accessCache![key] = AccessTypes.DATA
        return data[key]
    } else if (
        // only cache other properties when instance has declared (thus stable)
        // props
        (normalizedProps = instance.propsOptions[0]) &&
        hasOwn(normalizedProps, key)
    ) {
        accessCache![key] = AccessTypes.PROPS
        return props![key]
    } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
        accessCache![key] = AccessTypes.CONTEXT
        return ctx[key]
    } else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {
        accessCache![key] = AccessTypes.OTHER  //找不到了
    }
}

ref获取组件/元素对象



无法使用this

官方:

setup() 内部,this 不是该活跃实例的引用,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这使得 setup() 在和其它选项式 API 一起使用时可能会导致混淆。

coderwhy老师:组件实例被创建 -> setup被调用 -> 其他option出来

也就是说

  • setup 中this并没有绑定组件实例
  • 并且在 setup 被调用之前,data、computed、methods等option都没有被解析

readonly

有时候我们通过reactive 或者 ref 获取到一个响应式对象,但是我们传入其他地方(组件)的时候,并不想在另外一个地方(组件)被修改,只读就可以

比如说父组件传入到子组件,我们要遵循“单向数据流”的规范

使用readonly

  • readonly会返回原生对象的只读代理(也就是它依旧是一个Proxy,这是一个proxy的set方法被劫持,而不能对其进行修改)
const dataProxy = new Proxy(info, {
    get(target, key){ return target[key] },
    set(target, key, value) { warning(); }
})
import { reactive, ref, readonly } from 'vue'
//...
setup(){
    const data = reactive({name: 'allen'});
    const data2 = ref(100);
    const readonlyData = readonly(data);
    const readonlyData2 = readonly(data2);
    readonly.name = 'bruce'; //失败,警告
}

computed

通过导包的方式,直接再 setup 函数中使用

  • 传入一个函数作为参数,该返回值(也变身为ref对象)直接作为缓存
  • or 传入一个对象,包含get和set方法,同以前的computed option(也返回成一个ref对象)
import { ref, computed } from "vue";
export default {
  setup() {
    const firstname = ref("Allen");
    const lastname = ref("bruce");
    const fullname = computed(() => firstname.value + " " + lastname.value);
    return {
      fullname,
    };
  },
};

watch

在composition API中,我们可以使用watchEffect和watch来完成响应式数据侦听

  • watchEffect用于自动收集响应式数据依赖,首屏渲染就会立即执行一次

    立即执行一次就是为了查看函数里面包含了什么响应式的数据,进行依赖收集

    之后若该依赖发生改变,则watchEffect函数就会被触发;

    第二个参数是option,可以指定 flush:”pre”|”post”|”sync”,分别是 默认|dom挂载之后再执行|强制同步触发,低效的

function watchEffect(
  effect: (onInvalidate: InvalidateCbRegistrator) => void,
  options?: WatchEffectOptions
): StopHandle
setup() {
    const name = ref("Allen");
    const age = ref(18);
    const changeName = () => {
        name.value = "Bruce";
    };
    const changeAge = () => {
        age.value++;
    };
    //依赖项发生改变自动重新执行,个人感觉有点像react的useEffect来用了
    watchEffect(() => {
        console.log("name:", name.value);  // name发生改变则调用,因为name被watchEffect收集了
    });
    return {
        name,
        age,
        changeAge,
        changeName,
    };
},
  • watch需要手动指定侦听数据源,几乎等同于watch option
    • 但是在setup里面,格式上还是有少许不同
    • 第一个参数传入一个getter函数(reactive 或者 ref 或者一个装着多个ref/reactive的数组)
    • 第二个参数传入一个函数,代表监听数据变化后进行的操作
    • 第三个参数可选,作为watch的option设置监听,比如deep(深度侦听)、immediate(是否首屏渲染就会立即执行一次)
setup() {
    const info = reactive({ name: "allen", age: 18 });
    const changeName = () => (info.name = "Bruce");
    watch(info, (newval, oldval) => {
        console.log(newval, oldval);
    });
    /*
    避免newval,oldval变为一个代理proxy,解构掉reactive
    watch(() => {
        return {...info};
    }, (newval, oldval) => {
        console.log(newval, oldval);
    });
    */
    return {
        changeName,
        info,
    };
},

注意:如果第一个参数是reactive,则默认深度监听

停止侦听器 and 清除副作用

watchEffect 返回一个 停止器,调用该停止器之后,watchEffect将不会被触发

watchEffect还接受一个参数,该参数传入的函数会在watchEffect被销毁时调用,个人感觉可以把它当成React中 useEffect的 返回值

setup() {
    const name = ref("Allen");
    const age = ref(18);
    // watchEffect 返回一个停止侦听器,调用即停止
    const stop = watchEffect((onInvalidate) => {
      onInvalidate(() => {
        // 用于清除函数中的副作用,比如可以放一些取消请求功能request.cancel()
        // 当依赖项发生改变,watchEffect重新执行,则这里面的的代码也会被执行。
        console.log("我被执行了!");
      });
      console.log("name:", age.value);
    });
    const changeAge = () => {
      age.value++;
      if (age.value > 25) stop();
    };
    return {
      name,
      age,
      changeAge,
    };
  },

生命周期替代

使用:可以直接导入 onX 函数,注册生命周期钩子函数

option API Hook inside setup
beforeCreate no need(由于setup比它早,放在setup执行即可)
created no need(由于setup比它早,放在setup执行即可)
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
activated onActivated
deactivated onDeactivated

provide / inject

父组件:导包之后,以键值对的形式,存储于父组件的provide中

子组件:导包之后,通过inject(键)来获取传入的数据;也可以给inject传入第二个参数来添加默认值

script setup

script setup 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。

2021.6.21为止还是实验性,但是到了2021.11.28好像已经被纳入正式版了

相比于普通的 <script> 语法,它具有更多优势:

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 Typescript 声明 props 和抛出事件。
  • 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
  • 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)


<script setup> 中必须使用 definePropsdefineEmits API 来声明 propsemits

获取路由对象

依旧是因为setup中获取不到this,此时导入路由的hook

此时这里的 useRoute返回的route = this.$route

import { useRoute } from 'vue-router';
export default {
  setup() {
    const route = useRoute();
  },
};

同样的,想要获取 $router 对象,也需要导入路由hook,通过返回的router对象,使用原来 this.$router.push 方法

import { useRouter } from 'vue-router';
export default {
  setup() {
    const router = useRouter();
    router.push("/xxx")
  },
};

获取vuex

依旧是因为setup中获取不到this,此时导入vuex的hook

这里引入了computed、Vuex的mapState(一个对象,传入的属性是多个函数)简化模板中的数据简写

import { useStore, mapState } from 'vuex'
import { computed } from 'vue'
export default {
  setup() {
    const store = useStore();
    const sCounter = computed(() => store.state.counter);
    // 除了一个一个computed声明,也可以使用mapState
    const storeStateFns = mapState(['name', 'age']);
    const storeState = {};
    Object.keys(storeStateFns)
    .forEach(key => {
        // mapState里的函数调用时return this.$store.state.xxx,所以这里要处理this
        const fn = storeStateFns[key].bind({ $store: store });
        storeState[key] = computed(fn);
    })
    return {
        sCounter,
        ...storeState
    }
  },
};

nextTick

import { nextTick } from "vue";
export default {
  setup() {
    // 使用onUpdated的话也可以,但是onUpdated比较公用,任何DOM更新都会触发
    nextTick(() => {
      //...
    });
  },
};

vue的nextTick原理思路有点像node.js中事件循环的nextTick

vue把watch的回调、组件更新触发的事件、生命周期的回调等任务,而任务全部被加入到微任务队列里面去!!!!

而使用nextTick,就会让nextTick中的回调任务加入到微任务任务队列中

export function nextTick<T = void>(
  this: T,
  fn?: (this: T) => void
): Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? Promise.resolve().then(this ? fn.bind(this) : fn) : p
}

5.废弃

$children属性

在 3.x 中,$children property 已被移除,且不再支持。如果你需要访问子组件实例,我们建议使用 $refs

filters option

我们建议用计算属性或方法代替过滤器,而不是使用过滤器。

迁移构建开关:

  • FILTERS
  • COMPILER_FILTERS

路由 router-link 的 tag 属性

因为现在可以直接再 router-link 中写入标签,当作slot使用

可选的第三个参数 next

在之前的 Vue Router 版本中(当前2021年为4.x版本),也是可以使用 第三个参数 next 的。这是一个常见的错误来源,可以通过 RFC 来消除错误。然而,它仍然是被支持的,这意味着你可以向任何导航守卫传递第三个参数。在这种情况下,确保 next 在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。这里有一个在用户未能验证身份时重定向到/login错误用例

// BAD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  // 如果用户未能验证身份,则 `next` 会被调用两次
  next()
})

下面是正确的版本:

// GOOD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  else next()
})

现在更多通过返回值来控制跳转(next)

  1. false:取消当前导航

  2. 不反悔/ undefined:默认导航

  3. 返回一个路由地址:可以是String,也可以是一个对象(包含path、params、query等信息),跳到该导航(把这个看成 this.$router.push("/home"); 即可)

    比如

    router.beforeEach((to) => {
      if (to.path !== '/login') {
        const token = LocalCache.getCache('toekn')
        if (!token) {
          return '/login'
        }
      }
    })

Vue.extend

在 Vue 2.x 中,Vue.extend 曾经被用于创建一个基于 Vue 构造函数的“子类”,其参数应为一个包含组件选项的对象。在 Vue 3.x 中,我们已经没有组件构造器的概念了。应该始终使用 createApp 这个全局 API 来挂载组件

v-on.native

它原来是用于组件中原生事件的触发

原来的样子

而现在v-on.native 修饰符已被移除。同时,新增的 emits 选项允许子组件定义真正会被触发的事件。

因此,对于子组件中被定义为组件触发的所有事件监听器,Vue 现在将把它们作为原生事件监听器添加到子组件的根元素中 (除非在子组件的选项中设置了 inheritAttrs: false)。

(可以理解为直接用就好了,不用加native

<my-component
  v-on:close="handleComponentEvent"
  v-on:click="handleNativeClickEvent"
/>

事件总线

Vue3官方实例移除了 $on$off$once方法,所以不能像Vue2.x一样通过创建Vue实例来进行事件总线。如果我们希望继续使用事件总线,可以通过一些第三方的库,Vue3官方推荐 mitt或者tiny-emitter

mitt库的使用

npm i mitt

js文件封装

import mitt from 'mitt'
const emitter = mitt();
export default emitter;

组件使用(直接发布订阅完事)

import emitter from 'xxx.js'
事件(){
    emitter.emit('事件名称', {a: 1, b: 2})
}
import emitter from 'xxx.js'
//创建初期直接监听发布
created(){
    emitter.on('事件名称', data => {
        //....
    });
    emitter.on('*', (type, data) => {
        console.log(`如果是*则监听所有事件,事件类型:${type},传递参数:${data}`);
    })
}

mit事件监听取消

//全部一次性取消
emitter.all.clear();

//单个取消,函数需要定义
function onFoo() {}
emitter.on('foo', onFoo);
emitter.off('foo', onFoo);

文章作者: Hello
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hello !
 上一篇
数据结构 数据结构
线性结构线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系 比如数组、链表 非线性结构一般未一对多的形式 比如树、多维数组、图结构 双向链表双向链表,又稱為双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直
2021-11-20
下一篇 
Vue3(下) Vue3(下)
6.Vue动画处理vue有个内置组件 transition 基本使用:使用name定义过渡类名,然后常常与v-if、v-show、动态组件进行搭配 官方: 可以给任何元素和组件添加进入/离开过渡 条件渲染 (使用 v-if) 条件展示 (
2021-11-05
  目录