欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

bootstrap 模态框源码剖析

来源: 开发者 投稿于  被查看 4989 次 评论:59

bootstrap 模态框源码剖析


Modal就是就是bootstrap中的模态框,首先我们看看他的运行效果。
代码片段 1

<!DOCTYPE html>
<html>
<head>
    <title>模态框</title>
<link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
<script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</head>
<body>
<button type="button" class="btn btn-primary btn-lg" data-toggle="modal" data-target="#myModal">
  demo modal
</button>
<!-- Modal -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <h4 class="modal-title" id="myModalLabel">Modal title</h4>
      </div>
      <div class="modal-body">
        ...
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div>
  </div>
</div>
</body>
</html>

如上所示,页面会出现弹出框。下面我们就来看看bootstrap是如何实现这一功能的。首先我把所有代码折叠起来,大家可以看到这样两行代码
[code]+function($){
.....
}(jQuery);[/code]
前面为什么会有个加号呢,有的人可能会说防止和别人的代码冲突。其实+号是写给javascript引擎的,告诉引擎这是一个函数表达式,而不是一个函数声明。不仅仅+号有这个功能,分号,括号,感叹号,减号等等都可以实现相同的功能。好了回归正题,下面,我们就打开折叠,看看里面到底是什么东东。
[code]var Modal = function (element, options) {
this.options = options
this.$body = $(document.body)
this.$element = $(element)
this.$dialog = this.$element.find('.modal-dialog')
this.$backdrop = null
this.isShown = null
this.originalBodyPad = null
this.scrollbarWidth = 0
this.ignoreBackdropClick = false

if (this.options.remote) {
  this.$element
    .find('.modal-content')
    .load(this.options.remote, $.proxy(function () {
      this.$element.trigger('loaded.bs.modal')
    }, this))
}

}[/code]首先是Modal的构造函数,里面声明了需要用到的变量,随后又设置了一些常量。
[code] Modal.VERSION = '3.3.5'

Modal.TRANSITION_DURATION = 300
Modal.BACKDROP_TRANSITION_DURATION = 150

Modal.DEFAULTS = {
backdrop: true,
keyboard: true,
show: true
}[/code]
变量设置完毕,接着就该上函数了。Modal的扩展函数有这么几个:toggel,show,hide,enforceFocus,escape,resize,hideModal,removeBackdrop,backdrop,handleUpdate,adjustDialog,resetAdjustments,checkScrollbar,setScrollbar,resetScrollbar,measureScrollbar终于列完了,恩一共是16个。toggel函数比较简单,是一个显示和隐藏的切换函数。代码如下[code]Modal.prototype.toggle = function (_relatedTarget) {
return this.isShown ? this.hide() : this.show(_relatedTarget)
}[/code]怎么样,是不是很简单。接下来跳过show和hide函数,你只要知道show是显示,hide是隐藏即可。我们先看看enforceFocus函数
[code]Modal.prototype.enforceFocus = function () {
$(document)
.off('focusin.bs.modal') // guard against infinite focus loop
.on('focusin.bs.modal', $.proxy(function (e) {
if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
this.$element.trigger('focus')
}
}, this))
}[/code]
enforceFocus就是强制获取焦点的意思,首先,解除了当前文档的focusin事件,原因作者也进行了解释,防止无线聚焦循环。其中bs.modal是一个命名空间,没有太大意义,可以理解为事件加了个姓。解除原始focusin事件后为,为文档重新绑定focusin事件。函数的作用是判断focusin的target是否是$element本身或他的子元素,如果都不是,就触发$element的focus事件,$element就是我们看到的弹出模态框。(focus和focusin是有点小区别的,有兴趣的同学可以找找看。)$,proxy函数的作用是转换函数执行的上下文,把函数中的this转化成Modal。如果没有这句话,那么函数内的this将指向$(document)。强制获取焦点到这里就结束啦,是不是依然简单。接下来介绍escape函数
[code]Modal.prototype.escape = function () {
if (this.isShown && this.options.keyboard) {
this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) {
e.which == 27 && this.hide()
}, this))
} else if (!this.isShown) {
this.$element.off('keydown.dismiss.bs.modal')
}
}[/code]escape的意思是逃走,那么是什么逃走呢?自然是我们的模态框了。这段代码的作用就是为了响应ESC按键,如果模态框弹出且用户设置了keyboard选项,当用户按下ESC时,模态框会隐藏。这里面有个&&的小用法,有没有感觉代码简洁了不少呢。
下面我要介绍一个相当关键的函数,他的名字叫backdrop。为什么说他关键呢,一是很多地方都用到了他,比如在show的时候,二是他控制了动画效果和背景。backdrop代码如下所示
[code]Modal.prototype.backdrop = function (callback) {
var that = this
var animate = this.$element.hasClass('fade') ? 'fade' : ''

if (this.isShown && this.options.backdrop) {
  var doAnimate = $.support.transition && animate

  this.$backdrop = $(document.createElement('div'))
    .addClass('modal-backdrop ' + animate)
    .appendTo(this.$body)

  this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) {
    if (this.ignoreBackdropClick) {
      this.ignoreBackdropClick = false
      return
    }
    if (e.target !== e.currentTarget) return
    this.options.backdrop == 'static'
      ? this.$element[0].focus()
      : this.hide()
  }, this))

  if (doAnimate) this.$backdrop[0].offsetWidth // force reflow

  this.$backdrop.addClass('in')

  if (!callback) return

  doAnimate ?
    this.$backdrop
      .one('bsTransitionEnd', callback)
      .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
    callback()

} else if (!this.isShown && this.$backdrop) {
  this.$backdrop.removeClass('in')

  var callbackRemove = function () {
    that.removeBackdrop()
    callback && callback()
  }
  $.support.transition && this.$element.hasClass('fade') ?
    this.$backdrop
      .one('bsTransitionEnd', callbackRemove)
      .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
    callbackRemove()

} else if (callback) {
  callback()
}

}[/code]
bootstrap插件中介绍了禁止动画的功能,就是去掉fade类名。第三行代码就是禁止动画的关键所在了。如果有fade类,那么animate变量为fade,否则为空。代码看似很长,一共只有三个逻辑。如果模态框显示,且opinion中的backdrop为true,执行流程为下:设置doAnimate变量,如果animate存在且浏览器是否支持transition,那么doAnimate变量为真。随后为body元素添加类名为modal-backdrop fade的div,其css代码为
[code].modal-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1040;
background-color: #000;
}
.modal-backdrop.fade {
filter: alpha(opacity=0);
opacity: 0;
}[/code]
其中设置的四个0值,是为了让这个背景充满屏幕。不理解的话可以自行百度一下,国外的很多代码都有用到类似的写法。添加完背景div后,为模态框绑定click事件。其中if (e.target !== e.currentTarget) return;这句话比较关键,e.currentTarget就是当前的模态框,而e.target则是事件的实际出发对象,比如模态框的子元素,如果你单击的是模态框的一个输入框,没有这句话输入框将永远无法获得焦点。剩下的几行代码我觉得是更加关键的
[code] this.options.backdrop == 'static'
? this.$element[0].focus()
: this.hide()[/code]
如果背景是static的,那么点击会使得模态框获得焦点,如果不是,将隐藏模态框。你肯能会很疑惑,为什么点击模态框还会使他隐藏,不是应该获得焦点才对嘛?其实,你看到的并不是真正的模态框,而是modal-dialog,模态框有多大?充满屏幕!我们来看看modal的css就明白了
[code].modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1050;
display: none;
overflow: hidden;
-webkit-overflow-scrolling: touch;
outline: 0;
}[/code]看到了吗,他的大小和背景是一样的,不过他在背景的上面。这下你明白了,为什么点击“背景”会隐藏“模态框”,其实你点击的是模态框本身,而隐藏的是整个模态框,包括你看到的modal-dialog。
好了,下面你将会看到一句最令人费解的话[code]if (doAnimate) this.$backdrop[0].offsetWidth // force reflow[/code]作者的注释是强制回流。那么如何理解呢?你可以在下面的网址里再继续探寻http://stackoverflow.com/questions/6955912/can-i-use-javascript-to-force-the-browser-to-flush-any-pending-layout-changes。间单的说,就是强制让页面重新计算布局,冲刷渲染树。这之后,又为背景div绑定了一些事件,这些你可能没看懂,不过没关系,他只是bootstrap另一个叫做transition的插件,在动画结束后调用回调函数。
好了现在是第二层逻辑。如果模态框没有显示,且已经存在背景div时,移除背景的‘in’ class。随后定义了callbackRemove函数,一方面用来移除背景div,另一方面用来调用callback函数。最后当动画执行完毕时,调用callbackRemove函数。
最后一层逻辑,直接调用callback函数。大家可能已经晕了,简而言之,backdrop函数的作用就是没有背景div就添加,有就删除,并无条件执行回调函数。说了这么多,让我们看看backdrop函数有什么实际应用。代码如下
[code]Modal.prototype.hideModal = function () {
var that = this
this.$element.hide()
this.backdrop(function () {
that.$body.removeClass('modal-open')
that.resetAdjustments()
that.resetScrollbar()
that.$element.trigger('hidden.bs.modal')
})
}[/code]Modal里面有一个hide函数,他负责的东西比较多,比如解除绑定的事件,更改标志变量调用其他辅助函数等。此处,就是hide函数调用的一个关键函数,他的作用相对简单,隐藏模态框,注意此处的hide()是指jQuery内部的hide方法,而不是Modal扩展的hide。随后调用backdrop,隐藏背景div,并使回调函数执行。bootstrap文档中说模态框弹出时会为body元素添加modal-open标志,此处则是为了删除此标志。接着又调用了一些重置方法,不再赘述。
下面就来讲解最重要的show方法,在介绍这个方法的时候,也许会穿插一些小方法,我只会介绍他们的作用,不会详细介绍,以免影响大家的思路。下面我们就来看看show方法的完整代码。
[code]Modal.prototype.show = function (_relatedTarget) {
var that = this
var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })

this.$element.trigger(e)

if (this.isShown || e.isDefaultPrevented()) return

this.isShown = true

this.checkScrollbar()
this.setScrollbar()
this.$body.addClass('modal-open')

this.escape()
this.resize()

this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))

this.$dialog.on('mousedown.dismiss.bs.modal', function () {
  that.$element.one('mouseup.dismiss.bs.modal', function (e) {
    if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true
  })
})

this.backdrop(function () {
  var transition = $.support.transition && that.$element.hasClass('fade')

  if (!that.$element.parent().length) {
    that.$element.appendTo(that.$body) // don't move modals dom position
  }

  that.$element
    .show()
    .scrollTop(0)

  that.adjustDialog()

  if (transition) {
    that.$element[0].offsetWidth // force reflow
  }

  that.$element.addClass('in')

  that.enforceFocus()

  var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })

  transition ?
    that.$dialog // wait for modal to slide in
      .one('bsTransitionEnd', function () {
        that.$element.trigger('focus').trigger(e)
      })
      .emulateTransitionEnd(Modal.TRANSITION_DURATION) :
    that.$element.trigger('focus').trigger(e)
})

}
[/code]
其中的参数_relatedTarget就是模态框的控制按钮,通过点击可以出现模态框。可能很多同学看到第二行就一头雾水了,其实我也一样。后来经过反复查阅资料,得出了以下分析。如果有什么疑议,请访问jQuery官方文档<a>http://api.jquery.com/category/events/event-object/</a>。首先作者通过$.Event对show.bs.modal事件进行了包装,并且为事件指定了relatedTarget属性。随后通过trigger函数默认触发该事件,并把relatedTarget绑定到事件上。那么relatedTarget属性是什么作用呢,就是用于返回当前事件涉及到的其他DOM元素。注意,是其他!也就是说,指明了show.bs.modal事件的relatedTarget为模态框的控制按钮,如果是其他relatedTarget则不会触发该事件。随后,用到了checkScrollbar和setScrollbar两个函数,第一个用来检测body是否会产生scrollbar,第二个用来为body元素设置padding-right的值,防止body的元素被scrollbar阻挡。接着为body元素添加modal-open类,监听Esc按键,窗口大小变换,具有data-dismiss="modal"属性元素的click事件。之后的一段代码我至今没有想明白,希望大神可以指点一下。就是这么几行代码[code]this.$dialog.on('mousedown.dismiss.bs.modal', function () {
that.$element.one('mouseup.dismiss.bs.modal', function (e) {
if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true
})
})[/code]我不太清楚这样来回绑定有什么作用。
好了跳过这一段,马上就又会看到我们的backdrop函数登场了。你会看到,他是我们show函数的压轴函数。首先检查了模态框的父元素,如果没有父元素则将其加入到body元素中,接着是设置模态框可见。adjustDialog函数用来设置模态框的padding。下面进行强制回流,添加in类,调用强制获取焦点函数。包装shown.bs.modal事件,传入relatedTarget参数,最后在动画执行完毕后触发focus和shown.bs.modal事件。这样就完成了show函数的全部工作,只能说还是有点复杂的。hide函数就不再讲解了,原理都是一样的。
今天先写到这里,如果你在阅读源码的时候有什么问题欢迎提出来,我们一起讨论。请在最后留下你的宝贵意见,你们的鼓励就是我的动力!

用户评论