土豆爱鸡蛋

土豆爱鸡蛋 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

整理控,照片控,爱好前端。

个人动态

土豆爱鸡蛋 赞了文章 · 2月18日

精确并自动化地获取页面首屏时间

如何自动获取首屏时间

作者:刘远洋

公司:微店 - 前端团队

日期:2018-03-05

本文发表在 微店前端团队 blog

背景

在前端性能数据的获取方法上,现在业内大多使用手动埋点的方式,即在代码中,人工判断首屏完成的位置,并在该处添加首屏记录的代码,类似:firstscreen.report() 这样。

这样做的简单省事,但缺点也很明显:

  • 和业务代码混用

    通用的监控需求混入了业务代码中

  • 覆盖不完整

    需要页面开发者自觉手动添加埋点代码,在业务中埋点覆盖率不一定能达到 100%

  • 准确性不一定高

    由于需要开发者自行判断统计脚本放置的位置,就会存在一些不准确的情况,因为每个人对首屏的理解不同

基于上面的分析,我们近期尝试了一些方案,试图将首屏时间计算自动化,节省人力、并提高准确性。

定义

对首屏时间的定义,每个公司可能会有所不同,在本文中,首屏时间指的是:

  • 如果页面首屏有图片

    首屏时间 = 首屏图片全部加载完毕的时刻 - window.performance.timing.navigationStart
  • 如果页面首屏没有图片

    首屏时间 = 页面处于稳定状态前最后一次 dom 变化的时刻 - window.performance.timing.navigationStart

实现原理

总体思路为:

  • 从页面加载开始,按照一定的间隔打点,不断记录各个时刻下页面首屏图片列表和其他信息

    问题:按照怎样的间隔打点?

  • 找出页面首屏处于稳定状态的时刻 T1(到这个时刻为止,页面首屏可能已经稳定了一段时间)

    问题:如何找出这个 T1?

  • 以 T1 时刻的首屏图片数量为准,向前倒推,找到所有打点中最后一次和 T1 时刻首屏图片一致的打点时刻 T2
  • 统计 T2 时刻的所有图片加载完成时间 T3
  • T3 即为首屏完成的时刻,进行上报

下面,一个个解决上文中提到的问题:

  • 问题:如何找出首屏处于稳定状态的时刻 T1?

    我们将页面从加载到渲染分为两大阶段:1. 获取数据;2. 数据获取完毕,渲染页面。

    这个逻辑符合绝大部分的页面逻辑:先获取数据,再渲染页面。

    解决方案:

    1. 通过 AOP 切面方式监听 XHR 的 send 对象,抓取页面中的第一个 XHR 请求,以第一个 XHR 请求发出的时刻为起点,统计在 1000ms 以内所有发出的请求到数组 Request 中。

      我们认为可能影响首屏的请求在 [第一个 xhr 请求发出的时刻,第一个 xhr 请求发出的时刻 + 1000ms] 的时间段内均已发出。

    2. 针对串联型的请求(即下一个请求依赖上一个请求的返回数据),同时统计每个请求返回后,500ms 以内新发出的请求到数组 Request 中。

      有些页面的数据请求方式是串行的,可能经过两个串联的请求后首屏的数据才能加载。

      影响首屏的请求可能也会以这样的形式发出。

    3. 数组 Request 中统计到的请求,基本包含了所有影响首屏的数据请求,同时也包含了部分不影响首屏的数据请求。
    4. 针对上述统计到的请求,找到所有数据返回的时刻 T1,然后,T1 = T1 + 300ms,保证页面接收数据后渲染完毕(300ms 用于一次渲染足够了)。
    5. 此时的 T1 时刻,页面首屏被认为处于稳定状态。
  • 问题:按照怎样的间隔打点?

    • MutationObserver

      大家都知道 MutationObserver 对象用于捕捉页面 dom 变化,因此在脚本中,我们使用了 MutationObserver 监听 dom 变化,并在每次 dom 变化时触发一次打点(统计该时刻首屏图片信息)

    • setInterval

      setInterval 也能实现定时打点

    • MutationObserver 和 setInterval 组合

      但 MutationObserver 回调函数的触发时机开发者并不可控,有几种情况:

      • 两次回调之间可能距离几百毫秒甚至 1秒多,导致统计误差较大
      • 某些情况下,dom 不再变化,但页面元素中,imgsrc 发生了变化或元素的 background-image 发生了变化,并不会触发在 MutationObserver 的回调,导致统计失误

        因此,我们现在的方案是结合 MutationObserver 和 setInterval,在 MutationObserver 回调的间歇,启动 setInterval,保证页面加载过程中打点间隔不会过长,提高统计准确率。

统计误差

即使使用了上述复杂的打点与判断,误差仍然存在,那么,误差到底在哪里?

如下图所示:

不稳定状态(1 images)   稳定状态2(2 images)      稳定状态1(2 images)
    |                        |                       |
    |________________________|_______________________|
    t1                       t2                      t3

按照上面的理论,我们会取 t2 时刻为可以统计首屏的时刻,两张图片加载完成的时刻即为首屏完成的时刻。

t2t1 时刻差了 1 张图片。

按照我们的理论,首屏完成时间一定在 t2 之后的某个时刻 t2.n

而实际相差的那张图片,什么时候加载完成的,我们不得而知,可能在 t2 前已经加载完毕了,也可能已经发出请求,但还没加载完毕。

误差就在这里,它总会存在。

但我们需要统计的是在误差可以接受范围内的首屏数据,根据在公司业务实践的反馈来看,数据可靠性很高。

Talk is cheap, show me the code

我们也开源了这个小工具:

github: auto-compute-first-screen-time

npm: auto-compute-first-screen-time

欢迎小伙伴们使用,吐槽,改进。

查看原文

赞 7 收藏 9 评论 2

土豆爱鸡蛋 提出了问题 · 2018-11-19

为什么 iOS 中 wxs 会比 js 快 2-20 倍?

为什么 iOS 中 wxs 会比 js 快 2-20 倍?wxs 背后的原理是什么?

关注 3 回答 2

土豆爱鸡蛋 赞了问题 · 2018-10-26

解决微信小程序中遮罩层的滚动穿透问题

图片描述

使用小程序的modal组件实现遮罩层效果时,会出现滚动穿透的问题,即遮罩层后面的页面依旧可以滚动,这个问题有解决办法吗?

关注 17 回答 10

土豆爱鸡蛋 收藏了问题 · 2018-10-26

微信小程序中遮罩层的滚动穿透问题

图片描述

使用小程序的modal组件实现遮罩层效果时,会出现滚动穿透的问题,即遮罩层后面的页面依旧可以滚动,这个问题有解决办法吗?

土豆爱鸡蛋 收藏了文章 · 2018-07-26

javascript 哈希表

其实javascript的对象就是一个哈希表,为了学习真正的数据结构,我们还是有必要自己重新实现一下。

基本概念

哈希表(hash table )是一种根据关键字直接访问内存存储位置的数据结构,通过哈希表,数据元素的存放位置和数据元素的关键字之间建立起某种对应关系,建立这种对应关系的函数称为哈希函数

clipboard.png

哈希表的构造方法

假设要存储的数据元素个数是n,设置一个长度为m(m > n)的连续存储单元,分别以每个数据元素的关键字Ki(0<=i<=n-1)为自变量,通过哈希函数hash(Ki),把Ki映射为内存单元的某个地址hash(Ki),并将数据元素存储在内存单元中

从数学的角度看,哈希函数实际上是关键字到内存单元的映射,因此我们希望通过哈希函数通过尽量简单的运算使得哈希函数计算出的花溪地址尽量均匀的背影射到一系列的内存单元中,构造哈希函数有三个要点:(1)运算过程要尽量简单高效,以提高哈希表的插入和检索效率;(2)哈希函数应该具有较好的散列型,以降低哈希冲突的概率;第三,哈希函数应具有较大的压缩性,以节省内存。

以下有三种常用方法:

  • 直接地址法:以关键字的某个线性函数值为哈希地址,可以表示为hash(K)=aK+C;优点是不会产生冲突,缺点是空间复杂度可能会较高,适用于元素较少的情况
  • 除留余数法:它是由数据元素关键字除以某个常数所留的余数为哈希地址,该方法计算简单,适用范围广,是经常使用的一种哈希函数,可以表示为:
hash(K=K mod C;该方法的关键是常数的选取,一般要求是接近或等于哈希表本身的长度,研究理论表明,该常数选素数时效果最好
  • 数字分析法:该方法是取数据元素关键字中某些取值较均匀的数字来作为哈希地址的方法,这样可以尽量避免冲突,但是该方法只适合于所有关键字已知的情况,对于想要设计出更加通用的哈希表并不适用
  • 平方求和法:对当前字串转化为Unicode值,并求出这个值的平方,去平方值中间的几位为当前数字的hash值,具体取几位要取决于当前哈希表的大小。
  • 分段求和法:根据当前哈希表的位数把所要插入的数值分成若干段,把若干段进行相加,舍去调最高位结果就是这个值的哈希值。

哈希冲突的解决方案

在构造哈希表时,存在这样的问题:对于两个不同的关键字,通过我们的哈希函数计算哈希地址时却得到了相同的哈希地址,我们将这种现象称为哈希冲突

clipboard.png

哈希冲突主要与两个因素有关,(1)填装因子,填装因子是指哈希表中已存入的数据元素个数与哈希地址空间的大小的比值,a=n/m ; a越小,冲突的可能性就越小,相反则冲突可能性较大;但是a越小空间利用率也就越小,a越大,空间利用率越高,为了兼顾哈希冲突和存储空间利用率,通常将a控制在0.6-0.9之间,而.net中的HashTable则直接将a的最大值定义为0.72 (虽然微软官方MSDN中声明HashTable默认填装因子为1.0,但实际上都是0.72的倍数),(2)与所用的哈希函数有关,如果哈希函数得当,就可以使哈希地址尽可能的均匀分布在哈希地址空间上,从而减少冲突的产生,但一个良好的哈希函数的得来很大程度上取决于大量的实践,不过幸好前人已经总结实践了很多高效的哈希函数,可以参考大神Lucifer文章:数据结构:HahTable: http://www.cnblogs.com/lucife...

1)开放定址法

Hi=(H(key) + di) MOD m i=1,2,...k(k<=m-1)
其中H(key)为哈希函数;m为哈希表表长;di为增量序列。有3中增量序列:
1)线性探测再散列:di=1,2,3,...,m-1
2)二次探测再散列:di=1^2,-1^2,2^2,-2^2,....+-k^2(k<=m/2)
3)伪随机探测再散列:di=伪随机数序列

缺点:

我们可以看到一个现象:当表中i,i+1,i+2位置上已经填有记录时,下一个哈希地址为i,i+1,i+2和i+3的记录都将填入i+3的位置,这种在处理冲突过程中发生的两个第一个哈希地址不同的记录争夺同一个后继哈希地址的现象称为“二次聚集”,即在处理同义词的冲突过程中又添加了非同义词的冲突。但另一方面,用线性探测再散列处理冲突可以保证做到:只要哈希表未填满,总能找到一个不发生冲突的地址Hk。而二次探测再散列只有在哈希表长m为形如4j+3(j为整数)的素数时才可能。即开放定址法会造成二次聚集的现象,对查找不利。

clipboard.png

2)再哈希法
Hi = RHi(key),i=1,2,...k RHi均是不同的哈希函数,即在同义词产生地址冲突时计算另一个哈希函数地址,直到不发生冲突为止。这种方法不易产生聚集,但是增加了计算时间。

缺点:增加了计算时间。

3)链地址法(拉链法)

将所有关键字为同义词的记录存储在同一线性链表中。

优点:

①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点

缺点:
拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度

clipboard.png

4)建立一个公共溢出区
假设哈希函数的值域为[0,m-1],则设向量HashTable[0...m-1]为基本表,每个分量存放一个记录,另设立向量OverTable[0....v]为溢出表。所有关键字和基本表中关键字为同义词的记录,不管他们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。

一个简单哈希函数不做冲突处理的哈希表实现

// by 司徒正美
class Hash{
    constructor(){
        this.table = new Array(1024);
    }
    hash(data) {
    //就将字符串中的每个字符的ASCLL码值相加起来,再对数组的长度取余
        var total = 0;
        for(var i = 0; i < data.length; i++) {
            total += data.charCodeAt(i);
        }
        console.log("Hash Value: " +data+ " -> " +total);
        return total % this.table.length;
    }
    insert(key, val){
        var pos = this.hash(key);
        this.table[pos] = val;
    }
    get(key){
        var pos = this.hash(key);
        return this.table[pos] 
    }
    show(){
        for(var i = 0; i < this.table.length; i++) {
            if(this.table[i] != undefined) {
                console.log(i + ":" +this.table[i]);
            }
        }
    }
    }
    var someNames = ["David","Jennifer","Donnie","Raymond","Cynthia","Mike","Clayton","Danny","Jonathan"];
    var hash = new Hash();
    for(var i = 0; i < someNames.length; ++i) {
    hash.insert(someNames[i],someNames[i]);
    }
    
    hash.show(); 

clipboard.png

采用的是平方取中法构建哈希函数,开放地址法线性探测法进行解决冲突。

class Hash{
    constructor(){
        this.table = new Array(1000);
    }
    hash(data) {
        var total = 0;
        for(var i = 0; i < data.length; i++) {
            total += data.charCodeAt(i);
        }
            //把字符串转化为字符用来求和之后进行平方运算
        var s = total * total + ""
            //保留中间2位
        var index = s.charAt( s.length/2 -1) *10 + s.charAt( s.length/2  ) * 1
        console.log("Hash Value: " +data+ " -> " +index);
        return index;
    }
    solveClash(index, value){
        var table = this.table
        //进行线性开放地址法解决冲突
        for(var i=0; index+i<1000;i++){
            if(table[index+i] == null){
                table[index+i] = value;
                break;
            }
        }
    }
    insert(key, val){
        var index = this.hash(key);
        //把取中当做哈希表中索引
        if(this.table[index] == null){
            this.table[index] = val;
        }else{
            this.solveClash(index, val);
        }
    }
    get(key){
        var pos = this.hash(key);
        return this.table[pos] 
    }
    show(){
        for(var i = 0; i < this.table.length; i++) {
            if(this.table[i] != undefined) {
                console.log(i + ":" +this.table[i]);
            }
        }
    }
}
var someNames = ["David","Jennifer","Donnie","Raymond","Cynthia","Mike","Clayton","Danny","Jonathan"];
var hash = new Hash();
for(var i = 0; i < someNames.length; ++i) {
    hash.insert(someNames[i],someNames[i]);
}

hash.show(); 

clipboard.png

几种常见的hash函数

DJBHash

unsigned int DJBHash(char *str)    
{    
    unsigned int hash = 5381;    
     
    while (*str){    
        hash = ((hash << 5) + hash) + (*str++); /* times 33 */    
    }    
    hash &= ~(1 << 31); /* strip the highest bit */    
    return hash;    
}

javascript版

function DJBHash(str)    {    
    var hash = 5381;   
    var len = str.length , i = 0
     
    while (len--){    
        hash = (hash << 5) + hash + str.charCodeAt(i++); /* times 33 */    
    }    
    hash &= ~(1 << 31); /* strip the highest bit */    
    return hash;    
}

JS

Justin Sobel写的一个位操作的哈希函数。
原版

public long JSHash(String str)  
   {  
      long hash = 1315423911;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash ^= ((hash << 5) + str.charAt(i) + (hash >> 2));  
      }  
      return hash;  
   }  

javascript版

function JSHash(str)  {  
      var hash = 1315423911;  
      for(var i = 0; i < str.length; i++)  {  
         hash ^= ((hash << 5) + str.charCodeAt(i) + (hash >> 2));  
      }  
      return hash;  
}  

PJW

该散列算法是基于贝尔实验室的彼得J温伯格的的研究。在Compilers一书中(原则,技术和工具),建议采用这个算法的散列函数的哈希方法。

public long PJWHash(String str)  
   {  
      long BitsInUnsignedInt = (long)(4 * 8);  
      long ThreeQuarters     = (long)((BitsInUnsignedInt  * 3) / 4);  
      long OneEighth         = (long)(BitsInUnsignedInt / 8);  
      long HighBits          = (long)(0xFFFFFFFF) << (BitsInUnsignedInt - OneEighth);  
      long hash              = 0;  
      long test              = 0;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = (hash << OneEighth) + str.charAt(i);  
         if((test = hash & HighBits)  != 0)  
         {  
            hash = (( hash ^ (test >> ThreeQuarters)) & (~HighBits));  
         }  
      }  
      return hash;  
   }  

javascript版

function PJWHash( str)  {  
      var BitsInUnsignedInt = 4 * 8;  
      var ThreeQuarters     =  (BitsInUnsignedInt  * 3) / 4;  
      var OneEighth         = (BitsInUnsignedInt / 8);  
      var HighBits          = (0xFFFFFFFF) << (BitsInUnsignedInt - OneEighth);  
      var hash              = 0;  
      var test              = 0;  
      for(var i = 0; i < str.length; i++)  {  
         hash = (hash << OneEighth) + str.charCodeAt(i);  
         if((test = hash & HighBits)  != 0)  
         {  
            hash = (( hash ^ (test >> ThreeQuarters)) & (~HighBits));  
         }  
      }  
      return hash;  
} 

如果将上面的哈表的hash函数改成这个,打印如下:

clipboard.png

性能会大幅下隆,因为这让我们的table数组表得非常庞大。

ELF

和PJW很相似,在Unix系统中使用的较多。

public long ELFHash(String str)  
   {  
      long hash = 0;  
      long x    = 0;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = (hash << 4) + str.charAt(i);  
         if((x = hash & 0xF0000000L) != 0)  
         {  
            hash ^= (x >> 24);  
         }  
         hash &= ~x;  
      }  
      return hash;  
   }  

BKDR

这个算法来自Brian Kernighan 和 Dennis Ritchie的 The C Programming Language。这是一个很简单的哈希算法,使用了一系列奇怪的数字,形式如31,3131,31...31,看上去和DJB算法很相似。

public long BKDRHash(String str)  
   {  
      long seed = 131; // 31 131 1313 13131 131313 etc..  
      long hash = 0;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = (hash * seed) + str.charAt(i);  
      }  
      return hash;  
   }  

SDBM

这个算法在开源的SDBM中使用,似乎对很多不同类型的数据都能得到不错的分布。

public long SDBMHash(String str)  
   {  
      long hash = 0;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = str.charAt(i) + (hash << 6) + (hash << 16) - hash;  
      }  
      return hash;  
   }  

DJB

这个算法是Daniel J.Bernstein 教授发明的,是目前公布的最有效的哈希函数。

public long DJBHash(String str)  
   {  
      long hash = 5381;  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = ((hash << 5) + hash) + str.charAt(i);  
      }  
      return hash;  
   }  

DEK

由伟大的Knuth在《编程的艺术 第三卷》的第六章排序和搜索中给出。

public long DEKHash(String str)  
   {  
      long hash = str.length();  
      for(int i = 0; i < str.length(); i++)  
      {  
         hash = ((hash << 5) ^ (hash >> 27)) ^ str.charAt(i);  
      }  
      return hash;  
   }  

AP

由Arash Partow贡献的一个哈希函数,继承了上面以旋转以为和加操作

public long APHash(String str)  
{  
      long hash = 0xAAAAAAAA;  
      for(int i = 0; i < str.length(); i++)  
      {  
         if ((i & 1) == 0)  
         {  
            hash ^= ((hash << 7) ^ str.charAt(i) * (hash >> 3));  
         }  
         else  
         {  
            hash ^= (~((hash << 11) + str.charAt(i) ^ (hash >> 5)));  
         }  
      }  
      return hash;  
   }   

clipboard.png
其中数据1为100000个字母和数字组成的随机串哈希冲突个数。数据2为100000个有意义的英文句子哈希冲突个数。数据3为数据1的哈希值与 1000003(大素数)求模后存储到线性表中冲突的个数。数据4为数据1的哈希值与10000019(更大素数)求模后存储到线性表中冲突的个数。

经过比较,得出以上平均得分。平均数为平方平均数。可以发现,BKDRHash无论是在实际效果还是编码实现中,效果都是最突出的。APHash也是较为优秀的算法。DJBHash,JSHash,RSHash与SDBMHash各有千秋。PJWHash与ELFHash效果最差,但得分相似,其算法本质是相似的。

查看原文

土豆爱鸡蛋 收藏了文章 · 2018-04-23

小程序坑-canvas

canvas中单位问题

在canvas中绘制的单位都是px,但由于不同屏幕的像素比不同,在小程序中样式我们使用的单位是rpx,所以在canvas中就需要把rpx换成对应的px;由于rpx可以根据屏幕宽度进行自适应,规定屏幕宽为750rpx,所以rpx换算成px的公式是:
1rpx = 屏幕宽度 / 750
这一点在小程序的官方文档也有讲到:https://mp.weixin.qq.com/debu...
屏幕宽度可以使用wx.getSystemInfoSync();获取;
所以例如在样式中你的canvas宽度650rpx,那么在canvas中绘制使用的宽度就是:(屏幕宽度 / 750)* 650 ;

如何在canvas上弹窗

由于canvas组件是小程序创建的原生组件,它的层级是最高的,其他不是原生的组件都没法盖住它,但有些使用我们要必须在上面弹窗,那这时怎么办呢???

解决办法:

在弹窗时将canvas转换成图片并隐藏,使用image标签代替canvas,这样弹窗就可以盖在上面啦!!!
使用wx.canvasToTempFilePath将canvas临时转为图片(https://mp.weixin.qq.com/debu...
这里要注意一个问题,参数中的width、height等等单位都是px,需要使用上面将的方式转换。

如何划一条流畅的曲线

图片描述 修改之前
图片描述 修改之后
如果我们像将一条折线变得流畅应该怎么做呢?
这里涉及到1. 头尾的圆滑 2. 折线处的平顺;

  1. 头尾的圆滑:ctx.setLineCap('round')
  2. 折线处的平顺:ctx.setLineJoin('round')
    两个api的文档说明:

https://mp.weixin.qq.com/debu...
https://mp.weixin.qq.com/debu...

如何划虚线

由于小程序划虚线的API需要基础库1.6.0才开始支持,所以需要自己实现

    /**
     * 
     * @param {*} context canvas上下文
     * @param {*} x1 起点x
     * @param {*} y1 起点y
     * @param {*} x2 终点x
     * @param {*} y2 终点y
     * @param {*} dashLen 虚线每段的长度 
     */
    drawDashLine(context, x1, y1, x2, y2, dashLen) {  
        const getBeveling = Math.sqrt(Math.pow(x,2)+Math.pow(y,2));
        dashLen = dashLen === undefined ? 5 : dashLen;  
        //得到斜边的总长度  
        var beveling = getBeveling(x2-x1,y2-y1);  
        //计算有多少个线段  
        var num = Math.floor(beveling/dashLen);  
          
        for(var i = 0 ; i < num; i++)  
        {  
            context[i%2 == 0 ? 'moveTo' : 'lineTo'](x1+(x2-x1)/num*i,y1+(y2-y1)/num*i);  
        }  
        context.stroke();  
    }, 
查看原文

土豆爱鸡蛋 收藏了文章 · 2018-04-22

avalon js实现仿google plus图片多张拖动排序

效果

google plus
拖动效果
拖动+响应式效果:http://v.youku.com/v_show/id_XMTM0MjQyMTI0OA==.html

要求

  1. 两边对齐布局,即图片间间距一致,但左右两边的图片与边界的间距不一定等于图片间间距,兼容ie7,8,firefox,chrome.

  2. 浏览器尺寸变化,在大于一定尺寸时,每行自动增加或减少图片,自动调整图片间间距,以满足两边对齐布局,这时每张图片尺寸固定(这里是200*200px);而小于一定尺寸时,每行图片数量固定(这里最小列数是3),这时图片总是等比例拉伸或缩放。

  3. 浏览器不同尺寸下,仍然可以拖动排序。

  4. 图片,拖动代理里的图片始终保持等比例且水平垂直居中。

  5. 拖动到相应位置时,位置左右的图片发生一定偏移。如果在最左边或最右边,则只是该行的第一张图片或最后一张图片发生偏移。

  6. 支持多张图片拖动排序。

实现

布局及css

    <div id='wrap'>
        <ul class='justify'>
            <li>
                <a href="javascript:;" class="no_selected"></a>
                <div class='photo_mask'></div>
                <div>
                    <div class="dummy"></div>
                    <p><img><i></i></p>
                </div>
            </li>
            <li class='justify_fix'></li>
        </ul>
    </div>

inline-block+flex-box+text-align:justify

这里要兼容低版本浏览器,所以列表li布局用的是inline-block.而两边对齐布局
-低版本:inline-block+text-align:justify
-现代:inline-block+flex-box
具体参见本屌的模拟flexbox justify-content的space-between
这里没有用flex-box的align-content:space-around是因为无法通过text-align:justify兼容低版本浏览器。
text-align:justify无法让最左边,最右边文字自动调整与box边的间距。即使在外面box添加padidng,比如:

li{
    margin:0 1%;
    ...
}
#wrap{
    padding:0 1%;
}

看起来好像是最左边,最右边与box边界的间距和li之间的间距一样,都是2%了。实际上,外面box设置的padding是永远不会变的,而li之间的margin是它们之间间距的最小值。如果所有li之间的间距都是1%,这时,一行上仍然有多余的空白,这些li会把空白均分了,这时它们之间的间距会大于1%.
具体的实现

li{
    list-style-type: none;
    display:inline-block;
    *display: inline;
    zoom:1;
    max-width: 200px;
    max-height: 200px;
    width: 28%;
    border:1px solid red;
    position: relative;
    overflow: hidden;
    margin:10px 2%;
}
li[class='justify_fix']{
    border:none;
}
.justify {
    display: flex;
    align-items: flex-start;
    flex-flow: row wrap;
    justify-content: space-between;
    text-align: justify;
    text-justify: inter-ideograph;
    *zoom: 1; 
    -moz-text-align-last: justify;
    -webkit-text-align-last: justify;
    text-align-last: justify;
}
@media (-webkit-min-device-pixel-ratio:0) {
 .justify:after {
        content: "";
        display: inline-block;
        width: 100%;
    }
}

这里要加上max-width,max-height.后面可以看到单元格里面都是百分比,需要在外面限定最大尺寸。

图片响应式+水平垂直居中

具体参见本屌的css图片响应式+垂直水平居中
简单说,就是

  • 添加一个“多余”的div,padding-top: 100%,使得整个box响应式并且宽高比始终是1.

  • 如果不考虑ie7,直接图片

img{
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    position:absolute;
    margin: auto;
    padding: auto;
}
  • 如果考虑ie7,

<p><img><i></i></p>

将上一点img样式添加到这里的p,然后

p{
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    position:absolute;
    margin: auto;
    padding: auto;
}
img{
    display: inline-block;
    *display: inline;
    zoom:1;
    vertical-align: middle;
}
i{
    display: inline-block;
    *display: inline;
    zoom:1;
    vertical-align: middle;
    height:100%;
}
  • 图片响应式

img{
    max-width: 100%;
    max-height: 100%;
}

选中图片

google plus是按住ctrl,点击图片,完成多选,这里是点击"方框"(这里的<a class='no_selected'></a>)。
点击后,把当前图片的index传给保存选中图片index的数组(这里的selected_index)。如果该index不存在,则添加;已存在,则删除。而"方框"此时根据数组中是否存在该index调整样式。

    <div id='wrap' ms-controller='photo_sort'>
        <ul class='justify'>
            <li ms-repeat='photo_list'>
                <a href="javascript:;" class="no_selected" 
                ms-class-selected_icon='selected_photo.indexOf(el.src)>-1' 
                ms-click='select($index)'></a>
                ...
            </li>
            <li class='justify_fix'></li>
        </ul>
    </div>
var photo_sort=avalon.define({
    selected_index:[],//选中图片的index列表,
    ...
    select:function(i){
        var selected_index=photo_sort.selected_index,
        selected_photo=photo_sort.selected_photo,//存储选中图片的名字(id)
        photo=photo_sort.photo_list[i].$model.src;//这里以图片的src为标志
        if(selected_photo.indexOf(photo)==-1){//选中图片的index列表不存在,添加
            selected_index.ensure(i);
            selected_photo.ensure(photo);
        }else{
            selected_index.remove(i);
            selected_photo.remove(photo);
        }
    }
});

图片的选中状态必须用selected_photo.indexOf(photo)==-1判断,最后会解释为什么这样做.

mousedown

这里用了遮罩层,并在上面绑定mousedown事件。

<a href="javascript:;" class="no_selected" ms-class-selected_icon='selected_photo.indexOf(el.src)>-1' 
ms-click='select($index)'></a>
<div class='photo_mask' ms-mousedown='start_drag($event,$index)'></div>
...
        var photo_sort=avalon.define({
            $id:'photo_sort',
            photo_list:[],//图片列表
            selected_photo:[],//选中图片的id列表
            selected_index:[],//选中图片的index列表
            drag_flag:false,
            sort_array:[],//范围列表,
            cell_size:0,//每个单元格尺寸,这里宽高比为1
            target_index:-1,//最终目标位置的index
            col_num:0,//列数
            x_index:-1,//当前拖动位置的x方向index
            ...
        });
start_drag:function(e,index){
    if(photo_sort.selected_index.size()){//有选中的图片
        photo_sort.target_index=index;//避免用户没有拖动图片,但点击了图片,设置默认目标即当前点击图片
        photo_sort.cell_size=this.clientWidth;
        var xx=e.clientX-photo_sort.cell_size/2,yy=e.clientY-photo_sort.cell_size/2;//点下图片,设置代理位置以点击点为中心
        $('drag_proxy').style.top=yy+avalon(window).scrollTop()+'px';
        $('drag_proxy').style.left=xx+'px';
        $('drag_proxy').style.width=photo_sort.cell_size+'px';
        $('drag_proxy').style.height=photo_sort.cell_size+'px';
        drag_proxy.select_num=photo_sort.selected_index.length;//设置代理中选择图片的数量
        if(drag_proxy.select_num>0){
            var drag_img=photo_sort.photo_list[photo_sort.selected_index[drag_proxy.select_num-1]];
            drag_proxy.data-original=drag_img.src;//将选中的图片中最后一张作为代理对象的"封面"
            photo_sort.drag_flag=true;
            $('drag_proxy').style.display='block';
        }
        //cell_gap:图片间间距,first_gap:第一张图片和外部div间间距
        var wrap_width=avalon($('wrap')).width(),wrap_offset=$('wrap').offsetLeft,first_left=$('wrap_photo0').offsetLeft,
        second_left=$('wrap_photo1').offsetLeft,first_gap=first_left-wrap_offset,cell_gap=second_left-first_left;
        photo_sort.col_num=Math.round((wrap_width-2*first_gap+(cell_gap-photo_sort.cell_size))/cell_gap);
        for(var i=0;i<photo_sort.col_num;i++)//把一行图片里的每张图片中心坐标x方向的值作为分割点,添加到范围列表
            photo_sort.sort_array.push(first_gap+cell_gap*i+photo_sort.cell_size/2);
        var target=this.parentNode;
        avalon.bind(document,'mouseup',function(e){
            onMouseUp(target);
        });
        if(isIE)
            target.setCapture();//让ie下拖动顺滑
        e.stopPropagation();
        e.preventDefault();
    }
}

鼠标点下,选中的图片的遮罩出现,这里是对其添加.photo_maskon

<div class='photo_mask' ms-class-photo_maskon='drag_flag&&selected_index.indexOf($index)>-1' 
ms-mousedown='start_drag($event,$index)'></div>

mousemove

drag_move:function(e){
    if(photo_sort.drag_flag){
        var xx=e.clientX,yy=e.clientY,offset=avalon($('wrap')).offset();
        var offsetX=xx-offset.left,offsetY=yy-offset.top;
        photo_sort.sort_array.push(offsetX);//把当前鼠标位置添加的范围列表
        photo_sort.sort_array.sort(function(a,b){//对范围列表排序
            return parseInt(a)-parseInt(b);//转为数值类型,否则会出现'1234'<'333'
        });
        //从已排序的范围列表中找出当前鼠标位置的index,即目标位置水平方向的index
        var x_index=photo_sort.sort_array.indexOf(offsetX),y_index=Math.floor(offsetY/(photo_sort.cell_size+20)),
        size=photo_sort.photo_list.size();
        photo_sort.x_index=x_index;
        photo_sort.target_index=photo_sort.col_num*y_index+x_index;//目标在所有图片中的index
        if(photo_sort.target_index>size)//目标位置越界
            photo_sort.target_index=size;
        photo_sort.sort_array.remove(offsetX);//移除当前位置
        $('drag_proxy').style.top=avalon(window).scrollTop()+yy-photo_sort.cell_size/2+'px';
        $('drag_proxy').style.left=xx-photo_sort.cell_size/2+'px';
    }
    e.stopPropagation();
}

几点说明

  • 关于当前拖动到的位置判定
    位置判定

图中每个单元格的竖线,在水平方向把单元格分为两边。每个竖线把一行分为5部分,判断的时候,看鼠标当前的e.clientX在5个部分里的哪一部分。

  • 这里在判断的时候用了排序。具体的,把每个竖线的x坐标和当前鼠标位置的x坐标保存到数组(这里的sort_array),排好序,然后indexOf看当前鼠标位置的x坐标在数组中的位置,即可得到当前拖动的目标位置。
    如果不用排序的话,代码会像这样

var target;
if(x>50+50){
    if(x>3*100+3*100+50+50){//最后一部分
        target=4;
    }else{
        target=(x-50-50)/(50+100+50);
    }
}else
    target=0;
  • 后面删除当前鼠标位置的x坐标,空出位置,留给下一次mousemove事件的x坐标。

  • 关于当前拖动的目标位置左右的图片发生一定偏移,无非就是对目标位置左右的图片加上相应的class.

.prev{
    right: 40px;
}
.next{
    left: 40px;
}
    <div id='wrap' ms-controller='photo_sort'>
        <ul class='justify' ms-mousemove='drag_move($event)'>
            <li ms-repeat='photo_list' ms-attr-id='wrap_photo{{$index}}' ms-class-prev='$index==target_index-1' 
            ms-class-next='$index==target_index'>
            ...
            </li>
            <li class='justify_fix'></li>
        </ul>
    </div>

这里需要注意,当代理拖动到最左边或最右边时,由于布局是inline-block,此时目标位置所在行的上一行(如果有)的最后一个单元格或下一行(如果有)的第一个单元格也会发生偏移。
图片描述
解决方法是设置变量x_index,表示单元格在x方向的index.在添加偏移class的时候,增加判定条件。

<li ms-repeat='photo_list' ms-attr-id='wrap_photo{{$index}}' ms-class-prev='$index==target_index-1&&x_index>0' 
ms-class-next='$index==target_index&&x_index<col_num'>
...
</li>

mouseup

        function onMouseUp(target){
            if(photo_sort.drag_flag){
                for(var i=0,len=photo_sort.selected_index.size();i<len;i++){//遍历选中图片
                    var item_index=photo_sort.selected_index[i],data=photo_sort.photo_list,
                    target_index=photo_sort.target_index,temp;
                    if(item_index<target_index){//目标位置在选中图片之后
                        temp=data[item_index].src;
                        for(var j=item_index;j<target_index;j++)
                            data[j].data-original=data[j+1].src;
                        data[target_index-1].data-original=temp;
                    }else{//目标位置在选中图片之前
                        temp=data[item_index].src;
                        for(var j=item_index;j>target_index;j--)
                            data[j].data-original=data[j-1].src;
                        data[target_index].data-original=temp;
                    }
                }
                photo_sort.photo_list=data;//更新数据
                photo_sort.target_index=-1;//各种重置,初始化
                photo_sort.sort_array=[];
                photo_sort.col_num=0;
                photo_sort.x_index=-1;
                photo_sort.selected_photo.clear();
                photo_sort.selected_index.clear();
                $('drag_proxy').style.display='none';
                photo_sort.drag_flag=false;
                avalon.unbind(document,'mouseup');
                if(isIE)
                    target.releaseCapture();
            }
        }

这里主要就是对图片列表的重排。

  • 目标位置在选中图片之前
    图片描述

先把原始图片保存在temp,然后把从目标位置图片到原始图片前一位置的图片,依次后移一个位置,最后把temp放到目标位置。

  • 目标位置在选中图片之后
    图片描述

和上面差不多,只不过这里是把从目标位置图片到原始图片后一位置的图片,依次前移一个位置。

说明

  • 不能像data[j]=data[j+1]这样赋值,进而更新视图。因为avalon不支持单个转换,如果想更新,需要将整个子VM重新赋以一个新的对象。也就是photo_sort.photo_list=sortedData重新赋值,更新视图。

  • 前面判断图片选中状态为什么用selected_photo.indexOf(photo),而不是selected_index.indexOf(i),是因为更新视图后,avalon不能自动更新当前图片的index,也就是说如果图片一出来就是第一张,那它的index就永远是0,不会跟着它的位置改变。

移动顺序问题

重现

这里为了方便查看顺序,稍微修改了下html.
1
可以看到,拖动第2,3张图片到第8,9张图片之间,结果应该是第8张图片在第二行最左边,然后向右依次是第2,3张图片,最后是第9张图片.而这里显然不是想要的结果。

根源

出现这个问题的原因在于
依次移动目标图片到目标位置,对多张目标图片,会有一个移动先后的考量。具体的,
2
假设上图要移动的目标图片是第2,3张图片,先移动第2张图片到目标位置,这时第3张图片会在第2张原来的位置上,这时成为第2张。但是前面,

        function onMouseUp(target){
            if(photo_sort.drag_flag){
                for(var i=0,len=photo_sort.selected_index.size();i<len;i++){//遍历选中图片
                    ...
                }
                ...
            }
        }

还是在依次遍历目标图片,selected_index=[2,3];,下一个会遍历现在的第3张图片.

解决

解决方法很容易想到,就是先移动第3张图片,后移动第2张图片.
上面的例子是目标位置在选中图片之后,当然目标位置在选中图片之前也存在这个问题。
具体到代码上

function onMouseUp(target){
    if(photo_sort.drag_flag){
        photo_sort.selected_index.sort(function(a,b){//对范围列表排序
            return parseInt(a)-parseInt(b);
        });
        var size=photo_sort.selected_index.size();
        var data=photo_sort.photo_list,target_index=photo_sort.target_index,
        pos_arr=photo_sort.selected_index.$model,
        result=data.slice(0,data.size());
        pos_arr.push(target_index);//pos_arr存放选中的目标图片+目标位置,并排好序
        pos_arr.sort(function(a,b){//对范围列表排序
            return parseInt(a)-parseInt(b);
        });
        var target_pos=pos_arr.indexOf(target_index),temp;
        //目标位置在选中图片之后,从目标位置开始,依次向前遍历目标图片
        for(var i=target_pos-1;i>=0;i--){
            var item_index=pos_arr[i];
            temp=data[item_index].$model;
            for(var j=item_index;j<target_index;j++){
                data[j].$model=data[j+1].$model;
            }
            data[target_index-1].$model=temp;
        }
        //目标位置在选中图片之前,从目标位置开始,依次向后遍历目标图片
        for(var i=target_pos+1;i<pos_arr.length;i++){
            var item_index=pos_arr[i];
            temp=data[item_index].$model;
            for(var j=item_index;j>target_index;j--)
                data[j].$model=data[j-1].$model;
            data[target_index].$model=temp;
        }
        photo_sort.photo_list=data;//更新数据
        ...
    }
}

实际上就是以目标位置为中心,向左右两边遍历选中图片。具体的
图片描述
选中图片是第1,2,8,9张图片,目标位置是4.这时pos_arr=[1,2,4,8,9];,然后先遍历4之前的选中图片,2->1,然后是4之后的选中图片,8->9.这样就避免了移动顺序问题。

后记

事实上,google plus在细节上还做了

  • 框选图片

  • 如果有滚动条,且拖动位置快要超出当前界面,滚动条会自动上移或下移。
    这两个本屌就不做了,原理也是很简单的。


下载

查看原文

土豆爱鸡蛋 提出了问题 · 2018-02-26

如何通过Api唤起小程序分享

Readhub小程序里面的分享功能如何实现

进入Readhub小程序-随便点击一个新闻进入新闻详情页-点击右下角分享按钮-分享给朋友,此时程序能直接唤起分享功能。

但是小程序没有支持通过api唤起分享,只支持通过button和系统按钮唤起分享。请问如何实现:)

图片描述

图片描述

图片描述

关注 2 回答 1

土豆爱鸡蛋 赞了文章 · 2018-02-12

为什么说DOM操作很慢

也可以在这里看:http://leozdgao.me/why-dom-slow/

一直都听说DOM很慢,要尽量少的去操作DOM,于是就想进一步去探究下为什么大家都会这样说,在网上学习了一些资料,这边整理出来。

首先,DOM对象本身也是一个js对象,所以严格来说,并不是操作这个对象慢,而是说操作了这个对象后,会触发一些浏览器行为,比如布局(layout)和绘制(paint)。下面主要先介绍下这些浏览器行为,阐述一个页面是怎么最终被呈现出来的,另外还会从代码的角度,来说明一些不好的实践以及一些优化方案。

浏览器是如何呈现一张页面的

一个浏览器有许多模块,其中负责呈现页面的是渲染引擎模块,比较熟悉的有WebKit和Gecko等,这里也只会涉及这个模块的内容。

先用文字大致阐述下这个过程:

  • 解析HTML,并生成一棵DOM tree

  • 解析各种样式并结合DOM tree生成一棵Render tree

  • 对Render tree的各个节点计算布局信息,比如box的位置与尺寸

  • 根据Render tree并利用浏览器的UI层进行绘制

其中DOM tree和Render tree上的节点并非一一对应,比如一个display:none的节点就在会存在与DOM tree上,而不会出现在Render tree上,因为这个节点不需要被绘制。

上图是Webkit的基本流程,在术语上和Gecko可能会有不同,这里贴上Gecko的流程图,不过文章下面的内容都会统一使用Webkit的术语。

影响页面呈现的因素有许多,比如link的位置会影响首屏呈现等。但这里主要集中讨论与layout相关的内容。

paint是一个耗时的过程,然而layout是一个更耗时的过程,我们无法确定layout一定是自上而下或是自下而上进行的,甚至一次layout会牵涉到整个文档布局的重新计算。

但是layout是肯定无法避免的,所以我们主要是要最小化layout的次数。

什么情况下浏览器会进行layout

在考虑如何最小化layout次数之前,要先了解什么时候浏览器会进行layout。

layout(reflow)一般被称为布局,这个操作是用来计算文档中元素的位置和大小,是渲染前重要的一步。在HTML第一次被加载的时候,会有一次layout之外,js脚本的执行和样式的改变同样会导致浏览器执行layout,这也是本文的主要要讨论的内容。

一般情况下,浏览器的layout是lazy的,也就是说:在js脚本执行时,是不会去更新DOM的,任何对DOM的修改都会被暂存在一个队列中,在当前js的执行上下文完成执行后,会根据这个队列中的修改,进行一次layout。

然而有时希望在js代码中立刻获取最新的DOM节点信息,浏览器就不得不提前执行layout,这是导致DOM性能问题的主因。

如下的操作会打破常规,并触发浏览器执行layout:

  • 通过js获取需要计算的DOM属性

  • 添加或删除DOM元素

  • resize浏览器窗口大小

  • 改变字体

  • css伪类的激活,比如:hover

  • 通过js修改DOM元素样式且该样式涉及到尺寸的改变

我们来通过一个例子直观的感受下:

// Read
var h1 = element1.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';

// Read (triggers layout)
var h2 = element2.clientHeight;

// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';

// Read (triggers layout)
var h3 = element3.clientHeight;

// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px';

这里涉及一个属性clientHeight,这个属性是需要计算得到的,于是就会触发浏览器的一次layout。我们来利用chrome(v47.0)的开发者工具看下(截图中的timeline record已经经过筛选,仅显示layout):

上面的例子中,代码首先修改了一个元素的样式,接下来读取另一个元素的clientHeight属性,由于之前的修改导致当前DOM被标记为脏,为了保证能准确的获取这个属性,浏览器会进行一次layout(我们发现chrome的开发者工具良心的提示了我们这个性能问题)。

优化这段代码很简单,预先读取所需要的属性,在一起修改即可。

// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

看下这次的情况:

下面再介绍一些其他的优化方案。

最小化layout的方案

上面提到的一个批量读写是一个,主要是因为获取一个需要计算的属性值导致的,那么哪些值是需要计算的呢?

这个链接里有介绍大部分需要计算的属性:http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html

再来看看别的情况:

面对一系列DOM操作

针对一系列DOM操作(DOM元素的增删改),可以有如下方案:

  • documentFragment

  • display: none

  • cloneNode

比如(仅以documentFragment为例):

var fragment = document.createDocumentFragment();
for (var i=0; i < items.length; i++){
  var item = document.createElement("li");
  item.appendChild(document.createTextNode("Option " + i);
  fragment.appendChild(item);
}
list.appendChild(fragment);

这类优化方案的核心思想都是相同的,就是先对一个不在Render tree上的节点进行一系列操作,再把这个节点添加回Render tree,这样无论多么复杂的DOM操作,最终都只会触发一次layout

面对样式的修改

针对样式的改变,我们首先需要知道并不是所有样式的修改都会触发layout,因为我们知道layout的工作是计算RenderObject的尺寸和大小信息,那么我如果只是改变一个颜色,是不会触发layout的。

这里有一个网站CSS triggers,详细列出了各个CSS属性对浏览器执行layout和paint的影响。

像下面这种情况,和上面讲优化的部分是一样的,注意下读写即可。

elem.style.height = "100px"; // mark invalidated
elem.style.width = "100px";
elem.style.marginRight = "10px";

elem.clientHeight // force layout here

但是要提一下动画,这边讲的是js动画,比如:

function animate (from, to) {
  if (from === to) return

  requestAnimationFrame(function () {
    from += 5
    element1.style.height = from + "px"
    animate(from, to)
  })
}

animate(100, 500)

动画的每一帧都会导致layout,这是无法避免的,但是为了减少动画带来的layout的性能损失,可以将动画元素绝对定位,这样动画元素脱离文本流,layout的计算量会减少很多。

使用requestAnimationFrame

任何可能导致重绘的操作都应该放入requestAnimationFrame

在现实项目中,代码按模块划分,很难像上例那样组织批量读写。那么这时可以把写操作放在requestAnimationFrame的callback中,统一让写操作在下一次paint之前执行。

// Read
var h1 = element1.clientHeight;

// Write
requestAnimationFrame(function() {
  element1.style.height = (h1 * 2) + 'px';
});

// Read
var h2 = element2.clientHeight;

// Write
requestAnimationFrame(function() {
  element2.style.height = (h2 * 2) + 'px';
});

可以很清楚的观察到Animation Frame触发的时机,MDN上说是在paint之前触发,不过我估计是在js脚本交出控制权给浏览器进行DOM的invalidated check之前执行。

其他注意点

除了由于触发了layout而导致性能问题外,这边再列出一些其他细节:

缓存选择器的结果,减少DOM查询。这里要特别体下HTMLCollection。HTMLCollection是通过document.getElementByTagName得到的对象类型,和数组类型很类似但是每次获取这个对象的一个属性,都相当于进行一次DOM查询

var divs = document.getElementsByTagName("div");
for (var i = 0; i < divs.length; i++){  //infinite loop
  document.body.appendChild(document.createElement("div"));
}

比如上面的这段代码会导致无限循环,所以处理HTMLCollection对象的时候要最些缓存。

另外减少DOM元素的嵌套深度并优化css,去除无用的样式对减少layout的计算量有一定帮助。

在DOM查询时,querySelectorquerySelectorAll应该是最后的选择,它们功能最强大,但执行效率很差,如果可以的话,尽量用其他方法替代。

下面两个jsperf的链接,可以对比下性能。

https://jsperf.com/getelementsbyclassname-vs-queryselectorall/162
http://jsperf.com/getelementbyid-vs-queryselector/218

自己对View层的想法

上面的内容理论方面的东西偏多,从实践的角度来看,上面讨论的内容,正好是View层需要处理的事情。已经有一个库FastDOM来做这个事情,不过它的代码是这样的:

fastdom.read(function() {
  console.log('read');
});

fastdom.write(function() {
  console.log('write');
});

问题很明显,会导致callback hell,并且也可以预见到像FastDOM这样的imperative的代码缺乏扩展性,关键在于用了requestAnimationFrame后就变成了异步编程的问题了。要让读写状态同步,那必然需要在DOM的基础上写个Wrapper来内部控制异步读写,不过都到了这份上,感觉可以考虑直接上React了......

总之,尽量注意避免上面说到的问题,但如果用库,比如jQuery的话,layout的问题出在库本身的抽象上。像React引入自己的组件模型,用过virtual DOM来减少DOM操作,并可以在每次state改变时仅有一次layout,我不知道内部有没有用requestAnimationFrame之类的,感觉要做好一个View层就挺有难度的,之后准备学学React的代码。希望自己一两年后会过来再看这个问题的时候,可以有些新的见解。

参考

查看原文

赞 19 收藏 126 评论 1

土豆爱鸡蛋 收藏了问题 · 2018-02-07

移动端css动画播放状态暂停在ios不起作用 animation-play-state

移动端css动画播放状态暂停在ios不起作用 animation-play-state

我想点击图片图片静止,再点一下图片继续旋转。在安卓手机上可以实现,但是在ios,第一次点击图片继续旋转,没有静止动作,反而在第二点击的时候,图片在第一次点击的位置闪一下,继续旋转。

<style type="text/css">
#wls .musicCon{width: 35px;height: 35px;position:fixed;top:15px;right:15px;z-index: 9999; }
 img.rotate{
     animation:spin 4s infinite linear;
     -moz-animation:spin 4s infinite linear;
     -webkit-animation:spin 4s infinite linear;
        -webkit-animation-play-state: running;
       -moz-animation-play-state: running;
       animation-play-state: running;
       -ms-animation-play-state: running;
       -o-animation-play-state: running;
}
@keyframes spin {
     0%{
      transform:rotate(0deg);
     }
     100%{
       transform:rotate(360deg);
     }
}
@-o-keyframes spin {
    0% {
        -o-transform: rotate(0deg);
    }
    100%{
       -o-transform:rotate(360deg);
     }
}
@-moz-keyframes spin {
    0% {
        -moz-transform: rotate(0deg);
    }
     100%{
       -moz-transform:rotate(360deg);
     }
}
@-webkit-keyframes spin {
    0% {
        -webkit-transform:  rotate(0deg);
    }
    100%{
       -webkit-transform:rotate(360deg);
     }
}
 img.pauseWalk {
    animation:spin 4s infinite linear;
   -moz-animation:spin 4s infinite linear;
   -webkit-animation:spin 4s infinite linear;
   -webkit-animation-play-state: paused;
   -moz-animation-play-state: paused;
   animation-play-state: paused;
   -ms-animation-play-state: paused;
   -o-animation-play-state: paused;
}
</style>
<body style="background:#fff" id="wls"> 
        <img data-original="imgage/music.png" class="musicCon rotate" />
         <audio autoplay="autoplay" loop="loop" id="bgm">
          <source data-original="music/bgm.mp3" type="audio/mpeg">
          <source data-original="music/bgm.ogg" type="audio/ogg">
        </audio>
        <script> 
        var num=1;
            $("#wls .musicCon").bind("click",function(){
                if(num==1){
                    $(this).removeClass("rotate");
                    $(this).addClass("pauseWalk");
                    $("#bgm")[0].pause();
                    num++;
                    return num;
                }else{
                    $(this).removeClass("pauseWalk");
                    $(this).addClass("rotate");
                    $("#bgm")[0].play();
                    num=1;
                    return num;
                }
            })
        </script>
</body>

认证与成就

  • 获得 19 次点赞
  • 获得 62 枚徽章 获得 3 枚金徽章, 获得 23 枚银徽章, 获得 36 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-04-17
个人主页被 538 人浏览