• 【zepto学习笔记02】零碎点


    前言

    上次我们看了zepto的选择器方面的东西,其实zepto简单很大程度是因为他用了最新的检索器querySelectorAll,
    今天我们来学习下zepto的一些零碎点的地方吧,主要根据zepto官方文档顺序来

    contains (parent, node)

    该方法用于检测父节点是否包含给定的dom节点,如果两者相同则返回false
    contains 为javascript的基本语法,首先在ie中,最后其它浏览器也扩展了,如果a包含b则返回true

    1 p = document.getElementById('parent'),
    2 c = document.getElementById('child');
    3 log(p.contains(c)); //true
    4 log(c.contains(p));//false

    这个是zepto的实现,与其说实现不如说是封装

    1 $.contains = function (parent, node) {
    2     return parent !== node && parent.contains(node)
    3 }

    each

    each这个方法我们平时用的比较多,并且感觉很好用,很多朋友使用for可能会导致这样那样的问题,但是使用each却变好了
    是因为each封装了一个闭包,所以可以解决一些初学朋友的BUG,到时隐藏的BUG总会爆发,先遇到也不是什么坏事
    zepto的实现如下:

     1 $.each = function (elements, callback) {
     2     var i, key
     3     if (likeArray(elements)) {
     4         for (i = 0; i < elements.length; i++)
     5             if (callback.call(elements[i], i, elements[i]) === false) return elements
     6     } else {
     7         for (key in elements)
     8             if (callback.call(elements[key], key, elements[key]) === false) return elements
     9     }
    10     return elements
    11 }

    如果我们的回调有一个返回了false,那么就会跳出整个循环,我曾经看到有人在里面写break,break对js有点不靠谱的

    我们这里提一个可能发生的问题,代码可能没有实际意义,大概可以表达意思:

     1 var sum1 = 0, sum2 = 0, sum3 = 0; len = 2;
     2 var arr = [];
     3 for (var i = 0; i < len; i++) {
     4     arr.push(i)
     5 }
     6 for (var i = 0; i < len; i++) {
     7     setTimeout(function () {
     8         sum1 += arr[i];
     9     }, 0);
    10 }
    11 $.each(arr, function (i, v) {
    12     setTimeout(function () {
    13         sum2 += v;
    14     }, 0);
    15 });
    16 for (var i = 0; i < len; i++) {
    17     sum3++;
    18 }
    19 //sum3不管,答出len=2与len=200000时,sum1,sum2的值
    20 console.log(sum1);
    21 console.log(sum2);
    22 console.log(sum3);

    这个例子是我昨天一个问题思考出来的,答案非常经典,因为本来我是想要说明闭包的问题,却不自主的引入了另外一个神器

    settimeout

    这样写的话,无论如何sum1与sum2都是0,就算把len改的很大,明明已经过了1s了答案依旧是0
    由此各位可以意识到settimeout不是我最初想象那么简单了,并不是多少秒后就会执行,而是完全从主干流程脱离出来
    主干如果进行复杂的代码运算,甚至耗费几秒,我们的settimeout也不会执行

    比如我们说下一个例子:

     1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
     2 <html xmlns="http://www.w3.org/1999/xhtml">
     3 <head>
     4     <title></title>
     5     <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
     6     <style>
     7         #list { display: block; position: absolute; top: 100px; left: 10px; width: 200px; height: 100px; }
     8         div { display: block; border: 1px solid black; height: 500px; width: 100%; }
     9         #input { width: 80px; height: 200px; display: block; }
    10     </style>
    11     <script id="others_zepto_10rc1" type="text/javascript" class="library" src="/js/sandbox/other/zepto.min.js"></script>
    12 </head>
    13 <body>
    14     <input type="button" value="获取焦点" id="bt" />
    15     <div id="divBt" style=" 100px; height: 40px; background-color: Red;">
    16         div按钮</div>
    17     <br />
    18     <div id="d">
    19         <input type="text" id="input" />
    20         <div id="list">
    21         </div>
    22     </div>
    23 </body>
    24 <script type="text/javascript">
    25     var list = $('#list');
    26     var d = $('#d');
    27     var input = $('#input');
    28     input.tap(function (e) {
    29         e.stopPropagation();
    30         e.preventDefault();
    31         input.val(new Date().getTime());
    32         return false;
    33     });
    34     list.tap(function (e) {
    35         $('body').css("pointer-events", "none");
    36         list.hide();
    37         console.log(e);
    38         setTimeout(function () {
    39             e.stopPropagation();
    40             console.log(e);
    41         }, 0)
    42         setTimeout(function () {
    43             list.show();
    44         }, 1250);
    45         setTimeout(function () {
    46             $('body').css("pointer-events", "auto");
    47         }, 50);
    48     });
    49     d.tap(function () {
    50         d.append($('<p>div tap</p>'));
    51     });
    52     $('#bt').tap(function () {
    53         var s = input.val() + new Date().getTime();
    54         input.val(s)
    55         input.focus();
    56     });
    57     $('#divBt').tap(function () {
    58         input.focus();
    59     });
    60 </script>
    61 </html>

    这个例子是我最近遇到的一个问题,个人认为比较经典,我们点击里面的div会冒泡执行外面的div事件,但是我们可以 e.stopPropagation();
    这样阻止冒泡,但是如果我们代码写成这样的话:

    1 setTimeout(function () {
    2     list.show();
    3 }, 1250);

    那么对不起,阻止冒泡是不起作用的

    这个问题影响比较深远比如zepto的touch源码最后关键部分:

     1 on('touchend MSPointerUp', function (e) {
     2     // ......
     3     else if ('last' in touch)
     4         if (deltaX < 30 && deltaY < 30) {
     5             tapTimeout = setTimeout(function () {
     6                 var event = $.Event('tap')
     7                 event.cancelTouch = cancelAll
     8                 touch.el.trigger && touch.el.trigger(event)
     9                 if (touch.isDoubleTap) {
    10                     touch.el.trigger && touch.el.trigger('doubleTap')
    11                     touch = {}
    12                 }
    13                 else {
    14                     touchTimeout = setTimeout(function () {
    15                         touchTimeout = null
    16                         touch.el.trigger && touch.el.trigger('singleTap')
    17                         touch = {}
    18                     }, 250)
    19                 }
    20             }, 0)
    21         } else {
    22             touch = {}
    23         }
    24     deltaX = deltaY = 0
    25 })

    这里触发了tap事件(touch.el.trigger(event) ),但是在这个位置执行什么阻止冒泡等操作毫无意义,原因就是外层的settimeout(function(){}, 0)

    好了,这里扯得有点远,我们继续刚刚的闭包问题,如果我们将最后打印改成这样,答案依旧难辨,因为我们看着题目容易臆测,而忽略实际问题:

    1 setTimeout(function () {
    2     console.log(sum1);
    3     console.log(sum2);
    4     console.log(sum3);
    5 }, 1)

    这里sum1的值居然是NaN,因为我以为他会是0undefined,所以不能臆测啊!
    这里说回来大家都会知道产生了闭包,而each解决了闭包问题,而for最后i的值是2,而我们的arr自然取不到值
    PS:不知道这个例子可以说明each的用途没有......

    $.extend(target, [source, [source2, ...]])

    该方法比较有用,用于通过源对象扩展目标对象属性,源对象属性将覆盖目标对象属性,默认为浅赋值,true的话便会连余下对象一起复制

     1 var target
     2     = { one:
     3     'patridge'
     4     },
     5     source = {
     6         two: 'turtle doves'
     7     }
     8 $.extend(target, source)
     9 //=> { one: 'patridge',
    10 // two: 'turtle doves' }

    我们来看看源码实现:

     1 function extend(target, source, deep) {
     2     for (key in source)
     3     //如果深度扩展
     4         if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
     5             //如果要扩展的数据是对象且target相对应的key不是对象
     6             if (isPlainObject(source[key]) && !isPlainObject(target[key])) target[key] = {}
     7             //如果要扩展的数据是数组且target相对应的key不是数组
     8             if (isArray(source[key]) && !isArray(target[key])) target[key] = []
     9             extend(target[key], source[key], deep)
    10         } else if (source[key] !== undefined) target[key] = source[key]
    11 }

    这个代码很清晰,不是深度扩展时候仅仅是单纯的复制,由此可能可以回答一些可能被问到的问题:
    源对象和复制对象有相同的属性会被覆盖吗?答案是会的

    然后看这个深度拷贝,就会递归的将复制对象的对象给复制过去

    PS:深度克隆与浅克隆的区别是复制对象变了源对象相关属性不会跟着改变,这就是区别(在java就是引用与值的区别)

    $.grep(items, function(item){ ... })

    这个方法是用于筛选数组的,新数组只包含回调函数中返回 ture 的数组项,这个代码就不关注了,他底层还是调用的javascript数组的方法

     1 if (!Array.prototype.filter)
     2 {
     3     Array.prototype.filter = function(fun /*, thisp*/)
     4     {
     5         var len = this.length;
     6         if (typeof fun != "function")
     7             throw new TypeError();
     8  
     9         var res = new Array();
    10         var thisp = arguments[1];
    11         for (var i = 0; i < len; i++)
    12         {
    13             if (i in this)
    14             {
    15                 var val = this[i]; // in case fun mutates this
    16                 if (fun.call(thisp, val, i, this))
    17                     res.push(val);
    18             }
    19         }
    20         return res;
    21     };
    22 }
    23 
    24 $.inArray(element, array, [fromIndex])

    搜索数组中指定值并返回它的索引(如果没有找到则返回-1)。[fromIndex] 参数可选,表示从哪个索引值开始向后查找。
    这个函数底层依旧是调用javascript数组原生的方法:

    1 $.inArray = function (elem, array, i) {
    2     return emptyArray.indexOf.call(array, elem, i)
    3 }

    $.parseJSON(string) 

    这个方法在最新的javascript语法出来时非常有用,我们原来一般是这样干的:

    var json = eval('(' + str + ')');

    后面我们就这样干了:

    if (window.JSON) $.parseJSON = JSON.parse

    所以这个方法,我们暂时不必关注了,因为zepto面向的是高版本浏览器,所以他基本也不关注这个问题

    好了,我们看到这里有几个方法比较重要了!

    add(selector, [context])

    添加元素到匹配的元素集合。如果content参数存在,只在content中进行查找,否则在document中查找。

     1 <ul>    
     2     <li>list item 1</li>    
     3     <li>list item 2</li>    
     4     <li>list item 3</li>  
     5 </ul>  
     6 <p>a paragraph</p>
     7 
     8 <script type="text/javascript">
     9     $('li').add('p').css('background-color', 'red');
    10 </script>
    1 add: function (selector, context) {
    2     return $(uniq(this.concat($(selector, context)))) //追加并去重
    3 },
    4 
    5 uniq = function (array) {
    6     return filter.call(array, function (item, idx) {
    7         return array.indexOf(item) == idx
    8     })
    9 }

    PS:concat是数组本身的方法

    我们这里来一点点搞下这个代码逻辑:
    ① $(selector, context)
    该方法为一个dom选择器,根据我们上次的研究,他会返回我们的封装后的dom集合

    ② this.concat(el)
    我们知道this当前指向就是被包装的dom数组对象,所以这里就将两个方法连接起来了
    现在不管真实页面结构渲染是否变化,反正包装的dom结构被连接了

    ③ uniq(el)
    稍后,这个代码读了后我整个人迷糊了!!!我们来看个例子

     1 <html xmlns="http://www.w3.org/1999/xhtml">
     2 <head>
     3     <title></title>
     4     <script id="others_zepto_10rc1" type="text/javascript" class="library" src="/js/sandbox/other/zepto.min.js"></script>
     5     <script src="../../zepto.js" type="text/javascript"></script>
     6 </head>
     7 <body>
     8     <ul>
     9         <li>list item 1</li>
    10         <li>list item 2</li>
    11         <li>list item 3</li>
    12     </ul>
    13     <p>
    14         a paragraph</p>
    15 </body>
    16 <script type="text/javascript">
    17     $('ul').add('p').css('background-color', 'red');
    18 </script>
    19 </html>

    按照他的意思,我们ul就应该加到p后面,但是运行结构并不是这样的......所以该方法暂时忽略......

    addClass

    好了,我们来看看我们的addClass是干什么的

     1 addClass: function (name) {
     2     return this.each(function (idx) {
     3         classList = []
     4         var cls = className(this),
     5 newName = funcArg(this, name, idx, cls)
     6         //处理同时多个类的情况,用空格分开
     7         newName.split(/s+/g).forEach(function (klass) {
     8             if (!$(this).hasClass(klass)) classList.push(klass)
     9         }, this)
    10         classList.length && className(this, cls + (cls ? " " : "") + classList.join(" "))
    11     })
    12 },

    这个each是可以理解的,就是操作每一个dom结构,所以我们将代码看成这个样子:

     1 classList = []
     2 var cls = className(this),
     3 newName = funcArg(this, name, idx, cls)
     4 //处理同时多个类的情况,用空格分开
     5 newName.split(/s+/g).forEach(function (klass) {
     6     if (!$(this).hasClass(klass)) classList.push(klass)
     7 }, this)
     8 classList.length && className(this, cls + (cls ? " " : "") + classList.join(" "))
     9 
    10 这里有使用了className方法,所以我们来看看:
    11 
    12 function className(node, value) {
    13     var klass = node.className, svg = klass && klass.baseVal !== undefined
    14     if (value === undefined) return svg ? klass.baseVal : klass
    15     svg ? (klass.baseVal = value) : (node.className = value)
    16 }

    多余的东西也不管,意思就是没有value就是获取值,有就是设置,这里是原生的dom操作

    然后是funcArg方法

    1 //这个函数在整个库中取着很得要的作用,处理arg为函数或者值的情况
    2 //下面很多设置元素属性时的函数都有用到
    3 function funcArg(context, arg, idx, payload) {
    4     return isFunction(arg) ? arg.call(context, idx, payload) : arg
    5 }

    这个第二个参数可以是一个函数,如果是就执行,并使用本身作为作用域,如果不是就自己返回,我们现在的做法就直接返回class名了

    下面的代码就比较简单了,就是拼接字符串,组成新的class,然后赋给dom就结束了,所以addClass也就结束了,removeClass我们就不管了

    append

    这个方法非常庞大,虽然只有这么一个方法,但是before、after等都在这里实现了

     1 adjacencyOperators = ['after', 'prepend', 'before', 'append']
     2 adjacencyOperators.forEach(function (operator, operatorIndex) {
     3     var inside = operatorIndex % 2 //=> prepend, append
     4     $.fn[operator] = function () {
     5         // arguments can be nodes, arrays of nodes, Zepto objects and HTML strings
     6         var argType, nodes = $.map(arguments, function (arg) {
     7             argType = type(arg)
     8             return argType == "object" || argType == "array" || arg == null ? arg : zepto.fragment(arg)
     9         }),
    10         parent, copyByClone = this.length > 1 //如果集合的长度大于集,则需要clone被插入的节点
    11         if (nodes.length < 1) return this
    12         return this.each(function (_, target) {
    13             parent = inside ? target : target.parentNode
    14             //通过改变target将after,prepend,append操作转成before操作,insertBefore的第二个参数为null时等于appendChild操作
    15             target = operatorIndex == 0 ? target.nextSibling : operatorIndex == 1 ? target.firstChild : operatorIndex == 2 ? target : null
    16             nodes.forEach(function (node) {
    17                 if (copyByClone) node = node.cloneNode(true)
    18                 else if (!parent) return $(node).remove()
    19                 //插入节点后,如果被插入的节点是SCRIPT,则执行里面的内容并将window设为上下文
    20                 traverseNode(parent.insertBefore(node, target), function (el) {
    21                     if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' && (!el.type || el.type === 'text/javascript') && !el.src) window['eval'].call(window, el.innerHTML)
    22                 })
    23             })
    24         })
    25     }
    26     // after => insertAfter
    27     // prepend => prependTo
    28     // before => insertBefore
    29     // append => appendTo
    30     $.fn[inside ? operator + 'To' : 'insert' + (operatorIndex ? 'Before' : 'After')] = function (html) {
    31         $(html)[operator](this)
    32         return this
    33     }
    34 })

    这里inside只会取到0,1两种值,然后就开始初始化方法了,比如:
    $.fn.append = function() {};
    因为我们调用一般是这样干的:
    $('$id').append($('dom'))
    这里的this就是前面的dom集合,因为可能不止一个节点,所以每个节点都会被插入新节点,但是我们一般只给一个节点插东西
    这里用到了map方法,我们来看看:

     1 //遍历elements,将每条记录放入callback里进宪处理,保存处理函数返回值不为null或undefined的结果
     2 //注意这里没有统一的用for in,是为了避免遍历数据默认属性的情况,如数组的toString,valueOf
     3 $.map = function (elements, callback) {
     4     var value, values = [],
     5       i, key
     6     //如果被遍历的数据是数组或者nodeList
     7     if (likeArray(elements)) for (i = 0; i < elements.length; i++) {
     8         value = callback(elements[i], i)
     9         if (value != null) values.push(value)
    10     } else
    11     //如果是对象
    12         for (key in elements) {
    13             value = callback(elements[key], key)
    14             if (value != null) values.push(value)
    15         }
    16     return flatten(values)
    17 }

    我们从代码看来,这个方法是用于数组过滤,与filter有点类似
    所以,这里的使用map函数保证了nodes是比较靠谱的dom节点集合,如果长度为0 就直接返回了

    然后下面开始遍历我们的this dom节点,依次做操作,这里有个需要注意的地方,如果this包含的节点不止一个,那么每个节点都会被插入
    所以他这里提供了一个克隆的功能,可能出来节点,因为dom上只有一个节点,被不断的append也只是移动位置
    cloneNode是javascript dom 本身的一个方法,直接使用即可,但是要注意id不要重复

    然后根据inside不同而选取不同的parentNode,这应该与插入点有关系了,因为原生javascript只支持appendChild与insertBefore
    这里调用traverseNode方法前,就将dom操作结束了,

    这里还区分了是不是script标签,这里又有一个较关键的方法:traverseNode,他会执行我们的javascript

    1 function traverseNode(node, fun) {
    2     fun(node)
    3     for (var key in node.childNodes) traverseNode(node.childNodes[key], fun)
    4 }

    这里有两个循环,外层each里层forEach,所以节点就全部插入了......至此这个方法也基本结束
    值得一提的是最后这个代码段:

    1 $.fn[inside ? operator + 'To' : 'insert' + (operatorIndex ? 'Before' : 'After')] = function (html) {
    2     $(html)[operator](this)
    3     return this
    4 }
    5 //相当于:
    6 $.fn.insertAfter = function(html) {
    7 $(html).prepend(this);
    8 retrun;
    9 }

    其中,this指的是包装的dom集合,html为我们传入的dom对象或者dom字符串,下面的方法就是我们上面定义的

    attr(name, value)

    该方法,比较常用,我们一般用他来为dom元素设置属性,获取属性,但是他还可以传入函数哦......

     1 attr = function (name, value) {
     2     var result
     3     //当只有name且为字符串时,表示获取第一条记录的属性
     4     return (typeof name == 'string' && value === undefined) ?
     5     //集合没有记录或者集合的元素不是node类型,返回undefined
     6 (this.length == 0 || this[0].nodeType !== 1 ? undefined :
     7     //如果取的是input的value
     8 (name == 'value' && this[0].nodeName == 'INPUT') ? this.val() :
     9     //注意直接定义在node上的属性,在标准浏览器和ie9,10中用getAttribute取不到,得到的结果是null
    10     //比如div.aa = 10,用div.getAttribute('aa')得到的是null,需要用div.aa或者div['aa']这样来取
    11 (!(result = this[0].getAttribute(name)) && name in this[0]) ? this[0][name] : result) :
    12 this.each(function (idx) {
    13     if (this.nodeType !== 1) return
    14     //如果name是一个对象,如{'id':'test','value':11},则给数据设置属性
    15     if (isObject(name)) for (key in name) setAttribute(this, key, name[key])
    16     //如果name只是一个普通的属性字符串,用funcArg来处理value是值或者function的情况最终返回一个属性值
    17     //如果funcArg函数返回的是undefined或者null,则相当于删除元素的属性
    18     else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
    19 })
    20 }

    获取代码比较简单,值得注意的是,他只会获取我们第一个dom属性的值
    this[0].getAttribute(name)
    设置值的时候当然又是一个循环了(setAttribute)

    css(property, value)

    这个方法也比较常用,很大情况下雨attr比较类似,看源码前我们先思考下,为什么没有removeCss

    为什么没有removeCss?

    当然是因为,我们样式表问题,所以removeCss就没有意义了

    好了,现在我们来看看源码:

     1 css: function (property, value) {
     2     //获取指定的样式
     3     if (arguments.length < 2 && typeof property == 'string') return this[0] && (this[0].style[camelize(property)] || getComputedStyle(this[0], '').getPropertyValue(property))
     4     //设置样式
     5     var css = ''
     6     if (type(property) == 'string') {
     7         if (!value && value !== 0) //当value的值为非零的可以转成false的值时如(null,undefined),删掉property样式
     8             this.each(function () {
     9                 //style.removeProperty 移除指定的CSS样式名(IE不支持DOM的style方法)
    10                 this.style.removeProperty(dasherize(property))
    11             })
    12         else css = dasherize(property) + ":" + maybeAddPx(property, value)
    13     } else {
    14         //当property是对象时
    15         for (key in property)
    16             if (!property[key] && property[key] !== 0)
    17             //当property[key]的值为非零的可以转成false的值时,删掉key样式
    18                 this.each(function () {
    19                     this.style.removeProperty(dasherize(key))
    20                 })
    21             else css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';'
    22         }
    23         //设置
    24         return this.each(function () {
    25             this.style.cssText += ';' + css
    26         })
    27 }

    camelize是将aa-bb这种明明改为aaBb这种驼峰命名,首先比较简单,会从style里面获取style的值,不行就看样式表
    具代码,是对象情况还要做其它处理,其中好像给css设置为null时候可以取消样式,我们来试试(IE不管)
    最后试验证明是不靠谱的,所以我们不要向去removeCss了吧:

     1 <html xmlns="http://www.w3.org/1999/xhtml">
     2 <head>
     3     <title></title>
     4     <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
     5     <style>
     6         #list { display: block; position: absolute; top: 100px; left: 10px; width: 200px; height: 100px; background-color: black; }
     7     </style>
     8     <script src="../../zepto.js" type="text/javascript"></script>
     9 </head>
    10 <body>
    11     <div id="list">
    12     </div>
    13 </body>
    14 <script type="text/javascript">
    15     $('#list').css('background-color', false);
    16     alert($('#list').css('background-color'))
    17 </script>
    18 </html>

    width/height

    这个与el.css('width')类似:

     1 ['width', 'height'].forEach(function (dimension) {
     2     $.fn[dimension] = function (value) {
     3         var offset, el = this[0],
     4         //将width,hegiht转成Width,Height,用于取window或者document的width和height
     5     Dimension = dimension.replace(/./, function (m) {
     6         return m[0].toUpperCase()
     7     })
     8         //没有参数为获取,获取window的width和height用innerWidth,innerHeight
     9         if (value === undefined) return isWindow(el) ? el['inner' + Dimension] :
    10         //获取document的width和height时,用offsetWidth,offsetHeight
    11     isDocument(el) ? el.documentElement['offset' + Dimension] : (offset = this.offset()) && offset[dimension]
    12         else return this.each(function (idx) {
    13             el = $(this)
    14             el.css(dimension, funcArg(this, value, idx, el[dimension]()))
    15         })
    16     }
    17 })

    ready

    该方法也比较常用,在页面dom加载结束后执行里面的方法
    DOMContentLoaded事件是文档加载结束后执行,老浏览器不支持就是load

    1 ready: function (callback) {
    2     if (readyRE.test(document.readyState)) callback($)
    3     else document.addEventListener('DOMContentLoaded', function () {
    4         callback($)
    5     }, false)
    6     return this
    7 },

    结语

    今天暂时到这,我们下次看看zepto事件相关的实现

  • 相关阅读:
    Monkeyrunner环境搭建
    学习Monkeyrunner过程
    uiautomatorviewer使用报错
    安装JMeter
    如何测试网页的访问速度
    安装Android studio
    软件测试工程师具备技能
    WinRAR去除广告弹框(精华在末尾)
    android studio adb连接不上手机
    DOM的理解
  • 原文地址:https://www.cnblogs.com/yexiaochai/p/3447767.html
Copyright © 2020-2023  润新知