vue3的相关知识点归纳

2022-06-07  本文已影响0人  宫若石

Topic:Vue3的设计与改变

Vue3发展历程

  1. 2018/09/30:尤雨溪在medium个人博客上发布了 Vue 3.0 的开发路线
  2. 2019年初:采用了RFC(征求意见)流程
  3. 2019/10/05:Vue 3 源码开放(pre-alpha状态)
  4. 2020/04/17:Vue 3.0 beta
  5. 2020/05/28:发表文章「Vue 3 设计过程」
  6. 2020/07/18:Vue 3.0 RC
  7. 2020/09/18:Vue 3.0 “One Piece"

Vue3的核心设计与提升

响应式核心原理:Object.defineProperty 切换到 Proxy

Vue Composition API

Vue3代码库完全使用typescript编写

内部模块解耦(采用monorepo)
模块之间的依赖关系更加明确, 降低项目贡献壁垒并提高其长期可维护性

内部包有自己的单独API,类型定义和测试
解锁高级用法
编译器支持自定义AST转换,用于在构建时自定义(如,在构建时进行i18n操作)
核心运行时提供了一系列 API,用于针对不同渲染目标(如native moile、WebGL或终端)的自定义容器。默认的 DOM 渲染器也使用这系列 API 构建。
@vue/reactivity 模块——Vue响应式系统;可作为独立包进行使用。也可以与其他模块解决方案配对使用(如 lit-html),甚至是在非 UI 场景中使用。

性能提升

实验特性

<template>
  <button @click="inc">{{ count }}</button>
</template>

<script setup>
  import { ref } from 'vue'
  export const count = ref(0)
  export const inc = () => count.value++
</script>

SFC: Singe-File Components(单文件组件,.vue文件)

Vue3的性能优化

减小包的尺寸:

渲染策略优化

vue渲染策略:

  1. HTML的模板 => (编译) => 虚拟DOM树的渲染函数
  2. 通过递归遍历两个虚拟DOM树,并比较每个节点上的每个属性来确定实际DOM的哪些部分需要更新

瓶颈:

任何一点改变却仍然需要递归整个虚拟DOM树,以了解发生了什么变化,尤其在查看包含大量静态内容且只有少量动态绑定(整个虚拟DOM)的模板时,效率低下。

最终目的:

克服虚拟DOM的瓶颈,最好的方法是消除不必要的虚拟DOM树遍历和属性比较

优化工作:

编译器分析模板并生成带有优化提示的代码,而运行时将拾取提示,并在可能的情况下采用快速路径:

  1. 将模版分为动态的和静态的“块”,对动态块进行了扁平化处理,减少运行时的遍历开销:在树的层面上,我们注意到,节点结构在没有模板指令的时候是完全静态的(例如,v-if和v-for)。如果我们将模板分为动态的和静态的“块”,每个块内的节点结构再次变得完全静态。当我们更新一个块内的节点时,我们不再需要递归遍历树,因为我们可以在平面数组中跟踪该块内的动态绑定。通过将我们需要执行的树遍历量减少一个数量级,从而节约了虚拟DOM的大部分开销。

  2. 静态树提升与静态prop提升:编译器会主动检测模板中的静态节点,子树甚至数据对象,并将其提升到生成代码中的render函数之外。这样可以避免在每个渲染上重新创建这些对象,从而大大提高了内存使用率并减少了垃圾回收的频率。

  3. 生成编译器的优化提示:在元素级别,编译器还会根据需要执行的更新类型为具有动态绑定的每个元素生成一个优化标志。例如,具有动态类绑定和许多静态属性的元素将收到一个标志,指示仅需要进行类检查。运行时将获取这些提示并采用专用的快速路径。

响应式原理:Proxy
Vue会使用带有getter和setter的处理程序遍历其所有property并将其转换为Proxy;
这个Proxy使Vue能够在property被访问或修改时执行依赖项跟踪和更改通知。

const handler = {
  get(target, prop, receiver) {
    // 追踪函数:依赖追踪
    track(target, prop)
    const value = Reflect.get(...arguments)
    if (isObject(value)) {
      return reactive(value)
    } else {
      return value
    }
  },
  set(target, key, value, receiver) {
    // 触发函数:更改通知
    trigger(target, key)
    return Reflect.set(...arguments)
  }
}
// 简化版
function track(target: object, type: TrackOpTypes, key: unknown) {
  const depsMap = targetMap.get(target);
  // 收集依赖时 通过 key 建立一个 set
  let dep = new Set()
  targetMap.set(ITERATE_KEY, dep)
  // effect可以先理解为更新函数,存放在 dep 里
  dep.add(effect)    
}
// 简化版
function trigger(target: object, type: TriggerOpTypes, key?: unknown,) {
  // 是通过key找到所有更新函数 依次执行
  const dep = targetMap.get(target)
  dep.get(key).forEach(effect => effect())
}

Vue Composition API(简称VCA)

什么是Composition API

Composition API是对于现有Option API的补充,构建于响应式API基础之上

setup函数
新的组件选项,VCA的入口点

export default {
  props: {
    title: String
  },
  setup(props, context) {
    console.log(props.title)
    // Attribute (非响应式对象)
    console.log(context.attrs)
    // 插槽 (非响应式对象)
    console.log(context.slots)
    // 触发事件 (方法)
    console.log(context.emit)
    return {}
  }
}

注意点:

  1. 参数props不能解构,会失去响应性
  2. this在 setup() 中不可用
  3. 在setup函数中,不能访问组件选项data、computed与methods

响应式API

ref
接受一个参数值并返回一个响应式且可改变的 ref 对象,通过.value获取属性值

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

reactive
接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()

const state = reactive({ count: 0 })

computed
传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误!

或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。

const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  },
})

plusOne.value = 1
console.log(count.value) // 0

readonly
传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理

const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
  // 依赖追踪
  console.log(copy.count)
})
// original 上的修改会触发 copy 上的侦听
original.count++
// 无法修改 copy 并会被警告
copy.count++ // warning!

watchEffect
立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> 打印出 0

setTimeout(() => {
  count.value++
  // -> 打印出 1
}, 100)

停止侦听

const stop = watchEffect(() => {
  /* ... */
})

// 之后
stop()

清除副作用

watchEffect((onInvalidate) => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id 改变时 或 停止侦听时
    // 取消之前的异步操作
    token.cancel()
  })
})
watch

watch API 完全等效于 2.x this.$watch

侦听单个数据

const count = ref(0)
// 侦听单个数据
watch(count, (count, prevCount) => {
  /* ... */
})

// 侦听多个数据
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})
<template>
    <button @click="increase">increase</button><span> count is: {{count}}</span>
</template>

<script lang="ts">
import { computed, defineComponent, reactive, readonly, ref, toRefs, watch, watchEffect } from 'vue'
export default defineComponent({
    setup() {
        // 响应式对象
        const count = ref(0)
        const state = reactive({ 
            count, 
            // 响应式计算对象
            double: computed(() => count.value)
        })
        // 与vue2.x的watch选项等效:监听响应式对象count
        watch(count, (count, prevCount) => {
            /** do something */
        })
        // 副作用函数:监听响应式对象count
        const stop = watchEffect(() => console.log(count.value))
        if (count.value > 5) {
            // 停止侦听
            stop()
        }
        // 只读响应式对象
        const copy = readonly(count)
        // 无法修改 copy 并会被警告
        copy.value++ // warning!
                const increase = () => count.value++
                // ES6解构,注意;需通过toRefs转换为响应式对象,否则property的响应式都会丢失
                const { double } = toRefs(state) 
        return {
                        count,
                        double,
            increase
        }
    },
})
</script>

生命周期钩子
beforeCreate -> 使用 setup()
created -> 使用 setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
errorCaptured -> onErrorCaptured

新增钩子(用于调试)
onRenderTracked
onRenderTriggered

import { 
    defineComponent, ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated,
    onBeforeUnmount, onUnmounted, onErrorCaptured, onRenderTracked, onRenderTriggered
} from 'vue'
export default defineComponent({
    setup() {
        const count = ref(0)
        // 在挂载开始之前被调用调用
        onBeforeMount(() => {}) 
        // 在挂载后被调用调用
        onMounted(() => {})
        // 组件更新前调用
        onBeforeUpdate(() => {})
        // 组件更新后调用
        onUpdated(() => {})
        // 组件卸载前调用
        onBeforeUnmount(() => {})
        // 组件卸载后调用
        onUnmounted(() => {})
        // 当捕获一个来自子孙组件的错误时被调用
        onErrorCaptured(() => {})
        
        onRenderTracked((DebuggerEvent) => {
            debugger
            // 检查哪个依赖性被组件追踪
        })
        onRenderTriggered((DebuggerEvent) => {
            debugger
            // 检查哪个依赖性导致组件重新渲染
        })
        return { count }
    },
})

设计动机

更友好的类型推断(defineComponent结合typescript)

vue2.x对typescript的类型推断不友好:

2. 实现逻辑提取与复用

// 追踪鼠标位置的例子
import { ref, onMounted, onUnmounted } from 'vue'

export default function useMousePosition() {
  const x = ref(0)
  const y = ref(0)
  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update)})

  return { x, y }
}
import { useMousePosition } from './useMousePosition'

