5.2.4 源码分析
在源码分析之前,确保我们已经记住了弹窗Modal的基本元素,这样在阅读JavaScript代码时才能对照元素进行理解。示例如下:
- <!-- 触发元素 -->
- <button data-toggle="modal" data-target="#myModal" class="btn btn-primary">
- 触发元素</button>
- <!-- 弹窗内容 -->
- <div class="modal fade" id="myModal">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">...</div>
- <div class="modal-body">...</div>
- <div class="modal-footer">...</div>
- </div>
- </div>
- </div>
步骤1 立即调用的函数。回顾2.4节,JavaScript插件架构里的步骤,第一步是最通用的步骤,也就是使用立即调用的函数,防止插件内代码外泄,从而形成一个闭环,并且只能从jQuery的fn里进行扩展。
- // 1.定义立即调用的函数
- +function ($) {
- "use strict"; // 使用严格模式 ES5支持
- // 后续步骤
- // 2.Modal插件类及原型方法的定义
- // 3.在jQuery上定义Modal插件,并重设插件构造器
- // 可以通过该属性获取插件的真实类函数
- // 4. 防冲突处理
- // 5. 绑定触发事件
- }(window.jQuery);
步骤2 插件核心代码。主要是Modal核心类函数的定义、默认参数的定义和8个原型方法的定义,这8个原型方法主要是处理弹窗的反转、打开、关闭和弹窗背景设置、取消等操作。核心代码如下:
- var Modal = function (element, options) {
- // element表示modal弹出框容器及内部元素,options是设置选项
- this.options = options // 传进来的各种参数
- this.$element = $(element)
- this.$backdrop = // modal下面的背景对象
- this.isShown = null // 默认情况下,不设置是否已经显示弹窗
- // 如果设置了remote,就加载remote指定url的内容到modal-content样式的元素内,并触发
- // loaded.bs.modal事件
- if (this.options.remote) {
- this.$element
- .find('.modal-content')
- .load(this.options.remote, $.proxy(function () {
- this.$element.trigger('loaded.bs.modal') // 触发loaded.bs.modal事件
- }, this))
- }
- }
- Modal.DEFAULTS = {{ // 默认设置
- backdrop: true, // 默认单击弹窗以外的地方时自动关闭弹窗
- keyboard: true, // 默认设置,按Esc键关闭弹窗
- show: true // 默认设置,单击触发元素时打开弹窗
- }
- // 反转弹窗状态
- Modal.prototype.toggle = function () { };
- // 打开弹窗
- Modal.prototype.show = function () { };
- // 关闭弹窗
- Modal.prototype.hide = function (e) { };
- // 确保当前打开的弹窗处于焦点状态
- Modal.prototype.enforceFocus = function () { };
- // 按Esc键是否退出的处理
- Modal.prototype.escape = function () { };
- // 关闭弹窗
- Modal.prototype.hideModal = function () { };
- // 删除背景,关闭弹窗时触发
- Modal.prototype.removeBackdrop = function () { };
- // 添加背景,打开弹窗时触发
- Modal.prototype.backdrop = function (callback) { };
注意
上面的this.$element.load(this.options.remote)语句表示将内容加载至modal-content样式的元素内,而不是modal-body样式元素内,这和2.x版完全不一样了。
下面的代码是上述8个原型方法的详细注释,阅读的时候主要是要注意元素焦点、动画、回调函数等主要代码的执行原理。
- // 反转弹窗状态
- Modal.prototype.toggle = function (_relatedTarget) {
- return this[!this.isShown ? 'show' : 'hide'](_relatedTarget)
- // 如果是关闭状态,则打开弹窗,否则就关闭
- }
- // 打开弹窗
- Modal.prototype.show = function (_relatedTarget) {
- var that = this // 当前modal对象赋值为that,防止作用域冲突
- var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
- // 定义弹窗前的触发事件
- this.$element.trigger(e) // 打开弹窗前,触发事件
- // 如果已经打开了(或者曾经被阻止过),则退出执行,后续代码不做处理
- if (this.isShown || e.isDefaultPrevented()) return
- this.isShown = true // 设置状态为打开
- this.escape() // 处理键盘事件,主要是设置按Esc键的时候是否关闭弹窗
- this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]',
- (this.hide, this))
- // 如果单击了元素内的子元素(带有[data-dismiss="modal"]属性),则关闭弹窗
- this.backdrop(function () { // 绘制弹窗背景以后,处理以下代码
- var transition = $.support.transition && that.$element.hasClass('fade')
- // 判断浏览器是否支持动画,并且弹窗是否设置了动画过渡效果(是否有fade样式)
- if (!that.$element.parent().length) {
- that.$element.appendTo(document.body)
- // 如果modal弹窗没有父容器,则将它附加到body上
- }
- that.$elementshow().scrollTop(0) // 显示modal弹窗
- if (transition) {
- that.$element[0].offsetWidth // 如果支持动画,强制刷新UI现场,重绘弹窗
- }
- that.$element
- .addClass('in') // 给modal弹窗添加in样式,和modal样式一起
- .attr('aria-hidden', false) // 设置aria-hidden为假(告诉阅读器该元素是非
- // 隐藏状态)
- that.enforceFocus() // 强制给弹窗设定焦点
- var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
- // 打开弹窗显示后的触发事件
- transition ?
- that.$element.find('.modal-dialog') // 找到弹窗元素
- .one($.support.transition.end, function () {
- // 如果支持动画,则动画结束以后给弹窗内的元素设置焦点,并触发shown事件
- that.$element.focus().trigger(e)
- })
- .emulateTransitionEnd(300) :
- that.$element.focus().trigger(e) // 否则直接设置焦点,并触发shown事件
- })
- }
- // 关闭弹窗
- Modal.prototype.hide = function (e) {
- if (e) e.preventDefault() // 先阻止冒泡行为
- e = $.Event('hide.bs.modal') // 关闭弹窗前的触发事件
- this.$element.trigger(e) // 关闭弹窗前触发事件
- // 如果已经关闭了(或者曾经被阻止过),则退出执行后续代码不做处理
- if (!this.isShown || e.isDefaultPrevented()) return
- this.isShown = false // 设置状态为关闭
- this.escape() // 处理键盘事件,主要是设置按Esc键的时候是否关闭弹窗
- $(document).off('focusin.bs.modal') // 取消所有的focusin.bs.modal事件
- this.$element
- .removeClass('in') // 删除in样式
- .attr('aria-hidden', true) // 设定aria-hidden为true(告诉阅读器该元素是隐藏状态)
- .off('click.dismiss.bs.modal') // 取消dismiss的单击事件
- // 如果支持动画,则动画结束以后再关闭,否则直接关闭
- $.support.transition && this.$element.hasClass('fade') ?
- this.$element
- .one($.support.transition.end, $.proxy(this.hideModal, this))
- .emulateTransitionEnd(300) :
- this.hideModal()
- }
- // 确保当前打开的弹窗处于焦点状态
- Modal.prototype.enforceFocus = function () {
- $(document)
- .off('focusin.bs.modal') // 禁用所有的focusin事件,防止无限循环
- .on('focusin.bs.modal', $.proxy(function (e) {
- if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
- // 如果处于焦点的元素不是当前元素(或不包含当前元素),则强制给当前元素设置焦点
- this.$element.focus()
- }
- }, this))
- }
- // 按Esc键是否退出的处理
- Modal.prototype.escape = function () {
- if (this.isShown && this.options.keyboard) {
- // 如果弹窗是打开状态,并且keyboard选项不为false,则说明允许按Esc键关闭弹窗
- this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) {
- // 检测键盘事件,如果是Esc(keycode=27)键,则关闭
- e.which == 27 && this.hide()
- }, this))
- } else if (!this.isShown) { // 否则,取消键盘事件检测
- this.$element.off('keyup.dismiss.bs.modal')
- }
- }
- // 关闭弹窗
- Modal.prototype.hideModal = function () {
- var that = this
- this.$element.hide() // 关闭弹窗
- this.backdrop(function () {
- that.removeBackdrop() // 清除背景
- that.$element.trigger('hidden.bs.modal')
- // 关闭以后,触发hidden事件(hide事件是在关闭前执行的)
- })
- }
- // 删除背景,关闭弹窗时触发
- Modal.prototype.removeBackdrop = function () {
- this.$backdrop && this.$backdrop.remove() // 删除背景元素
- this.$backdrop = null // 设置背景对象为null
- }
- // 添加背景,打开弹窗时触发
- Modal.prototype.backdrop = function (callback) {
- var animate = this.$element.hasClass('fade') ? 'fade' : ''
- // 是否设置了动画过渡效果,如果是则设置为fade
- if (this.isShown && this.options.backdrop) {
- // 如果是打开状态,并且设置了backdrop参数
- var doAnimate = $.support.transition && animate // 定义动画标识
- // 在body上定义背景div元素,并附加fade标识以支持动画
- this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
- .appendTo(document.body)
- // 背景被单击时进行判断:如果backdrop参数为static,则强制将弹窗设置为焦点;否则,关闭弹窗
- this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) {
- if (e.target !== e.currentTarget) return
- this.options.backdrop == 'static'
- ? this.$element[0].focus.call(this.$element[0])
- : this.hide.call(this)
- }, this))
- if (doAnimate) this.$backdrop[0].offsetWidth
- // 如果支持动画,则强制刷新UI现场,重绘弹窗
- this.$backdrop.addClass('in') // 添加in样式
- if (!callback) return // 如果没有回调,则直接返回
- // 如果支持动画,则动画结束后执行回调函数;否则,直接执行回调函数
- doAnimate ?
- this.$backdrop
- .one($.support.transition.end, callback)
- .emulateTransitionEnd(150) :
- callback()
- } else if (!this.isShown && this.$backdrop) {
- // 如果是关闭状态,但背景对象依然还存在
- this.$backdrop.removeClass('in') // 去除in样式
- // 如果支持动画,则动画结束后执行回调函数;否则,直接执行回调函数
- $.support.transition && this.$element.hasClass('fade') ?
- this.$backdrop
- .one($.support.transition.end, callback)
- .emulateTransitionEnd(150) :
- callback()
- } else if (callback) { // 如果不是以上两种情况,但回调函数却存在
- callback() // 直接执行回调函数
- }
- }
步骤3 jQuery插件定义。在jQuery上定义$.fn.modal插件,有点特殊的代码是options参数的收集和合并,主要收集了3部分:插件的默认参数Defaults、modal元素上的data-属性、执行插件时候传入的option对象,三部分的优先级依次升高。源码如下:
- var old = $.fn.modal
- // 保留其他库的$.fn.modal代码(如果定义的话),以便在noConflict之后可以继续使用该老代码
- $.fn.modal = function (option, _relatedTarget) {
- return this.each(function () { // 根据选择器,遍历所有符合规则的元素
- var $this = $(this)
- var data = $this.data('bs.modal') // 获取自定义属性bs.modal的值
- // 将默认参数、选择器所在元素的自定义属性(data-开头)和option参数,这三种值合并在
- 一起,作为options参数
- // 优先级:后面的参数优先级高于前面的参数
- var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option ==
- 'object' && option)
- // 如果值不存在,则将Modal实例设置为bs.modal的值
- if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
- // 如果option传递了string,则表示要执行某个方法
- // 比如传入了show,则要执行Modal实例的show方法,data["show"]相当于data.show()
- if (typeof option == 'string') data[option](_relatedTarget)
- else if (options.show) data.show(_relatedTarget)
- })
- }
- $.fn.modal.Constructor = Modal // 重设插件构造器,可以通过该属性获取插件的真实类函数
步骤4 防冲突处理。源码如下:
- // 防冲突处理
- $.fn.modal.noConflict = function () {
- $.fn.modal = old // 恢复以前的旧代码
- return this // 将$.fn.modal.noConflict()设置为Bootstrap的Modal插件
- }
步骤5 绑定触发事件。源码如下:
- // 绑定触发事件
- $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
- // 监测所有拥有自定义属性data-toggle="modal"的元素上的单击事件
- var $this = $(this)
- var href = $this.attr('href') // 获取href属性值
- // 获取data-target属性值,如果没有,则获取href值,该值是所弹出元素的id
- var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, '')))
- // 如果弹窗元素上已经有该弹窗实例(即弹出过一次了),则设置option值为字符串toggle
- // 否则将remote值(如果有的话)、弹窗元素上的自定义属性值集合、触发元素上的自定义属性值集合,
- 合并为option对象
- var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.
- test(href) && href }, $target.data(), $this.data())
- if ($this.is('a')) e.preventDefault()() // 如果是a链接元素,则还要阻止默认行为
- $target
- .modal(option, this) // 给弹窗元素绑定Modal插件(也就是实例化Modal),并传入option参数
- .one('hide', function () {
- $this.is(':visible') && $this.focus() // 定义一次hide事件,给所单击元素加上焦点
- })
- })
- $(document)
- // 给所有的弹窗元素绑定shown事件,一旦该弹窗打开以后,就在body上添加modal-open样式
- .on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') })
- // 同理,给所有的弹窗元素绑定hidden事件,一旦该弹窗关闭以后,就删除body上的modal-open样式
- .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') })
在绑定触发事件上,最后在body元素上添加、删除modal-open样式的操作时需要注意,因为该样式控制了整个body样式(超出边界是自动隐藏),然后给其内部的modal元素(或backdrop背景)做样式设置的时候提供了参考,但切勿通过此样式影响其他的内容布局等,所以要确保该样式不被其他功能或者插件所引用。
- // 源码5093行
- .modal-open {
- overflow: hidden;
- }
至此,模态窗体的适应方法和源码分析就结束了。通过分析我们可以看出它的设计精妙的部分,尤其是几个元素的z-index设置,以及在触发事件时候的各种样式的操作。最后还有可扩展的事件,比如在弹出前触发的show,弹出后触发的shown,以及关闭前触发的hide,和关闭后触发的hidden,都为我们做自定义插件提供了良好的参考。