이 글에서는 FastClick 소스 코드 분석을 통해 탭 "클릭 연결" 문제를 완전히 해결하기 위한 지식 콘텐츠를 주로 소개합니다.
최근 탭 이벤트 사용으로 인해 여러 가지 문제가 발생했습니다. 문제 중 하나는 문제를 해결하려면 원래 클릭을 탭으로 변경해야 한다는 것입니다. 이 경우 IE 사용자는
물론 호환성을 포기합니다. 하지만 아무도 이전 코드를 건드리고 싶어하지 않았기 때문에 오늘은 fastclick 문제를 생각해냈습니다.
최근에 탭스루 사건에 대해 포스팅한 것은 이번이 네 번째입니다. "클릭스루" 마스크, 그래서 오늘 상사가 fastclick이라는 라이브러리를 제안했는데, 그것이 마침내 우리의 문제를 해결했음을 입증했습니다
클릭을 탭으로 대체할 필요가 없기 때문에 우리 상사는 저에게 진지하게 말했습니다. 오해합니다. 이메일을 모두 보냈습니다. .....
그래서 문제가 해결될 수 있는지 확인하기 위해 오후에 fastclick 라이브러리를 살펴보았습니다. 그럼 시작해 보겠습니다.
fastclick 소스 코드 읽기
Nima, 사용하기 너무 간단합니다.
FastClick.attach(document.body);
라고 말하세요. 이제 모든 클릭 응답 속도가 직접적으로 향상됩니다! 어떤 입력이 포커스를 받는 문제도 해결! ! ! 젠장, 정말 가능하다면 페이지를 바꾼 동료가 분명 나를 힘들게 할 거야
차근히 따라가자, 입구는 첨부 방식이다:
FastClick.attach = function(layer) { 'use strict'; return new FastClick(layer); };
이 형이 방금 코드를 인스턴스화했으니까 아직 살펴봐야 합니다. 생성자:
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; } }
이 코드를 보세요. 위의 속성 중 많은 부분이 어떤 역할을 하는지 모르겠습니다... 그래서
if (!layer || !layer.nodeType) { throw new TypeError('Layer must be a document node'); }
를 무시했습니다. 그렇지 않으면 문제가 발생합니다
그런 다음 이 사람은 자신의 속성 메서드에 몇 가지 기본 마우스 이벤트를 등록합니다. 자세한 내용은 나중에 이야기하겠습니다
뒤에 notNeeded 메서드가 있습니다.
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; };
이 메서드가 사용되었습니다. fastclick을 사용해야 하는지 판단하려면 주석의 의미가 명확하지 않습니다. 코드를 살펴보겠습니다. 첫 번째 문장:
if (typeof window.ontouchstart === 'undefined') { return true; }
터치스타트 이벤트가 지원되지 않으면 true를 반환하세요
PS: 현재 느낌은 다음과 같습니다. 그 fastclick도 동일해야 합니다. 터치 이벤트는 시뮬레이션되었지만 포인트 스루 문제는 없습니다
또한 Android의 몇 가지 문제는 나중에 판단하여 여기서만 지원해야 한다는 의미입니다. 터치를 지원하므로 메인 코드로 돌아갑니다
메인 코드에서 브라우저가 터치 이벤트나 다른 문제를 지원하지 않으면 바로 팝업이 뜹니다
그런 다음 deviceIsAndroid 속성이 있으니 살펴보겠습니다. (사실 안봐도 안드로이드 기기인지 알 수 있어요)
FastClick.prototype .deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
Binding events
알겠습니다, 이 사람 등록 이벤트 바인딩을 시작했는데 지금까지 이상한 점은 발견되지 않았습니다
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);
특정 이벤트 기능은 전면에 다시 작성되었으므로 일단 무시하고 계속해서 후면을 살펴보겠습니다(그런데 이 사람은 충분한 이벤트 바인딩)
stopImmediatePropagation
속성이 하나 더 있습니다.
현재 이벤트의 버블링 동작을 중지하고 현재 이벤트가 있는 요소에서 동일한 유형의 모든 이벤트에 대한 이벤트 핸들러의 지속적인 실행을 방지합니다.
요소에 동일한 유형의 이벤트에 대한 여러 이벤트 리스너 함수가 있는 경우 이 유형의 이벤트가 트리거되면 여러 이벤트 리스너 함수가 순서대로 실행됩니다. 리스닝 함수가 event.stopImmediatePropagation() 메서드를 실행하는 경우 차단되는 이벤트의 버블링 동작(event.stopPropagation 메소드의 역할) 외에도 해당 요소에 바인딩된 동일한 유형의 다른 이벤트도 차단됩니다.
<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 === '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); } }; }
그러면 이 사람이 재정의됩니다.
먼저 Node의 addEventListener를 사용하는 등록 이벤트를 살펴보겠습니다.
이 관점에서 Node는 우리 노드를 대표하는 시스템 속성이므로 여기서 로그아웃 이벤트를 다시 작성합니다
여기서 실제로는 클릭 시에만 특수 처리를 수행한다는 것을 알 수 있습니다
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { if (!event.propagationStopped) { callback(event); } }), capture);
그 중 하나가 하이재킹되었습니다. 하이재킹의 목적이 무엇인지는 모르겠지만 중간에 다시 작성한다는 의미인 것 같습니다. 그러면 여기서 다시 작성했습니다. Hijacked는 아마도 여러 이벤트가 DOM에 등록되어 여러 번 실행되는 것을 방지하기 위한 방법일 것입니다. 의 상황
우리는 로그아웃과 등록에 신경쓰지 않습니다. 실제로 우리가 DOM에 전달한 등록과 로그아웃 이벤트는 향후에 DOM이 사용할 것임을 의미합니다. 클릭 이벤트를 호출하는 클릭 이벤트는 물론 저희의 일시적인 판단이므로 자세한 내용은 읽어봐야 하고 현재 판단은 신뢰할 수 없는 것 같으니 계속 진행하겠습니다
if (typeof layer.onclick === 'function') { oldOnClick = layer.onclick; layer.addEventListener('click', function(event) { oldOnClick(event); }, false); layer.onclick = null; }여기서 그의 기본 프로세스는 실제로 완료되었습니다. 이는 입구 또는 출구에 관계없이 그의 모든 논리가 여기에 있음을 의미합니다. 이벤트가 등록되어야 하므로 코드를 작성합니다. 테스트 입구
<input type="button" value="addevent"> <input type="button" value="addevent1"> $('#addEvent').click(function () { var dom = $('#addEvent1')[0] dom.addEventListener('click', function () { alert('') var s = ''; }) });를 살펴보겠습니다. 이 중단점을 통해 클릭한 후 무엇을 했는지 살펴보겠습니다. 이제 버튼 1을 클릭하면 버튼 2에 이벤트가 등록됩니다. :
但是很遗憾,我们在电脑上不能测试,所以增加了我们读代码的困难,在手机上测试后,发现按钮2响应很快,但是这里有点看不出问题
最后alert了一个!Event.prototype.stopImmediatePropagation发现手机和电脑都是false,所以我们上面搞的东西暂时无用
FastClick.prototype.onClick = function (event) { 'use strict'; var permitted; alert('终于尼玛进来了'); if (this.trackingClick) { this.targetElement = null; this.trackingClick = false; return true; } if (event.target.type === 'submit' && 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) { 'use strict'; var targetElement, touch, selection; log('touchstart'); 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) { 'use strict'; if (eventTarget.nodeType === Node.TEXT_NODE) { return eventTarget.parentNode; } return eventTarget; };
他是获取我们当前touchstart的元素
然后将鼠标的信息记录了下来,他记录鼠标信息主要在后面touchend时候根据x、y判断是否为click
是ios情况下还搞了一些事情,我这里跳过去了
然后这里记录了一些事情就跳出去了,没有特别的事情,现在我们进入我们的出口touchend
FastClick.prototype.onTouchEnd = function (event) { 'use strict'; var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement; log('touchend'); 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 === 'label') { 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 === 'input')) { this.targetElement = null; return false; } this.focus(targetElement); if (!this.deviceIsIOS4 || targetTagName !== 'select') { 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) { 'use strict'; 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) { 'use strict'; 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('MouseEvents'); clickEvent.initMouseEvent('click', 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('ondataavailable', function (event) { alert(event.eventType); }, false); var obj = document.getElementById("obj"); //obj元素上绑定click事件 obj.addEventListener('click', function (event) { alert(event.eventType); }, false); //调用document对象的 createEvent 方法得到一个event的对象实例。 var event = document.createEvent('HTMLEvents'); // initEvent接受3个参数: // 事件类型,是否冒泡,是否阻止浏览器的默认行为 event.initEvent("ondataavailable", true, true); event.eventType = 'message'; //触发document上绑定的自定义事件ondataavailable document.dispatchEvent(event); var event1 = document.createEvent('HTMLEvents'); event1.initEvent("click", true, true); event1.eventType = 'message'; //触发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('MouseEvents'); 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('touchstart', function (e) { var firstTouch = e.touches[0] el = firstTouch.target; t1 = e.timeStamp; }) list.addEventListener('touchend', function (e) { e.preventDefault(); var event = getEvent(el, e, 'click'); el.dispatchEvent(event); }) var list = document.getElementById('list'); list.addEventListener('click', function (e) { list.style.display = 'none'; setTimeout(function () { list.style.display = ''; }, 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上
그래서 e.preventDefault가 효과적입니다. 버블링과 브라우저 기본 이벤트를 방지할 수 있습니다. 이것이 바로 fastclick의 핵심입니다. 나쁘지 않습니다! ! !
Fastclick 코드 전체가 읽기에 훌륭합니다. 오늘 많은 것을 얻었습니다. 여기에 기록합니다
Postscript
위 설명에 문제가 있습니다. 수정하겠습니다.
우선, 다시 돌아가겠습니다. 원래 zepto 계획 및 내용 확인 질문:
js 표준은 탭 이벤트를 지원하지 않기 때문에 Zepto 탭은 초기화될 때 터치 이벤트를 문서에 바인딩합니다. 이벤트 매개변수에 따라 현재 요소를 저장하고 내려갈 때의 마우스 위치는 현재 요소의 마우스 이동 범위에 따라 클릭 이벤트인지 여부를 결정합니다. 그렇다면 등록된 탭 이벤트가 트리거됩니다
그러면 fastclick 처리는 기본적으로 zepto와 동일하지만 다릅니다
fastclick은 전달한 요소(보통 document.body)에 이벤트를 바인딩합니다.
② touchstart 및 touchend 이후(현재 클릭 el을 수동으로 가져옵니다) , 클릭 이벤트인 경우 dom 요소의 클릭 이벤트가 수동으로 트리거됩니다
그래서 click 이벤트는 touchend에서 트리거되고 전체 응답 속도는 실제로 zepto 탭과 동일합니다
알겠습니다. 기본적으로 동일한 코드를 사용하면 zepto는 클릭할 수 있지만 fastclick은 클릭할 수 없는 이유는 무엇입니까?
이유는 zepto의 코드에 settimeout이 있고 이 코드에서 e.preventDefault()를 실행해도 작동하지 않기 때문입니다.
이것이 근본적인 차이점입니다. 왜냐하면 settimeout의 우선순위가 낮기 때문입니다
타이머를 사용하면, 코드는 setTimeout으로 실행되며, 이 코드는 JS 엔진의 끝에 배치됩니다.
그리고 우리 코드는 e.preventDefault를 즉시 감지합니다. settimeout이 추가되면 e.preventDefault가 적용되지 않습니다. 문제의 근본 원인
결론
위 내용은 앞으로 모든 사람에게 도움이 되기를 바랍니다.
관련 기사:
Puppeteer 이미지 인식 기술을 사용하여 Baidu 인덱스 크롤러를 구현하는 방법
위 내용은 fastclick 코드에서 탭 '클릭 연결'을 해결하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!