Home  >  Article  >  Web Front-end  >  How to solve tap "click through" in fastclick code

How to solve tap "click through" in fastclick code

亚连
亚连Original
2018-06-15 17:39:411432browse

This article mainly introduces the knowledge content to completely solve the tap "click through" problem through fastclick source code analysis. Friends who are interested can learn about it.

The recent use of tap events has brought me various problems. One of the problems is that to solve the problem of clicking through, you still need to change the original clicks into taps. In this case, we will abandon IE users

Of course compatibility can be done, but no one wants to touch the old code, so today we came up with the fastclick thing.

This is the fourth time we have posted about tap’s click-through incident recently. We have been working hard to solve it. I was worried about the "tap through" mask, so today my boss proposed a library fastclick, which finally proved to solve our problem

and click does not need to be replaced with tap, so our boss said to me in a serious way Please don't misunderstand me. I have sent all the emails...

So I was looking at the fastclick library in the afternoon to see if it could solve our problem, so we started Let’s

Read the fastclick source code

It’s too easy to use, just say:

FastClick.attach(document.body);

So all the click response speeds are directly improved, just now ! The problem of which input gets focus is also solved! ! ! Damn it, if it really works, the colleague who changed the page will definitely give me a hard time.

Let’s follow step by step. The entrance is the attach method:

FastClick.attach = function(layer) {
'use strict';
return new FastClick(layer);
};

This brother is just instantiated. Next code, so we still have to look at our constructor:

function FastClick(layer) {
'use strict';
var oldOnClick, self = this;
  this.trackingClick = false;
  this.trackingClickStart = 0;
  this.targetElement = null;
  this.touchStartX = 0;
  this.touchStartY = 0;
  this.lastTouchIdentifier = 0;
  this.touchBoundary = 10;
  this.layer = layer;
  if (!layer || !layer.nodeType) {
   throw new TypeError('Layer must be a document node');
  }
  this.onClick = function() { return FastClick.prototype.onClick.apply(self, arguments); };
  this.onMouse = function() { return FastClick.prototype.onMouse.apply(self, arguments); };
  this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self, arguments); };
  this.onTouchMove = function() { return FastClick.prototype.onTouchMove.apply(self, arguments); };
  this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self, arguments); };
  this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self, arguments); };
  if (FastClick.notNeeded(layer)) {
   return;
  }
  if (this.deviceIsAndroid) {
   layer.addEventListener('mouseover', this.onMouse, true);
   layer.addEventListener('mousedown', this.onMouse, true);
   layer.addEventListener('mouseup', this.onMouse, true);
  }
  layer.addEventListener('click', this.onClick, true);
  layer.addEventListener('touchstart', this.onTouchStart, false);
  layer.addEventListener('touchmove', this.onTouchMove, false);
  layer.addEventListener('touchend', this.onTouchEnd, false);
  layer.addEventListener('touchcancel', this.onTouchCancel, false);
 
  if (!Event.prototype.stopImmediatePropagation) {
   layer.removeEventListener = function(type, callback, capture) {
    var rmv = Node.prototype.removeEventListener;
    if (type === 'click') {
     rmv.call(layer, type, callback.hijacked || callback, capture);
    } else {
     rmv.call(layer, type, callback, capture);
    }
   };
 
   layer.addEventListener = function(type, callback, capture) {
    var adv = Node.prototype.addEventListener;
    if (type === 'click') {
     adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
      if (!event.propagationStopped) {
       callback(event);
      }
     }), capture);
    } else {
     adv.call(layer, type, callback, capture);
    }
   };
  }
  if (typeof layer.onclick === 'function') {
   oldOnClick = layer.onclick;
   layer.addEventListener('click', function(event) {
 oldOnClick(event);
 }, false);
 layer.onclick = null;
}
}

Look at this code, I don’t know what many of the attributes above do...so I ignored it

if (!layer || !layer.nodeType) {
throw new TypeError('Layer must be a document node');
}

It should be noted here that we must pass in a node to the constructor, otherwise problems will occur

Then this guy registers some basic mouse events in his own attribute methods, specifically Ganshen Let’s talk about it later

There is a notNeeded method at the back:

 FastClick.notNeeded = function(layer) {
  'use strict';
  var metaViewport;
  if (typeof window.ontouchstart === 'undefined') {
   return true;
  }
  if ((/Chrome\/[0-9]+/).test(navigator.userAgent)) {
   if (FastClick.prototype.deviceIsAndroid) {
    metaViewport = document.querySelector('meta[name=viewport]');
    if (metaViewport && metaViewport.content.indexOf('user-scalable=no') !== -1) {
     return true;
    }
   } else {
    return true;
   }
  }
  if (layer.style.msTouchAction === 'none') {
   return true;
  }
  return false;
 };

