Skip to content

Vue3.0

1. Vue3.0做了哪些优化?

优化内容

  • 更小
  • 更快
  • TypeScript支持
  • API设计一致性
  • 提高自身可维护性
  • 开放更多底层功能

更小

  • Vue3移除一些不常用的API
  • 引入tree-shaking,可以将无用模块“剪辑”,仅打包需要的,使打包的整体体积变小了

更快

  • 主要体现在编译方面:
    • diff算法优化
    • 静态提升
    • 事件监听缓存
    • SSR优化

更友好

  • vue3在兼顾vue2options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力
  • 这里代码简单演示下:
    • 存在一个获取鼠标位置的函数
    javascript
    import { toRefs, reactive } from 'vue'
    function useMouse(){
      const state = reactive({x:0,y:0})
      const update = e =>{
        state.x = e.pageX
        state.y = e.pageY
      }
      onMounted(()=>{
        window.addEventListener('mousemove',update)
      })
      onUnmounted(()=>{
        window.removeEventListener('mousemove',update)
      })
      return toRefs(state)
    }
    • 我们只需要调用这个函数,即可获取xy的坐标,完全不用关注实现过程
    • 如果很多类似的第三方库,我们只需要调用即可,不必关注实现过程,开发效率大大提高
    • Vue3是基于typeScript编写的,可以享受到自动的类型定义提示

优化方案

  • 源码
    • Vue3整个源码是通过monorepo的方式维护的,根据功能将不同的模块拆分到packages目录下面不同的子目录中
    • 这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性
    • 另外一些package(比如reactivity响应式库)是可以独立于Vue使用的,这样用户如果只想使用Vue3的响应式能力,可以单独依赖这个响应式库而不用去依赖整个Vue
    • Vue3是基于typeScript编写的,提供了更好的类型检查,能支持复杂的类型推导
  • 性能
    • 体积优化
    • 编译优化
    • 数据劫持优化
  • 语法API
    • 优化逻辑组织
      • 一张图,我们可以很直观地感受到 Composition API在逻辑组织方面的优势
      • 相同功能的代码编写在一块,而不像Options API那样,各个功能的代码混成一块
    • 优化逻辑复用
      • Vue2中,我们是通过mixin实现功能混合,如果多个mixin混合,会存在两个非常明显的问题:命名冲突和数据来源不清晰
      • 而通过Composition API这种形式,可以将一些复用的代码抽离出来作为一个函数,只要的使用的地方直接进行调用即可
      • 同样是上文的获取鼠标位置的例子:
      javascript
      import { toRefs, reactive, onUnmounted, onMounted } from 'vue';
      function useMouse(){
        const state = reactive({x:0,y:0})
        const update = e=>{
          state.x = e.pageX
          state.y = e.pageY
        }
        onMounted(()=>{
          window.addEventListener('mousemove',update)
        })
        onUnmounted(()=>{
          window.removeEventListener('mousemove',update)
        })
        return toRefs(state)
      }
      // 组件使用
      import useMousePosition from './mouse'
      export default {
        setup() {
          const { x, y } = useMousePosition()
          return { x, y }
        }
      }
Details

pic

2. Vue3.0性能提升主要是通过哪几方面体现的

编译阶段

  • 回顾Vue2我们知道每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把用到的数据property记录为依赖,当依赖发生改变,触发setter,则会通知watcher,从而使关联的组件重新渲染
    javascript
    <template>
      <div id="content">
        <p class="text">静态文本</p>
        <p class="text">静态文本</p>
        <p class="text">{{ message }}</p>
        <p class="text">静态文本</p>
        ...
        <p class="text">静态文本</p>
      </div>
    </template>
  • 可以看到上面例子中,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多Diff和遍历其实都是不需要的,造成性能浪费
  • 因此Vue3在编译阶段,做了进一步优化。主要有如下:
    • Diff算法优化
    • 静态提升
    • 事件监听缓存
    • SSR优化
Details

pic

Diff算法优化

  • Vue3Diff算法中相比Vue2增加了静态标记

  • 关于这个静态标记,其作用是为了会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较

  • 下图这里,已经标记静态节点的p标签在Diff过程中则不会比较,把性能进一步提高 pic

  • 关于静态类型枚举如下:

    javascript
    export const enum PatchFlags {
      TEXT = 1,// 动态的文本节点
      CLASS = 1 << 1,  // 2 动态的 class
      STYLE = 1 << 2,  // 4 动态的 style
      PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
      FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
      HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
      STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
      KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
      UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
      NEED_PATCH = 1 << 9,   // 512
      DYNAMIC_SLOTS = 1 << 10,  // 动态 solt
      HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
      BAIL = -2 // 一个特殊的标志,指代差异算法
    }

静态提升

  • Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用
  • 这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用
  • 例子:
    javascript
    <span>你好</span>
    <div>{{ message }}</div>
  • 没有做静态提升之前:
    javascript
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createBlock(_Fragment, null, [
        _createVNode("span", null, "你好"),
        _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
      ], 64 /* STABLE_FRAGMENT */))
    }
  • 做了静态提升之后:
    javascript
    const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */)
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createBlock(_Fragment, null, [
        _hoisted_1,
        _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
      ], 64 /* STABLE_FRAGMENT */))
    }
    // Check the console for the AST
  • 静态内容_hoisted_1被放置在render函数外,每次渲染的时候只要取_hoisted_1即可
  • 同时_hoisted_1被打上了PatchFlag,静态标记值为-1,特殊标志是负整数表示永远不会用于Diff

