背景
最近公司业务涉及到大量表单,每个表单上都有日志。于是封装了一个日志弹窗组件,然后在每个页面去引用, 并且写这么大一坨。高大上的程序员 webCoder 瞬间秒变码农。
-- template
<el-button type="text" @click="handlViewLog(scope.row)">{{ $t('日志') }}</el-button>
<LogTableModal v-model="logModalVisible" :ref-id="currentDetail.id" biz-log-key="TMS_PORT" />
-- js
import LogTableModal from '@/components/LogTableModal'
{
component: {
LogTableModal
},
data(){
return {
logModalVisible: false
}
},
methods: {
handlViewLog(row){
// todo...
}
}
}
我开始思考,是否有一种方式来简化这种操作,咱们能不能也用一个指令来替代这些冗余重复的工作。例如elment-ui的 v-loading,。
说干就干,干完之后,我们的代码变成了这样, 一行代码解决 Perfect~
<el-button type="text" v-log="{bizLogKey: 'TMS_PORT', refId: scope.row.id}">{{ $t('日志') }}</el-button>
实现
直接看看是怎么实现的。
实现思路主要参考Elment-ui, 涉及到知识点 Directive, Vue.extend, Vue.$mount, 函数式组件。
import Vue from 'vue'
import LogTableModal from '@/components/LogTableModal'
let instance
const showDialog = function(el, binding) {
return function() {
instance = new Loigc({
propsData: {
init: true,
bizLogKey: binding.value.bizLogKey,
refId: binding.value.refId
}
})
instance.$on('input', function(v) {
instance.value = v
if (!v) {
instance.$el.parentNode.removeChild(instance.$el)
instance = null
}
})
instance.value = true
instance.$mount()
document.body.appendChild(instance.$el)
instance.initEditData()
}
}
const Loigc = Vue.extend(LogTableModal)
export default {
inserted(el, binding) {
el.addEventListener('click', showDialog(el, binding))
},
unbind(el) {
el.removeEventListener('click', showDialog)
}
}
函数式组件
三无产品- 函数式组件
- 无状态 (没有
响应式数据) - 无生命周期 (没有
created mounted update) - 没有实例 (没有
this上下文)
有状态组件和无状态组件指的是组件是否有自己的数据(state)
React里面的状态跟Vue的data一个概念, 并且Vue函数式组件里面没有响应式数据,所以这里理解为无状态既无响应式数据, 如果理解有错误,欢迎评论区指正。
参考React无状态
有状态组件
class App extends React.Component {
constructor() {
super();
this.state = {
name: '张三'
}
}
render() {
return (
<div>
<h1>{this.state.name}</h1>
</div>
)
}
}
注:类组件继承React.Component组件,会从父类中继承一个state属性,通过这个属性可以定义自己的状态
无状态组件
function App() {
return (
<div>
<h1>hello</h1>
</div>
)
}
注:函数式组件没有继承React.Component组件,没有state属性,没有自己的状态 (使用HOOK可以给函数式组件添加状态)
函数式无状态组件特点
● 它是为了创建纯展示组件,这种组件只负责根据传入的props来展示,不涉及到state状态的操作。
● 组件不能访问this对象
● 不能访问生命周期方法 函数式组件的性能优化 减少重新 render 的次数。因为在 React 里最重(花时间最长)的一块就是 reconction(简单的可以理解为 diff),如果不 render,就不会 reconction。 减少计算的量。主要是减少重复计算,对于函数式组件来说,每次 render 都会重新从头开始执行函数调用
Vue 官方描述
之前创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。
无状态的组件用函数式组件 对于一些纯展示,没有响应式数据,没有状态管理,也不用生命周期钩子函数的组件, 我们就可以设置成函数式组件,提高渲染性能,因为会把它当成一个函数来处理, 所以开销很低
原理是在 patch 过程中对于函数式组件的 render 生成的虚拟 DOM,不会有递归 子组件初始化的过程,所以渲染开销会低很多
它可以接受 props,但是由于不会创建实例,所以内部不能使用 this.xx 获取组 件属性,写法如下
<template functional>
<div>
<div class="content">{{ value }}</div>
</div>
</template>
<script>
export default {
props: ['value']
}
</script>
// 或者
Vue.component('my-component', {
functional: true, // 表示该组件为函数式组件
props: { ... }, // 可选
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
// ...
}
})
Vue.extend
使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
data 选项是特例,需要注意 - 在 Vue.extend() 中它必须是函数
extend 创建的是 Vue 构造器,而不是我们平时常写的组件实例,所以不可以通过 new Vue({ components: Loigc }) 来直接使用,需要通过 new Loigc().$mount({el: '#app'}) 来挂载到指定的元素上。
$mount
如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例。
如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中。
这个方法返回实例自身,因而可以链式调用其它实例方法。
var MyComponent = Vue.extend({
template: '<div>Hello!</div>'
})
// 创建并挂载到 #app (会替换 #app)
new MyComponent().$mount('#app')
// 同上
new MyComponent({ el: '#app' })
// 或者,在文档之外渲染并且随后挂载
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)
踩坑 i18n
i18n报错
做的过程中发现控制台有报错,打开控制台看了下是i18n的问题
<template>
<div>{{ $t('commen.close') }}</div>
<template>
咋一看没有毛病啊,为啥换了种用法就出现问题了呢。 回顾一下上文中提到的 函数式组件 是三无组件,无状态,无生命周期,无this. 在注册i18n的时候,t() 调用到i18n, 但是当它变成函数式组件后, 变成了三无产品,此时我们是无法再通过这种方法去取到Vue实例上的$t,此时的this 指向的是当前函数实例。
所以我们只需要把i18n挂在到这个实例对象上就可以解决问题了
-- i18n.js
export const $t = (localeKey, options) => {
// 使用i18n的 te 方法来检查是否能够匹配到对应键值
const hasKey = i18n.te(localeKey, store.state.common?.lang)
const translatedStr = i18n.t(localeKey, options)
if (hasKey) {
return translatedStr
}
return localeKey
}
-- component.vue
<template>
<div>{{ t('commen.close') }}</div>
<template>
import { $t } from '@/plugins/i18n'
{
methods: {
t(...args) {
return $t.apply(this, args)
},
}
}