WeUI源码解析(一) --- util.js

WeUI是腾讯的前端UI框架,里面的组件样式,特殊组件的动效处理有很多值得我学习的规范和技巧,所以选取几个关键地方的源码来进行研究。

这里有一个基础的js,util.js,大部分组件的交互都依赖这个js,所以在学习组件前,我们需要先看看util.js完成了哪些工作。

util.js

1.外部模块

先看看util.js依赖的外部模块

import 'element-closest';
import objectAssign from 'object-assign';
import $ from 'balajs';
element-closest

The #Element.closest method returns the closest ancestor of the current element (or the current element itself) which matches the selectors given in parameter. If there isn’t such an ancestor, it returns null.

返回闭合的父元素DOM或者是元素本身。

object-assign

Node.js 4 and up, as well as every evergreen browser (Chrome, Edge, Firefox, Opera, Safari), support Object.assign() ?. If you target only those environments, then by all means, use Object.assign() instead of this package.

ES6中的对象赋值方法的兼容包,作用类似于Object.assign(),如果你的浏览器是主流的支持ES6的,这个包就不需要。

balajs

bala.js is a function that allows you easily select elements on a web page and get rid of jQuery in most of cases.

不需要使用jquery就可以利用$选择元素,你也可以把它当作一个全局变量。具体用法我们从代码里面分析。

2.判断系统

首先我觉得这个绑定系统的函数就很写得很考验基本功。。。可能是我基本功差。

/* 判断系统 */
function _detect(ua){
    let os = this.os = {}, android = ua.match(/(Android);?[\s\/]+([\d.]+)?/);
    if (android) {
        os.android = true;
        os.version = android[2];
    }
}
_detect.call($, navigator.userAgent);

这里主要涉及的是
1. 利用Function.call()函数进行显示绑定,作用是把$对象绑定到this对象,然后在调用时指定这个this。
2. 对象的共享传递,函数中的os是对象类型,所以os和this.os是共享同一个内存地址,改变了os的同时其实也改变了this.os,再根据显式调用,其实就是对$这个对象进行了赋值。所以我们在执行完这个函数后会发现其实$其实已经存os对象了。

console.log($.os) ==> {android: true, version: “5.0”}

所以我们就对$完成了全局的系统判断标识。

3.绑定函数

这里是对$.fn绑定了一系列的DOM操作函数,包括DOM的增删改查,DOM 类,元素样式的修改,DOM事件的绑定与移除。我们对较为复杂的几个进行研究:


objectAssign($.fn, {
    /**
     * 只能是一个 HTMLElement 元素或者 HTMLElement 数组,不支持字符串
     * @param {Element|Element[]} $child
     * @returns {append}
     */
    append: function ($child) {
        if (!($child instanceof HTMLElement)) {
            $child = $child[0];
        }
        this.forEach(($element) => {
            $element.appendChild($child);
        });
        return this;
    },

    ...,


    /**
     *
     * @param eventType
     * @param selector
     * @param handler
     */

    on: function (eventType, selector, handler) {
        const isDelegate = typeof selector === 'string' && typeof handler === 'function';
        if (!isDelegate) {
            handler = selector;
        }
        this.forEach(($element) => {
            eventType.split(' ').forEach((event) => {
                $element.addEventListener(event, function (evt) {
                    if (isDelegate) {
                        // http://caniuse.com/#search=closest
                        if (this.contains(evt.target.closest(selector))) {
                            handler.call(evt.target, evt);
                        }
                    }
                    else {
                        handler.call(this, evt);
                    }
                });
            });
        });
        return this;
    },
    /**
     *
     * @param {String} eventType
     * @param {String|Function} selector
     * @param {Function=} handler
     * @returns {off}
     */
    off: function (eventType, selector, handler) {
        if (typeof selector === 'function') {
            handler = selector;
            selector = null;
        }

        this.forEach(($element) => {
            eventType.split(' ').forEach((event) => {
                if (typeof selector === 'string') {
                    $element.querySelectorAll(selector).forEach(($element) => {
                        $element.removeEventListener(event, handler);
                    });
                }
                else {
                    $element.removeEventListener(event, handler);
                }
            });
        });
        return this;
    },
    /**
     *
     * @returns {Number}
     */
    index: function () {
        const $element = this[0];
        const $parent = $element.parentNode;
        return Array.prototype.indexOf.call($parent.children, $element);
    },
    /**
     * @desc 因为off方法目前不可以移除绑定的匿名函数,现在直接暴力移除所有listener
     * @returns {offAll}
     */
    offAll: function () {
        this.forEach(($element, index) => {
            var clone = $element.cloneNode(true);
            $element.parentNode.replaceChild(clone, $element);

            this[index] = clone;
        });
        return this;
    },
    /**
     *
     * @returns {*}
     */
    val: function () {
        if(arguments.length){
            this.forEach(($element) => {
                $element.value = arguments[0];
            });
            return this;
        }
        return this[0].value;
    },
    /**
     *
     * @returns {*}
     */
    attr: function(){
        if(typeof arguments[0] == 'object'){
            const attrsObj = arguments[0];
            const that = this;
            Object.keys(attrsObj).forEach((attr) => {
                that.forEach(($element) => {
                    $element.setAttribute(attr, attrsObj[attr]);
                });
            });
            return this;
        }

        if(typeof arguments[0] == 'string' && arguments.length < 2){
            return this[0].getAttribute(arguments[0]);
        }

        this.forEach(($element) => {
            $element.setAttribute(arguments[0], arguments[1]);
        });
        return this;
    }

});

看一下绑定事件的函数on():

/**
 *
 * @param eventType
 * @param selector
 * @param handler
 */

on: function (eventType, selector, handler) {
    const isDelegate = typeof selector === 'string' && typeof handler === 'function';
    if (!isDelegate) {
        handler = selector;
    }
    this.forEach(($element) => {
        eventType.split(' ').forEach((event) => {
            $element.addEventListener(event, function (evt) {
                if (isDelegate) {
                    // http://caniuse.com/#search=closest
                    if (this.contains(evt.target.closest(selector))) {
                        handler.call(evt.target, evt);
                    }
                }
                else {
                    handler.call(this, evt);
                }
            });
        });
    });
    return this;
}

这个函数的作用呢就是给指定的DOM元素(selector)绑定指定类型(eventType)的指定事件(handler)。evt.target.closest(selector)返回符合选择器的DOM元素。其它的对号入座都不难理解,就不再写在文中了。

再看一下返回属性函数attr():

/**
 *
 * @returns {*}
 */
attr: function(){
    if(typeof arguments[0] == 'object'){
        const attrsObj = arguments[0];
        const that = this;
        Object.keys(attrsObj).forEach((attr) => {
            that.forEach(($element) => {
                $element.setAttribute(attr, attrsObj[attr]);
            });
        });
        return this;
    }

    if(typeof arguments[0] == 'string' && arguments.length < 2){
        return this[0].getAttribute(arguments[0]);
    }

    this.forEach(($element) => {
        $element.setAttribute(arguments[0], arguments[1]);
    });
    return this;
}

我们要明确一个元素可以有哪些属性值,包括像id, children, innerHtml, name等等,可以去MDN上查阅,当然我们也可以通过setAttribute(name,value)设置自定义属性值,getAttribute(name)返回指定的属性值。这个东西具体处理的工作只看函数似乎还不太能理解,所以留着等到使用时再分析。

4.修改全局对象$

主要是给$添加了两个重要的方法,一个是render(),简单的模板引擎,一个是getStyle(),获得元素计算后的样式值。

render()
/**
 * render
 * 取值:<%= variable %>
 * 表达式:<% if {} %>
 * 例子:
 *  <div>
 *    <div class="weui-mask"></div>
 *    <div class="weui-dialog">
 *    <% if(typeof title === 'string'){ %>
 *           <div class="weui-dialog__hd"><strong class="weui-dialog__title"><%=title%></strong></div>
 *    <% } %>
 *    <div class="weui-dialog__bd"><%=content%></div>
 *    <div class="weui-dialog__ft">
 *    <% for(var i = 0; i < buttons.length; i++){ %>
 *        <a href="javascript:;" class="weui-dialog__btn weui-dialog__btn_<%=buttons[i]['type']%>"><%=buttons[i]['label']%></a>
 *    <% } %>
 *    </div>
 *    </div>
 *  </div>
 * A very simple template engine
 * @param {String} tpl
 * @param {Object=} data
 * @returns {String}
 */
render: function (tpl, data) {
    const code = 'var p=[];with(this){p.push(\'' +
        tpl
            .replace(/[\r\t\n]/g, ' ')
            .split('<%').join('\t')
            .replace(/((^|%>)[^\t]*)'/g, '$1\r')
            .replace(/\t=(.*?)%>/g, '\',$1,\'')
            .split('\t').join('\');')
            .split('%>').join('p.push(\'')
            .split('\r').join('\\\'')
        + '\');}return p.join(\'\');';
    return new Function(code).apply(data);
}

这个模板引擎对模板进行了什么操作呢?首先看一下大的思路:利用模板创建了一个新的函数并利用data对象调用该函数。这里面有个with语句的语法,指定了当前的this对象为默认对象,后面又使用了apply,即让data对象成为调用函数的对象,也就是this。这样我们模板引擎中的变量其实就是我们调用该函数的对象的变量。

关于正则,我们先看一下上面这个demo的函数结果:

function () {
    var p = []; with (this) { p.push('<div>     <div class="weui-mask"></div>     <div class="weui-dialog">     '); if (typeof title === 'string') { p.push('            <div class="weui-dialog__hd"><strong class="weui-dialog__title">', title, '</strong></div>     '); } p.push('     <div class="weui-dialog__bd">', content, '</div>     <div class="weui-dialog__ft">     '); for (var i = 0; i < buttons.length; i++) { p.push('         <a href="javascript:;" class="weui-dialog__btn weui-dialog__btn_', buttons[i]['type'], '">', buttons[i]['label'], '</a>     '); } p.push('     </div>     </div>   </div>'); } return p.join('');
})

首先把换行制表这些转移字符替换成空格,然后根据 <% 把tpl分割,第三行难懂一点,意思是非制表符的多个其他字符开始或者%>后面接非制表符的多个字符替换成第一个括号匹配的字符串加回车符(把这些用\t分割开和带有%>的并以'结尾字符串加上回车,其实就是把那些字符串变量提出来),然后分隔语句变量。。。总的来说就是提取js逻辑,保留html,在利用生成的函数构造完整的html。最后就可以生成如上的那个模板引擎函数,我们再传入data对象调用:

let options = $.extend({
    title: 'aaa',
    content: '',
    className: '',
    buttons: [{
        label: '确定',
        type: 'primary',
        onClick: $.noop
    }],
    isAndroid: true
}, {});

就得到一个这样的html

<div>     <div class="weui-mask"></div>     <div class="weui-dialog">                 <div class="weui-dialog__hd"><strong class="weui-dialog__title">aaa</strong></div>          <div class="weui-dialog__bd"></div>     <div class="weui-dialog__ft">              <a href="javascript:;" class="weui-dialog__btn weui-dialog__btn_primary">确定</a>          </div>     </div>   </div>

的确是非常的神奇。而且这个是不是ejs的源码思路?我不太清楚,有兴趣的可以去看看。

getStyle()
/**
 * getStyle 获得元素计算后的样式值
 * (from http://stackoverflow.com/questions/2664045/how-to-get-an-html-elements-style-values-in-javascript)
 */
getStyle: function (el, styleProp) {
    var value, defaultView = (el.ownerDocument || document).defaultView;
    // W3C standard way:
    if (defaultView && defaultView.getComputedStyle) {
        // sanitize property name to css notation
        // (hypen separated words eg. font-Size)
        styleProp = styleProp.replace(/([A-Z])/g, '-$1').toLowerCase();
        return defaultView.getComputedStyle(el, null).getPropertyValue(styleProp);
    } else if (el.currentStyle) { // IE
        // sanitize property name to camelCase
        styleProp = styleProp.replace(/\-(\w)/g, (str, letter) => {
            return letter.toUpperCase();
        });
        value = el.currentStyle[styleProp];
        // convert other units to pixels on IE
        if (/^\d+(em|pt|%|ex)?$/i.test(value)) {
            return ((value) => {
                var oldLeft = el.style.left, oldRsLeft = el.runtimeStyle.left;
                el.runtimeStyle.left = el.currentStyle.left;
                el.style.left = value || 0;
                value = el.style.pixelLeft + 'px';
                el.style.left = oldLeft;
                el.runtimeStyle.left = oldRsLeft;
                return value;
            })(value);
        }
        return value;
    }
}

在浏览器中,defaultView属性返回当前 document 对象所关联的 window 对象,如果没有,会返回 null。所以我们是获取某元素的某指定css值,并且对IE环境做了一些兼容。Element.currentStyle 是一个与 window.getComputedStyle方法功能相同的属性。这个属性实现在旧版本的IE浏览器中.Element.runtimeStyle 是一个元素专有属性,和 HTMLElement.style 相似,除了其中的样式属性外,HTMLElement.style 具有更高的优先级和修改能力。runtimeStyle 不能修改 style 中的content属性,其在旧版的IE浏览器上可用。这些对于IE浏览器的兼容代码真的看得很头疼,因为我们现在对这方面的兼容性代码真的做的很少了。

小结

这个UI框架说到底就是一个个封装好的组件,这些组件用到的一些公用方法我们把它封装成了util.js,最后再把组件汇总到weui.js中就成为了一个可供其它项目调用的js库。至于什么本地起服务,webpack打包我们就不去看了,都是大同效益,而且和我们研究源码的初衷也不同。之后的任务就是把一些比较复杂组件源码来看一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值