事件监听缓存

  • 默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化
    javascript
    <div>
      <button @click = 'onClick'>点我</button>
    </div>
  • 没开启事件监听器缓存
    javascript
    export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createBlock("div", null, [
        _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
        // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
      ]))
    })
  • 开启事件侦听器缓存后
    javascript
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createBlock("div", null, [
        _createVNode("button", {
          onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
        }, "点我")
      ]))
    }

SSR优化

  • 当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染
    javascript
    <div>
      <div>
        <span>你好</span>
      </div>
      ...  // 很多个静态属性
      <div>
        <span>{{ message }}</span>
      </div>
    </div>
  • 编译后
    javascript
    import { mergeProps as _mergeProps } from "vue"
    import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"
    export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
      const _cssVars = { style: { color: _ctx.color }}
      _push(`<div${
        _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
      }><div><span>你好</span>...<div><span>你好</span><div><span>${
        _ssrInterpolate(_ctx.message)
      }</span></div></div>`)
    }

源码体积

  • 相比Vue2Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking
  • 任何一个函数如refreactivecomputed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小
javascript
import { computed, defineComponent, ref } from 'vue'
export default defineComponent({
  setup(props, context) {
    const age = ref(18)
    let state = reactive({
      name: 'test'
    })
    const readOnlyAge = computed(() => age.value++) // 19
    return {
      age,
      state,
      readOnlyAge
    }
  }
})

响应式系统

  • Vue2中采用defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加gettersetter,实现响应式
    • 对象新增或者删除一个属性无法实现响应式(可以通过Vue.set/Vue.delete来实现响应式)
    • 通过修改数组的length,下标无法实现响应式(通过改写数组的7种方法实现响应式)
    • 如果对象的层级很深,需要深度遍历影响性能
  • Vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历
    • 可以监听动态属性的添加
    • 可以监听到数组的索引和数组length属性
    • 可以监听删除属性

3. Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?

Object.defineProperty

  • 定义:Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
    • get:属性的getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)该函数的返回值会被用作属性的值
    • set:属性的setter函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this对象。默认为undefined
  • 定义一个响应式函数defineReactive
    javascript
    function update() {
      app.innerText = obj.foo
    }
    function defineReactive(obj, key, val) {
      Object.defineProperty(obj, key, {
        get() {
          console.log(`get ${key}:${val}`);
          return val
        },
          set(newVal) {
            if (newVal !== val) {
              val = newVal
              update()
            }
          }
        })
      }
  • 调用defineReactive,数据发生变化触发update方法,实现数据响应式
    javascript
    const obj = {}
    defineReactive(obj, 'foo', '')
    setTimeout(()=>{
      obj.foo = new Date().toLocaleTimeString()
    },1000)
  • 在对象存在多个key情况下,需要进行遍历
    javascript
    function observe(obj) {
      if (typeof obj !== 'object' || obj == null) {
        return
      }
      Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
      })
    }
  • 如果存在嵌套对象的情况,还需要在defineReactive中进行递归
    javascript
    function defineReactive(obj, key, val) {
      observe(val)
      Object.defineProperty(obj, key, {
        get() {
          console.log(`get ${key}:${val}`);
          return val
        },
        set(newVal) {
          if (newVal !== val) {
            val = newVal
            update()
          }
        }
      })
    }
  • 当给key赋值为对象的时候,还需要在set属性中进行递归
    javascript
    set(newVal) {
      if (newVal !== val) {
        observe(newVal) // 新值是对象的情况
        notifyUpdate()
      }
    }
  • 上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题:
    • 现在对一个对象进行删除添加属性操作,无法劫持
      javascript
      const person = {
        name: 'Tom',
        age:18
      }
      observe(person)
      person.sex = 'male' // 无法劫持
      delete person.name  // 无法劫持
    • 对一个数组进行监听的时候,也不是响应式的了
      javascript
      const arr = [1,2,3,4,5]
      arrData.forEach((val,index)=>{
          defineProperty(arrData,index,val)
      })
      arrDate[0] = 99 // 无法劫持

小结

  • 检测不到对象属性的添加和删除
  • 数组API方法无法监听到
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

Proxy

  • Proxy的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了
    • 定义一个响应式方法reactive
      javascript
      function reactive(obj) {
        if (typeof obj !== 'object' && obj != null) {
            return obj
        }
        // Proxy相当于在对象外层加拦截
        const observed = new Proxy(obj, {
            get(target, key, receiver) {
              const res = Reflect.get(target, key, receiver)
              console.log(`获取${key}:${res}`)
              return res
            },
            set(target, key, value, receiver) {
              const res = Reflect.set(target, key, value, receiver)
              console.log(`设置${key}:${value}`)
              return res
            },
            deleteProperty(target, key) {
              const res = Reflect.deleteProperty(target, key)
              console.log(`删除${key}:${res}`)
              return res
            }
        })
        return observed
      }
    • 测试一下简单数据的操作,发现都能劫持
      javascript
      const person = reactive({
          name: 'John'
      })
      // 1.获取
      state.name  // John
      // 2.设置已存在属性
      state.name = 'Joy' // ok
      // 3.设置不存在属性
      state.age = 18  // ok
      // 4.删除属性
      delete state.name // ok
    • 再测试嵌套对象情况,这时候发现就不那么OK了
      javascript
      const person = reactive({
        hobby: {
          running:true
        }
      })
      // 设置嵌套对象属性
      person.hobby.running = false // no ok
    • 解决上面问题需要在get之上再进行一层代理
      javascript
      function reactive(obj) {
        if (typeof obj !== 'object' && obj != null) {
          return obj
        }
        // Proxy相当于在对象外层加拦截
        const observed = new Proxy(obj, {
          get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(`获取${key}:${res}`)
            return isObject(res) ? reactive(res) : res
          }
          return observed
        })
      }

总结

  • Object.defineProperty只能遍历对象属性进行劫持
    javascript
      function observe(obj) {
        if (typeof obj !== 'object' || obj == null) {
          return 
      }
        Object.keys(obj).forEach(key => {
          defineReactive(obj, key, obj[key])
        })
      }
  • Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的
    javascript
      function reactive(obj) {
      if (typeof obj !== 'object' && obj != null) {
        return obj
      }
      // Proxy相当于在对象外层加拦截
      const observed = new Proxy(obj, {
        get(target, key, receiver) {
          const res = Reflect.get(target, key, receiver)
          console.log(`获取${key}:${res}`)
          return res
        },
        set(target, key, value, receiver) {
          const res = Reflect.set(target, key, value, receiver)
          console.log(`设置${key}:${value}`)
          return res
        },
        deleteProperty(target, key) {
          const res = Reflect.deleteProperty(target, key)
          console.log(`删除${key}:${res}`)
          return res
        }
      })
        return observed
      }
  • Proxy可以直接监听数组的变化(pushshiftsplice
    javascript
    const arr = [1,2,3]
    const arrP = reactive(arr)
    arr.push(4) // ok
  • Proxy有多达13种拦截方法,不限于applyownKeysdeletePropertyhas等等,这是Object.defineProperty不具备的
  • 正因为defineProperty自身的缺陷,导致Vue2在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外setdelete方法)
    javascript
    // 数组重写
    const originalProto = Array.prototype
    const arrayProto = Object.create(originalProto)
    ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
      arrayProto[method] = function () {
        originalProto[method].apply(this.arguments)
        dep.notice()
      }
    })
    // set、delete
    Vue.set(obj,'bar','newbar')
    Vue.delete(obj,'bar')
  • Proxy不兼容IE,也没有polyfill, defineProperty能支持到IE9

4. watch与watchEffect

不同形式的“数据源”

  • Vue3watchEffect的作用和Vue2中的watch作用是一样的,他们都是用来监听响应式状态发生变化的,当响应式状态发生变化时,都会触发一个回调函数
  • 侦听数据源类型:
    • 它可以是一个ref(包括计算属性)
    • 一个响应式对象
    • 一个getter函数
    • 或多个数据源组成的数组

监听 ref 代理的单个数据

javascript
let num1 = ref(1)
watch(num1, (newValue, oldValue) => {
  console.log(newValue, oldValue)
})
let update = () =>{
  num1.value += 1
}

监听ref代理的多个数据

javascript
let num1 = ref(1)
let num2 = ref(2)
watch([num1,num2], (newValue, oldValue) => {
  console.log(newValue, oldValue)
})
let update =()=>{
  num1.value += 1
  num2.value += 2
}

ref监听对象(需要手动增加deep:true)

javascript
let person = ref({
  name: 'John',
  age: 18
  foo:{
    bar:'running'
  }
})
watch(person, (newValue, oldValue) => {
  console.log(newValue, oldValue)
},{deep:true})
let update = () =>{
  person.value.name += '!'
  person.value.age += 1
  person.value.foo.bar += '~'
}

reactive监听一个对象

javascript
let person = reactive({
  name: 'John',
  age: 18,
  foo:{
    bar:'running'
  }
})
watch(person, (newValue, oldValue) => {
  console.log(newValue, oldValue)
})
let update = () =>{
  person.name += '!'
  person.age += 1
  person.foo.bar += '~'
}

reactive监听对象上的一个属性

javascript
let person = reactive({
  name: 'John',
  age: 18,
  foo:{
    bar:'running'
  }
})
watch(()=> person.name, (newValue, oldValue) => {
  console.log(newValue, oldValue)
})
let update = () =>{
  person.name += '!'
  person.age += 1
  person.foo.bar += '~'
}

reactive监听对象上的多个属性

javascript
let person = reactive({
  name: 'John',
  age: 18,
  foo:{
    bar:'running'
  }
})
watch([()=> person.name,()=> person.foo.bar], (newValue, oldValue) => {
  console.log(newValue, oldValue)
})
let update = () =>{
  person.name += '!'
  person.age += 1
  person.foo.bar += '~'
}

watchEffect

  • 使用watchEffect方法,它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数
    javascript
    let num1 = ref(1)
    let num2 = ref(2)
    let person = reactive({
      name: 'John',
      age: 18,
      foo:{
        bar:'running'
      }
    })
    watchEffect(() => {
      console.log(num1.value, num2.value)
      console.log(person.name, person.age,person.foo.bar)
    })
    let update = () =>{
      num1.value += 1
      num2.value += 2
      person.name += '!'
      person.age += 1
      person.foo.bar += '~'
    }
  • 清除副作用
    • 副作用就是执行某种操作,如对外部可变数据或变量的修改,外部接口的调用等
    • watchEffect的回调函数就是一个副作用函数,因为我们使用watchEffect就是侦听到依赖的变化后执行某些操作
    • 当执行副作用函数时,它势必会对系统带来一些影响
    • 如在副作用函数里执行了一个定时器setInterval,因此我们必须处理副作用
    • Vue3的watchEffect侦听副作用传入的函数可以接收一个onInvalidate函数作为入参
    • 用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:
      • 副作用即将重新执行时(即依赖的值改变)
      • 侦听器被停止 (通过显示调用返回值停止侦听,或组件被卸载时隐式调用了停止侦听)
      javascript
      // 定时器注册和销毁
      watchEffect((onInvalidate) => {
        const timer = setInterval(()=> {
          // ...
        }, 1000)
        onInvalidate(() => clearInterval(timer))
      })
      const handleClick = () => {
      // ...
      }
      // dom的监听和取消监听
      onMounted(()=>{
        watchEffect((onInvalidate) => {
          document.querySelector('.btn').addEventListener('click', handleClick, false)
          onInvalidate(() => document.querySelector('.btn').removeEventListener('click', handleClick))
        })
      })
      // 利用watchEffect作一个防抖节流(如取消请求)
      const id = ref(13)
      watchEffect(onInvalidate => {
        // 异步请求
        const token = performAsyncOperation(id.value)
        // 如果id频繁改变,会触发失效函数,取消之前的接口请求
        onInvalidate(() => {
          // id has changed or watcher is stopped.
          // invalidate previously pending async operation
          token.cancel()
        })
      })

停止监听器

javascript
const unWatch1 = watch(person,() => {})
const unWatch2 = watchEffect(() => {})
unWatch1()
unWatch2()

总结

  • watch是惰性执行的,而watchEffect不是
  • 不考虑watch第三个配置参数的情况下,watch在组件第一次执行的时候是不会执行的
  • 只有在之后依赖项变化的时候再执行,而watchEffect是在程序执行到此处的时候就会立即执行,而后再响应其依赖变化执行
  • watch需要传递监听的对象,watchEffect不需要

5. 自定义指令 (Custom Directives)

  • 自定义指令+后台返回数据-鉴权
    vue
    <script setup lang="ts">
    import { ref, reactive, Directive } from 'vue'
    localStorage.setItem('userId', 'userId001')
    const permission = [
      // 'userId001:product:create',
      'userId001:product:edit',
      'userId001:product:delete'
    ]
    const userId = localStorage.getItem('userId') as string
    const vHasShow: Directive<HTMLElement, string> = (el, binding) => {
      if (!permission.includes(userId + ':' + binding.value)) {
        el.style.display = 'none'
      }
    }
    </script>
    <template>
      <button v-has-show="'product:create'">创建</button>
      <button v-has-show="'product:edit'">修改</button>
      <button v-has-show="'product:delete'">删除</button>
    </template>
    <style scoped>

6. ref 和 reactive

总结

  • ref:基本类型数据对象类型数据
  • reactive:对象类型数据
  • 区别:
    • ref定义的变量必须使用.value
    • reactive重写分配一个新对象,会失去响应式(可以使用Object.assign(obj,obj1)
  • 使用规则:
    • 若需要一个基本类型的响应式数据,必须使用ref
    • 若需要一个响应式对象,层级不深,refreactive都可以
    • 若需要一个响应式对象,且层级较深,推荐使用reactive
  • 实例:
    vue
    <script setup lang="ts">
    import { ref, reactive } from 'vue'
    let name = ref('张三')
    let person1 = reactive({ name: 'John', age: 18 })
    let updatePerson1 = () => {
      Object.assign(person1, { name: 'Joy', age: 19 })}
    </script>
    <template>
    <div>姓名:{{ name }}</div>
    <div>姓名:{{ person1.name }}</div>
    <div>年龄:{{ person1.age }}</div>
    <button @click="updatePerson1">修改person1</button>
    </template>

7. toRef 和 toRefs

总结

  • 将一个响应式对象里的每个属性,转换为ref对象
  • toReftoRefs功能一致,但是toRefs可以批量操作
  • 实例:
    vue
    <template>
      <div>姓名:{{ name }}</div>
      <div>年龄:{{ age }}</div>
      <div>年龄:{{ age1 }}</div>
    </template>
    
    <script setup lang="ts">
      import { toRefs, toRef, reactive } from 'vue'
      let person1 = reactive({ name: 'John', age: 18 })
      let age1 = toRef(person1, 'age') //单个数据操作
      let {name,age} = toRefs(person1)   //批量操作
    </script>

8. computed

  • 通过计算属性的fullName是一个只读属性
vue
<template>
  姓:<input type="text" v-model="firstName" /> <br />
  名:<input type="text" v-model="lastName" /><br />
  <div>fullName:{{ fullName }}</div>
</template>

<script setup lang="ts">
  import { ref, computed } from 'vue'
  let firstName = ref('zhang')
  let lastName = ref('san')
  let fullName = computed(() => {
    return (
      firstName.value.slice(0, 1).toUpperCase() +
      firstName.value.slice(1) +
      '-' +
      lastName.value
    )
  })
</script>
  • 通过计算属性的fullName是一个可读可写属性
vue
<template>
  姓:<input type="text" v-model="firstName" /> <br />
  名:<input type="text" v-model="lastName" /><br />
  <div>fullName:{{ fullName }}</div>
  <button @click="changeFullName">更新fullName</button>
</template>
<script setup lang="ts">
  import { ref, computed } from 'vue'
  let firstName = ref('zhang')
  let lastName = ref('san')
  let fullName = computed({
    get() {
      return (
        firstName.value.slice(0, 1).toUpperCase() +
        firstName.value.slice(1) +
        '-' +
        lastName.value
      )
    },
    set(val) {
      let [str1, str2] = val.split('-')
      firstName.value = str1
      lastName.value = str2
    }
  })
  let changeFullName = () => {
    fullName.value = 'li-si'
  }
</script>

9. Router

ts
// main.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      name: 'zhuye',
      path: '/home',
      component: () => import('@/components/Home.vue')
    },
    {
      name: 'xinwen',
      path: '/news',
      component: () => import('@/components/News.vue'),
      children: [
        {
          name: 'xiangqing',
          path: 'details',
          component: () => import('@/components/Details.vue')
        }
      ]
    },
    {
      name: 'guanyu',
      path: '/about',
      component: () => import('@/components/About.vue')
    }
  ]
})
export default router
js
// App.vue
<RouterLink to="home" active-class="active">主页</RouterLink>
<RouterLink to="news" active-class="active">新闻</RouterLink>
<RouterLink to="about " active-class="active">关于</RouterLink>

query传参

  • query的第一种写法:直接通过模版字符串拼接
javascript
<div>
  <ul>
    <li v-for="item in newsList" :key="item.id">
      <RouterLink :to="`/news/details?id=${item.id}&title=${item.title}&content=${item.content}`">{{ item.title }}
      </RouterLink>
    </li>
  </ul>
  <div>
    <RouterView></RouterView>
  </div>
</div>
  • query的第二种写法(对象写法:path+query
javascript
// News.vue
<div>
  <ul>
    <li v-for="item in newsList" :key="item.id">
      <RouterLink
        :to="{
          path: '/news/details',
          query: {
            id: item.id,
            title: item.title,
            content: item.content
          }
        }"
        >{{ item.title }}
      </RouterLink>
    </li>
  </ul>
  <div>
    <RouterView></RouterView>
  </div>
</div>
  • query的第二种写法(对象写法:name+query
javascript
// News.vue
<div>
  <ul>
    <li v-for="item in newsList" :key="item.id">
      <RouterLink
        :to="{
          name: 'xiangqing',
          query: {
            id: item.id,
            title: item.title,
            content: item.content
          }
        }"
        >{{ item.title }}
      </RouterLink>
    </li>
  </ul>
  <div>
    <RouterView></RouterView>
  </div>
</div>
  • Detail页面接收query参数和使用
typescript
// Detail.vue
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { toRefs } from 'vue'
let route = useRoute()
let { query } = toRefs(route)
</script>
<template>
  <ul>
    <li>id:{{ query.id }}</li>
    <li>title:{{ query.title }}</li>
    <li>content:{{ query.content }}</li>
  </ul>
</template>

param传参

  • params的第一种写法:直接通过模版字符串拼接
javascript
<RouterLink
  :to="`/news/details/${item.id}/${item.title}/${item.content}`"
  >{{ item.title }}
</RouterLink>
  • params的第二种写法:(对象写法:name+params
typescript
<RouterLink
  :to="{
    name: 'xiangqing',
    params: {
      id: item.id,
      title: item.title,
      content: item.content
    }
  }"
  >{{ item.title }}
</RouterLink>
  • 需要给参数设置占位符(例如第12行)
javascript
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      name: 'xinwen',
      path: '/news',
      component: () => import('@/components/News.vue'),
      children: [
        {
          name: 'xiangqing',
          path: 'details/:id/:title/:content',
          component: () => import('@/components/Details.vue')
        }
      ]
    }
  ]
})
export default router
  • Detail页面接收params参数和使用
typescript
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { toRefs } from 'vue'
let route = useRoute()
console.log(route)
let { params } = toRefs(route)
</script>
<template>
  <ul>
    <li>id:{{ params.id }}</li>
    <li>title:{{ params.title }}</li>
    <li>content:{{ params.content }}</li>
  </ul>
</template>

路由的props配置

  • 将路由参数作为props传给组件
javascript
// Details.vue 页面直接使用id,title,content
<ul>
  <li>{{id}}<li>
  <li>{{title}}<li>
  <li>{{content}}<li>
<ul>
typescript
<script lang="ts" setup>
  defineProps(["id","title","content"])
</scrip>
  • 布尔写法
javascript
routes: [
{
  name: 'xinwen',
  path: '/news',
  component: () => import('@/components/News.vue'),
  children: [
    {
      name: 'xiangqing',
      path: 'details/:id/:title/:content',
      component: () => import('@/components/Details.vue'),
      props: true   // 只能传params参数
    }
  ]
}
]
  • 函数写法
javascript
routes: [
  {
    name: 'xinwen',
    path: '/news',
    component: () => import('@/components/News.vue'),
    children: [
      {
        name: 'xiangqing',
        path: 'details/:id/:title/:content',
        component: () => import('@/components/Details.vue'),
        props(route){
          // 函数写法,可以传query参数也可以传params参数
          return route.query //return route.query
        }
      }
    ]
  }
]
  • 对象写法
javascript
routes: [
  {
    name: 'xinwen',
    path: '/news',
    component: () => import('@/components/News.vue'),
    children: [
      {
        name: 'xiangqing',
        path: 'details/:id/:title/:content',
        component: () => import('@/components/Details.vue'),
        props:{ // 可以自己决定将什么作为props传给路由组件
          a:1,
          b:2,
          c:3
        }
      }
    ]
  }
]

10. 组件通信

  1. props是使用频率最高的一种通信方式 父子可以相互传递

    • 父传子:属性值非函数
    • 子传父:属性值是函数
    typescript
    // 父组件
    <script setup lang="ts">
      import { ref } from 'vue'
      import Child from '@/pages/01-props/Child.vue'
      let name = ref('张三')
      let age = ref()
      let getAge = (value: number) => {
        age.value = value
    }
    </script>
    <template>
      <h1>父组件</h1>
      <h3>父亲姓名:{{ name }}</h3>
      <h3 v-show="age">通过子传父获取到的年龄:{{ age }}</h3>
      <Child :name="name" :sentAge="getAge" />
    </template>
    typescript
    // 子组件
    <script setup lang="ts">
      import { ref } from 'vue'
      defineProps(['name', 'sentAge'])
      let age = ref(18)
    </script>
    <template>
      <div>
        <h1>子组件</h1>
        <h5>儿子年龄:{{ age }}</h5>
        <h5>name通过父传子过来的:{{ name }}</h5>
        <button @click="sentAge(age)">向父组件传递数据age</button>
      </div>
    </template>
  2. 自定义事件

    typescript
     // 父组件
     <script setup lang="ts">
       import { ref } from 'vue'
       import Child from '@/pages/02-custom-event/Child.vue'
       let name = ref('张三')
       let age = ref()
       let getAge = (value) => {
         age.value = value
       }
     </script>
     <template>
       <h1>父组件</h1>
       <h3>父亲姓名:{{ name }}</h3>
       <h4 v-show="age">子传父获取到的年龄:{{ age }}</h4>
       <Child @sent-age="getAge" />
     </template>
    typescript
    // 子组件
    <script setup lang="ts">
      import { ref } from 'vue'
      const emit = defineEmits(['sent-age'])
      let age = ref(18)
    </script>
    <template>
      <div>
        <h1>子组件</h1>
        <h5>儿子年龄:{{ age }}</h5>
        <button @click="emit('sent-age', age)">
          通过自定义事件向父组件传递数据age
        </button>
      </div>
    </template>
  3. mitt

    typescript
    // @/utils/emitter
    import mitt from 'mitt'
    export const emitter = mitt()
    typescript
    // 子组件1
    <script setup lang="ts">
      import { ref } from 'vue'
      import { emitter } from '@/utils/emitter'
      let age = ref(18)
      let toy = ref('')
      let sentAge = () => {
        emitter.emit('sent-age', age.value)
      }
      emitter.on('sent-toy', (value: any) => {
        console.log('子组件1收到玩具', value)
        toy.value = value
      })
    </script>
    <template>
      <div>
        <h1>子组件1</h1>
        <h4>儿子的年龄:{{ age }}</h4>
        <h4 v-show="toy">子组件2传递的玩具:{{ toy }}</h4>
        <button @click="sentAge">把年龄传递给子组件2</button>
      </div>
    </template>
    typescript
    // 子组件2
    <script setup lang="ts">
      import { ref } from 'vue'
      import { emitter } from '@/utils/emitter'
      let toy = ref('奥特曼')
      let age = ref()
      emitter.on('sent-age', (value: any) => {
        console.log('子组件2收到年龄', value)
        age.value = value
     })
      let sentToy = () => {
        emitter.emit('sent-toy', toy.value)
     }
    </script>
    <template>
     <div>
       <h1>子组件2</h1>
       <h4>儿子的玩具:{{ toy }}</h4>
       <h4 v-show="age">子组件1传递的年龄:{{ age }}</h4>
       <button @click="sentToy">把玩具传给子组件1</button>
     </div>
    </template>
  4. v-model

    typescript
    // 父组件
    <script setup lang="ts">
      import { ref } from 'vue'
      import ModelInput from './ModelInput.vue'
      let username = ref('张三')
      let password = ref(123456)
    </script>
    <template>
      <h1>父组件</h1>
      <!-- v-model 用在html标签上 -->
      <!-- v-model 本质上是语法糖 -->
      <!-- 13行是12行底层实现原理 -->
      姓名:
      <input type="text" v-model="username" />
      <input
        type="text"
        :value="username"
        @input="username = (<HTMLInputElement>$event.target).value"
      />
      <br /><br />
      <!-- v-model用在组件标签上  -->
      密码:
      <ModelInput v-model="password" />
      <ModelInput :modelValue="password" @update:modelValue="password = $event" />
    </template>
    typescript
    // 子组件
    <script setup lang="ts">
      defineProps(['modelValue'])
      let emit = defineEmits(['update:modelValue'])
    </script>
    <template>
      <input
        type="text"
        :value="modelValue"
        @input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)"
      />
    </template>
  5. $attr

    typescript
    // 祖组件
    <script setup lang="ts">
      import { ref } from 'vue'
      import Child from './Child.vue'
      let a = ref(1)
      let b = ref(2)
      let c = ref(3)
      let updateB = (value: number) => {
        b.value += value
      }
    </script>
    <template>
      <div>
        <h3>父组件</h3>
        <h4>a:{{ a }}</h4>
        <h4>b:{{ b }}</h4>
        <h4>c:{{ c }}</h4>
        <Child
          :a="a"
          :b="b"
          :c="c"
          v-bind="{ x: 100, y: 200 }"
          :updateB="updateB"
        />
      </div>
    </template>
    typescript
    // 子组件
    <script setup lang="ts">
      import GrandChild from './GrandChild.vue'
      defineProps(['a']) // 子组件只接收了数据a 其他数据全部在$attrs中
    </script>
    <template>
      <div>
        <h3>子组件</h3>
        a:{{ a }} 其他:{{ $attrs }}
        <GrandChild v-bind="$attrs" /> // 把$attrs中的数据传递给孙组件
      </div>
    </template>
    typescript
    // 孙组件
      <script setup lang="ts">
        defineProps(['b', 'c', 'x', 'y', 'updateB'])
      </script>
      <template>
        <div>
          <h3>孙组件</h3>
          <h4>b:{{ b }}</h4>
          <h4>c:{{ c }}</h4>
          <h4>x:{{ x }}</h4>
          <h4>y:{{ y }}</h4>
          // 在孙组件中修改b的值,祖组件b的值也会跟着改变
          <button @click="updateB(10000)">更新祖组件b的值</button>
        </div>
      </template>
  6. $refs-$parent

    • 父组件通过$refs获取子组件的实例,
    • 子组件通过$parent获取父组件的实例
    typescript
    //父组件
    <script setup lang="ts">
      import { ref, reactive } from 'vue'
      import Child1 from './Child1.vue'
      import Child2 from './Child2.vue'
      let money = ref(100)
      let car = reactive({ name: '奔驰', price: 40 })
      defineExpose({ money, car })
      let c1 = ref()
      let c2 = ref()
      // 对c1子组件进行数据更新
      let updateC1Data = () => {
        c1.value.toy = '挖掘机'
        c1.value.books += 1
      }
      // 对c2子组件进行数据更新
      let updateC2Data = () => {
        c2.value.computer = 'mac'
        c2.value.books += 2
      }
      // 对于很多个子组件进行数据更新
      let updateChild = (refs: { [key: string]: any }) => {
        console.log(refs)
        refs.c1.toy = '挖掘机'
        refs.c1.books += 1
        refs.c2.computer = 'mac'
        refs.c2.books += 2
      }
    </script>
    <template>
      <div class="fa">
        <h3>父组件</h3>
        <h4>money: {{ money }}万</h4>
        <h4>car: {{ car.name }} - {{ car.price }}万</h4>
        <button @click="updateC1Data">更新子组件1-所有数据</button><br />
        <button @click="updateC2Data">更新子组件2-所有数据</button><br />
        // 对于很多个子组件需要引入$refs
        <button @click="updateChild($refs)">获取所有数据-再更新数据</button><br />
        // 给2个子组件都打上ref标识
        <Child1 ref="c1" />
        <Child2 ref="c2" />
      </div>
    </template>
    typescript
    // Child1.vue
    <script setup lang="ts">
      import { ref } from 'vue'
      let toy = ref('奥特曼')
      let books = ref(5)
      defineExpose({ toy, books })
      // 更新父组件数据
      let updateMoney = (parent: any) => {
        parent.money += 1
      }
    </script>
    <template>
      <div class="ch1">
        <h3>子组件1</h3>
        <h4>toy:{{ toy }}</h4>
        <h4>books:{{ books }}本</h4>
       // 引入$parent
        <button @click="updateMoney($parent)">修改父组件数据money</button>
      </div>
    </template>
    typescript
    // Child2.vue
    <script setup lang="ts">
      import { ref } from 'vue'
      let computer = ref('华为')
      let books = ref(10)
      defineExpose({ computer, books })
      // 更新父组件数据
      let updateCar = (parent: any) => {
        parent.car.name = '宝马'
        parent.car.price = 20
      }
    </script>
    <template>
      <div class="ch1">
        <h3>子组件2</h3>
        <h4>computer:{{ computer }}</h4>
        <h4>books:{{ books }}本</h4>
        // 引入$parent
        <button @click="updateCar($parent)">修改父组件数据car</button>
      </div>
    </template>
  7. provideinject

    • 父组件中使用provide来提供数据
    • 在子组件中使用inject来开始使用这些数据
    typescript
    // 父组件
    <script setup lang="ts">
      import { ref, reactive, provide } from 'vue'
      import Child from './Child.vue'
      let money = ref(100)
      let car = reactive({
        brand: '奔驰',
        price: 50
      })
      let updateMoney = (value: number) => {
        money.value -= value
      }
      let updateCar = () => {
        car.price = 20
        car.brand = '宝马'
      }
      provide('money', {
        money,
        updateMoney
      })
      provide('car', {
        car,
        updateCar
      })
    </script>
    <template>
      <div class="fa">
        <h3>父组件</h3>
        <h4>money:{{ money }}</h4>
        <h4>品牌:{{ car.brand }}</h4>
        <h4>价格:{{ car.price }}</h4>
        <Child />
      </div>
    </template>
    typescript
    // 子孙组件
    <script setup lang="ts">
      import { inject } from 'vue'
      let { money, updateMoney } = inject('money', {
        money: 0,
        updateMoney: (param: number) => {}
      })
      let { car, updateCar } = inject('car', {
        car: { brand: '', price: 0 },
        updateCar: (param: number) => {}
      })
    </script>
    <template>
      <div class="gr">
        <h3>孙组件</h3>
        <h4>从祖组件获取到的money:{{ money }}</h4>
        <button @click="updateMoney(1)">更新祖组件money</button>
        <h4>从祖组件获取到车的品牌:{{ car.brand }}</h4>
        <h4>从祖组件获取到车的价格:{{ car.price }}</h4>
        <button @click="updateCar(1)">更新祖组件car</button>
      </div>
    </template>
  8. slot

  • 默认插槽
typescript
 <!--  父组件 -->
 <template>
   <div class="fa">
     <Category title="热门游戏推荐">
       <ul>
         <li v-for="item in games" :key="item.id">{{ item.name }}</li>
       </ul>
     </Category>
     <Category title="热门美食推荐">
       <img :src="imgUrl" alt="" />
     </Category>
     <Category title="热门电影推荐">
       <video :src="videoUrl" controls></video>
     </Category>
   </div>
 </template>
typescript
// 子组件
<script setup lang="ts">
  defineProps<{ title: string }>()
</script>
<template>
   <div class="ch">
     <h3>{{ title }}</h3>
     <slot>默认内容</slot>
   </div>
</template>
  • 具名插槽
typescript
 // 父组件 
 <template>
   <div class="fa">
     <Category>
       <template v-slot:s1>
         <h3>热门游戏推荐</h3>
         <ul>
           <li v-for="item in games" :key="item.id">{{ item.name }}</li>
         </ul>
       </template>
     </Category>
     <Category title="热门美食推荐">
       <template v-slot:s1>
         <h3>热门美食推荐</h3>
         <img :src="imgUrl" alt="" />
       </template>
     </Category>
     <Category title="热门电影推荐">
     // 简写 #s1与v-slot:s1相同
       <template #s1>
         <h3>热门美食推荐</h3>
         <video :src="videoUrl" controls></video>
       </template>
     </Category>
   </div>
 </template>
typescript
// 子组件
<template>
 <div class="ch">
   <slot name="s1">默认内容</slot>
 </div>
</template>
  • 作用域插槽(数据在子组件,但根据数据生成的结构由父组件决定)
typescript
// 父组件
<template>
 <div class="fa">
   <Games>
     <template v-slot="{ games }">
       <ul>
         <li v-for="item in games" :key="item.id">{{ item.name }}</li>
       </ul>
     </template>
   </Games>
   <Games>
     <template v-slot="{ games }">
       <ol>
         <li v-for="item in games" :key="item.id">{{ item.name }}</li>
       </ol>
     </template>
   </Games>
   <Games>
   // 以下3种写法都是相同的
   // v-slot="{ games }"
   // v-slot:default="{ games }"
   // #default="{ games }"
     <template v-slot:default="{ games }">
       <h3 v-for="item in games" :key="item.id">{{ item.name }}</h3>
     </template>
   </Games>
 </div>
</template>
typescript
// 子组件
 <script setup lang="ts">
   import { reactive } from 'vue'
 let games = reactive([
   { id: 1, name: '超级玛丽' },
   { id: 2, name: '冒险岛' },
   { id: 3, name: '魔兽争霸' },
   { id: 4, name: '穿越火线' }
 ])
 </script>
 <template>
   <div class="ch">
     <h2>热门游戏推荐</h2>
     <slot :games="games"></slot>
   </div>
 </template>
总结
组件关系传递方式
父传子props, v-model, $refs, 默认插槽,具名插槽
子传父props, 自定义事件, v-model, $parent, 作用域插槽
祖孙之间$attrs, provide inject
任意组件之间mitt, pinia

11. toRef,toRefs,toRaw,markRaw,customRef

总结

  • toRef: 基于响应式对象上的一个属性,创建一个对应的ref这样创建的ref与其源属性保持同步:改变源属性的值将更新 ref的值
  • toRefs: 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用toRef()创建的
  • toRaw:根据一个Vue创建的代理返回其原始对象
  • markRaw: 将一个对象标记为不可被转为代理。返回该对象本身
  • customRef: 创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显式控制
  • toRef
typescript
<script setup lang="ts">
import { reactive } from 'vue'
let state = reactive({ count: 0 })
// 自定义一个toRef
let _toRef = (object:any, key:any) => {
  return {
    get value() {
      return object[key]
    },
    set value(newValue) {
      object[key] = newValue
    }
  }
}
let count = _toRef(state, 'count')
</script>
<template>
  <h1>{{ state.count }}</h1>
  <h2>{{ count.value }}</h2>
  <br />
  // 修改count.value的值会影响state.count的值
  // 修改state.count的值也会影响count.value的值
  <button @click="count.value++">count.value</button><br />
  <button @click="state.count++">state.value.count</button>
</template>
<script>
  • toRefs
typescript
<script setup lang="ts">
import { Ref, reactive, toRef } from 'vue'
let state = reactive({ name: '张三', age: 18 })
let _toRefs = <T extends object>(object: T): { [K in keyof T]: Ref<T[K]> } => {
  // 创建一个对象,用于存储属性名和对应的 ref
  let refs = {} as { [K in keyof T]: Ref<T[K]> }
  // 遍历响应式对象的属性
  for (let key in object) {
    // 使用 ref 包装属性值
    refs[key] = toRef(object, key)
  }
  // 返回包含所有属性的 ref 对象
  return refs
}
let state1 = _toRefs(state)
</script>
<template>
  <h1>{{ state }}</h1>
  <h1>{{ state1 }}</h1>
</template>
  • toRaw
typescript
  function toRaw(observed) {
  // 检查传入的参数是否是一个响应式对象
  if (!observed || typeof observed !== 'object') {
    return observed;
  }
  // 如果传入的对象具有 __v_raw 字段,说明它是一个 Vue.js 的响应式对象
  if (observed.__v_raw) {
    return observed.__v_raw;
  }
  // 如果不是 Vue.js 的响应式对象,直接返回传入的对象
  return observed;
}
  • markRaw
typescript
  function markRaw(value) {
  // 将传入的对象标记为不可响应的
  // 通过设置 __v_skip 字段来实现
  Object.defineProperty(value, '__v_skip', {
    configurable: true,
    enumerable: false,
    value: true
  })
    return value
  }
  • customRef
typescript
// useMsgRef.ts  
import { customRef } from 'vue'
export default function (initValue: string, delay: number) {
  let timer: number
  let msg = customRef((track, trigger) => {
    return {
      get() {
        track()
        return initValue
      },
      set(newValue) {
        clearTimeout(timer)
        timer = setTimeout(() => {
          initValue = newValue
          trigger()
        }, delay)
      }
    }
  })
  return { msg }
}
// App.vue
import useMsgRef from './useMsgRef'
let {msg} = useMsgRef('hello', 500)
<h2>{{ msg}}<h2>
<input type="text" v-model="msg"/>

Copyright © 2024 Fang He