换个思路掌握Vue3

1,815 阅读7分钟

距离Vue3稳定版发布也已半年过去,你的团队里开始尝试了吗?还是你只是了解一些重大变化(proxy、composition api)的基本定义?反正我们团队还没开始用,我也只是刚看过一些文档。此篇文章我就带着自己的理解视角一起跟大家探讨一下Vue3的变化,以及对于我们工作中的可能影响。欢迎大家发表不同的观点。

猜想

为什么第一部分叫猜想呢?因为我感觉当我们接收一份新的知识点之前,最好先宏观的去看一下过往,然后猜测她可能的发展方向,希望支持什么能力。带着这些疑问去看知识点更有目标感和带入感,也能更快地吸收。

既然是2.x到3.0的大版本升级,大版本一定不止是缝缝补补,有大的更新和新功能,也有之前版本留下来的优化问题,所以猜想的方向有:

  • 更小:体积更小
  • 更快:性能更好
  • 更稳定:优化之前的不稳定issue
  • 更合理:写法或者性能更优雅
  • 一些新特性:新功能带来便捷
  • 一些废弃:肯定也要废弃一些不好的设计
  • 重要的原理:肯定也要理解一些重要原理

带着以上这些猜想在Vue3里找答案。

重要变化

更小

这里的指的是Vue3在加载时的体积更小。作为一个应用框架,她的实际使用体积作为了一个很重要的衡量指标,尤其对于前端重体验的框架。想想有什么方法可以减少体积?

  • 核心功能使用了更精简的实现方式:风险大,而且不明显
  • 删除了很对冗余的无用功能:这个....
  • 按需加载:结合es6模块化思路,比较靠谱

正房亮相

// vue2.x
import Vue from 'vue';

// vue3.0
import { withDirectives, vShow, createApp, ref, reactive } from 'vue';

果然,3.0使用了tree-shaking(不知道这个名词的,自行补习),新版本里将Vue源码进行了模块化,这样按需加载很大程度上减少了实际使用的体积。命中!

更快

我想这里大家都很关心新版本里有没有新的算法思路(好像很多人都关注这个,好像记得算法就很厉害,我总感觉搞错了重点),但是大版本里去探讨一下算法的优化思路是有必要的。没错,这个更快肯定就是性能的更快,算法优化。

那么猜想优化之前,我们看看目前的diff算法这部分有什么优化空间?

首先,2.x的diff算法是双端比较法,因篇幅问题,这个不清楚的自行去补习。这种双端比较法还有什么优化空间?

  • 双端进行四次比较,有没有办法更快地明确当前的节点之间需要哪种比较形式,将大部分的比较次数减少成一次
  • 有没有办法减少dom的移动成本
  • 加上key会提升diff效率,那么会不会默认所有的vnode都加上key
  • 减少dom创建和方法的绑定
  • 没有key的不确定节点对比更简化一些

正房亮相,篇幅问题简化表述逻辑

- 将VNode节点进行含有key和不含有key的两种类型区分;
--- 不含有Key的节点
----- 取新旧节点的最小长度,依据旧节点遍历最小长度里的公用节点进行复用
----- 如果新节点长于旧节点,那么创建剩下的节点
----- 如果新节点短于旧节点,那么删除旧节点里的多余节点
--- 含有key的节点
----- 从头部开始寻找相同的节点,一旦发现不同跳出寻找
----- 从尾部开始寻找相同的节点,一旦发现不同跳出寻找
----- 剩下的节点里进行三层逻辑
------- 如果删除操作,直接删除老节点
------- 如果是新增操作,新增操作即可
------- 剩下的是不确定的关系,可能是替换或者位置变动
--------- 这部分先找到最长递增子序列,定义需要toBePatched数组,然后记录在老节点里index,这个数组的值取最长递增子序列,这个序列作为标准,然后剩下的就是判定是新增节点还是需要移动的节点

diss:通过这里,其实Vue在暗示绑定key性能更好,这很react,Vue你还是自行做好性能优化逻辑吧,减少用户的使用心智才是生存之道。

更稳定

超速了,我没有想到有什么更稳定的Case,再结合2.x升级到3.0的阶段,稳定性不再是问题。

实际情况,我也确实没看到这方面的问题,如有漏下,请大神提醒。

更合理

这里就不猜测了,简单列举一些常用的合理api。

  • teleport 我们可以在组件内将某部分结构渲染到document文档流的任意位置,经典的场景就是在某个组件里需要渲染一个全局的结构(弹框),这确实更合理。react好像已经有了
  • 自定义事件
// 声明组件的将事件名称暴露,对于使用者来说比较直观,特别适合放在封装起来的组件里用,当然事件名称需要易读,否则效果不佳
app.component('custom-form', {
	emits: ['in-focus', 'submit']
})

// 可以针对自定义事件做逻辑预处理,同时,原生事件会被自定义事件取代
app.component('custom-form', {
	emits: {
    	submit: (...args) => {
        	// check
            // return boolean value
        }
    },
    methods: {
    	submitForm() {
        	this.$emit('submit', {
            	...args
            })
        }
    }
})
  • 支持多个根节点 再也不需要去多写一个无谓的根节点
  • 自定义指令 这里做了api的一个规范化,让用户使用的过程中心智负担更少
bind -> beforeMount
inserted -> mounted
beforeUpdate 新增
update 删除
componentUpdated updated
beforeUnmount 新增
unbind -> unmounted
  • unmounted取代destoryed
  • data始终为function
  • mixin支持浅合并 浅合并不多解释了,关于这个意图不知道这部分是不是想弱化mixin的能力,更加明确其使用范围,是不是也跟后面提到的composition api有关呢,毕竟二者有些相似点。

新变化——一些新增和废弃

篇幅问题,这里也是简单介绍一些常用的

一些新增

  • ts 是的,3.0支持ts了,并且是optional选项,不强制哦~
  • createApp 这是个更细致的处理,将原本一个全局Vue实例拆分成各自组件的独立应用实例,这么做的好处是:一些全局的api不再因为一个局部组件需求的调整而影响其他组件,任何全局改变Vue行为的api都会移动到实例应用,各个应用实例使用全局能力各自引用配置,各自改造,互不影响。
  • v-model
<user-name
  v-model:first-name="firstname"
  v-model:last-name="lastname"
></user-name>

// 支持修饰符
<user-name v-model:first-name.capitalize="first-name"></user-name>

是的,支持传递参数,将使用更加规范、更加灵活。在此之上又增加了修饰符的功能,通过约定的this.modelModifiers判定修饰符的存在,进而进行相应地扩展处理。

  • composition api 组合式api算是本次更新的最大亮点,也是对于开发者书写习惯挑战最大的部分。下面详细聊一下这部分的内容。

1、解决什么问题?

在2.x的开发习惯里,单文件组件是options的书写方式,这种的书写方式会有一个问题,变量和业务逻辑是分开的方式,这种情况下代码的复用性就很难做到,当组件越来越大的时候,代码就变得很冗长,并且排查问题也会带来麻烦,经常是翻到顶部看变量定义,然后回到底部去确认逻辑关系。那么composition api的出现就是为了解决这类问题的,实现变量与逻辑的绑定,进而实现复用的能力,同时也会让页面代码长度变短,提升代码的可读性。

2、怎么用?

// useUserRepositories.js
import { ref, onMounted, watch } from 'vue'
import { fetchUserRepositories } from '@/api/respositories'

export default function useUserRepositories(user) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(user.value)
  }
  
  onMounted(getUserRepositories)
  watch(user, getUserRepositories)
  
  return {
    repositories,
    getUserRepositories
  }
}

// main.js
import { toRefs } from 'vue'
import useUserRepositories from '@/useUserRepositories'

export default {
  props: {
    user: { type: String }
  },
  setup(props) {
    const { user } = toRefs(props)
    const { repositories, getUserRepositories } = useUserRepositories(user)
    
    return {
      getUserRepositories
    }
  }
}

关于逻辑复用的能力体现的十分明显,将变量和逻辑的复用能力下沉,同时支持变量透传,减少底层的耦合性。

3、与mixin的区别是什么?

composition api很容易与mixin联想到一起,都是在组件内部注入一部分逻辑。但实际二者还是有很大的差别:

a. 层级不同——composition api与组件是嵌套关系,而mixin与组件是同层级关系

b. 影响面不同——compostion api作为组件的被调用方,并且变量逻辑是组件控制,耦合性很低,而mixin是耦合在代码逻辑里,并且存在变量的互相引用,为将来的升级和维护埋下隐患。不清楚mixin的合并逻辑转化成浅比较是不是也是想要降低这种隐患做的处理。

  • defineAsyncComponent
import { defineAsyncComponent } from 'vue'
// 2.0
const asyncPage = () => import('./NextPage.vue')
// 3.0
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))
// 3.0带选项
const asyncPage = defineAsyncComponent({
  loader: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
})

一些废弃

  • v-on支持keycode
  • on,on, off, $once
  • Filter
  • Destroy
  • 内联模板 这部分知道就好,不熟悉不知道也不重要,反正不用。

重要原理

这块咱就必须得唠唠很重要的两个概念,这关系到我们用3.0最重的一部分

ref vs reactive

1、先关注一下使用规范

import { ref, reactive } from 'vue';

const counter = ref(0);
console.log(counter);    // { value: 0 }
console.log(counter.value)   // 0
const state = reactive({
  counter: 0
})

基本看来,ref和reactive都是作用在响应式数据方面,ref主要作用在基础数据类型,reactive作用在引用类型上,那如果ref作用在引用类型上会怎么样

const refState = ref({
  counter: 0
})

这里的底层调用的是reactive,更底层是Proxy,这么看二者在使用方面怎么区分呢?为什么要造两个api?先看两种代码规范

// 风格一
let x = 0
let y = 0

function updatePos(e) {
  x = e.pageX
  y = e.pageY
}

// 风格二
const pos = { x:0, y:0 }

function updatePos(e) {
  pos.x = e.pageX
  pos.y = e.pageY
}

这部分内容自行体会。另外提一个点:多次定义的ref可以合并到一个reactive里,而一个reactive可以拆分成多个ref,这里依据需求可以自行斟酌。

关于ref还有两个重要的api需要介绍,首先看一下toRef

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo)    // 2

state.foo++
console.log(fooRef.value)  // 3

这个api适合放在父组件里使用,然后将某个字段传递到子组件里,并且保证字段的响应式能力。另一个看一下toRefs

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })
  
  // return state
  return toRefs(state)
}

export default {
  setup() {
    // const state = useFeatureX()
    const { foo, bar } = useFeatureX()
    
    // return {
    //  foo: state.foo,
    //  bar: state.bar
    // }    
    return {
      foo,
      bar
    }
  }
}

解构reactive进而保持其响应式的能力。

下面探讨一下这方面的原理实现。

export function ref(raw: unknown) {
  if (isRef(raw)) {
    return raw
  }
  
  raw = convert(raw)
  
  const r = {
    _isRef: true,
    get value() {
      track(r, OperationTypes.GET, '')
      return raw
    },
    set value(newVal) {
      raw = convert(newVal)
      trigger(r, OperationTypes.SET, '')
    }
  }

  return r as Ref
}

const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val

reactive的原理,简化的流程如下:

总结

关于Vue3的细节和原理还有很多,这里只是展示了其中的一部分。整体感受使用的心智成本还是有一些的,目前团队里也还没尝试,坐等各位大佬先踩坑吧。欢迎交流~

时间紧急,写作思路比较跳跃,欢迎大佬指正。