移动webkit浏览器在JS中有舍入问题吗?
Do mobile webkit browsers have a rounding issue in JS?
我试图在移动浏览器上调试一些javascript滑块代码的问题。这似乎是一个舍入错误,只发生在移动设备上。下面的代码将工作良好的桌面(例如Chrome),但增加按钮无法工作在更高的值上滑动条在智能手机上,如iPhone iOS 5/6,三星S2 ICS在Webkit中查看。
试试这个http://jsfiddle.net/codecowboy/mLpfu/。点击"在移动设备上调试"按钮——它直接靠近左上角的运行按钮(你需要登录才能看到这个按钮)。在智能手机上的浏览器中输入生成的url(最好是iPhone/Android上的webkit浏览器)
拖动滑块到265,点击增加。有些值会让你点击增加,有些则不会。该值越高,问题越严重。
代码使用jQuery和noUISlider插件。按钮点击代码为:
var btnIncrease= document.getElementById("increase");
btnIncrease.addEventListener('click',function(e) {
var slider = $("#noUiSlider");
console.log(e);
var value = slider.noUiSlider('value')[1]; //the 'value' method returns an array.
console.log('value pre move '+value);
value = value+1;
slider.noUiSlider("move", { knob : 0, to: parseInt(value,10) });
console.log(slider.noUiSlider('value')[1]);
});
有谁能解释一下是什么引起的吗?这是大/小印第安人的问题吗?还是jQuery的bug ?
上面的代码调用了nouislider插件,源代码在这里:
(function( $ ){
$.fn.noUiSlider = function( method, options ) {
function neg(a){ return a<0; }
function abs(a){ return Math.abs(a); }
function roundTo(a,b) { return Math.round(a / b) * b; }
function dup(a){ return jQuery.extend(true, {}, a); }
var defaults, methods, helpers, options = options||[], functions, touch = ('ontouchstart' in document.documentElement);
defaults = {
/*
* {handles} Specifies the number of handles. (init)
* [INT] 1, 2
*/
'handles' : 2,
/*
* {connect} Whether to connect the middle bar to the handles. (init)
* [MIXED] "upper", "lower", false, true
*/
'connect' : true,
/*
* {scale}; The values represented by the slider handles. (init,move,value)
* [ARRAY] [-+x,>x]
*/
'scale' : [0,100],
/*
* {start} The starting positions for the handles, mapped to {scale}. (init)
* [ARRAY][INT] [y>={scale[0]}, y=<{scale[1]}], integer in range.
*/
'start' : [25,75],
/*
* {to} The position to move a handle to. (move)
* [INT] Any, but will be corrected to match z > {scale[0]} || _l, z < {scale[1]} || _u
*/
'to' : 0,
/*
* {handle} The handle to move. (move)
* [MIXED] 0,1,"lower","upper"
*/
'handle' : 0,
/*
* {change} The function to be called on every change. (init)
* [FUNCTION] param [STRING]'move type'
*/
'change' : '',
/*
* {end} The function when a handle is no longer being changed. (init)
* [FUNCTION] param [STRING]'move type'
*/
'end' : '',
/*
* {step} Whether, and at what intervals, the slider should snap to a new position. Adheres to {scale} (init)
* [MIXED] <x, FALSE
*/
'step' : false,
/*
* {save} Whether a scale give to a function should become the default for the slider it is called on. (move,value)
* [BOOLEAN] true, false
*/
'save' : false,
/*
* {click} Whether the slider moves by clicking the bar
* [BOOLEAN] true, false
*/
'click' : true
};
helpers = {
scale: function( a, b, c ){
var d = b[0],e = b[1];
if(neg(d)){
a=a+abs(d);
e=e+abs(d);
} else {
a=a-d;
e=e-d;
}
return (a*c)/e;
},
deScale: function( a, b, c ){
var d = b[0],e = b[1];
e = neg(d) ? e + abs(d) : e - d;
return ((a*e)/c) + d;
},
connect: function( api ){
if(api.connect){
if(api.handles.length>1){
api.connect.css({'left':api.low.left(),'right':(api.slider.innerWidth()-api.up.left())});
} else {
api.low ? api.connect.css({'left':api.low.left(),'right':0}) : api.connect.css({'left':0,'right':(api.slider.innerWidth()-api.up.left())});
}
}
},
left: function(){
return parseFloat($(this).css('left'));
},
call: function( f, t, n ){
if ( typeof(f) == "function" ){ f.call(t, n) }
},
bounce: function( api, n, c, handle ){
var go = false;
if( handle.is( api.up ) ){
if( api.low && n < api.low.left() ){
n = api.low.left();
go=true;
}
} else {
if( api.up && n > api.up.left() ){
n = api.up.left();
go=true;
}
}
if ( n > api.slider.innerWidth() ){
n = api.slider.innerWidth()
go=true;
} else if( n < 0 ){
n = 0;
go=true;
}
return [n,go];
}
};
methods = {
init: function(){
return this.each( function(){
/* variables */
var s, slider, api;
/* fill them */
slider = $(this).css('position','relative');
api = new Object();
api.options = $.extend( defaults, options );
s = api.options;
typeof s.start == 'object' ? 1 : s.start=[s.start];
/* Available elements */
api.slider = slider;
api.low = $('<div class="noUi-handle noUi-lowerHandle"><div></div></div>');
api.up = $('<div class="noUi-handle noUi-upperHandle"><div></div></div>');
api.connect = $('<div class="noUi-midBar"></div>');
/* Append the middle bar */
s.connect ? api.connect.appendTo(api.slider) : api.connect = false;
/* Append the handles */
// legacy rename
if(s.knobs){
s.handles=s.knobs;
}
if ( s.handles === 1 ){
/*
This always looks weird:
Connect=lower, means activate upper, because the bar connects to 0.
*/
if ( s.connect === true || s.connect === 'lower' ){
api.low = false;
api.up = api.up.appendTo(api.slider);
api.handles = [api.up];
} else if ( s.connect === 'upper' || !s.connect ) {
api.low = api.low.prependTo(api.slider);
api.up = false;
api.handles = [api.low];
}
} else {
api.low = api.low.prependTo(api.slider);
api.up = api.up.appendTo(api.slider);
api.handles = [api.low, api.up];
}
if(api.low){ api.low.left = helpers.left; }
if(api.up){ api.up.left = helpers.left; }
api.slider.children().css('position','absolute');
$.each( api.handles, function( index ){
$(this).css({
'left' : helpers.scale(s.start[index],api.options.scale,api.slider.innerWidth()),
'zIndex' : index + 1
}).children().bind(touch?'touchstart.noUi':'mousedown.noUi',functions.start);
});
if(s.click){
api.slider.click(functions.click).find('*:not(.noUi-midBar)').click(functions.flse);
}
helpers.connect(api);
/* expose */
api.options=s;
api.slider.data('api',api);
});
},
move: function(){
var api, bounce, to, handle, scale;
api = dup($(this).data('api'));
api.options = $.extend( api.options, options );
// rename legacy 'knob'
if(api.options.knob){
api.options.handle = api.options.knob;
}
// flatten out the legacy 'lower/upper' options
handle = api.options.handle;
handle = api.handles[handle == 'lower' || handle == 0 || typeof handle == 'undefined' ? 0 : 1];
bounce = helpers.bounce(api, helpers.scale(api.options.to, api.options.scale, api.slider.innerWidth()), handle.left(), handle);
handle.css('left',bounce[0]);
if( (handle.is(api.up) && handle.left() == 0) || (handle.is(api.low) && handle.left() == api.slider.innerWidth()) ){
handle.css('zIndex',parseInt(handle.css('zIndex'))+2);
}
if(options.save===true){
api.options.scale = options.scale;
$(this).data('api',api);
}
helpers.connect(api);
helpers.call(api.options.change, api.slider, 'move');
helpers.call(api.options.end, api.slider, 'move');
},
value: function(){
var val1, val2, api;
api = dup($(this).data('api'));
api.options = $.extend( api.options, options );
val1 = api.low ? Math.round(helpers.deScale(api.low.left(), api.options.scale, api.slider.innerWidth())) : false;
val2 = api.up ? Math.round(helpers.deScale(api.up.left(), api.options.scale, api.slider.innerWidth())) : false;
if(options.save){
api.options.scale = options.scale;
$(this).data('api',api);
}
return [val1,val2];
},
api: function(){
return $(this).data('api');
},
disable: function(){
return this.each( function(){
$(this).addClass('disabled');
});
},
enable: function(){
return this.each( function(){
$(this).removeClass('disabled');
});
}
},
functions = {
start: function( e ){
if(! $(this).parent().parent().hasClass('disabled') ){
e.preventDefault();
$('body').bind( 'selectstart.noUi' , functions.flse);
$(this).addClass('noUi-activeHandle');
$(document).bind(touch?'touchmove.noUi':'mousemove.noUi', functions.move);
touch?$(this).bind('touchend.noUi',functions.end):$(document).bind('mouseup.noUi', functions.end);
}
},
move: function( e ){
var a,b,h,api,go = false,handle,bounce;
h = $('.noUi-activeHandle');
api = h.parent().parent().data('api');
handle = h.parent().is(api.low) ? api.low : api.up;
a = e.pageX - Math.round( api.slider.offset().left );
// if there is no pageX on the event, it is probably touch, so get it there.
if(isNaN(a)){
a = e.originalEvent.touches[0].pageX - Math.round( api.slider.offset().left );
}
// a = p.nw == New position
// b = p.cur == Old position
b = handle.left();
bounce = helpers.bounce(api, a, b, handle);
a = bounce[0];
go = bounce[1];
if ( api.options.step && !go){
// get values from options
var v1 = api.options.scale[0], v2 = api.options.scale[1];
// convert values to [0-X>0] range
// edge case: both values negative;
if( neg(v2) ){
v2 = abs( v1 - v2 );
v1 = 0;
}
// handle all values
v2 = ( v2 + ( -1 * v1 ) );
// converts step to the new range
var con = helpers.scale( api.options.step, [0,v2], api.slider.innerWidth() );
// if the current movement is bigger than step, set to step.
if ( Math.abs( b - a ) >= con ){
a = a < b ? b-con : b+con;
go = true;
}
} else {
go = true;
}
if(a===b){
go=false;
}
if(go){
handle.css('left',a);
if( (handle.is(api.up) && handle.left() == 0) || (handle.is(api.low) && handle.left() == api.slider.innerWidth()) ){
handle.css('zIndex',parseInt(handle.css('zIndex'))+2);
}
helpers.connect(api);
helpers.call(api.options.change, api.slider, 'slide');
}
},
end: function(){
var handle, api;
handle = $('.noUi-activeHandle');
api = handle.parent().parent().data('api');
$(document).add('body').add(handle.removeClass('noUi-activeHandle').parent()).unbind('.noUi');
helpers.call(api.options.end, api.slider, 'slide');
},
click: function( e ){
if(! $(this).hasClass('disabled') ){
var api = $(this).data('api');
var s = api.options;
var c = e.pageX - api.slider.offset().left;
c = s.step ? roundTo(c,helpers.scale( s.step, s.scale, api.slider.innerWidth() )) : c;
if( api.low && api.up ){
c < ((api.low.left()+api.up.left())/2) ? api.low.css("left", c) : api.up.css("left", c);
} else {
api.handles[0].css('left',c);
}
helpers.connect(api);
helpers.call(s.change, api.slider, 'click');
helpers.call(s.end, api.slider, 'click');
}
},
flse: function(){
return false;
}
}
if ( methods[method] ) {
return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof method === 'object' || ! method ) {
return methods.init.apply( this, arguments );
} else {
$.error( 'No such method: ' + method );
}
};
})( jQuery );
可能的罪魁祸首(偏离源代码)在move: function( e )
:
a = e.pageX - Math.round( api.slider.offset().left );
// if there is no pageX on the event, it is probably touch, so get it there.
if(isNaN(a)){
a = e.originalEvent.touches[0].pageX - Math.round( api.slider.offset().left );
}
// a = p.nw == New position
假设所涉及的Math
库同样准确,并且api.slider.offset()。left在不同平台之间没有变化,我会从源代码中做一些日志记录,以确定e.pageX
/e.originalEvent.touches[0].pageX
的目的和值。
另一种可能性是滑块的左偏移量,它只是从DOM ($('.noUi-activeHandle').parent().parent().data('api').slider.offset().left
)中拉出来的,在不同平台之间的精度会有所不同。我想把它也记录下来。
两者都没有表明不准确性呈指数增长,所以可能还有其他原因。
javascript确实有舍入问题。所有数字都是内部浮点数,因此是由于浮点舍入问题。
试着使用http://silentmatt.com/biginteger/看看它是否能解决你的问题。
请查看http://blog.greweb.fr/2013/01/be-careful-with-js-numbers/获取更多详细信息。
- 浮点0.2+0.1舍入误差
- 谷歌材料图表:停止工具提示舍入
- jQuery - 带有一些数字的表格计算出现奇怪的舍入错误
- 有人可以详细解释这个 JavaScript 十进制舍入函数吗?
- 求和值时出现Javascript舍入问题
- SVG模式动画和背景淡入问题
- 在追加文本中舍入数字输出
- Infrastics网络货币编辑舍入问题
- 是否可以在 JavaScript 中实现没有舍入问题的任意精度算术
- JavaScript 中的舍入问题
- Javascript中的舍入问题
- 在 JavaScript 中向上舍入数字有问题
- JavaScript,数字舍入问题
- 修复CSS流体网格中的子像素舍入问题
- jQuery问题中的舍入数字
- 解析浮点数有舍入限制?我怎样才能解决这个问题
- 淘汰赛中的十进制值舍入问题
- Javascript:需要解决自动舍入的问题
- Javascript浮点问题-舍入问题
- 移动webkit浏览器在JS中有舍入问题吗?