This method is used to determine whether fastclick needs to be used. The meaning of the comment is not clear, let’s take a look at the code

First sentence:

if (typeof window.ontouchstart === 'undefined') {
 return true;
}

If the touchstart event is not supported, return true
PS: My current feeling is that fastclick should also be simulated by touch events, but there is no click through problem

I also judged some problems with android later, but I won’t pay attention to them here. It should mean that it can be supported only by supporting touch, so back to the main code

In the main code, we see that if If the browser does not support touch events or has other problems, it will pop up directly

Then there is an attribute of deviceIsAndroid, let’s take a look (in fact, you don’t need to look at it to know whether it is an android device)

FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;

Binding events

Okay, this guy started to bind registration events, so far No abnormalities are seen

 if (this.deviceIsAndroid) {
  layer.addEventListener('mouseover', this.onMouse, true);
  layer.addEventListener('mousedown', this.onMouse, true);
  layer.addEventListener('mouseup', this.onMouse, true);
 }
 layer.addEventListener('click', this.onClick, true);
 layer.addEventListener('touchstart', this.onTouchStart, false);
 layer.addEventListener('touchmove', this.onTouchMove, false);
 layer.addEventListener('touchend', this.onTouchEnd, false);
 layer.addEventListener('touchcancel', this.onTouchCancel, false);

The specific event function has been rewritten in the front, we will ignore it for now and continue to look at the back (in other words, this guy has bound enough events)

stopImmediatePropagation

There is one more attribute:

Prevent the bubbling behavior of the current event and prevent the continued execution of the event handler functions of all events of the same type on the element where the current event is located.

If an element has multiple event listening functions for the same type of event, when the event of that type is triggered, the multiple event listening functions will be executed in sequence. If a listening function executes event.stopImmediatePropagation() method, in addition to the bubbling behavior of the event being blocked (the role of the event.stopPropagation method), the execution of other listening functions of the same type of events bound to the element will also be blocked.

 <html>
     <head>
         <style>
             p { height: 30px; width: 150px; background-color: #ccf; }
             p {height: 30px; width: 150px; background-color: #cfc; }
         </style>
     </head>
     <body>
         <p>
             <p>paragraph</p>
         </p>
         <script>
             document.querySelector("p").addEventListener("click", function(event)
             {
                 alert("我是p元素上被绑定的第一个监听函数");
             }, false);
             document.querySelector("p").addEventListener("click", function(event)
             {
                 alert("我是p元素上被绑定的第二个监听函数");
                 event.stopImmediatePropagation();
                 //执行stopImmediatePropagation方法,阻止click事件冒泡,并且阻止p元素上绑定的其他click事件的事件监听函数的执行.
             }, false);
             document.querySelector("p").addEventListener("click", function(event)
             {
                 alert("我是p元素上被绑定的第三个监听函数");
                 //该监听函数排在上个函数后面,该函数不会被执行.
             }, false);
             document.querySelector("p").addEventListener("click", function(event)
             {
                 alert("我是p元素,我是p元素的上层元素");
                 //p元素的click事件没有向上冒泡,该函数不会被执行.
             }, false);
         </script>
     </body>
 </html>
 if (!Event.prototype.stopImmediatePropagation) {
  layer.removeEventListener = function(type, callback, capture) {
   var rmv = Node.prototype.removeEventListener;
   if (type === &#39;click&#39;) {
    rmv.call(layer, type, callback.hijacked || callback, capture);
   } else {
    rmv.call(layer, type, callback, capture);
   }
  };
 
  layer.addEventListener = function(type, callback, capture) {
   var adv = Node.prototype.addEventListener;
   if (type === &#39;click&#39;) {
    adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
     if (!event.propagationStopped) {
      callback(event);
     }
    }), capture);
   } else {
    adv.call(layer, type, callback, capture);
   }
  };
 }

Then This guy has redefined the method of registering and canceling events.

Let's look at the registration event first, in which Node's addEventListener is used. What is this Node?

From this point of view, Node is a system property, representing our node, so the logout event is rewritten here

Here, we find that in fact, he only specializes click Dealing with

adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
if (!event.propagationStopped) {
 callback(event);
}
}), capture);

There is a hijacked hijacker. I don’t know for the time being. I guess it was rewritten in the middle.
Then I rewrote it here. Hijacked is probably a method. It exists to prevent multiple events from being registered on a dom and executed multiple times.

We don’t care about the cancellation and registration. At this point, we have actually rewritten the registration and cancellation events we passed into the dom. It seems to be very powerful. It means that the DOM will use our click event in the future. Of course, this is just my temporary judgment. I have to read on for details. Moreover, I think the current judgment is unreliable, so let’s continue.

When we cancel the event, we can use addEventListener or dom.onclick=function(){}, so here is the following code:

if (typeof layer.onclick === &#39;function&#39;) {
 oldOnClick = layer.onclick;
 layer.addEventListener(&#39;click&#39;, function(event) {
 oldOnClick(event);
 }, false);
 layer.onclick = null;
}

Here, his main process is actually finished. It means that all his logic is here. Regardless of the entrance or exit, the event should be registered, so we write a code to take a look.

Test entrance

 <input type="button" value="addevent">
 <input type="button" value="addevent1">
 $(&#39;#addEvent&#39;).click(function () {
     var dom = $(&#39;#addEvent1&#39;)[0]
     dom.addEventListener(&#39;click&#39;, function () {
         alert(&#39;&#39;)
         var s = &#39;&#39;;
     })
 });

Let’s take a look at this breakpoint Look at what we did after clicking. Now that we click button 1, an event will be registered for button 2:

但是很遗憾,我们在电脑上不能测试,所以增加了我们读代码的困难,在手机上测试后,发现按钮2响应很快,但是这里有点看不出问题

最后alert了一个!Event.prototype.stopImmediatePropagation发现手机和电脑都是false,所以我们上面搞的东西暂时无用

 FastClick.prototype.onClick = function (event) {
     &#39;use strict&#39;;
     var permitted;
     alert(&#39;终于尼玛进来了&#39;);
     if (this.trackingClick) {
         this.targetElement = null;
         this.trackingClick = false;
         return true;
     }
     if (event.target.type === &#39;submit&#39; && event.detail === 0) {
         return true;
     }
     permitted = this.onMouse(event);
     if (!permitted) {
         this.targetElement = null;
     }
     return permitted;
 };

然后我们终于进来了,现在我们需要知道什么是trackingClick 了

/**
* Whether a click is currently being tracked.
* @type Boolean
*/
this.trackingClick = false;

我们最初这个属性是false,但是到这里就设置为true了,就直接退出了,说明绑定事件终止,算了这个我们暂时不关注,我们干点其它的,

因为,我觉得重点还是应该在touch事件上

PS:到这里,我们发现这个库应该不只是将click加快,而是所有的响应都加快了

我在各个事件部分log出来东西,发现有click的地方都只执行了touchstart与touchend,于是至此,我觉得我的观点成立
他使用touch事件模拟量click,于是我们就只跟进这一块就好:

 FastClick.prototype.onTouchStart = function (event) {
     &#39;use strict&#39;;
     var targetElement, touch, selection;
     log(&#39;touchstart&#39;);
     if (event.targetTouches.length > 1) {
         return true;
     }
     targetElement = this.getTargetElementFromEventTarget(event.target);
     touch = event.targetTouches[0];
     if (this.deviceIsIOS) {
         selection = window.getSelection();
         if (selection.rangeCount && !selection.isCollapsed) {
             return true;
         }
         if (!this.deviceIsIOS4) {
             if (touch.identifier === this.lastTouchIdentifier) {
                 event.preventDefault();
                 return false;
             }
             this.lastTouchIdentifier = touch.identifier;
             this.updateScrollParent(targetElement);
         }
     }
     this.trackingClick = true;
     this.trackingClickStart = event.timeStamp;
     this.targetElement = targetElement;
     this.touchStartX = touch.pageX;
     this.touchStartY = touch.pageY;
     if ((event.timeStamp - this.lastClickTime) < 200) {
         event.preventDefault();
     }
     return true;
 };

其中用到了一个方法:

FastClick.prototype.getTargetElementFromEventTarget = function (eventTarget) {
  &#39;use strict&#39;;
  if (eventTarget.nodeType === Node.TEXT_NODE) {
    return eventTarget.parentNode;
  }
  return eventTarget;
};

他是获取我们当前touchstart的元素

然后将鼠标的信息记录了下来,他记录鼠标信息主要在后面touchend时候根据x、y判断是否为click
是ios情况下还搞了一些事情,我这里跳过去了

然后这里记录了一些事情就跳出去了,没有特别的事情,现在我们进入我们的出口touchend

 FastClick.prototype.onTouchEnd = function (event) {
     &#39;use strict&#39;;
     var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
     log(&#39;touchend&#39;);
     if (!this.trackingClick) {
         return true;
     }
     if ((event.timeStamp - this.lastClickTime) < 200) {
         this.cancelNextClick = true;
         return true;
     }
     this.lastClickTime = event.timeStamp;
     trackingClickStart = this.trackingClickStart;
     this.trackingClick = false;
     this.trackingClickStart = 0;
     if (this.deviceIsIOSWithBadTarget) {
         touch = event.changedTouches[0];
         targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
         targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
     }
     targetTagName = targetElement.tagName.toLowerCase();
     if (targetTagName === &#39;label&#39;) {
         forElement = this.findControl(targetElement);
         if (forElement) {
             this.focus(targetElement);
             if (this.deviceIsAndroid) {
                 return false;
             }
             targetElement = forElement;
         }
     } else if (this.needsFocus(targetElement)) {
         if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === &#39;input&#39;)) {
             this.targetElement = null;
             return false;
         }
         this.focus(targetElement);
         if (!this.deviceIsIOS4 || targetTagName !== &#39;select&#39;) {
             this.targetElement = null;
             event.preventDefault();
         }
         return false;
     }
     if (this.deviceIsIOS && !this.deviceIsIOS4) {
         scrollParent = targetElement.fastClickScrollParent;
         if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
             return true;
         }
     }
     if (!this.needsClick(targetElement)) {
         event.preventDefault();
         this.sendClick(targetElement, event);
     }
     return false;
 };

这个家伙洋洋洒洒干了许多事情

这里纠正一个错误,他onclick那些东西现在也执行了......可能是我屏幕有变化(滑动)导致

if ((event.timeStamp - this.lastClickTime) < 200) {
 this.cancelNextClick = true;
 return true;
}

这个代码很关键,我们首次点击会执行下面的逻辑,如果连续点击就直接完蛋,下面的逻辑丫的不执行了......
这个不执行了,那么这个劳什子又干了什么事情呢?
事实上下面就没逻辑了,意思是如果确实点击过快,两次点击只会执行一次,这个阀值为200ms,这个暂时看来是没有问题的

好了,我们继续往下走,于是我意识到又到了一个关键点
因为我们用tap事件不能使input获得焦点,但是fastclick却能获得焦点,这里也许是一个关键,我们来看看几个与获取焦点有关的函数

 FastClick.prototype.focus = function (targetElement) {
     &#39;use strict&#39;;
     var length;
     if (this.deviceIsIOS && targetElement.setSelectionRange) {
         length = targetElement.value.length;
         targetElement.setSelectionRange(length, length);
     } else {
         targetElement.focus();
     }
 };

setSelectionRange是我们的关键,也许他是这样获取焦点的......具体我还要下来测试,留待下次处理吧
然后下面如果时间间隔过长,代码就不认为操作的是同一dom结构了

最后迎来了本次的关键:sendClick,无论是touchend还是onMouse都会汇聚到这里

 FastClick.prototype.sendClick = function (targetElement, event) {
     &#39;use strict&#39;;
     var clickEvent, touch;
     // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
     if (document.activeElement && document.activeElement !== targetElement) {
         document.activeElement.blur();
     }
     touch = event.changedTouches[0];
     // Synthesise a click event, with an extra attribute so it can be tracked
     clickEvent = document.createEvent(&#39;MouseEvents&#39;);
     clickEvent.initMouseEvent(&#39;click&#39;, true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
     clickEvent.forwardedTouchEvent = true;
     targetElement.dispatchEvent(clickEvent);
 };

他创建了一个鼠标事件,然后dispatchEvent事件(这个与fireEvent类似)

 //document上绑定自定义事件ondataavailable
 document.addEventListener(&#39;ondataavailable&#39;, function (event) {
 alert(event.eventType);
 }, false);
 var obj = document.getElementById("obj");
 //obj元素上绑定click事件
 obj.addEventListener(&#39;click&#39;, function (event) {
 alert(event.eventType);
 }, false);
 //调用document对象的 createEvent 方法得到一个event的对象实例。
 var event = document.createEvent(&#39;HTMLEvents&#39;);
 // initEvent接受3个参数:
 // 事件类型,是否冒泡,是否阻止浏览器的默认行为
 event.initEvent("ondataavailable", true, true);
 event.eventType = &#39;message&#39;;
 //触发document上绑定的自定义事件ondataavailable
 document.dispatchEvent(event);
 var event1 = document.createEvent(&#39;HTMLEvents&#39;);
 event1.initEvent("click", true, true);
 event1.eventType = &#39;message&#39;;
 //触发obj元素上绑定click事件
 document.getElementById("test").onclick = function () {
 obj.dispatchEvent(event1);
 };

至此,我们就知道了,我们为dom先绑定了鼠标事件,然后touchend时候触发了,而至于为什么本身注册的click未触发就要回到上面代码了

解决“点透”(成果)

有了这个思路,我们来试试我们抽象出来的代码:

 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
     <title></title>
     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
     <style>
         #list { display: block; position: absolute; top: 100px; left: 10px; width: 200px; height: 100px; }
         p { display: block; border: 1px solid black; height: 300px; width: 100%; }
         #input { width: 80px; height: 200px; display: block; }
     </style>
 </head>
 <body>
     <p>
     </p>
     <p>
         <p>
             <input type="text" />
         </p>
     </p>
     <script type="text/javascript">
         var el = null;
         function getEvent(el, e, type) {
             e = e.changedTouches[0];
             var event = document.createEvent(&#39;MouseEvents&#39;);
             event.initMouseEvent(type, true, true, window, 1, e.screenX, e.screenY, e.clientX, e.clientY, false, false, false, false, 0, null);
             event.forwardedTouchEvent = true;
             return event;
         }
         list.addEventListener(&#39;touchstart&#39;, function (e) {
             var firstTouch = e.touches[0]
             el = firstTouch.target;
             t1 = e.timeStamp;
         })
         list.addEventListener(&#39;touchend&#39;, function (e) {
             e.preventDefault();
             var event = getEvent(el, e, &#39;click&#39;);
             el.dispatchEvent(event);
         })
         var list = document.getElementById(&#39;list&#39;);
         list.addEventListener(&#39;click&#39;, function (e) {
             list.style.display = &#39;none&#39;;
             setTimeout(function () {
                 list.style.display = &#39;&#39;;
             }, 1000);
         })
     </script>
 </body>
 </html>

这样的话,便不会点透了,这是因为zepto touch事件全部绑定值document,所以 e.preventDefault();无用
结果我们这里是直接在dom上,e.preventDefault();
便起了作用不会触发浏览器默认事件,所以也不存在点透问题了,至此点透事件告一段落......

帮助理解的图

代码在公司写的,回家后不知道图上哪里了,各位将就看吧

为什么zepto会点透/fastclick如何解决点透

我最开始就给老大说zepto处理tap事件不够好,搞了很多事情出来

因为他事件是绑定到document上,先touchstart然后touchend,根据touchstart的event参数判断该dom是否注册了tap事件,有就触发

于是问题来了,zepto的touchend这里有个event参数,我们event.preventDefault(),这里本来都是最上层了,这就代码压根没什么用

但是fastclick处理办法不可谓不巧妙,这个库直接在touchend的时候就触发了dom上的click事件而替换了本来的触发时间

意思是原来要350-400ms执行的代码突然就移到了50-100ms,然后这里虽然使用了touch事件但是touch事件是绑定到了具体dom而不是document上

So e.preventDefault is effective. We can prevent bubbling, and we can also prevent browser default events. This is the essence of fastclick, which is not bad! ! !

Reading the entire fastclick code is enlightening. I have gained a lot today. I will record it here.

Postscript

There is something wrong with the above statement. Let me correct it:

First, let’s go back to the original zepto solution and see what problems it has:

Because the js standard does not support tap events, zepto tap is simulated by touchstart and touchend. zepto gives document during initialization Bind the touch event, get the current element according to the event parameter when we click, and save the mouse position when clicking and leaving. Determine whether it is a click event based on the mouse movement range of the current element. If so, the registered tap event will be triggered.

Then fastclick processing is basically the same as zepto, but it is different

fastclick binds the event to the element you pass (usually document.body)

② After touchstart and touchend (the current click el will be obtained manually), if it is a click event, the click event of the dom element will be manually triggered

So the click event will be triggered at touchend, and the entire response speed will increase. , the trigger is actually the same as zepto tap

Okay, why is it that with basically the same code, zepto can click through but fastclick cannot?

The reason is that there is a settimeout in zepto's code, and even if e.preventDefault() is executed in this code, it will not be useful

This is the fundamental difference, because settimeout will prioritize it. Low

With the timer, when the code is executed to setTimeout, this code will be placed at the end of the JS engine

And our code will immediately detect e.preventDefault, Once settimeout is added, e.preventDefault will not take effect. This is the root cause of zepto's point.

Conclusion

The above is what I compiled for everyone. I hope it will be helpful to everyone in the future.

Related articles:

Use gm to crop composite images under Nodejs

How to call json using js

How to implement Baidu index crawler using Puppeteer image recognition technology

The above is the detailed content of How to solve tap "click through" in fastclick code. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn