问题场景
最近在用layui写一个小项目,用到了layui的table组件,并且对某一列设置了可编辑属性,如下图,鼠标点击单元格的时候,就可以进行编辑了
image.png
UI很漂亮,但是因为这是一个财务方面的应用,而且这一列又是金额,所以需要做严格的输入验证,要求只能够输入数字和点。因为我没找到layui提供的属性,方法,那么只能自己给input绑定键盘按下事件,做验证了。相关单元格dom结构如下
<td data-field="initialBalance" data-edit="true" align="right">
<div class="layui-table-cell laytable-cell-1-initialBalance">0</div>
<!--<input class="layui-input layui-table-edit">-->
</td>
那么,为什么可以实现可编辑呢?当单击该单元格的时候,会动态的在td中创建input。我猜单元格必有click事件,所以在github上找到layui项目的table模块,通过搜索click关键字,查看相应源码,可以验证layui的确是这么做的,源码如下。
image.png
因为input是动态生成的,所以我们无法直接给input绑定事件。
setTimeout登场解围
既然input只有在单击单元格之后才出现,那么我先触发了其单击事件,然后在事件处理程序中,再给它绑定事件不就行了嘛。就像下面这样
$(".layui-table-body .laytable-cell-1-initialBalance").click(function(){
that.next()[0].onkeyup=function(){
this.value=this.value.replace(/[^1-9|.]/g,'');
};
});
但很不幸,这不work。我猜是因为input创建需要时间,就强制给键盘按下事件加了个setTimeout(fn,1000),想着等一段时间再去给input绑定事件,那肯定可以了。
$(".layui-table-body .laytable-cell-1-initialBalance").click(function(){
setTimeout(function(){
that.next()[0].onkeyup=function(){
this.value=this.value.replace(/[^1-9|.]/g,'');
};
},1000)
});
设置了定时器之后,的确起作用了。我想等待1000毫秒是不是有点长,就改成了100,10,发现都可以,最后我所幸改成了0,让我意外的是,改成0之后,仍然有效,但是去掉setTimeout就是不行!我想肯定我对setTimeout的理解有偏差,看来setTimeout不是简简单单的等待一段时间后执行fn那么简单。不查不要紧,一查才发现,setTimeout水深着呢。
setTimeout揭秘
看了阮一峰的博客之后,我大概明白了怎么回事。我的知识盲区在于js代码执行顺序上,js是一门单线程语言,任一时刻,同时只能做一件事,但是这并不代表js里不可以进行异步操作。起码事件处理程序是异步的吧,只有当我们触发相应的事件时,事件处理程序才会被执行,再或者Ajax我们都了解吧,Ajax请求时,后面的代码并不会等待网络请求成功之后才去执行,所以Ajax请求也是异步的。那么我们有必要了解一下代码执行顺序到底是怎样的,setTimeout又会对执行顺序产生什么影响。
js引擎(浏览器)将任务分为同步任务和异步任务。同步任务就是按顺序执行,上一个任务不完成,下一个任务就无法进行,同步任务在主线程上排队运行,是线程阻塞的,而异步任务则处于“任务队列中”,不会阻塞线程。
js运行机制的简单理解
程序开始执行之后,主程序则开始执行同步任务,一个接着一个,碰到异步任务就把它放到任务队列中,如事件处理程序,然后继续执行下面的代码,等到同步任务全部执行完毕之后,js引擎便去查看任务队列有没有可以执行的异步任务,比如看看有没有事件被触发,网络请求有没有结束,有的话,将异步任务转为同步任务,开始执行,执行完同步任务之后继续查看任务队列,这个过程是一直循环的,因此这个过程就是所谓的事件循环,其中任务队列也被称为事件队列。通过一个任务队列,单线程的js实现了异步任务的执行,给人感觉js好像是多线程的。
setTimeout工作机制(setInterval机制与其一致)
以前我对setTimeout的理解就是,先把其他代码全部执行完之后,等待一定延长时间,然后执行setTimeout的回调函数。所以当setTimeout延迟时间设置为0就意外着执行到setTimeout的时候,立刻执行其回调函数,然后再执行setTimeout后面的代码。
但setTimeout是异步的,不管你延迟时间是多少,执行到setTimeout的时候,它便会将其回调函数放入任务队列队尾,也就是说即使延迟时间为0,它也会继续执行setTimeout后面的代码,而非立即执行回调函数。我们可以做个实验。
控制台,永远打印的是a,c,b而非a,b,c.
setTimeout将其回调函数放入任务队列,所以只有同步任务执行完成,才有机会去执行该任务。它的延迟时间从什么时候开始算呢?从所有的同步任务执行完成之后,再处理完队列中处于该任务前面的任务后(队列先进先出)。延迟设置为0表示,同步任务执行完成之后,并且队列中它前面没有要处理的任务了,立刻执行该异步任务,设置为1000表示同步任务结束1秒后,并且队列中它前面没有要处理的任务了,再执行该异步任务。所以此代码到底多长时间之后执行,并不是完全确定的,还取决于同步任务的执行时间和任务队列中前面任务的执行时间。
用任务队列解释开头的问题
查阅了好多博客,得出了自己的理解,不知道自己的理解是否正确,所以试着用后面的理论去验证一下开头提出的问题。
先考虑不用setTimeout的时候
1.js引擎从前到后执行,执行到td的单击事件的时候,将其放入任务队列,执行到td标签里的div的单击事件时,也将其放入任务队列,然后继续执行后面的代码,直到同步任务结束。
2.js引擎检查任务队列,当我们单击了该div,触发了绑定在该div的上的单击事件处理程序,所以此时开始将处理程序转为同步任务,但此时无法给input绑定事件,因为input的动态生成在td的单击事件处理程序里,根据事件冒泡顺序,此时还没有触发td的单击事件,所以input还没有生成,所以无法绑定事件。
3.当同步任务执行完成,继续检查任务队列,这个时候单击事件从div冒泡冒到了td,触发了td单击事件,如上一步,相应的代码转入执行栈开始执行,生成input,程序执行完毕。
也就是说,不写setTimeout的结果就是先给input绑定事件,然后才生成了input,当然没效果了。那么我们加了setTimeout(fn,0)再分析一下这个过程。
1.js引擎执行同步任务,执行到td的单击事件的时候,将其放入任务队列,执行到td标签里的div的单击事件时,也将其放入任务队列,然后继续执行后面的代码,直到执行完成同步任务。
2.检查任务队列,当点击div的时候,触发了div的单击事件处理程序,然后主线程开始执行,执行到setTimeout(function(){ input.onkeyup=...;},0)时候,js引擎将此任务插入到任务队列尾部,并设置延迟时间为0。同步任务执行结束。
3.js引擎继续检查任务队列,此时触发了td的单击处理程序,因为td的单击事件在定时器之前放进了任务队列,所以先处理该任务,生成了input,同步任务结束。
4.js引擎在任务队列中开始处理定时器这个任务,因为延迟时间为0,所以立刻执行,因为此时input已经生成了,所以可以成功为其绑定事件。
其它
其实在查阅资料的过程中,我了解到jq中用on()方法就可以实现给动态生成的dom元素绑定事件,on函数,第二个参数设一个选择器,就可以实现此需求。
如
$("#nav").on('click','li',function(){
...
})
以前一直不知道on和bind有什么区别,都是混用,今天才发现on的强大,除了可以给动态生成的dom元素添加事件,on的选择器还可以起到过滤事件的作用,如给父元素绑定事件,之后触发选择器元素的事件,而不会触发父元素的事件。
写了很久,理解了很久,边写边改,如有错误之处,还请大家指出。
参考博客:JavaScript 运行机制详解,阮一峰老师的博客写更加清楚更加全面,需要了解的可以自行查看。
网友评论