摘要:在组件内,我们触及不到组件的模板,所以简单的在动态模板上添加并不能完成事件监听。简单来说,依赖收集是在渲染函数渲染的函数中进行的,在中一旦通过使用了这个变量,通过这个变量的就收集到了正在执行的渲染函数这一个依赖。
作为一个中后台表单&表格工程师,经常需要在一个页面中处理多个弹窗。我自己的项目中,一个复杂的审核页面中的弹窗数量超过了30个,如何管理大量的弹窗就成为了一个需要考虑的问题。
大量的弹窗有什么问题假设你有一个弹窗组件,类似于element-ui的Dialog,如果简单粗暴的每一个弹窗都写一个dialog,那么会有以下问题:
模板过长,且大量冗余
命名困难,每一个弹窗需要一个变量去控制显示,通常每一个弹窗里面也是一个表单,又需要一个变量保存表单数据,每个弹窗也有自己的逻辑(method),都要写在这个页面,要绞尽脑汁去取名
非常的不优雅,简直就是Repeat yourself反模式的示范。。。
把每个弹窗抽成模块一个很容易想到的优化方法就是把一个弹窗作为一个组件抽离出去,每个弹窗的逻辑多带带写在组件中。
这样通过组件拆分做很好的解决了模板过长的问题,也基本解决了命名困难的问题,不过还是需要很多的变量去控制每个组件的显示。
使用动态Component第一个办法本质上并没有减少重复的代码和逻辑(弹窗显示/关闭),只是把代码放在了不同的文件当中。
显然,我并不需要写那么多的Dialog,Dialog本身并没有变,作为一个「包裹」组件,变的只是内容。
所以,只需要写一个dialog,配合Vue的动态组件Component,切换不同的组件就行了。
全局Dialog使用Component,我们做到了一个页面只需要一个Dialog,但其实整个网页,也只需要一个全局的Dialog。
我们在根组件下挂一个Dialog组件,组件内容依然使用动态component,组件的数据流转,component传递等使用Vuex进行。
使用函数创建组件作为单个项目的解决方案,全局Dialog加动态Component其实已经足够好了,使用一个函数调用就可以显示弹窗。
this.$dialog({ title: "我是弹窗", component: Test, props: { props }, // Test的props通过这样传递 })
但是想要作为通用解决方案,还不够:
引入不方便,需要手动在跟组件下引入并写上封装好的弹窗组件
必须使用Vuex进行数据流转,而并不是每个Vue项目都使用Vuex的
没法监听事件,只能传入回调
props的传递方式不够优雅,不够声明式
在我心中,一个理想的弹窗组件,需要是这样的:
引入方便,Vue.use(Dialog)就行了
使用简洁
this.$dialog({ title: "哎呀不错哦", component: () =>})
Let"s go.
使用$mountVue作为一个视图层的框架,核心其实就是渲染函数,所以一定有一个办法,可以把一个Vue组件渲染成一个DOM,这个方法就是$mount。
// 这个Dialog组件就是写好的弹窗组件 import Dialog from "./Dialog" // dialog是一个单例,不需要重复创建 let dialog export default function createDialog(Vue, { store = {}, router = {} }, options) { if (dialog) { dialog.options = { ...options, } dialog.$children[0].visible = true } else { dialog = new Vue({ name: "Root-Dialog", router, store, data() { return { options: { ...options }, } }, render(h) { return h(Dialog, { props: this.options, }) }, }) // 渲染出DOM并手动插入到body dialog.$mount() document.body.appendChild(dialog.$el) } // 暴露close方法 return { close: () => dialog.$children[0].close(), } }Dialog组件
基于element-ui的Dialog组件二次封装,在原有的props之外,添加一个component,使用动态Component渲染上去就行了。
思路很简单,但是有几个问题需要考虑。
如果不做任何处理,当弹窗消失的时候component并不会销毁;当再次显示弹窗时,会传入一个新的组件,这个时候,上一个组件才销毁,这非常不合理。所以我们需要在弹窗消失的时候手动销毁传入的component。
注入事件Vue的动态Component组件的is属性接受的值有3种类型:
string,在当前组件内注册过的组件的名称
ComponentDefinition,就是一个组件的选项对象,new Vue时传的那个对象
ComponentConstructor,返回一个ComponentDefinition的函数,比如动态import函数
而我们希望的调用形式里,component是一个返回jsx的函数,而它会被babel插件babel-plugin-transform-vue-jsx转换为调用createElement函数的结果,也就是说
() =>
这个函数最终返回的是一个Virtual Node。
而Vue的选项里面,render最终返回的也是一个VNode。
也就是说,() =>
在这个过程中,我们可以在这个Vnode里面做一些有趣的事情,比如注入事件。
首先,这里有一个刚需:弹窗内的组件需要可以关闭弹窗,也就是它的父组件。
通常有两个办法可以做到:
通过props接收一个函数,调用它可以关闭弹窗
主动抛出一个事件,dialog组件监听这个事件,然后把自己关了
略微比较一下就可以发现,抛出事件的方法优于回调函数的办法(通常来说,「事件」都优于「回调」):
代码少, $emit("complete")就行了,使用回调需要添加一个props,调用的时候还需要判断它是否存在
通用性更好,这个组件可能不仅仅只在弹窗内调用,它可以在其它任何地方被调用,使用事件只需要简单的抛出一个事件,表示我完成了,调用它的组件根据自身的逻辑来进行接下来的工作,这样组件本身做到了低耦合。
但是,抛出事件的实现却要比传入回调难很多,需要对VNode比较熟悉。
在Dialog组件内,我们触及不到组件的模板,所以简单的在动态component模板上添加 @done 并不能完成事件监听。因为事件监听其实是在render的过程中进行的,而我们的render是通过jsx的方式在调用$dialog函数时传入的,所以只能手动在生成的VNode上添加事件监听:
在 vNode.componentOptions.listeners中,添加我们需要监听的事件和事件处理函数:
let listeners = vNode.componentOptions.listeners if (!listeners) { listeners = {} vNode.componentOptions.listeners = listeners } // 添加done const orginDoneHandler = listeners.done listeners.done = function () { if (orginDoneHandler) orginDoneHandler() doneHandler() } // 添加cancel const orginCancelHandler = listeners.cancel listeners.cancel = function () { if (orginCancelHandler) orginCancelHandler() cancelHandler() }
在Dialog中,监听了动态component的done和cancel事件,在任一事件触发后都会关闭Dialog,组件$emit("done")表示完成了自己的业务,$emit("cancel)表示取消了自己的业务
主动收集依赖到这里,还有一个问题没有解决:这个组件还不是响应式的,比如说,你在一个index组件中通过$dialog显示一个弹窗
this.$dialog({ title: "响应式", component: () =>})
当text更新时,弹窗中的内容并没有更新,也就说,组件没有重新渲染。
Vue的渲染流程与依赖收集这里就要涉及到一些Vue的原理了,比如说渲染流程,依赖收集,一两句话也讲不清楚,我试着大概的说一下:
首先,页面上显示的数据变了,一定是触发了重新渲染,this.text = "新的text" 之所以会更新页面,可以理解为一个渲染函数在this.text的setter中执行了。
那么,this.text的getter怎么样才能知道要执行哪些函数,就是通过所谓的依赖收集。简单来说,依赖收集是在渲染函数(渲染Vnode的函数)中进行的,在createElement中一旦通过this.text使用了这个变量,通过这个变量的getter就收集到了正在执行的渲染函数这一个依赖。
所以,粗暴的讲,需要把this.text的访问放在一个render函数(Vue选项对象的render)中进行。平常用的模板其实也是这样,因为它最终都被Vue-loader编译成了render。
_component() { // 这一步很重要,让component收集到了这个计算属性的依赖,否则当component变化时不会重新渲染组件 const fn = this.component let vNode // 返回vue选项对象 const that = this return { name: "dynamic-wrapper", render() { // fn的运行一定要在render函数中,也是为了挂载依赖 vNode = fn() ... } }
所以,这就是为什么一定要使用一个返回jsx的函数作为,而不是直接美滋滋的使用jsx。因为,臣妾实在是做不到响应式呀~
this.$dialog({ title: "臣妾做不到啊~", component:, })
等于
// this.text的值为text this.$dialog({ title: "臣妾做不到啊~", component: createElement( Text, props: { text: "text", } ) })
完整代码,拍着胸脯保证可用,已经在生产环境大量使用超过3个月的时间了。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/99807.html
摘要:发布是由团队开源的,操作接口库,已成为事实上的浏览器操作标准。本周正式发布,为我们带来了,,支持自定义头部与脚部,支持增强,兼容原生协议等特性变化。新特性介绍日前发布了大版本更新,引入了一系列的新特性与提升,本文即是对这些变化进行深入解读。 showImg(https://segmentfault.com/img/remote/1460000012940044); 前端每周清单专注前端...
摘要:三更新内容在原来项目的基础上,做了如下更新数据库重新设计,改成以用户分组的数据库结构应数据库改动,所有接口重新设计,并统一采用和网易立马理财一致的接口风格删除原来游客模式,增加登录注册功能,支持弹窗登录。 这个项目最初其实是fork别人的项目。当初想接触下mongodb数据库,找个例子学习下,后来改着改着就面目全非了。后台和数据库重构,前端增加了登录注册功能,仅保留了博客设置页面,但是...
摘要:本文是造轮系列第二篇。实现方式事件处理跟差不多,唯一多了一步就是当点击或者的时候,如果外部有回调就需要调用对应的回调函数。 本文是React造轮系列第二篇。 1.React 造轮子系列:Icon 组件思路 本轮子是通过 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,自己动手谷歌吧。当然可以参考我的源码。 想阅读更多优质文章请猛戳Git...
摘要:两个对象键名冲突时,取组件对象的键值对。允许声明扩展另一个组件可以是一个简单的选项对象或构造函数,而无需使用。这主要是为了便于扩展单文件组件。 Vue作为最近最炙手可热的前端框架,其简单的入门方式和功能强大的API是其优点。而同时因为其API的多样性和丰富性,所以他的很多开发方式就和一切基于组件的React不同,如果没有对Vue的API(有一些甚至文档都没提到)有一个全面的了解,那么在...
摘要:最近项目中遇到的需求是要操作大量的表单,之前的项目中有做过这方的研究,只不过是用来操作。添加操作上面的只是其中一个动态列表。 最近项目中遇到的需求是要操作大量的表单,之前的项目中有做过这方的研究,只不过是用jquery来操作。 项目A 先简单说说以前项目A中的应用场景,可能有小伙伴儿也遇到相同的需求。A项目是公司的OA系统中有的项目,是用java的jsp渲染的页面,需求是要改成:嵌入A...
阅读 2393·2021-11-25 09:43
阅读 1149·2021-09-07 10:16
阅读 2576·2021-08-20 09:38
阅读 2917·2019-08-30 15:55
阅读 1390·2019-08-30 13:21
阅读 860·2019-08-29 15:37
阅读 1407·2019-08-27 10:56
阅读 2076·2019-08-26 13:45