前言
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依赖收集脑图
Vue.js 3.0 通过 reactive 模块 把数据变成可代理对象,然后通过 Proxy 进行代理,handle拦截各种get,set 操作,当get操作时,会调用effect模块中的track方法进行依赖收集,当set时,会通过effect中trigger进行触发。
这里画了一个Vue.js 3.0依赖收集脑图
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选项来开始使用这些数据。
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 全局化配置 也是这样实现的。
五、音波狮子吼
少林派至高无上的内功,一声断喝蕴藏深厚内力,大有摄敌警友之效。 在武林中可使用音波狮子吼让对手感受到声音的传递,而在 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模式实现跨组件消息通信的。这里官方提供了一个三方库推荐mitt或tiny-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。
六、重点总结
h渲染函数可以灵活的使用,并且可进行样式、style、自定义属性、指令、事件的绑定、插槽的渲染,也可以使用JSX语法,让代码可读性更高。vue2.0和vue3.0数据代理实现方案区别。- 可以使用
provide实现深层次组件的数据共享、跨组件的消息通信机制(三方库mitt)。 processon邀请链接
参考资料: