https://www.jianshu.com/p/5b578e656966
http://www.php.cn/js-tutorial-387360.html
最近遇到奇異的bug,在ios 11.3移動端頁面 input輸入框第一次觸摸可以彈起鍵盤,后續再觸摸需要很難彈起鍵盤,或者需要在輸入框停一會才能彈起鍵盤。
bug復現條件:
一、ios 11.3中app的webview為 UI WebView
二、在項目中使用了FastClick.js,頁面包括輸入框
發現源頭問題:
在碰到問題腦子第一想法這不就是click延遲300ms的現象嗎?所以就想到是不是FastClick.js導致,注釋掉后發現bug現象消失了,代碼如下:
define(['zepto'], function ($) { 'use strict'; // FastClick.attach(document.body); ... });
但是這是為什么呢?我們一起看看為什么要加上FastClick,這個庫解決了什么問題?
- click 300ms延遲:瀏覽器click會比touch延遲300ms觸發
-
click穿透現象:當兩個div同處一個position,上層div綁定touch,下層div綁定click,當上層div觸發touch消失后,可能會觸發下層div的click事件
既然Fastclick是為了解決這兩類問題,其實現原理如下圖所示:
fastclick原理
fastclick利用捕獲頂層dom元素(如:body,html等)的click事件,攔截所有的click請求進行判斷:是否有touch觸發、是否需要阻礙click事件(stopImmediatePropagation)等。
分析問題解決方案:
步驟一:input無法聚焦彈出鍵盤,fastclick中有一塊判斷當前元素targetElement是否需要needsFocus,看看其方法的實現:
FastClick.prototype.needsFocus = function(target) { //判斷當前元素是否需要focus switch (target.nodeName.toLowerCase()) { case 'textarea': return true; case 'select': return !deviceIsAndroid; case 'input': switch (target.type) { case 'button': case 'checkbox': case 'file': case 'image': case 'radio': case 'submit': return false; } // No point in attempting to focus disabled inputs return !target.disabled && !target.readOnly; default: return (/\bneedsfocus\b/).test(target.className); } };
步驟二:看到needsFocus下執行了什么?在touchEnd方法中,代碼塊如下:
if (this.needsFocus(targetElement)) {if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) { this.targetElement = null; return false; } this.focus(targetElement); //調用focus進行聚焦 this.sendClick(targetElement, event); if (!deviceIsIOS || targetTagName !== 'select') { this.targetElement = null; event.preventDefault(); } return false; }
步驟三:focus方法分析(包含解決方案),如下:
FastClick.prototype.focus = function(targetElement) { var length; //兼容處理:在iOS7中,有一些元素(如date、datetime、month等)在setSelectionRange會出現TypeError //這是因為這些元素並沒有selectionStart和selectionEnd的整型數字屬性,所以一旦引用就會報錯,因此排除這些屬性才使用setSelectionRange方法 if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month' && targetElement.type !== 'email') { length = targetElement.value.length; targetElement.setSelectionRange(length, length); /*修復bug ios 11.3不彈出鍵盤,這里加上聚焦代碼,讓其強制聚焦彈出鍵盤*/ targetElement.focus(); } else { targetElement.focus(); } };
原理分析
OK,上真機iphoneX驗證bug已經消失了,但是我們並不知道為什么在ios 11.3會出現該問題,秉着探索真理的一顆心(ZZZZ),到github去查看FastClick的issues列表,果然發現早有人提出bug了,如下圖:

下方有評論如下:
A:說framework7框架那邊已經有解決方案啦,點擊這里。

另外一位仁兄的解決方案和我類似,修改focus方法。

因此跳到framework的issue中的解決方案,解決方案:點擊這里,描述如下:

跳過去stackoverflow后,其實根本源頭已經查到了,ios 11.3更新 Safari 11.1,支持新web API :允許對事件支持 {passive: false}
被動模式,減少滾動屏幕的性能損耗和奔潰。
passive mode解析
那么新的問題來了,{passive: false}是什么玩意?來,我們先看看它的使用方式:
document.addEventListener('touchmove', function(e) { e.preventDefault(); }, { passive: false });
按照以往我們對添加事件監聽的方法三個參數的認知,如下:
document.addEventListener(type , callback, capture); //type是事件類型,callback是執行函數, capture是否進行捕獲/冒泡,默認為false
Passive event listeners是2016年Google I/O 上同 PWA 概念一起被提出,但是同PWA不同,Passive event listeners 的作用很簡單,如果用簡單一句話來解釋就是:提升頁面滑動的流暢度。
target.addEventListener(type, listener[, options]);
/** options 可選 一個指定有關 listener 屬性的可選參數對象。 可用的選項如下: capture: Boolean,表示 listener 會在該類型的事件捕獲階段傳播到該 EventTarget 時觸發。 once: Boolean,表示 listener 在添加之后最多只調用一次。如果是 true, listener 會在其被調用之后自動移除。 passive: Boolean,表示 listener 永遠不會調用 preventDefault()。如果 listener 仍然調用了這個函數,客戶端將會忽略它並拋出一個控制台警告。 */ //示例代碼 target.addEventListener('touchstart', function(e){ e.preventDefault() // 無效,報錯 }, {passive: true});
為什么增加支持這個屬性會導致添加fastclick后input輸入框很難彈出鍵盤?
在ios更新日志了,寫到了“Updated root document touch event listeners to use passive mode improving scrolling performance and reducing crashes.”
翻譯過來就是:針對document的touch事件監聽添加passive配置,即是:{passive: true},會永遠不調用event.preventDefault(),以此來提高滾動性能。
源頭推測:
fastclick是采用攔截click和監聽touch事件去實現的,里面包括對tagetElement的focus方法重寫,因此在11.3之前可能event.preventDefault生效了,同時用setSelectionRange是可以聚焦input的。
另外一個bug也是由這個導致的是:
在iOS11.3的UI webview使用fastclick.js,頁面有個按鈕點擊事件,當app或鎖屏超過幾分鍾時間,回到頁面會導致click事件失效。
解決方案為:
var passiveListener = (function checkPassiveListener() { //判斷瀏覽器是否支持 {passive: true} var supportsPassive = false; try { var opts = Object.defineProperty({}, 'passive', { get: function() { supportsPassive = true; } }); window.addEventListener('testPassiveListener', null, opts); } catch (e) { supportsPassive = false; } return supportsPassive; }()); var activeListener = passiveListener ? {passive:false} : false; layer.addEventListener('click', this.onClick, true); layer.addEventListener('touchstart', this.onTouchStart, passiveListener); layer.addEventListener('touchmove', this.onTouchMove, passiveListener); layer.addEventListener('touchend', this.onTouchEnd, passiveListener); layer.addEventListener('touchcancel', this.onTouchCancel, passiveListener);
參考資料
小禮物
https://www.cnblogs.com/AlexBlogs/p/4947019.html
鄙人才疏學淺,新人一枚,不足之處還請諒解,寫下這個也只是為了給大家分享一下我解決這個BUG的方法,也是自己的一個筆記。
首先,我們使用fastclick插件的初衷是解決“tap”事件“點透”的BUG;fastclick與tap都是利用“touch”事件來模擬“click”事件的;
然后我們來大致的了解一下fastclick的工作原理(來自網上的copy):
在我們的app中跟蹤所有的TouchStart事件,在接收到touchend事件的時候,觸發一個click事 件;
使用方法可找度娘;
但是問題來了,當使用了fastclick的時候,我們發現“日期”控件無法被觸發了,是正常的點擊時無法被觸發,如果長按0.5S的樣子還是可以觸發的,但是問題還是已經存在,必須解決!
通過閱讀fastclick的源碼發現里面有個這個方法
/**
* On touch end, determine whether to send a click event at once.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchEnd = function(event) {
反正意思在上面也提到過,就是在接受到touchend事件的時候,觸發一個click事件;
那么我的解決方法就是:
當touchend的時候我們判斷一下他的event.target到底是啥,如果是date我們就不玩了,不要你fastclick了,用原生的去觸發不就OK了,來個return false;
我的代碼:
FastClick.prototype.onTouchEnd = function(event) {
/*加上這個*/
if(event.target.hasAttribute("type") && event.target.getAttribute("type") == "date"){
return false;
}
這里if里面的條件就隨便寫咯,自己根據項目需求而定,反正你要的是到達某個特殊的條件時給他退出就好了;
鄙人才疏學淺,新人一枚,不足之處還請諒解,寫下這個也只是為了給大家分享一下我解決這個BUG的方法,也是自己的一個筆記。
!!轉載還請確保是最新版本,謝謝