资讯专栏INFORMATION COLUMN

从Dialog管理谈到Vue渲染原理

darkbug / 3234人阅读

摘要:在组件内,我们触及不到组件的模板,所以简单的在动态模板上添加并不能完成事件监听。简单来说,依赖收集是在渲染函数渲染的函数中进行的,在中一旦通过使用了这个变量,通过这个变量的就收集到了正在执行的渲染函数这一个依赖。

作为一个中后台表单&表格工程师,经常需要在一个页面中处理多个弹窗。我自己的项目中,一个复杂的审核页面中的弹窗数量超过了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.

使用$mount

Vue作为一个视图层的框架,核心其实就是渲染函数,所以一定有一个办法,可以把一个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。
也就是说,() => 这个函数可以作为一个Vue组件的render选项,所以,我们需要构造一个完整的Vue选项对象,然后将这个对象作为动态component的is属性,这样就可以渲染出这个Test组件了。

在这个过程中,我们可以在这个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的donecancel事件,在任一事件触发后都会关闭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

相关文章

  • 前端每周清单第 48 期:Slack Webpack 构建优化,CSS 命名规范与用户追踪,Vue.

    摘要:发布是由团队开源的,操作接口库,已成为事实上的浏览器操作标准。本周正式发布,为我们带来了,,支持自定义头部与脚部,支持增强,兼容原生协议等特性变化。新特性介绍日前发布了大版本更新,引入了一系列的新特性与提升,本文即是对这些变化进行深入解读。 showImg(https://segmentfault.com/img/remote/1460000012940044); 前端每周清单专注前端...

    sean 评论0 收藏0
  • 一个基于Vue.js+Mongodb+Node.js的博客内容管理系统

    摘要:三更新内容在原来项目的基础上,做了如下更新数据库重新设计,改成以用户分组的数据库结构应数据库改动,所有接口重新设计,并统一采用和网易立马理财一致的接口风格删除原来游客模式,增加登录注册功能,支持弹窗登录。 这个项目最初其实是fork别人的项目。当初想接触下mongodb数据库,找个例子学习下,后来改着改着就面目全非了。后台和数据库重构,前端增加了登录注册功能,仅保留了博客设置页面,但是...

    wh469012917 评论0 收藏0
  • React造轮系列:对话框组件 - Dialog 思路

    摘要:本文是造轮系列第二篇。实现方式事件处理跟差不多,唯一多了一步就是当点击或者的时候,如果外部有回调就需要调用对应的回调函数。 本文是React造轮系列第二篇。 1.React 造轮子系列:Icon 组件思路 本轮子是通过 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,自己动手谷歌吧。当然可以参考我的源码。 想阅读更多优质文章请猛戳Git...

    qianfeng 评论0 收藏0
  • 面试被问到Vue?想进一步提升?那就停下来看一下吧

    摘要:两个对象键名冲突时,取组件对象的键值对。允许声明扩展另一个组件可以是一个简单的选项对象或构造函数,而无需使用。这主要是为了便于扩展单文件组件。 Vue作为最近最炙手可热的前端框架,其简单的入门方式和功能强大的API是其优点。而同时因为其API的多样性和丰富性,所以他的很多开发方式就和一切基于组件的React不同,如果没有对Vue的API(有一些甚至文档都没提到)有一个全面的了解,那么在...

    andot 评论0 收藏0
  • vue2 中如何实现动态表单增删改查

    摘要:最近项目中遇到的需求是要操作大量的表单,之前的项目中有做过这方的研究,只不过是用来操作。添加操作上面的只是其中一个动态列表。 最近项目中遇到的需求是要操作大量的表单,之前的项目中有做过这方的研究,只不过是用jquery来操作。 项目A 先简单说说以前项目A中的应用场景,可能有小伙伴儿也遇到相同的需求。A项目是公司的OA系统中有的项目,是用java的jsp渲染的页面,需求是要改成:嵌入A...

    StonePanda 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<