Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React iScroll Fastclick的事件机制讲解 #6

Open
houyulei opened this issue Feb 22, 2017 · 0 comments
Open

React iScroll Fastclick的事件机制讲解 #6

houyulei opened this issue Feb 22, 2017 · 0 comments

Comments

@houyulei
Copy link
Owner

React iScroll Fastclick的事件机制讲解

移动h5应用两个很影响用户体验的问题:

  1. 滚动不流畅
  2. 不支持弹性
  3. 点击300ms延迟

-webkit-overflow-scrolling : touch;可以部分解决1. 2.问题,但很不完美,至少有两个问题:

  1. 兼容性不好,不同设备表现不一致,老设备不支持
  2. 滚动时不能同时响应其他操作

iScroll或者类似模拟滚动的方案,是当前的最优选择.

问题3的300-350ms延迟是因为浏览器会等待来判断是否是双击.据说大部分现代浏览器已经没有这个问题.然而我们实际测试中,这个问题依然在很多设备中存在,包括IOS9以上的设备!

Ever since the first release of Chrome for Android, this delay was removed if pinch-zoom was also disabled. However, pinch zoom is an important accessibility feature. As of Chrome 32 (back in 2014) this delay is gone for mobile-optimised sites, without removing pinch-zooming! Firefox and IE/Edge did the same shortly afterwards, and in March 2016 a similar fix landed in iOS 9.3.(from google developer)

React也基于此否决官方实现一个无延迟的tap: facebook/react#436 (comment)

并推荐用插件react-tap-event-plugin来暂时解决这个问题.

但这里有一个引入插件和将onClick事件改成onTouchTap事件的成本(而且说不定以后确实不存在这个问题,又要改回onClick)

react-tap-event-plugin:

injectTapEventPlugin = require("react-tap-event-plugin");
injectTapEventPlugin();

var Main = React.createClass({
  render: function() {
    return (
      <a
        href="#"
        onTouchTap={this.handleTouchTap}
        Tap Me
      </a>
    );
  },

  handleTouchTap: function(e) {
    console.log("touchTap", e);
  }
});

这个成本是我更愿意使用react-fastclick的原因.

但是并没有完事大吉,如果不当的使用可能会出现:

  1. 无法点击(无法响应click事件)
  2. 重复点击(触发两次click事件回调)

为了彻底搞明白这里面的猫腻,解决恼人bug,我们去看看他们内部到底是如何实现的.

Q1 react-fastclick为什么fast?

react-fastclick重写了React.createElement以禁止执行组件的onClick回调,并添加onTouchStart,onTouchMove,onTouchEnd来监听模拟click事件,并在onTouchEnd中触发click回调.

    React.createElement = function () {
      // Check if basic element & has onClick prop
      if (type && typeof type === 'string' && (
        (props && typeof props.onClick === 'function') || handleType[type]
      )) {
        // Add our own events to props
        args[1] = propsWithFastclickEvents(type, props || {});
      }
    };
  };

由于touch事件不存在delay的问题,因此就解决这个问题.

Q2 React的事件处理机制是怎样的,使用事件代理了吗?

React采用的是顶层(document)的事件代理机制,并实现了一个synthetic event,解决 IE 与W3C 标准实现之间的兼容问题。并且React代理在顶层的事件监听也支持stopPropagation,也是因为React的synthetic event的内部实现.
所有通过 JSX 这种方式绑定的事件都是synthetic event。 在组件创建时react会把事件监听存储在一个map中,并且在组件卸载(unmount)的时候自动销毁绑定的事件。

Q3 iScroll为什么会使React onClick无效?

iScroll会监听元素的touch事件,并在touchstart的回调中执行:
e.preventDefault();
也就是禁止了touchstart的默认行为,这会产生一个副作用:同时会阻止click事件的触发.

If the preventDefault method is called on this event, it should prevent any default actions caused by any touch events associated with the same active touch point, including mouse events or scrolling.(W3C)

