聊聊vue3.0 在项目中使用的一些招式 - 外功篇(一)

1,649 阅读8分钟

前言

Vue3.0 在去年 9 月正式发布了,伴随着一些新的API以及黑魔法的到来,有些开发者已经应用在新创建的项目中,有的开发者进行了升级,有的还在观望中。那么我们今天不聊内功(源码),只聊聊 vue3.0 的招式套路,如何在武林中独树一帜的。

一、万剑归宗

万剑归宗乃是剑术最高境界,一经使出万剑归宗如仆见主,如朝拜到尊神一般。剑招一出,凌厉无匹的剑劲由体而生,身形可化着一股青烟,劲气四散弥漫。无数利剑狂风暴雨般的飞卷。漫天飞舞,剑势如网,凌厉无匹,蔚为奇观。也可以操纵万剑发动攻击。

武侠中剑法的万剑归宗,示意剑术的最高境界,而 vue3.0 中的h函数,可涵盖所有 dom 树的渲染及事件、属性的绑定。

2.1 h 渲染函数 介绍

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。

2.1.1 插槽渲染

有一名为demo的组件,组件内部留有插槽名 demoSlot,使用h渲染函数的写法是:

2.1.2 事件绑定

我们必须为事件处理程序提供一个正确的prop名称,例如,要处理 click 事件,prop名称应该是 onClick

2.2 写法示例

import { defineComponent, h } from 'vue';

export default defineComponent({
  name: 'renderDemo',
  props: ['modelValue'],
  setup(props, { slots, emit }) {
    return () => {
      return h('demo', {
        // dom style
        style: {},
        // class 样式名
        class: [],
        // dom 自定义属性
        'data-id': '123456',
        // v-on、@指令示例
        onClick: event => {
          console.log('event', event);
        },
        // demoSlot 插槽名渲染示例
        demoSlot: () => {
          return h('div', '1111');
        },
        // v-model 示例
        modelValue: props.modelValue,
        'onUpdate:modelValue': value => emit('update:modelValue', value)
      })
    };
  },
});

更多可查看: github.com/vuejs/rfcs/…

二、层层跌浪掌

层波叠浪,云舒浪卷,环环相扣,延绵不绝。

1.1 递归组件渲染

const vnodeSchema = {
  id: '001',
  class: 'parent-class',
  componentName: 'demo',
  children: [
    {
      id: '00101',
      class: 'children-class',
      componentName: 'demo',
    },
    {
      id: '00101',
      class: 'children-class',
      componentName: 'demo',
    }
  ]
}

思路是:根据componentName名称,使用 resolveComponent 获取到组件的实例vnode,将 vnodeSchema 中的组件属性进行解析,返回真实的props,最后调用 h 渲染函数,进行vnode节点的渲染并进行props赋值。递归调用就可根据vnodeSchema的描述,动态渲染dom节点。

三、阴阳两极剑

仙都派配合技,黄木道人所创,二人同使,一攻一守,按照易经八八六十四卦的卦象,剑招生生灭灭,消消长长,隐隐有风雷之势。 阴阳两仪剑,剑意主旨为一攻一守,生生不息,在 vue3.0 中响应式数据重点在于一读一写,分别触发依赖中收集的get、set。

3.1 响应式数据API介绍

3.1.1 reactive

接收一个普通对象然后返回该普通对象的Proxy,默认深层次代理,如果项目中不需要深层次的代理,可使用 shallowReactive 提高性能。

在Vue.js 3.0中 reactivity 模块封装了一些响应式系统的工具集,在内部源码中进行一些边界判断和类型转换的时候使用,把这些方法导出可让使用者根据自己的使用场景,markRaw 和 shallowXXX 用来跳过深度代理,例如:当渲染一个元素数量庞大,但是数据是不可变的,跳过 Proxy 的转换可以带来性能提升。

let result = { description: { version: '2.0' } }; 
const resultProxy = reactive(result); 
console.log(isReactive(resultProxy.description)); // true

3.1.2 ref

接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性,如果传入 ref 的是一个对象,将调用 reactive 方法进行深层响应转换。

3.1.3 toRef

可以用来为源响应式对象上的 property 性创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。

3.1.4 toRefs

将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref

解决解构赋值响应特性丢失问题: 丢失数据的响应式,你会发现数据改变了,视图没有发生改变。

// 组合函数:
function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0,
  })

  return pos;
}

// 消费者组件
export default {
  setup() {
    // 这里解构赋值会丢失响应性! 
    const { x, y } = useMousePosition()
    return {
      x,
      y,
    }

    // 这样写也会丢失响应性!
    return {
      ...useMousePosition(),
    }

    // 这样写,保持响应性!
    // 你必须返回 `pos` 本身,并按 `pos.x` 和 `pos.y` 的方式在模板中引用 x 和 y。
    return {
      pos: useMousePosition(),
    }
  },
}

// 或者在组合函数中
function useMousePositionRef() {
  const pos = reactive({
    x: 0,
    y: 0,
  })
  
  // 使用toRefs  
  return {...toRefs(pos)};
}

3.2 vue3.0 依赖收集实现

Vue.js 2.0 首先会调用所有使用的数据,从而触发所有的 getter 函数,进而通过Dep对象收集所有响应式依赖,调用所有Watcher执行Render 操作,其中会进行虚拟Dom的存储和比较,进而渲染页面。当有数据变更时会触发 setter 函数,触发dep.notify(),进而调用Watcher的update,推入Vue的异步观察队列中,并最终渲染到页面。

这里画了一个Vue.js 2.0依赖收集脑图

image.png

Vue.js 3.0 通过 reactive 模块 把数据变成可代理对象,然后通过 Proxy 进行代理,handle拦截各种get,set 操作,当get操作时,会调用effect模块中的track方法进行依赖收集,当set时,会通过effect中trigger进行触发。

