• vue3 快速入门系列 —— 其他API


    其他API

    前面我们已经学习了 vue3 的一些基础知识,本篇将继续讲解一些常用的其他api,以及较完整的分析vue2 和 vue3 的改变。

    浅层响应式数据

    shallowRef

    shallow 中文:“浅层的”

    shallowRef:浅的 ref()。

    先用 ref 写个例子:

    
    
    
    
    

    这4个按钮都会触发页面数据的变化。

    现在将 ref 改成 shallowRef,其他都不变。你会发现只有 change1 和 change4 能触发页面数据的变化:

    
    
    
    
    

    这是因为 change1 中的 a.value 是浅层,而 change2 中的 o.value.name 是深层。

    对于大型数据结构,如果只关心整体是否被替换,就可以使用 shallowRef,避免使用 ref 将大型数据结构所有层级都转成响应式,这对底层是很大的开销。

    shallowReactive

    知晓了 shallowRef,shallowReactive也类似。

    shallowReactive:浅的 reactive()。

    请看示例:

    现在3个按钮都能修改页面数据:

    
    
    
    
    

    将 reactive 改为 shallowReactive:

    import {shallowReactive} from 'vue'
    
    let o = shallowReactive({
        name: 'p',
        options: {
            age: 18,
        }
    })
    

    现在只有 change2 和 change4 能修改页面数据,因为 change3 是多层的,所以失效。

    只读数据

    readonly

    readonly : Takes an object (reactive or plain) or a ref and returns a readonly proxy to the original.

    readonly 能传入响应式数据,并返回一个只读代理

    请看示例:

    
    
    
    
    

    浏览器呈现:

    # 组件A
    
    name: p2
    // 按钮1
    change name
    
    copyName: p2
    // 按钮2
    change copyName
    

    点击第一个按钮,发现 copyName 的值也跟着变化了(说明不是一锤子买卖),但是点击第二个按钮,页面数据不会变化。浏览器控制台也会警告:

    [Vue warn] Set operation on key "value" failed: target is readonly. RefImpl {__v_isShallow: false, dep: Map(1), __v_isRef: true, _rawValue: 'p2', _value: 'p2'}
    

    readonly 只读代理是深的:任何嵌套的属性访问也将是只读的。对比 shallowReadonly 就知道了。

    Tip:使用场景,比如同事A定义了一个很重要的数据,同事B需要读取该数据,但又担心误操作修改了该数据,就可以通过 readonly 包含数据。

    shallowReadonly

    readonly 只读代理是深层的,而 shallowReadonly 是浅层的。也就是深层的 shallowReadonly 数据不是只读的。

    请看示例:

    
    
    
    
    

    通过 shallowReadonly 创建一个备份数据,点击第一个按钮没反应,点击第二个按钮,页面变成:

    # 组件A
    
    obj: { "name": "p", "options": { "age": 19 } }
    

    shallowReadonly 只处理浅层次的只读。深层次的不管,也就是可以修改。

    疑惑:笔者的开发者工具中, copyObj -> options 中的 age 属性没有表示能修改的铅笔图标。应该要有,这样就能保持和代码一致

    原始数据

    toRaw

    toRaw() can return the original object from proxies created by reactive(), readonly(), shallowReactive() or shallowReadonly().

    用于获取一个响应式对象的原始对象。修改原始对象,不会在触发视图。

    const foo = {}
    const reactiveFoo = reactive(foo)
    
    console.log(toRaw(reactiveFoo) === foo) // true
    

    比如这个使用场景:

    
    
    
    
    

    markRaw

    Marks an object so that it will never be converted to a proxy. Returns the object itself.

    标记一个对象,使其永远不会被转换为proxy。返回对象本身。

    • 有些值不应该是响应式的,例如一个复杂的第三方类实例,或者一个Vue组件对象。
    import {reactive} from 'vue'
    let o = {
        getAge() {
            console.log(18)
        }
    }
    // Proxy(Object) {getAge: ƒ}
    let o2 = reactive(o)
    
    • 当使用不可变数据源呈现大型列表时,跳过代理转换可以提高性能。

    请问输出什么:

    import {reactive} from 'vue'
    let o = {
        name: 'p',
        age: 18,
    }
    let o2 = reactive(o)
    
    console.log(o);
    console.log(o2);
    

    答案是:

    {name: 'p', age: 18}
    Proxy(Object) {name: 'p', age: 18}
    

    通过 reactive 会将数据转为响应式。

    请看 markRaw 示例:

    import {reactive, markRaw} from 'vue'
    // 标记 o 不能被转成响应式
    let o = markRaw({
        getAge() {
            console.log(18)
        }
    })
    let o2 = reactive(o)
    
    // {__v_skip: true, getAge: ƒ}
    console.log(o2);
    

    比如中国的城市,数据是固定不变的,我不做成响应式的,别人也不许做成响应式的。我可以这么写:

    // 中国就这些地方,不会变。我自己不做成响应式的,别人也不许做成响应式的
    let citys = markRow([
        {name: '北京'},
        {name: '上海'},
        {name: '深圳'},
        ...
    ])
    

    customRef

    自定义 ref 可用于解决内置 ref 不能解决的问题。

    ref 用于创建响应式数据,数据一变,视图也会立刻更新。比如要1秒后更新视图,这个 ref 办不到。

    先用ref写个例子:input 输入字符,msg 立刻更新:

    
    
    
    
    

    现在要求:input输入字符后,等待1秒msg才更新。

    我们可以用 customRef 解决这个问题。

    实现如下:

    
    
    
    
    

    customRef() 接收一个工厂函数作为参数,这个工厂函数接受 track 和 trigger 两个函数作为参数,并返回一个带有 get 和 set 方法的对象。

    track()trigger() 缺一不可,需配合使用:

    • 缺少 track,即使通知vue 更新了数据,但不会更新视图
    • 缺少 trigger,track 则一直在等着数据变,快变,我要更新视图。但最终没人通知它数据变了

    实际工作会将上述功能封装成一个 hooks。使用起来非常方便。就像这样:

    // hooks/useMsg.ts
    import { customRef, } from 'vue'
    
    export function useMsg(value: string, delay = 1000) {
    
      // customRef 传入函数,里面又两个参数
      let msg = customRef((track, trigger) => {
        // 防抖
        let timeout: number
        return {
          get() {
            // 告诉 vue 这个数据很重要,要持续关注,数据一旦变化,更新视图
            track()
            return value
          },
          set(newValue) {
            clearTimeout(timeout)
            timeout = setTimeout(() => {
              value = newValue
              // 告诉vue我更新数据了,你更新视图去吧
              trigger()
            }, delay)
          }
        }
      })
    
      return msg
    }
    

    使用起来和 ref 一样方便。就像这样:

    
    
    
    
    

    Teleport

    Teleport 中文“传送”

    Teleport 将其插槽内容渲染到 DOM 中的另一个位置。

    比如 box 内的内容现在在 box 元素中:

    
    
    

    我可以利用 Teleport 新增组件将其移到body下面。

    
    
    
    
    

    现在这段ui内容就移到了 body 下,并且数据链还是之前的,也就是 msg 仍受 button 控制。

    Tip:to 必填,语法是选择器或实际元素

    
    
    
    

    Suspense

    suspense 官网说是一个实验性功能。用来在组件树中协调对异步依赖的处理。

    我们首先在子组件中异步请求,请看示例:

    
    
    
    
    
    
    
    
    
    

    Tip:我们现在用了 setup 语法糖,没有机会写 async,之所以能这么写,是因为底层帮我们做了。

    浏览器查看,发现子组件没有渲染出来。控制台输出:

    // main.ts:14 [Vue 警告]: 组件 : setup 函数返回了一个 Promise,但在父组件树中未找到  边界。带有异步 setup() 的组件必须嵌套在  中才能被渲染。
    main.ts:14 [Vue warn]: Component : setup function returned a promise, but no  boundary was found in the parent component tree. A component with async setup() must be nested in a  in order to be rendered. 
    
    data: {code: 1, data: {…}}
    

    vue 告诉我们需要使用 Suspense。

    假如我们将 await 用 async 方法包裹,子组件能正常显示。

    
    
    
    
    

    继续讨论异步的 setup()的解决方案。在父组件中使用 Suspense 组件即可。请看代码:

    
    
    
    
    

    子组件也稍微调整下:

    
    
    
    
    

    利用开发者工具将网速跳到 3G,再次刷新页面,发现先显示Loading...,然后在显示

    # 组件A
    
    data: { "code": 1, "data": { "name": "阿普的思念", "url": "http://music.163.com/song/media/outer/url?id=2096764279", "picurl": "http://p1.music.126.net/Js1IO7cwfEe6G6yNPyv5FQ==/109951169021986117.jpg", "artistsname": "诺米么Lodmemo" } }
    

    :数据是一次性出来的,不是先展示 {} 在展示 {...}。所以我们再看官网,就能理解下面这段内容:

    
    └─ 
       ├─ 
       │  └─ (组件有异步的 setup())
       └─ 
          ├─  (异步组件)
          └─ (异步组件)
    

    在这个组件树中有多个嵌套组件,要渲染出它们,首先得解析一些异步资源。如果没有 ,则它们每个都需要处理自己的加载、报错和完成状态。在最坏的情况下,我们可能会在页面上看到三个旋转的加载态,在不同的时间显示出内容。

    有了 组件后,我们就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态。

    Tip: 在 React 中可以使用 Suspense 组件和 React.lazy() 函数来实现组件的延迟加载。就像这样:

    import React, {Suspense} from 'react'
    // 有当 OtherComponent 被渲染时,才会动态加载 ‘./math’ 组件
    const OtherComponent = React.lazy(() => import('./math'))
    
    function TestCompoment(){
        return 
    loading
    }>
    }

    全局 api 转移到应用对象

    在 Vue 3 中,一些全局 API 被转移到了应用对象(app)中。

    app就是这个:

    import { createApp } from 'vue'
    
    const app = createApp({
      /* 根组件选项 */
    })
    

    这些 API 以前在 Vue 2 中是全局可用的,但在 Vue 3 中,出于更好的模块化和灵活性考虑,许多 API 被转移到了应用对象中。

    app.component

    对应 vue2 中 Vue.component,用于注册和获取全局组件。

    例如定义一个组件:

    
    

    在 main.ts 中注册:

    import Apple from '@/views/Apple.vue'
    app.component('Apple', Apple)
    

    现在在任何地方都能直接使用,例如在 ChildA.vue 中:

    
    
    
    
    

    app.config

    vue2 中有 Vue.prototype. 比如 Vue.prototype.x = 'hello',在任意模板中 {{x}} 都会输出 hello

    这里有 app.config。

    比如在 main.ts 中增加:app.config.globalProperties.x = 'hello',在任意组件中就可以获取:

    
    

    但是 ts 会报错,因为找不到 x。

    解决方法在官网中有提供。创建一个 ts:

    // test.ts
    // 官网:https://cn.vuejs.org/api/application.html#app-config-globalproperties
    // 正常工作。
    export {}
    
    declare module 'vue' {
      interface ComponentCustomProperties {
        x: string,
      }
    }
    

    然后在 main.ts 中引入:

    import '@/utils/test'
    app.config.globalProperties.x = 'hello'
    

    不要随便使用,否则你一下定义100个,以后出问题不好维护。

    app.directive

    Vue.directive() - 注册或获取全局指令。

    我们用函数形式的指令,就像这样:

    // https://v2.cn.vuejs.org/v2/guide/custom-directive.html#函数简写
    Vue.directive('color-swatch', function (el, binding) {
      el.style.backgroundColor = binding.value
    })
    

    比如我写一个这样的指令:

    // main.ts 注册一个全局指令
    app.directive('green', (element, {value}, vnode) => {
        element.innerText += value
        element.style.color = 'green'
    })
    

    接着使用指令:

    
    
    
    
    

    页面呈现:

    # 组件A
    // 绿色文字
    你好兄弟
    

    其他

    app.mount - 挂载
    app.unmount - 卸载
    app.use - 安装插件。例如路由、pinia

    非兼容性改变

    非兼容性改变Vue 2 迁移中的一章,列出了 Vue 2 对 Vue 3 的所有非兼容性改变

    Tip:强烈建议详细阅读该篇。

    全局 API 应用实例

    Vue 2.x 有许多全局 API 和配置,它们可以全局改变 Vue 的行为。例如,要注册全局组件,可以使用 Vue.component API

    虽然这种声明方式很方便,但它也会导致一些问题。从技术上讲,Vue 2 没有“app”的概念,我们定义的应用只是通过 new Vue() 创建的根 Vue 实例。从同一个 Vue 构造函数创建的每个根实例共享相同的全局配置

    全局配置使得在同一页面上的多个“应用”在全局配置不同时共享同一个 Vue 副本非常困难

    为了避免这些问题,在 Vue 3 中我们引入了...

    一个新的全局 API:createApp

    全局和内部 API 都经过了重构,现已支持 TreeShaking (摇树优化)

    如果你曾经在 Vue 中手动操作过 DOM,你可能会用过这种方式:

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

    但是,如果你从来都没有过手动操作 DOM 的必要,或者更喜欢使用老式的 window.setTimeout() 来代替它,那么 nextTick() 的代码就会变成死代码。

    如 webpack 和 Rollup (Vite 基于它) 这样的模块打包工具支持 tree-shaking,遗憾的是,由于之前的 Vue 版本中的代码编写方式,如 Vue.nextTick() 这样的全局 API 是不支持 tree-shake 的,不管它们实际上是否被使用了,都会被包含在最终的打包产物中。

    Tip:Vite 基于 Rollup

    在 Vue 3 中,全局和内部 API 都经过了重构,并考虑到了 tree-shaking 的支持。因此,对于 ES 模块构建版本来说,全局 API 现在通过具名导出进行访问。例如,我们之前的代码片段现在应该如下所示:

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

    通过这一更改,如果模块打包工具支持 tree-shaking,则 Vue 应用中未使用的全局 API 将从最终的打包产物中排除,从而获得最佳的文件大小。

    v-model 指令在组件上的使用已经被重新设计,替换掉了 v-bind.sync

    • 非兼容:用于自定义组件时,v-model prop 和事件默认名称已更改:
      • prop:value -> modelValue;
      • 事件:input -> update:modelValue;
    • 非兼容:v-bind 的 .sync 修饰符和组件的 model 选项已移除,可在 v-model 上加一个参数代替;
    • 新增:现在可以在同一个组件上使用多个 v-model 绑定;
    • 新增:现在可以自定义 v-model 修饰符。

    sync 和 model 选项已废除