好,我们知道了,元素的click事件由于iScrolltouchstart preventDafault()导致无法触发,也就无法冒泡到document上触发reactonClick回调.

Q4 iScroll为什么在某些浏览器下click却正常?

Q3回答了iScroll导致click无效,奇怪的是在有些浏览器下,click是能正常触发的.
这个原因要落在另一个事件pointer上.某些浏览器支持pointer事件,这时iScroll会去监听pointer事件而不是touch事件,然而pointer事件的preventDefault()并不会阻止click事件的触发.

In user agents that support firing click and/or contextmenu, calling preventDefault during a pointer event typically does not have an effect on whether click and/or contextmenu are fired or not.(W3C)

这是iScroll一个坑.
我们很容易想到让iScroll不监听pointer从而使各浏览器表现一致,这样设置:

new iScroll({
  disableMouse: true,
  disablePointer: true
})

然而这里有另一个坑,在支持pointer事件的浏览器里,iscroll无法滚动了!

Q5 为什么在支持pointer事件的浏览器里,iscroll无法滚动了?

按理我们设置disablePointer了,iScroll应该用touch事件去处理啊.
这个锅仍然是iScroll的,iScroll判断当浏览器支持pointer事件时,会禁用touch.即使我们设置了disablePointer : true也一样.
具体情况是disableMouse,disablePointer,disableTouch的默认值并不是false,而是动态去设置的:

        this.options = {
            // INSERT POINT: OPTIONS
            disablePointer : !utils.hasPointer,
            disableTouch : utils.hasPointer || !utils.hasTouch,
            disableMouse : utils.hasPointer || utils.hasTouch
        }

这是一种fallback的机制,当utils.hasPointertrue,即浏览器支持pointer事件时,disableTouch会被默认设为true!
然而在iScroll文档中没有任何说明.这也是一个bug,正确的判断方式应该为disableTouch : (utils.hasPointer && !options.disablePointer) || !utils.hasTouch

在这个bug修复之前,我们需要这样设置:

new iScroll({
  disableMouse: true,
  disablePointer: true,
  disableTouch: false
})

Q6 怎么解决iScroll的点击无效?

网上答案和iScroll官方给的答案是在iScroll初始化时加参数:
click : truetap : true
这实际上是iScroll触发了一个自定义的click事件

me.click = function (e) {
    var target = e.target,
        ev;

    if ( !(/(SELECT|INPUT|TEXTAREA)/i).test(target.tagName) ) {
        // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent
        // initMouseEvent is deprecated.
        ev = document.createEvent(window.MouseEvent ? 'MouseEvents' : 'Event');
        ev.initEvent('click', true, true);
        ev.view = e.view || window;
        ev.detail = 1;
        ev.screenX = target.screenX || 0;
        ev.screenY = target.screenY || 0;
        ev.clientX = target.clientX || 0;
        ev.clientY = target.clientY || 0;
        ev.ctrlKey = !!e.ctrlKey;
        ev.altKey = !!e.altKey;
        ev.shiftKey = !!e.shiftKey;
        ev.metaKey = !!e.metaKey;
        ev.button = 0;
        ev.relatedTarget = null;
        ev._constructed = true;
        target.dispatchEvent(ev);
    }
};

这是有效的.但是如果是和fastclick配合,实际上不需这个设置.

Q7 使用fastclick不加click : true也能点击?

没错,正如Q1和Q3所说,fastclick实际上监听的是touch事件,而iScroll影响的是click事件,所以使用fastclick是可以正常点击的.

Q8 使用了onClick : true为什么会造成重复点击?

实际是和Q4是同一问题,在那些支持pointer事件的浏览器中,会浏览器触发一次,iScroll模拟事件触发一次.

至此,我们已经彻底弄清楚了React iScroll Fastclick的事件机制,安心的去使用这些技术吧!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant