利用轮播原理结合hammer.js实现简洁的滑屏功能

2016/02/02 · JavaScript
· 滑屏

原文出处: 流云诸葛   

最近有个任务,做一个非常小的h5的应用,只有2屏,需要做横向的全屏滑动切换和一些简单的动画效果,之前做这种东西用的是fullpage.js和jquery,性能不是很好,于是就想自己动手弄一个简单的东西来实现。最后我用zepto

  • hammer.js
    和轮播的方式解决了这个问题,效果还不错,整个页面不开启Gzip时所有资源请求的数据大小为200KB左右。这篇文章总结下这个方法的实现思路。

效果演示(代码下载):

澳门微尼斯人手机版 1

给初学者:JavaScript 中数组操作注意点

2017/12/27 · JavaScript
· 数组

原文出处: CarterLi   

换个思路理解Javascript中的this

2017/07/27 · JavaScript
· this

原文出处: Leechikit   

在网上很多文章都对 Javascript 中的 this
做了详细的介绍,但大多是介绍各个绑定方式或调用方式下 this
的指向,于是我想有一个统一的思路来更好理解 this
指向,使大家更好判断,以下有部分内容不是原理,而是一种解题思路。

1. 实现要点

1)滑屏借鉴bootstrap的carousel插件,不过完全没有它那个复杂,只需要借鉴它的轮播实现思路即可;

2)滑屏切换的触发,跟PC不一样,PC通常都是通过元素的点击回调来触发,对于滑屏的页面,完全可以利用window的hashchange事件来处理,这样只要通过超链接设置锚点或者通过js改变location.hash就能触发切换;

3)考虑到移动还得支持手势操作,可以使用hammer.js这个手势库,API非常简单易用;

4)动画效果可以用animate.css,不过不用把它所有的代码都弄到代码里,只需要拷贝需要的动画效果相关的代码即可;

5)替代jquery,首选zepto;

6)滑屏效果使用transition动画,为了能够响应动画结束的回调,可以考虑使用transition.js,这个也是Bootstrap提供的工具,不过它默认只能跟jquery使用,要对它稍微改变一下才能跟zepto联合使用。

这些要点说的比较粗糙,后面的内容会一一详细介绍。

不要用 for_in 遍历数组


这是 JavaScript 初学者常见的误区。for_in
用于遍历对象中包括原型链上的所有可枚举的(enumerable)的
key,本来不是为遍历数组而存在。

使用 for_in 遍历数组有三点问题:

遍历顺序不固定

JavaScript
引擎不保证对象的遍历顺序。当把数组作为普通对象遍历时同样不保证遍历出的索引顺序。

会遍历出对象原型链上的值。

如果你改变了数组的原型对象(比如 polyfill)而没有将其设为 enumerable:
false,for_in 会把这些东西遍历出来。

运行效率低下。

尽管理论上 JavaScript 使用对象的形式储存数组,JavaScript
引擎还是会对数组这一非常常用的内置对象特别优化。

可以看到使用 for_in 遍历数组要比使用下标遍历数组慢 50 倍以上

PS:你可能是想找
for_of

从call方法开始

call 方法允许切换函数执行的上下文环境(context),即 this
绑定的对象。

大多数介绍 this 的文章中都会把 call
方法放到最后介绍,但此文我们要把 call 方法放在第一位介绍,并从
call 方法切入来研究 this ,因为 call 函数是显式绑定 this
的指向,我们来看看它如何模拟实现(不考虑传入 nullundefined
和原始值):

Function.prototype.call = function(thisArg) { var context = thisArg; var
arr = []; var result; context.fn = this; for (let i = 1, len =
arguments.length; i < len; i++) { arr.push(‘arguments[‘ + i + ‘]’);
} result = eval(“context.fn(” + arr + “)”); delete context.fn; return
result; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.call = function(thisArg) {
    var context = thisArg;
    var arr = [];
    var result;
 
    context.fn = this;
 
    for (let i = 1, len = arguments.length; i < len; i++) {
        arr.push(‘arguments[‘ + i + ‘]’);
    }
 
    result = eval("context.fn(" + arr + ")");
 
    delete context.fn;
 
    return result;
}

从以上代码我们可以看到,把调用 call
方法的函数作为第一个参数对象的方法,此时相当于把第一个参数对象作为函数执行的上下文环境,而
this 是指向函数执行的上下文环境的,因此 this
就指向了第一个参数对象,实现了 call
方法切换函数执行上下文环境的功能。

2. html结构

空的滑屏页的html结构是这样的:

XHTML

<div id=”container” class=”container”> <section id=”page-1″
class=”page page–1″> </section> <section id=”page-2″
class=”page page–2″> </section> <section id=”page-3″
class=”page page–3″> </section> </div>

1
2
3
4
5
6
7
8
<div id="container" class="container">
    <section id="page-1" class="page page–1">
    </section>
    <section id="page-2" class="page page–2">
    </section>
    <section id="page-3" class="page page–3">
    </section>
</div>

html, body { height: 100%; -webkit-tap-highlight-color: transparent; }
.container, .page { position: absolute; top: 0; left: 0; width: 100%;
height: 100%; overflow: hidden; } .page { overflow: hidden; display:
none; -webkit-transition: -webkit-transform .4s ease; transition:
transform .4s ease; -webkit-backface-visibility: hidden;
backface-visibility: hidden; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
html,
body {
    height: 100%;
    -webkit-tap-highlight-color: transparent;
}
 
.container,
.page {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
}
 
.page {
    overflow: hidden;
    display: none;
    -webkit-transition: -webkit-transform .4s ease;
    transition: transform .4s ease;
    -webkit-backface-visibility: hidden;
    backface-visibility: hidden;
}

.container与.page初始化的时候采用绝对定位,全屏布局。每一个section.page代表一页,并且默认不显示,所有页的定位都相同,也就是说如果所有页都显示的话,这些页会重叠在一块。

demo页的html结构是:

XHTML

<div id=”container” class=”container”> <section id=”page-1″
class=”page page–1″> <div class=”page__jump”><a
href=”#page-2″ title=””>下一页</a></div> <p
class=”page__num animated”>1</p> </section> <section
id=”page-2″ class=”page page–2″> <div
class=”page__jump”><a href=”#page-1″
title=””>上一页</a><a href=”#page-3″
title=””>下一页</a></div> <p class=”page__num
animated”>2</p> </section> <section id=”page-3″
class=”page page–3″> <div class=”page__jump”><a
href=”#page-2″ title=””>上一页</a></div> <p
class=”page__num animated”>3</p> </section>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="container" class="container">
    <section id="page-1" class="page page–1">
        <div class="page__jump"><a href="#page-2" title="">下一页</a></div>
        <p class="page__num animated">1</p>
    </section>
    <section id="page-2" class="page page–2">
        <div class="page__jump"><a href="#page-1" title="">上一页</a><a href="#page-3" title="">下一页</a></div>
        <p class="page__num animated">2</p>
    </section>
    <section id="page-3" class="page page–3">
        <div class="page__jump"><a href="#page-2" title="">上一页</a></div>
        <p class="page__num animated">3</p>
    </section>
</div>

demo相关的css就不展示了。其中animated是应用animate.css需要的,animate.css是一个动画库,github上有。

不要用 JSON.parse(JSON.stringify()) 深拷贝数组


有人使用 JSON
中深拷贝对象或数组。这虽然在多数情况是个简单方便的手段,但也可能引发未知
bug,因为:

会使某些特定值转换为 null

NaN, undefined, Infinity 对于 JSON 中不支持的这些值,会在序列化 JSON
时被转换为 null,反序列化回来后自然也就是 null

会丢失值为 undefined 的键值对

JSON 序列化时会忽略值为 undefined 的 key,反序列化回来后自然也就丢失了

会将 Date 对象转换为字符串

JSON 不支持对象类型,对于 JS 中 Date 对象的处理方式为转换为 ISO8601
格式的字符串。然而反序列化并不会把时间格式的字符串转化为 Date 对象

运行效率低下。

作为原生函数,JSON.stringify 和 JSON.parse 自身操作 JSON
字符串的速度是很快的。然而为了深拷贝数组把对象序列化成 JSON
再反序列化回来完全没有必要。

我花了一些时间写了一个简单的深拷贝数组或对象的函数,测试发现运行速度差不多是使用
JSON 中转的 6 倍左右,顺便还支持了 TypedArray、RegExp 的对象的复制

https://jsperf.com/deep-clone…

对象方法中的this

在模拟 call 方法的时候,我们使用了对象方法来改变 this
的指向。调用对象中的方法时,会把对象作为方法的上下文环境来调用。

既然 this
是指向执行函数的上下文环境的,那我们先来研究一下调用函数时的执行上下文情况。

下面我门来看看调用对象方法时执行上下文是如何的:

var foo = { x : 1, getX: function(){ console.log(this.x); } }
foo.getX();

1
2
3
4
5
6
7
var foo = {
    x : 1,
    getX: function(){
        console.log(this.x);
    }
}
foo.getX();

澳门微尼斯人手机版 2

从上图中,我们可以看出getX方法的调用者的上下文是foo,因此getX方法中的
this 指向调用者上下文foo,转换成 call
方法为foo.getX.call(foo)

下面我们把其他函数的调用方式都按调用对象方法的思路来转换。

3. 滑屏切换的实现思路

滑屏切换就是通过js控制2个要滑动的页增加和删除以下定义的这一些css类实现的:

.page.page–active, .page.page–prev, .page.page–next { display: block;
} .page.page–next, .page.page–active.page–active-right {
-webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%,
0, 0); } .page.page–prev, .page.page–active.page–active-left {
-webkit-transform: translate3d(-100%, 0, 0); transform:
translate3d(-100%, 0, 0); } .page.page–next.page–next-left,
.page.page–prev.page–prev-right, .page.page–active {
-webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0,
0); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.page.page–active,
.page.page–prev,
.page.page–next {
    display: block;
}
 
.page.page–next,
.page.page–active.page–active-right {
    -webkit-transform: translate3d(100%, 0, 0);
    transform: translate3d(100%, 0, 0);
}
 
.page.page–prev,
.page.page–active.page–active-left {
    -webkit-transform: translate3d(-100%, 0, 0);
    transform: translate3d(-100%, 0, 0);
}
 
.page.page–next.page–next-left,
.page.page–prev.page–prev-right,
.page.page–active {
    -webkit-transform: translate3d(0, 0, 0);
    transform: translate3d(0, 0, 0);
}

.page–active表示当前显示的页,页面初始化后,通过以下js调用,给第一页加上.page—active:

var $activePage; //初始化显示第一页 (function () { $activePage =
$(‘#page-1’); $activePage.addClass(‘page–active’); })();

1
2
3
4
5
6
7
var $activePage;
 
//初始化显示第一页
(function () {
    $activePage = $(‘#page-1’);
    $activePage.addClass(‘page–active’);
})();

这样页面默认就显示了第一页。以向左滑屏说明这些css的使用原理:

第一步,找到下一页的section,添加page–next类,将它定位当前页的右边,为滑屏做准备;

第二步,找到当前页的section,给它添加page–active-left,由于这个类改变了translate3D属性的值,所以当前页会往左滑动一屏;

在第二步的同时,给下一页的section,添加page–next-left,由于这个类改变了translate3D属性的值,所以下一页会往左滑动一屏;

第三步,在当前页跟下一页滑屏动画结束后,找到原来的当前页,移除掉page–active和page–active-left类;

在第三步的同时,找到下一页,移除掉page–next和page–next-left类,添加page–active。

gif图说明如下

澳门微尼斯人手机版 3

向右滑屏原理类似

第一步,找到上一页的section,添加page–prev类,将它定位当前页的左边,为滑屏做准备;

第二步,找到当前页的section,给它添加page–active-right,由于这个类改变了translate3D属性的值,所以当前页会往右滑动一屏;

在第二步的同时,给上一页的section,添加page–prev-right,由于这个类改变了translate3D属性的值,所以上一页会往右滑动一屏;

第三步,在当前页跟上一页滑屏动画结束后,找到原来的当前页,移除掉page–active和page–active-right类;

在第三步的同时,找到上一页,移除掉page–prev和page–prev-right类,添加page–active。

综合以上实现原理,封装成JS函数如下:

JavaScript

var TRANSITION_DURATION = 400, sliding = false; function
getSlideType($targetPage) { var activePageId = $activePage.attr(‘id’),
targetPageId = $targetPage.attr(‘id’); return activePageId <
targetPageId ? ‘next’ : activePageId == targetPageId ? ” : ‘prev’; }
function slide(targetPageId) { var $targetPage = $(‘#’ + targetPageId);
if (!$targetPage.length || sliding) return; var slideType =
getSlideType($targetPage), direction = slideType == ‘next’ ? ‘left’ :
‘right’; if (slideType == ”) return; sliding = true;
$targetPage.addClass(‘page–‘ + slideType);
$targetPage[0].offsetWidth; $activePage.addClass(‘page–active-‘ +
direction); $targetPage.addClass(‘page–‘ + slideType + ‘-‘ +
direction); $activePage .one($.transitionEnd.end, function () {
$targetPage.removeClass([‘page–‘ + slideType, ‘page–‘ + slideType +
‘-‘ + direction].join(‘ ‘)).addClass(‘page–active’);
$activePage.removeClass([‘page–active’, ‘page–active-‘ +
direction].join(‘ ‘)); $activePage = $targetPage; sliding = false; })
.emulateTransitionEnd(TRANSITION_DURATION); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var TRANSITION_DURATION = 400, sliding = false;
 
function getSlideType($targetPage) {
    var activePageId = $activePage.attr(‘id’),
            targetPageId = $targetPage.attr(‘id’);
    return activePageId < targetPageId ? ‘next’ : activePageId == targetPageId ? ” : ‘prev’;
}
 
function slide(targetPageId) {
    var $targetPage = $(‘#’ + targetPageId);
    if (!$targetPage.length || sliding) return;
 
    var slideType = getSlideType($targetPage),
            direction = slideType == ‘next’ ? ‘left’ : ‘right’;
    if (slideType == ”) return;
 
    sliding = true;
    $targetPage.addClass(‘page–‘ + slideType);
    $targetPage[0].offsetWidth;
    $activePage.addClass(‘page–active-‘ + direction);
    $targetPage.addClass(‘page–‘ + slideType + ‘-‘ + direction);
 
    $activePage
            .one($.transitionEnd.end, function () {
                $targetPage.removeClass([‘page–‘ + slideType, ‘page–‘ + slideType + ‘-‘ + direction].join(‘ ‘)).addClass(‘page–active’);
                $activePage.removeClass([‘page–active’, ‘page–active-‘ + direction].join(‘ ‘));
                $activePage = $targetPage;
                sliding = false;
            })
            .emulateTransitionEnd(TRANSITION_DURATION);
}

由于$activePage在页面初始化的时候默认指定为第一页,在每次滑屏结束后都会更新成最新的当前页,所以调用的时候只要把目标页的ID传给slide函数即可。以上代码可能会有疑问的是:

1)$targetPage[0].offsetWidth的作用,这个代码用来触发浏览器的重绘,因为目标页原来是display:
none的,如果不触发重绘的话,下一步添加css类后将看不到动画效果;

2)$.transitionEnd.end以及emulateTransitionEnd的作用,这个在下一部分说明。

不要用 arr.find 代替 arr.some


Array.prototype.find 是 ES2015 中新增的数组查找函数,与
Array.prototype.some 有相似之处,但不能替代后者。

Array.prototype.find 返回第一个符合条件的值,直接拿这个值做 if
判断是否存在,如果这个符合条件的值恰好是 0 怎么办?

arr.find
是找到数组中的值后对其进一步处理,一般用于对象数组的情况;arr.some
才是检查存在性;两者不可混用。

构造函数中的this

function Foo(){ this.x = 1; this.getX = function(){ console.log(this.x);
} } var foo = new Foo(); foo.getX();

1
2
3
4
5
6
7
8
function Foo(){
    this.x = 1;
    this.getX = function(){
        console.log(this.x);
    }
}
var foo = new Foo();
foo.getX();

执行 new
如果不考虑原型链,只考虑上下文的切换,就相当于先创建一个空的对象,然后把这个空的对象作为构造函数的上下文,再去执行构造函数,最后返回这个对象。

var newMethod = function(func){ var context = {}; func.call(context);
return context; } function Foo(){ this.x = 1; this.getX = function(){
console.log(this.x); } } var foo = newMethod(Foo); foo.getX();

1
2
3
4
5
6
7
8
9
10
11
12
13
var newMethod = function(func){
    var context = {};
    func.call(context);
    return context;
}
function Foo(){
    this.x = 1;
    this.getX = function(){
        console.log(this.x);
    }
}
var foo = newMethod(Foo);
foo.getX();

发表评论

电子邮件地址不会被公开。 必填项已用*标注