export default {
  setup() {
    const { x, y } = useMousePosition()
    // 其他逻辑...
    return { x, y }
  },
}
  1. 更加灵活的代码组织模式,不需要总是通过选项来组织代码:
    Option API:通过选项(如methords、computed、watch等)来组织代码,逻辑关注点分散,这种碎片化使得复杂的组件难以维护
    Composition API:通过逻辑来组织代码,逻辑关注点内聚,便于复用与维护


    image.png

对比Mixins、高阶组件和无渲染组件

弊端:

相比而言,组合式 API:

缺点与槽点

缺点

槽点

// 尤雨溪 原话
其实真的用过并且懂 React hooks 的人看到这个都会意识到 Vue Composition API (VCA)跟 hooks 本质上的区别。VCA 在实现上也其实只是把 Vue 本身就有的响应式系统更显式地暴露出来而已。真要说像的话,VCA 跟 MobX 还更像一点。

但对于不懂 React hooks 的人来说,长的像就是一样了,懒得解释。

关于Class API的提议 (opens new window)[Abandoned]

首先,使用Class API(装饰器方案)来支持更好的类型推断;但Class API的合理性存疑
不确定性:装饰器对第二阶段规范的依赖,存在很多不确定性,尤其是当TypeScript的当前实现与TC39提案完全不同步
props的类型推断

  1. 尴尬的双重声明:
interface Props {
  message: string
}
class App extends Component<Props> {
  @prop message: string
}
  1. 无法将装饰器声明的props类型暴露给this.$props,这会破坏TSX的支持

与React Hook对比

React Hook设计动机:

  1. 在组件之间复用状态逻辑很难:复用不同组件之间的状态逻辑
  2. 复杂组件变得难以理解(每个生命周期常常包含一些不相关的逻辑,且逻辑分散)
  3. 难以理解的 class
    • 理解this的工作方式;
    • 不能忘记绑定事件处理器;
    • class 组件会无意中鼓励开发者使用一些让优化措施无效的方案:不好压缩、热重载不稳定。

Vue Composition API与React Hook的用法对比

// Vue Composition API
import { ref, onMounted, onUnmounted } from 'vue'

export default function useMousePosition() {
  const x = ref(0)
  const y = ref(0)
  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update)})

  return { x, y }
}
// React Hook

import { useState, useEffect } from 'react';

export default function usePosition() {
    const [ position, setPosition ] = useState({ x: 0, y: 0 }) 
    function update(e) {
        setPosition({
            x: e.pageX,
            y: e.pageY
        })
    }
    useEffect(() => {
        window.addEventListener('mousemove', update)
        return () => {
            window.removeEventListener('mousemove', update)
        }
    }, [])

    return position
}

React Hook的规则(限制,增加心智负担)

  1. 不要在循环,条件或嵌套函数中调用Hook;
  2. 只能React函数的最顶层调用(遵守这条规则,你就能确保Hook在每一次渲染中都按照同样的顺序被调用。这让React能够在多次的useState和useEffect调用之间保持hook状态的正确);
  3. 只能在React的函数组件中调用Hook。

Vue Composition API的优势

  1. 与 React Hook不同,setup 函数仅被调用一次,这在性能上比较占优。
  2. 对调用顺序没什么要求,每次渲染中不会反复调用Hook函数,产生的的 GC 压力较小。
  3. 不必考虑总是需要useCallback的问题,以防止传递函数prop给子组件的引用变化,导致无必要的重新渲染。
  4. React Hook 里的「依赖」是需要你去手动声明的;Vue会自动追踪依赖。
  5. React Hook有臭名昭著的闭包陷阱问题,如果用户忘记传递正确的依赖项数组,useEffect 和 useMemo 可能会捕获过时的变量,这不受此问题的影响。Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。
// React Demo
import React, { useState, useEffect, useCallback } from 'react';

//  state变化时,重新执行
function Example() {
  const [ count, setCount ] = useState(0)
  const [ val, setVal ] = useState('')
  // count 或 val变化时都会执行
  useEffect(() => console.log('count -', count, 'val -', val))
  // 申明依赖count:只有count变化时执行,val变化时不执行
  useEffect(() => console.log('count -', count), [ count ])
  // 执行一次
  useEffect(() => {
    console.log('mounted!')
    return () => console.log('unmounted!')
  }, [])
  // count变化时,updateVal的引用不变,子组件Child不会重新渲染
  const updateVal = useCallback((val) => setVal(val), [ val ])
  // count变化时,updateVal2的引用改变,子组件Child重新渲染
  const updateVal2 = (val) => setVal(val)
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increase</button>
      count is {count}
      <Child val={val} updateVal={updateVal} />
      <Child val={val} updateVal={updateVal2} />
    </div>
  );
}
<template>
    <button @click="increase">increase</button><span> count is: {{count}}</span>
    <input type="text" v-model="val">
    <!-- 只有val变化时, HelloWorld组建更新,但不会重载 -->
    <HelloWorld :msg="val" />
</template>

<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref, watchEffect } from 'vue'
import HelloWorld from './HelloWorld.vue'
export default defineComponent({
    components: { HelloWorld },
    // 只执行一次
    setup() {
        const count = ref(0)
        const val = ref('')
        // 只有count变化才执行
        watchEffect(() => console.log('count -', count.value))
        // 只有val变化才执行
        watchEffect(() => console.log('val -', val.value))
        // 组建挂载时执行
        onMounted(() => console.log('<mounted>'))
        // 组件卸载时执行
        onUnmounted(() => console.log('<unmount>'))
        const increase = () => count.value += 1

        return {
            val, count, 
            increase
        }
    },
})
</script>

使用 react hooks 带来的收益抵得过使用它的成本吗?(opens new window)

如何在Vue2中使用Composition API

# 安装插件
npm install @vue/composition-api
// 入口文件中
import VueCompositionAPI from '@vue/composition-api'
// 挂载Composition API
Vue.use(VueCompositionAPI)
// vue组件
import { computed, ref, reactive, watchEffect, onMounted } from '@vue/composition-api'
export default {
  setup(props, ctx) {
    // 创建响应式数据对象
    const count = ref(0)
    const state = reactive({ 
        count 
        double: computed(() => count * 2)
    })
    watchEffect(() => console.log(state.count))
    onMounted(() => console.log('mounted'))
    return {
        state,
        increment() { state.count += 1 }
    }
  }
}

注意点:

相关链接

兼容性

与Vue2兼容,但有重大更改与小改变:

全局API

全局API已更改为使用应用程序实例

2.x 全局 API 3.x 实例 API (app)
Vue.config app.config
Vue.config.productionTip removed
Vue.config.ignoredElements app.config.isCustomElement
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
// vue2
import Vue from 'vue'
import App from './App.vue'
import VueCompositionAPI from '@vue/composition-api'

Vue.use(VueCompositionAPI)
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')
// vue3
import { createApp } from 'vue'
import App from './App.vue'
import HelloWorld from './components/HelloWorld.vue';
import Counter from './components/Counter.vue';
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
    history: createWebHistory(),
    routes: [ ... ]
})
const app = createApp(App)
app.use(router)
app.mount('#app')

全局和内部 API 已经被重构为可 tree-shakable:
Vue 2.x 中的这些全局 API 受此更改的影响:

Vue.nextTick
Vue.observable (用 Vue.reactive 替换)
Vue.version
Vue.compile (仅全构建)
Vue.set (仅兼容构建)
Vue.delete (仅兼容构建)

// vue2
import Vue from 'vue'

Vue.nextTick(() => {
  // 一些和DOM有关的东西
})
// vue3
import { nextTick } from 'vue'

nextTick(() => {
  // 一些和DOM有关的东西
})

模版指令

组件

渲染函数

移除API

脚手架

vite

  1. 一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析<script module>,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用(冷启动快)
  2. 不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢(即时的模块热更新)。
  3. 针对生产环境则可以把同一份代码用 rollup 打包(按需编译)。

缺点: 支持转译ts文件,但不执行类型检查

<!--Vite App入口文件-->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vite App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
# 创建Vite App项目
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

vue-cli
一个基于 Vue.js 进行快速开发的完整系统

# 安装
npm install -g @vue/cli

# 创建vue3项目
vue create <project-name>

# 选择`vue3 preview`
? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 2] babel, eslint) 
  Default (Vue 3 Preview) ([Vue 3] babel, eslint) 
  Manually select features 

# 添加typescript插件
vue add typescript

Vue插件

Vue-router

安装

npm install vue-router@next --save

Vue3中的使用方式

// 1. Define route components.
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }

// 2. Define some routes
const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
]

// 3. Create the router instance and pass the `routes` option
const router = VueRouter.createRouter({
  // 4. Provide the history implementation to use. 
  history: VueRouter.createWebHashHistory(),
  routes,
})

// 5. Create and mount the root instance.
const app = Vue.createApp({})
app.use(router)
app.mount('#app')

Vuex

安装

npm install vuex@next --save

vue3中的使用方式

import { createApp } from 'vue'
import { createStore } from 'vuex'

const app = createApp({ ... })
const store = createStore({ ... })

app.use(store)
上一篇 下一篇

猜你喜欢

热点阅读