需求背景:
开发的某一天,产品经理找到我说需要在列表每个卡片上新增一个按钮,用户点击后能唤醒弹窗,然后用户可以在弹窗内输入并提交输入的内容,很容易吧!收到需求后,那我们就开始需求分析一下,而不是立马就去codeing(以下使用技术栈为Vue2.x,Element UI 2.15.6)
需求分析
方案1:
在每个卡片组件中都写一个按钮,然后再写一个弹窗组件,伪代码如下:
<template>
<div class="post-card-component">
...省略部分代码
<el-button type="text" @click="dialogVisible = true">点击打开 Dialog</el-button>
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="30%"
:before-close="handleClose">
<span>这是一段信息</span>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'PostCard',
data() {
return {
dialogVisible: false
};
},
methods: {
handleClose(done) {
this.$confirm('确认关闭?')
.then(_ => {
done();
})
.catch(_ => {});
}
}
};
</script>
优点: 使用该卡片组件的人而言,不需要做任何变动,只需要照常引入即可
缺点:
很明显就是,如果一个列表中有多个卡片组件渲染,那必然也会渲染多个隐藏的dialog组件
方案2:
修改使用卡片组件的方式,传入唤醒dialog组件的方法,卡片组件内部触发,同时父组件只需要渲染一个dialog组件,以供所有的卡片组件使用,伪代码如下:
<template>
<div class="post-list-component">
<PostCard v-for="item in list"
:key="item.id"
:changeDialogVisible="changeDialogVisible"
></PostCard>
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="30%"
:before-close="handleClose">
<span>这是一段信息</span>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import PostCard from '@/components/PostCard'
export default {
name: 'PostList',
components: {
PostCard
},
data() {
return {
list: [],
dialogVisible: false
};
},
methods: {
changeDialogVisible() {
this.dialogVisible = !this.dialogVisible
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(_ => {
done();
})
.catch(_ => {});
}
}
};
</script>
优点:
不用重复渲染多个dialog组件,性能提升了
缺点: 每个使用到卡片组件的地方都需要定义一个dialog组件,维护成本上去了
通过上面的分析,我们发现通用的上述2种方案并不能很好的满足我们的需求,那有没有更好的方式呢?
动态渲染组件
那是否可以将上面2种方案都合并一下,既可以不用修改原来引入组件的地方,又可以不用重复渲染?最后通过查阅资料发现 Vue.extend 就可以满足我们以上需求,不太了解该 Api的小伙伴可以点击链接复习一哦 ,先贴一下官方文档使用示例:
// 创建构造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
嗯,看起来很容易~那我们就来试试看我们要如何修改,在卡片组件内先引入我们的弹窗组件,并不立即渲染,而是将渲染过程延后,当用户点击时才去动态渲染,同时将组件实例动态挂载在body上
<template>
<div class="post-card-component">
...省略部分代码
<el-button type="text" @click="dynamicCreatedialog">点击打开 Dialog</el-button>
</div>
</template>
<script>
import Dialog from './components/Dialog.vue'
import Vue from 'vue'
export default {
name: 'PostCard',
components: {
Dialog
},
data() {
return {
dialogVisible: false
};
},
methods: {
dynamicCreatedialog(done) {
const DialogComponentCtor = Vue.extend(Dialog)
const dialogComponent = new DialogComponentCtor()
document.body.appendChild(dialogComponent.$el)
}
}
};
</script>
然而你以为就这样结束了?在实际过程中,当我们每次点击都会创建 dialog 组件,然后body上就会追加有很多我们创建的Dom,那要如何做呢?在组件内,我们可以使用 v-if来控制组件是否渲染,当组件关闭后,将值改为 false 然后就可以销毁组件Dom了
<template>
<el-dialog
v-if="live"
title="提示"
:visible.sync="dialogVisible"
width="30%"
:before-close="handleClose">
<span>这是一段信息</span>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
data() {
return {
live: true,
dialogVisible: true
};
},
methods: {
handleClose(done) {
this.$confirm('确认关闭?')
.then(_ => {
this.live = false
done();
})
.catch(_ => {});
}
}
};
</script>
至此就解决了之前2种方案的不足之处,既不用修改原来已引入该组件的地方,同时又没有照成不必要的渲染,那有同学就问了,这样渲染组件要如何传递 Props 呢? 答案也很简单,在 new Vue 过程中,会执行initState,initState 又会执行 initProps 所以我们就看到了 props 是需要传入PropsData 字段的。
// src/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
上面的示例我们就可以改成如下所示
<template>
<div class="post-card-component">
...省略部分代码
<el-button type="text" @click="dynamicCreatedialog">点击打开 Dialog</el-button>
</div>
</template>
<script>
import Dialog from './components/Dialog.vue'
import Vue from 'vue'
export default {
name: 'PostCard',
components: {
Dialog
},
data() {
return {
dialogVisible: false
};
},
methods: {
dynamicCreatedialog(done) {
const DialogComponentCtor = Vue.extend(Dialog)
const dialogComponent = new DialogComponentCtor({
propsData: {
id: xxx,
name: xxx
}
})
document.body.appendChild(dialogComponent.$el)
}
}
};
</script>
总结:
当我们拿到需求后,并不要一上来就去写代码,而是要先分析梳理需求,然后去编码实现,看似都能完成需求(又不是不能用)那是否有还有更好的实现方式来实现,最后的方式可能比不是最优解,但目前来看也是不错的解决方案,最后如果你有更优解,可以评论回复一下哦~ 感激~