这里画了一个Vue.js 3.0依赖收集脑图 image.png

3.2.1 Object.defineProperty 对比 Proxy

Object.defineProperty
  • 只能劫持对象的属性;
  • 对原始值进行修改;
Proxy
  • 代理的是对象,不需要递归遍历属性,性能提升, 使用Reflect解决深层次嵌套;
  • 相对于defineProperty,Proxy无疑更加强大,可以代理数组,对象、方法、并且提供了13中操作拦截属性访问的方法traps(get,set,has,deleteProperty等等);
  • 对原始数据进行代理,不会改变原始数据的值;

3.3 跨组件数据共享

通常在我们的项目中有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。对于这种情况,我们可以使用 vuex,也可以一对 provide 和 inject实现跨组件数据共享。

无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。

image.png

4.3.1 项目应用

我们在最外层的根组件使用provide,其内部的所有后代组件都可以使用inject进行数据接受,如果对其数据进行了更改,所有使用该变量的地方watch、watchEffect、computed都会重新执行,并且重新渲染变量依赖的相关dom,从而实现跨组件的数据更改,并且视图进行更新。

globalProvide.tsx

import { defineComponent, provide, reactive, Fragment } from 'vue';

export default defineComponent({
  name: 'globalProvide',  
  setup(props, { slots }) {
    // 全局化的配置响应式数据
    const globalData = reactive({
      // 这里只是举例,全局数据结构需自己设计  
      ctx: {
        currentPage: 0,
      },
    });

    provide('globalData', globalData);

    return () => {
      return <Fragment>{slots.default?.()}</Fragment>;
    };
  },
});

center.vue

最外层组件使用上面封装的组件,内部所有深层次组件都可使用 provide 定义的全局数据。

<template>
  <globalProvide>
    <leftComponent/>
    <rightComponent/>
  </globalProvide>
</template>

leftComponent.tsx

组件内对 globalData 数据进行了更改,所有其他深层次的组件,对 globalData.ctx.currentPage 依赖的地方,都会自动更新相关依赖和视图。

import { defineComponent, inject, onMounted } from 'vue';

export default defineComponent({
  name: 'leftComponent',
  setup() {
    const globalData = inject('globalData');
    onMounted(() => {
      globalData.ctx.currentPage = 2;
    })
    return () => {
      return (
        <div>{globalData.ctx.currentPage}</div>
      )
    }
  }
})

我们可以看到ant-design-vue ConfigProvider 全局化配置 也是这样实现的。

image.png

五、音波狮子吼

少林派至高无上的内功,一声断喝蕴藏深厚内力,大有摄敌警友之效。 在武林中可使用音波狮子吼让对手感受到声音的传递,而在 vue3.0 中如何实现组件之间的跨组件通信?看看我们下面的例子。

5.1 emits 介绍

emits 可以是数组或对象,从组件触发自定义事件,emits 可以是简单的数组,也可以是对象,后者允许配置事件验证。

5.1.1 emits 验证

子组件示例 :

import { defineComponent } from 'vue';
export default defineComponent({
  name: 'demoEmits',
  // 可以对emits消息进行校验
  emits: {
    clickItem: ({type}) => {
      // type == ok,校验通过  
      if (type == 'ok') {
        return true;
      }
      // 否则提示
      console.warn('校验未通过');
    }
  },
  setup(props, {slots, emit}) {
    const clickHandle = (event) => {
      emit('clickItem', {type: 'ok'});
    }
    const demoProps = {
      style: {},
      class: [],
      onClick: clickHandle,
    }
    return () => {
      <div {...demoProps}>发送消息</div>
    }
  }
})

5.2 父子通信:

在使用vue2.0版本时,使用父子通信比较常用,这里不进行特别详细的介绍,子组件可参考上面的示例。

父组件示例:

<demo-emits @clickItem="() => console.log('demoFn')"/>

5.2.2 跨组件通信

在实际开发中,在层级深的组件和其他组件进行通信场景中,跨组件之间的通信尤为重要。 vue3.0 是不支持vue2.0 event-bus模式实现跨组件消息通信的。这里官方提供了一个三方库推荐mitttiny-emitter,这里示例使用mitt

将三方库的方法挂载在provide定义的全局共享数据中的 eventBus 命名空间中。

这样深层次嵌套的组件可以通过 globalData.eventBus.emit 方法进行消息发送,globalData.eventBus.on进行消息监听,同eventBus使用相同。

import { defineComponent, provide, reactive, Fragment } from 'vue';
import mitt  from 'mitt';

export default defineComponent({
  name: 'globalProvide',
  setup(props, { slots }) {
    const emitter = mitt();
    const globalData = reactive({
      ctx: {
        currentPage: 0,
      },
      eventBus: emitter, // 挂载在eventBus命名空间上
    });
    provide('globalData', globalData);
    return () => {
      return <Fragment>{slots.default?.()}</Fragment>;
    };
  },
});

其他替代方案可参考 event-bus

六、重点总结

  1. h 渲染函数可以灵活的使用,并且可进行样式、style、自定义属性、指令、事件的绑定、插槽的渲染,也可以使用 JSX 语法,让代码可读性更高。
  2. vue2.0vue3.0 数据代理实现方案区别。
  3. 可以使用 provide 实现深层次组件的数据共享、跨组件的消息通信机制(三方库mitt)。 processon邀请链接

参考资料:

  1. vue3js.cn/docs/api/op…
  2. v3.cn.vuejs.org/guide/migra…
  3. 2x.antdv.com/components/…
  4. github.com/vuejs/jsx-n…
  5. github.com/vuejs/rfcs/…