函数式编程 - 组合compose

1,193 阅读1小时+
原文链接: segmentfault.com
首页 问答专栏 讲堂 更多

SegmentFault

搜索 不可能的是

    不可能的是 查看完整档案

    1428声望 北京编辑 武汉工程大学  |  软件工程 编辑 @nbsp;  |  前端开发 编辑 1234.github.io/ 编辑 编辑
    stay hungry&&foolish
    ¥1.11 向他提问 加关注 发私信 关注了10 人 粉丝30 人

    社区属性

    • javascript
    • vue.js
    • html
    • css
    • element-ui
    • html5
    • 前端
    • node.js
    • webpack
    • react.js
    没有足够的数据

    没有足够的数据

    高分内容

    概览提问 回答文章

    (゚∀゚ ) 暂时没有任何数据

    查看全部 提问回答文章

    个人动态

    不可能的是 发布了文章 · 10 小时前

    函数式编程 - 组合compose

    函数式编程中有一个比较重要的概念就是函数组合(compose),组合多个函数,同时返回一个新的函数。调用时,组合函数按顺序从右向左执行。右边函数调用后,返回的结果,作为左边函数的参数传入,严格保证了执行顺序,这也是compose 主要特点。

    入门简介

    组合两个函数

    compose 非常简单,通过下面示例代码,就非常清楚

    function compose (f, g) {
        return function(x) {
            return f(g(x));
        }
    }
    
    var arr = [1, 2, 3],
        reverse = function(x){ return x.reverse()},
        getFirst = function(x) {return x[0]},
        compseFunc = compose(getFirst, reverse);
        
    compseFunc(arr);   // 3

    参数在函数间就好像通过‘管道’传输一样,最右边的函数接收外界参数,返回结果传给左边的函数,最后输出结果。

    组合任意个函数

    上面组合了两个函数的compose,也让我们了解了组合的特点,接着我们看看如何组合更多的函数,因为在实际应用中,不会像入门介绍的代码那么简单。

    主要注意几个关键点:

    1. 利用arguments的长度得到所有组合函数的个数
    2. reduce 遍历执行所有函数。
        var compose = function() {
          var args = Array.prototype.slice.call(arguments);
          
          return function(x) {
           if (args.length >= 2) {
           
              return args.reverse().reduce((p, c) => {
                return p = c(p)
             }, x)
             
           } else {
               return args[1] && args[1](x);
           }
          }
        }
       
        // 利用上面示例 测试一下。
        var arr = [1, 2, 3],
        reverse = function(x){ return x.reverse()},
        getFirst = function(x) {return x[0]},
        trace = function(x) {  console.log('执行结果:', x); return x}
        
        
        compseFunc = compose(trace, getFirst, trace, reverse);
        
    compseFunc(arr);   
     // 执行结果: (3) [3, 2, 1]
     // 执行结果: 3
     // 3

    如此实现,基本没什么问题,变量arr 在管道中传入后,经过各种操作,最后返回了结果。

    深入理解

    认识pipe

    函数式编程(FP)里面跟compose类似的方法,就是pipe
    pipe,主要作用也是组合多个函数,称之为'流', 肯定得按照正常方法,从左往右调用函数,与compose 调用方法相反。

    ES6 实现Compose function

    先看下compose 最基础的两参数版本,

    const compose = (f1, f2) => value => f1(f2(value));

    利用箭头函数,非常直接的表明两个函数嵌套执行的关系,

    接着看多层嵌套。

        (f1, f2, f3...) => value => f1(f2(f3));

    抽象出来表示:

         () => () => result;

    先提出这些基础的组合方式,对我们后面理解高级es6方法实现compose有很大帮助。

    实现pipe

    前面提到pipe 是反向的compose,pipe正向调用也导致它实现起来更容易。

    pipe = (...fns) => x => fns.reduce((v, f) => f(v), x)
        

    一行代码就实现了pipe, 套用上面抽象出来的表达式,reduce刚好正向遍历所有函数, 参数x作为传递给函数的初始值, 后面每次f(v)执行的结果,作为下一次f(v)调用的参数v,完成了函数组合调用。

    或者,可以把函数组合中,第一个函数获取参数后,得到的结果,最为reduce遍历的初始值。

    pipe = (fn,...fns) => (x) => fns.reduce( (v, f) => f(v), fn(x));

    利用es6提供的rest 参数 ,用于获取函数的多余参数.提取出第一个函数fn,多余函数参数放到fns中,fns可以看成是数组,也不用像arguments那种事先通过Array.prototype.slice.call转为数组,arguments对性能损耗也可以避免。 fn(x) 第一个函数执行结果作为reduce 初始值。

    实现compose

    1. pipe 部分,利用reduce实现,反过来看,compose就可以利用reduceRight

      compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
      
    2. 利用递归

      compose = (fn, ...fns) => fns.length === 0 ? fn: (...args) => fn(compose(...fns)(...args))

      递归代码,首先看出口条件, fns.length === 0, 最后一定执行最左边的函数,然后把剩下的函数再经过compose调用,

    3. 利用reduce实现。
      具体实现代码点击这里,一行实现,而且还是用正向的 reduce

      const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)))

      作者其实用例子做了解释,可以看下reduce 迭代的方向是从左往右的,而compose 要求执行的方向是从从右往左。对数组中每一项执行函数,正常情况下都应该放回执行结果,比如(v, f) => f(v),返回f(v)执行结果,这里是(f, g) => (...args) => f(g(...args))返回一个函数(...args) => f(g(...args)),这样就可以保证后面的函数g在被作为参数传入时比前面的函数f先执行。

      简单利用前面的组合两个函数的例子分析一下。

      ...
      composeFunc = compose(getFirst, trace, reverse);
      composeFunc(arr);
        

      主要看reduce 函数里面的执行过程:

      • 入口 composeFunc(arr), 第一次迭代,reduce函数执行 (getFirst, trace) => (...args)=>getFirst(trace(...args)),函数(...args)=>getFirst(trace(...args))作为下一次迭代中累计器f的值。
      • 第二次迭代,reduce函数中

         f == (...args)=>getFirst(trace(...args))
         g == reverse。
         // 替换一下 (f, g) => (...args) => f(g(...args))
        ((...args)=>getFirst(trace(...args)), reverse) => (...args) => ((...args)=>getFirst(trace(...args)))(reverse(...args))
        
      • 迭代结束,最后得到的comoseFunc就是

           // 对照第二次的执行结果, (...args) => f(g(...args))
        
          (...args) => ((...args)=>getFirst(trace(...args)))(reverse(...args))
      • 调用函数composeFunc(arr)。

        (arr) => ((...args)=>getFirst(trace(...args)))(reverse(arr))
        
        ===》reverse(arr) 执行结果[3, 2, 1] 作为参数
        
         ((...args)=>getFirst(trace(...args)))([3,2,1])
        
        ==》入参调用函数
        
           getFirst(trace[3,2,1])
        
        ===》 
        
           getFirst([3, 2, 1])
        
        ===》
        
           结果为 3

        非常巧妙的把后一个函数的执行结果作为包裹着前面函数的空函数的参数,传入执行。其中大量用到下面的结构

        ((arg)=> f(arg))(arg) 
        // 转换一下。
          (function(x) {
             return f(x)
          })(x)

    最后

    无论是compose, 还是后面提到的pipe,概念非常简单,都可以使用非常巧妙的方式实现(大部分使用reduce),而且在编程中很大程度上简化代码。最后列出优秀框架中使用compose的示例:

    参考链接

    查看原文

    函数式编程中有一个比较重要的概念就是函数组合(compose),组合多个函数,同时返回一个新的函数。调用时,组合函数按顺序从右向左执行。右边函数调用后,返回的结果,作为左边函数的参数传入,严格保证了执行顺序,这也是compose 主要特点。

    赞 5 收藏 4 评论 0

    不可能的是 发布了文章 · 4 天前

    高级函数技巧-函数柯里化

    我们经常说在Javascript语言中,函数是“一等公民”,它们本质上是十分简单和过程化的。可以利用函数,进行一些简单的数据处理,return 结果,或者有一些额外的功能,需要通过使用闭包来实现,最后经常会return 匿名函数。

    如果你对函数式编程有一定了解,函数柯里化(function currying)是不可或缺的,利用函数柯里化,可以在开发中非常优雅的处理复杂逻辑。

    函数柯里化

    柯里化(Currying),维基百科上的解释是,把接受多个参数的函数转换成接受一个单一参数的函数
    先看一个简单例子

        // 柯里化
        var foo = function(x) {
            return function(y) {
                return x + y
            }
        }
        
        foo(3)(4)       // 7
    
        
        // 普通方法
        var add = function(x, y) {
            return x + y;
        }
        
        add(3, 4)       //7 
    

    本来应该一次传入两个参数的add函数,柯里化方法,变成每次调用都只用传入一个参数,调用两次后,得到最后的结果。

    再看看,一道经典的面试题。

    编写一个sum函数,实现如下功能:
     console.log(sum(1)(2)(3)) // 6.
    

    直接套用上面柯里化函数,多加一层return

       function sum(a) {
            return function(b) {
                return function(c) {
                    return a + b + c;
                }
            }
        }

    当然,柯里化不是为了解决面试题,它是应函数式编程而生,

    如何实现

    还是看看上面的经典面试题。
    如果想实现 sum(1)(2)(3)(4)(5)...(n)就得嵌套n-1个匿名函数,

       function sum(a) {
            return function(b) {
                 ...
                return function(n) {
                    
                }
            }
        }
        

    看起来并不优雅,如果我们预先知道有多少个参数要传入,可以利用递归方法解决

        var add = function(num1, num2) {
            return num1 + num2;
        }
        
        // 假设 sum 函数调用时,传入参数都是标准的数字
        function curry(add, n) {
           var count = 0,
               arr = [];
               
           return function reply(arg) {
               arr.push(arg);
               
               if ( ++count >= n) {
                   //这里也可以在外面定义变量,保存每次计算后结果
                   return arr.reduce(function(p, c) {
                       return p = add(p, c);
                   }, 0) 
               } else {
                   return reply;
               }
           }
        }
        var sum = curry(add, 4);
        
        sum(4)(3)(2)(1)  // 10

    如果调用次数多于约定数量,sum 就会报错,我们就可以设计成类似这样

    sum(1)(2)(3)(4)(); // 最后传入空参数,标识调用结束,

    只需要简单修改下curry 函数

    function curry(add) {
           var arr = [];
           
           return function reply() {
             var arg = Array.prototype.slice.call(arguments);
             arr = arr.concat(arg);
             
              if (arg.length === 0) { // 递归结束条件,修改为 传入空参数
                  return arr.reduce(function(p, c) {
                      return p = add(p, c);
                  }, 0)
              } else {
                  return reply;
              }
          }
        }
      
      console.log(sum(4)(3)(2)(1)(5)())   // 15

    简洁版实现

    上面针对具体问题,引入柯里化方法解答,回到如何实现创建柯里化函数的通用方法。
    同样先看简单版本的方法,以add方法为例,代码来自《JavaScript高级程序设计》

     function curry(fn) {
        var args = Array.prototype.slice.call(arguments, 1);
        return function() {
            var innerArgs = Array.prototype.slice.call(arguments);
            var finalArgs = args.concat(innerArgs);
            return fn.apply(null, finalArgs);
        };
    }
    
    function add(num1, num2) {
        return num1 + num2;
    }
    var curriedAdd = curry(add, 5);
    
    var curriedAdd2 = curry(add, 5, 12);
    
    alert(curriedAdd(3))    // 8
    alert(curriedAdd2())    // 17

    加强版实现

    上面add函数,可以换成任何其他函数,经过curry函数处理,都可以转成柯里化函数。
    这里在调用curry初始化时,就传入了一个参数,而且返回的函数 curriedAdd , curriedAdd2也没有被柯里化。要想实现更加通用的方法,在柯里化函数真正调用时,再传参数,

    function curry(fn) {
         ...
     }
    
    function add(num1, num2) {
        return num1 + num2;
    }
    
    var curriedAdd = curry(add);
    
    curriedAdd(3)(4) // 7

    每次调用curry返回的函数,也被柯里化,可以继续传入一个或多个参数进行调用,

    跟上面sum(1)(2)(3)(4) 非常类似,利用递归就可以实现。 关键是递归的出口,这里不能是传入一个空参数的调用, 而是原函数定义时,参数的总个数,柯里化函数调用时,满足了原函数的总个数,就返回计算结果,否则,继续返回柯里化函数

    原函数的入参总个数,可以利用length 属性获得

    function add(num1, num2) {
        return num1 + num2;
    }
    
    add.length // 2

    结合上面的代码,

        var curry = function(f) {
          var len = f.length;
          
            return function t() {
              var innerLength = arguments.length,
                args = Array.prototype.slice.call(arguments);
                
              if (innerLength >= len) {   // 递归出口,f.length
                 return f.apply(undefined, args)
              } else {
                return function() {
                  var innerArgs = Array.prototype.slice.call(arguments),
                    allArgs = args.concat(innerArgs);
                    
                  return t.apply(undefined, allArgs)
                }
              }
            }
        }
        
       // 测试一下
      function add(num1, num2) {
        return num1 + num2;
      }
    
       var curriedAdd = curry(add);
       add(2)(3);     //5
    
      // 一个参数
      function identity(value) {
         return value;
     }
    
       var curriedIdentify = curry(identify);
       curriedIdentify(4) // 4
    

    到此,柯里化通用函数可以满足大部分需求了。

    在使用 apply 递归调用的时候,默认传入 undefined, 在其它场景下,可能需要传入 context, 绑定指定环境

    实际开发,推荐使用 lodash.curry , 具体实现,可以参考下curry源码

    使用场景

    讲了这么多curry函数的不同实现方法,那么实现了通用方法后,在那些场景下可以使用,或者说使用柯里化函数是否可以真实的提高代码质量,下面总结一下使用场景

    • 参数复用
      在《JavaScript高级程序设计》中简单版的curry函数中

        var curriedAdd = curry(add, 5)

      在后面,使用curriedAdd函数时,默认都复用了5,不需要重新传入两个参数

    • 延迟执行
      上面传入多个参数的sum(1)(2)(3),就是延迟执行的最后例子,传入参数个数没有满足原函数入参个数,都不会立即返回结果。

      类似的场景,还有绑定事件回调,更使用bind()方法绑定上下文,传入参数类似,

         addEventListener('click', hander.bind(this, arg1,arg2...))
         
         addEventListener('click', curry(hander)) 
         

      延迟执行的特性,可以避免在执行函数外面,包裹一层匿名函数,curry函数作为回调函数就有很大优势。

    • 函数式编程中,作为compose, functor, monad 等实现的基础

      有人说柯里化是应函数式编程而生,它在里面出现的概率就非常大了,在JS 函数式编程指南中,开篇就介绍了柯里化的重要性。

    关于额外开销

    函数柯里化可以用来构建复杂的算法 和 功能, 但是滥用也会带来额外的开销。

    从上面实现部分的代码中,可以看到,使用柯里化函数,离不开闭包, arguments, 递归。

    闭包,函数中的变量都保存在内存中,内存消耗大,有可能导致内存泄漏。
    递归,效率非常差,
    arguments, 变量存取慢,访问性很差,

    参考链接

    查看原文

    我们经常说在Javascript语言中,函数是“一等公民”,它们本质上是十分简单和过程化的。可以利用函数,进行一些简单的数据处理,return 结果,或者有一些额外的功能,需要通过使用闭包来实现,最后经常会return 匿名函数。

    赞 19 收藏 16 评论 0

    不可能的是 赞了文章 · 2月21日

    链家网前端总架构师杨永林:我的8年架构师成长之路

    杨永林,人称“教主”,八年前端开发经验,原新浪微博前端技术专家,现任链家网前端总架构师。长期研究Web访问性能优化和前端框架搭建。
    作为初始团队成员,教主参与了新浪微博所有PC版本的开发,其中4~6版以架构师的身份设计了微博PC版的前端架构。在新浪微博任职期间,教主设计实现了流水线加载技术与模块化代码组织,达到了在提高访问性能的同时极大降低了开发成本的目的。主要研究方向是Web访问性能优化与框架组织。在国内为数不多地实现了BigPipe技术,极大地提升了微博的访问速度。同时,微博的前端代码基础包、前端框架和构建工具均出自教主之手。
    2015年年底,教主加入链家网,负责前端的整体架构工作。
    在8年的前端开发生涯中,教主是如何一步一步地成为知名前端架构师的呢?为何选择加入了链家网呢?

    您在微博和链家都是前端架构师,能说说前端架构师这个工种具体是做什么的吗?

    杨永林:我对架构师所担任的职责的认识是一步步变化,慢慢深入的。

    在刚参加工作的时候,我觉得架构师就是代码写得又快又好的人,是工程师的晋级版本。

    工作过一些年以后,我发现仅仅提高自身的开发效率是远远不够的,团队需要整体的提升。发现这一点后,我开始制作并完善各种开发工具,编写开发框架。

    最近几年,随着迭代开发了一些产品本,我又发现之前能够提升效率的框架工具很有可能在后来成了产品发展的绊脚石。这时,我开始考虑架构设计的指导原则,开始考虑取舍。一些在短期内能够提升效率但不符合原则的东西,我就选择不做或者想办法在原则的指导下进行改进。比如我相信可变化的代码才是有生命力的代码,在架构设计上我也会趋向于让项目的代码可以一点一点的变化演进,不是那种一言不合就重构到状态。所以我认为前端架构师就是那种在前端领域提出开发的指导原则,在原则下设计开发框架和开发工具,让更多的开发者可以协同工作的人。

    您在新浪微博的时候设计了前端架构,能否介绍一下包括了哪些组成部分,有什么关键技术?

    杨永林:主要是代码基础包,页面加载框架和前端构建工具。

    早期前端开发面临两个主要问题是浏览器兼容和API不够丰富,基础包一般都是用来解决这两个问题。当时新浪有一个自己的Sina包,但是代码比较零散,模式也不统一,各产品线有自己的扩展,同样的功能可能有多种实现,不太好维护。后来我用业余时间开发了一个带有命名空间管理功能的基础包,特点就是简单清晰,易于使用,被团队采纳作为了微博的基础包使用至今。

    页面加载框架是被需倒逼着产生的,2010年微博业务膨胀,页面展示的内容越来越多,这使得页面响应速度也变得越来越慢。我所在的团队接到的需求是要求在内容变多的情况下将响应速度变得更快。

    这个时候Facebook推出了BigPipe技术,我们觉得这个理念正好能够解决我们应对的问题,所以决定实施,但当时Facebook只是分享了他们的做法,并没有提供实现,所以对我来说也是巨大的挑战。我当时将页面划分成多个独立的子模块,模块是完全可以自主运行的,模块可以嵌套,所以页面就是一批模块的树形堆叠。服务端用Chunked的方式将模块的信息以JavaScript代码块的方式传输到页面,而前端需要做的很重要的工作是管理每个模块的生命周期。

    我很荣幸那时能有机会和团队成员一起开发了这个加载框架,我们可能是国内第一个在大型互联网应用上全面使用这项技术的。之后的一年我一直致力于此项技术的优化工作,比如支持服务端乱序输出,保证服务端可以使用并行策略,压缩,减少前置依赖条件等,并在2013年与@Laruence(鸟哥)合作实施了CBigPipe(并行的BigPipe)技术,进一步提高了这项技术的性能。微博的V5版的加载性能也达到了顶峰,页面的加载速度几乎相当于静态网页。

    前端构建工具是这几年才开始流行,其实早在2008年的时候,新浪就已经使用前端小文件开发,使用构建工具进行开发,测试和上线。现在想想应该是比较超前了,不过那时的版本是需要PHP、Python和Java环境,团队维护起来比较困难,而且使用的是字符串替换方案,功能比较有限。2012年我将这个工具进行了改造,使其仅需要Node环境,同时支持开发、测试部署和打包上线。由于使用了UglifyJS,有了语法树,我加了一些以前没有的功能,比如预编译的模版引擎、支持模版嵌套和母模版、代码健康度检测、冗余模块分析等。

    前端构建工具前后有Grunt/Gulp、Webpack、npm scripts等,您对这些工具有什么看法,哪个更好?如何选择适合公司产品的工具?应从哪些方面考虑?

    杨永林:我觉得这些工具有效地解决了前端开发效率的问题,它们的出现都是对技术的推动,如果在我做工具的时候有这些项目的出现,会减少我很多的工作量。至于哪个更好,我觉得,你能掌握哪个,哪个就是最好的。因为说到底,工具是为你的业务服务的,你可能需要对它做些改造或者是写一些扩展,在这个时候你发现你对他的熟悉变的很重要。构建工具的迁移成本还是挺高的,我不太推荐频繁地变更它,所以最好不要追着流行走,还是要根据自己团队的特点,因地制宜地选择一款合适的。如果不是超大型的应用,其实build的结果的影响并没有太大的差异,与其想着哪个更好哪个更牛逼,不如将其中一个玩熟玩透。

    如何保证团队成员不会踩到同样的坑?在设计框架和构建工具时有无这方面的考虑?请举例说明。

    杨永林:首先,制定规范、分享经验是免不了的,但纸上得来终觉浅吧,很多时候,亲身踩一次坑,得到的经验才会深刻。而我所要做的是在团队成员踩到坑的时候降低这件事造成的后果。比如我提供的开发环境是可以完全模拟线上环境的,测试代码和线上保持一致,很多意外情况都可以在开发、测试期被发现。同时,制定的开发规范要由工具检测来保证,不符合规范的代码不能够打包上线。对于规范代码可以使用工具计算出业务影响范围,能有效保证测试覆盖面。总的来说,踩坑不要紧,架构来帮你兜底,爬出坑的过程就成为了团队成员所得到的财富。

    您认为对Web访问性能的优化需要关注哪些方面?其中,最值得关注的点是什么?为什么?

    杨永林:我觉得性能优化需要方方面面都要兼顾,包括网络时间、服务器计算时间、页面请求数、下载量、页面载入模型等。而这里面任何一项的性能提升可能都需要你修改大量代码或者调整架构来实现,但是得到的效果可能就是一点点。因此很少见到银弹,一般都是一点一点地做出来的。我这里谈两个我觉得比较值得关注但很容易被忽视的点吧。

    一是你所服务产品的形态,用户关心什么,这是一些工程师比较容易忽略的。有些产品需要用户打开时很快,有些需要用户使用时流畅;有些产品用户可以容忍看旧数据,而有些则必须是新内容;有些产品用户一天打开很多次,而有些看一次就关掉了。这些产品需求的差异都会影响你的决策。

    二是评测标准,用什么来测量性能的好坏。一些人认为请求数或者请求量减少了,访问就快了,其实这是不一定的,有可能你花了很大精力做的事情在用户看来并没有什么太大变化。所以,找一个评测标准让每一个优化在数据上有所体现是很重要的。

    度量前端性能的指标有哪些?如何对Web访问性能进行监控?

    杨永林:我所服务的产品一般都关注访问性能,也就是用户看到内容的快慢,所以我们一般用首屏时间来评估,一般的性能检测服务商都能提供这个指标。

    选这个指标有两点考虑:一是因为它并不是一个技术指标,而是一个感知指标,所以更接近人类的感受。二是旁路检测,它并不在系统内,不是系统汇报上来的数据,这样就有效的规避了幸存者偏差的问题。当然它也有些不足:一是数据采样小,二是可以被欺骗。所以可能需要一点儿统计学功底和性能监控的正确认识。

    在监控的过程中,一是要关注长期趋势的变化,如果不是突发状况,单点的数据的绝对值是没有意义的,要收集长期的数据,分析其中的变化,当有变更的时候尤其要关注数据的变化。二是关注最差25%的状况,有些人,会在公司内网刷自己的产品,感觉挺快,其实不论你用什么手段,只要网快,用户的体验都不会太差,体验的差异在于最差那部分用户。三是从不同维度分析数据,如地区、网络、时段、运行环境等。

    前端工程师如何成为前端架构师,除了编程能力和架构知识,还需要培养哪些能力?

    杨永林:我想,大部分领域的架构师工作都是差不多的,就是搭建一个解决问题的框架,让团队成员能在框架下良好的配合工作,完成产品的开发需求。

    我们知道,解决一个问题的手段有很多,在这个过程中取舍就很重要了,我们也知道,没有银弹,很少能遇见那种全面优势的解决方案,大部分方案都是牺牲掉一部分东西来换取一部分东西。因此,作为架构师,不仅要对各个技术方案的特点、成本要熟知(也就是编程能力和架构知识),还要学会如何选择。显然,架构师需要根据产品的特点和发展方向做出决定,在前端领域的架构要能让配合的团队对接的顺畅。那么在这个过程中,良好的沟通能力、同理心、利他的思维方式,就显得很重要了。因为我们不仅要完成开发任务,也要思考在自己的领域内如何帮助项目解决问题。

    据说有些同事在对技术的讨论中以“击败”您为荣,您是如何看待的?这对团队及其个人的发展带来了哪些影响?

    杨永林:这是我一个毛病,喜欢给别人的方案着茬。我觉得这是一个思辨的过程,通过从不同角度分析问题,去挑战解决方案的合理性,才能让问题解决的更稳妥。在知识的获取中也是这样,一次一次地去问为什么,去追根溯源,才能让知识体系更牢固。

    我很喜欢在团队内扮演一种“反派”的角色,从反面的角度分析问题,去挑战别人的方案。其实,我不是真的去否定他,而是希望他的方案是经过反复推敲、深思熟虑产生的,这样的方案会更健壮。时间长了,他们会觉得我是一个爱抬杠的人,就会做足准备来“挑战”我。能把我说得接不上话来,他们会觉得很开心。这个结果是我想看到的,因为这说明团队成员在解决问题时进行了充分的思考。

    您为什么放弃了在之前新浪微博的元老级身份,而选择加入链家网?

    杨永林:这可能源自我对工作的看法吧,我觉得人生活在社会上,工作是在为社会创造价值和财富,这和他具体从事哪种职业没有直接关系。现在行业里有一种风气,就是觉得程序员写好代码就好了,不用关心自己做的事情是什么。甚至社会上也给程序员打一些什么“木讷”、“情商低”之类的标签。我觉得不应该是这样的,程序员也是社会人,也有他的社会责任,也有家庭责任,也需要陪伴他的伴侣,照顾他的小孩,不是每天只是面对代码而不管其他的事。人不要因为群体印象就把自己限制住,人的生活就应该是多种多样、丰富多采的,人生应该是有意义的。

    就我个人而言,在过去的几年,我所服务的产品不仅加深了人们之间的沟通和理解,也使得国家的信息变得更透明。而我所做的工作对这样的一个产品做出了贡献,可以说我的工作让世界变得美好了那么一点点。这让我觉得我的人生增添了那么一点意义。而当我搭建起前端框架后,我个人能起的作用变得越来越小,我能继续创造的价值也越来越少,所以需要另一个平台来继续发挥我的能量。

    这时我有机会接触到链家网,这家公司致力于解决人们的居住问题,它让中国最大的市场变得透明、有序。我觉得链家网做的是很有意义的事,同时,它仅仅用了不到两年的时间,就集结了一批各领域的牛人,维护了国内规模最大的房地产交易系统,用技术手段让房屋的买卖变的更轻松、透明、快捷。在与链家网的接触中,我感受到了那种积极解决问题的活力和务实做事的态度。再加上链家网中大部分技术人,在之前也都是各个大型互联网公司的中坚力量,我想没有什么比与志同道合的人来一起改变世界更令人激动的了。此时,鸟哥专门来邀请我加入链家网,我就毫不犹豫地同意了。

    教主答群友问:
    Q:您对初级前端人员有什么好的建议吗?
    A:多尝试一些解决方案,多想想这些方案的特点,对别人的方案深究原理。
    Q:前端学习方面有什么书籍可以介绍吗?
    A:现在前端书籍都挺多挺好多呀,我一般推荐高级程序设计,图解CSS3。
    Q:您在担任架构师这个角色中遇到的最严重的线上事故是什么?当时是怎么解决的?
    A:工作这么多年,在前端应该就只有一次B级故障,做非前端的时候倒是通过大篓子,因为上线速度比较快,而且大问题也都是很明显的解决方案,所以就是快速上线了。这个要感谢测试同学,很给力,避免了很多线上故障。
    Q:学前端是否去参加商业培训更见效?亦或是这种商业培训反而更会僵化思维?这样流水线培训出来的学员在企业认可度如何。
    A:我没参见过商业培训,也不太好评价,我是觉得被灌输的知识可能不如自己躺坑得来的扎实吧。企业认可这方面,我基本只看能力。
    Q:对于您来说技术比较重要还是产品比较重要?因为刚才您说是因为觉得链家的“产品”比较有意义才考虑去的,那能理解为你觉得产品比技术更重要吗?
    A:我所说的产品不是“产品人员”,是公司的产出的服务。显然对于一家公司来说,产品是最重要的,技术是如何实现产品的手段啊。
    Q:您觉得什么样的代码才算是可变化的代码?这方面又做出了哪些实践?有哪些系统化的产出?
    A:我说变化的代码其实代码是可控的,可以方便的去调整项目,可以一步一步的改造项目而不是重构,我做开发一致遵循这个理念。
    Q:前面提到搭建团队可用的框架,但我感觉这个工作量非常巨大而且需要很多改进和测试,教主当时有同感吗?怎么解决这么大的工作量问题?
    A:我可能比较幸运,曾经有一段时间来调整结构,我是这样想的,每当我向前迈一步的时候,我就是在进步,所以我没有急于让架构搭建一次到位,我会想好调整的步骤,每一步都会让架构进步,把大问题拆解成小问题一步一步做。
    Q:小公司开发前端,由于缺少项目管理经验,导致许多冗余性的工作,请问教主在管理方面有何建议?
    A:这个不同公司的情况都不一样吧,不太好建议。
    Q:多尝试一些解决方案和“一步一步逐步改进产品”是否矛盾?
    A:不矛盾啊,多尝试不代表多实施啊。

    查看原文

    杨永林,人称“教主”,八年前端开发经验,原新浪微博前端技术专家,现任链家网前端总架构师。长期研究Web访问性能优化和前端框架搭建。作为初始团队成员,教主参与了新浪微博所有PC版本的开发,其中4~6版以架构师的身份设计了微博PC版的前端架构。在新浪微博任职期间...

    赞 62 收藏 38 评论 4

    不可能的是 赞了文章 · 2月16日

    mobx学习总结

    Mobx解决的问题

    传统React使用的数据管理库为Redux。Redux要解决的问题是统一数据流,数据流完全可控并可追踪。要实现该目标,便需要进行相关的约束。Redux由此引出了dispatch action reducer等概念,对state的概念进行强约束。然而对于一些项目来说,太过强,便失去了灵活性。Mobx便是来填补此空缺的。

    这里对Redux和Mobx进行简单的对比:

    1. Redux的编程范式是函数式的而Mobx是面向对象的;

    2. 因此数据上来说Redux理想的是immutable的,每次都返回一个新的数据,而Mobx从始至终都是一份引用。因此Redux是支持数据回溯的;

    3. 然而和Redux相比,使用Mobx的组件可以做到精确更新,这一点得益于Mobx的observable;对应的,Redux是用dispath进行广播,通过Provider和connect来比对前后差别控制更新粒度,有时需要自己写SCU;Mobx更加精细一点。

     Mobx核心概念

    图片来自官方文档

    Mobx的核心原理是通过action触发state的变化,进而触发state的衍生对象(computed value & Reactions)。

    State

    在Mobx中,State就对应业务的最原始状态,通过observable方法,可以使这些状态变得可观察。

    通常支持被observable的类型有三个,分别是Object, Array, Map;对于原始类型,可以使用Obserable.box。

    值得注意的一点是,当某一数据被observable包装后,他返回的其实是被observable包装后的类型。

    
    const Mobx = require("mobx");
    const { observable, autorun } = Mobx;
    const obArray = observable([1, 2, 3]);
    console.log("ob is Array:", Array.isArray(obArray));
    console.log("ob:", obArray);
    

    控制台输出为:

    
    ob is Array: false
    ob: ObservableArray {}
    

    对于该问题,解决方法也很简单,可以通过Mobx原始提供的observable.toJS()转换成JS再判断,或者直接使用Mobx原生提供的APIisObservableArray进行判断。

    computed

    Mobx中state的设计原则和redux有一点是相同的,那就是尽可能保证state足够小,足够原子。这样设计的原则不言而喻,无论是维护性还是性能。那么对于依赖state的数据而衍生出的数据,可以使用computed。

    简而言之,你有一个值,该值的结果依赖于state,并且该值也需要被obserable,那么就使用computed。

    通常应该尽可能的使用计算属性,并且由于其函数式的特点,可以最大化优化性能。如果计算属性依赖的state没改变,或者该计算值没有被其他计算值或响应(reaction)使用,computed便不会运行。在这种情况下,computed处于暂停状态,此时若该计算属性不再被observable。那么其便会被Mobx垃圾回收。

    简单介绍computed的一个使用场景

    假如你观察了一个数组,你想根据数组的长度变化作出反应,在不使用computed时代码是这样的

    
    const Mobx = require("mobx");
    const { observable, autorun, computed } = Mobx;
    var numbers = observable([1, 2, 3]);
    autorun(() => console.log(numbers.length));
    // 输出 '3'
    numbers.push(4);
    // 输出 '4'
    numbers[0] = 0;
    // 输出 '4'
    

    最后一行其实只是改了数组中的一个值,但是也触发了autorun的执行。此时如果用computed便会解决该问题。

    
    const Mobx = require("mobx");
    const { observable, autorun, computed } = Mobx;
    var numbers = observable([1, 2, 3]);
    var sum = computed(() => numbers.length);
    autorun(() => console.log(sum.get()));
    // 输出 '3'
    numbers.push(4);
    // 输出 '4'
    numbers[0] = 1;
    

    autorun

    另一个响应state的api便是autorun。和computed类似,每当依赖的值改变时,其都会改变。不同的是,autorun没有了computed的优化(当然,依赖值未改变的情况下也不会重新运行,但不会被自动回收)。因此在使用场景来说,autorun通常用来执行一些有副作用的。例如打印日志,更新UI等等。

    action

    在redux中,唯一可以更改state的途径便是dispatch一个action。这种约束性带来的一个好处是可维护性。整个state只要改变必定是通过action触发的,对此只要找到reducer中对应的action便能找到影响数据改变的原因。强约束性是好的,但是Redux要达到约束性的目的,似乎要写许多样板代码,虽说有许多库都在解决该问题,然而Mobx从根本上来说会更加优雅。

    首先Mobx并不强制所有state的改变必须通过action来改变,这主要适用于一些较小的项目。对于较大型的,需要多人合作的项目来说,可以使用Mobx提供的api configure来强制。

    
    Mobx.configure({enforceActions: true})
    

    其原理也很简单

    
    function configure(options){
    
        if (options.enforceActions !== undefined) {
            globalState.enforceActions = !!options.enforceActions
            globalState.allowStateChanges = !options.enforceActions
        }
    
    }
    

    通过改变全局的strictMode以及allowStateChanges属性的方式来实现强制使用action。

    Mobx异步处理

    和Redux不同的是,Mobx在异步处理上并不复杂,不需要引入额外的类似redux-thunkredux-saga这样的库。

    唯一需要注意的是,在严格模式下,对于异步action里的回调,若该回调也要修改observable的值,那么

    该回调也需要绑定action。

    
    const Mobx = require("mobx");
    Mobx.configure({ enforceActions: true });
    const { observable, autorun, computed, extendObservable, action } = Mobx;
    class Store {
      @observable a = 123;
    
      @action
      changeA() {
        this.a = 0;
        setTimeout(this.changeB, 1000);
      }
      @action.bound
      changeB() {
        this.a = 1000;
      }
    }
    var s = new Store();
    autorun(() => console.log(s.a));
    s.changeA();
    

    这里用了action.bound语法糖,目的是为了解决javascript作用域问题。

    另外一种更简单的写法是直接包装action

    
    const Mobx = require("mobx");
    Mobx.configure({ enforceActions: true });
    const { observable, autorun, computed, extendObservable, action } = Mobx;
    class Store {
      @observable a = 123;
      @action
      changeA() {
        this.a = 0;
        setTimeout(action('changeB',()=>{
          this.a = 1000;
        }), 1000);
      }
    }
    var s = new Store();
    autorun(() => console.log(s.a));
    s.changeA();
    

    如果不想到处写action,可以使用Mobx提供的工具函数runInAction来简化操作。

    
    ...
    
     @action
      changeA() {
        this.a = 0;
        setTimeout(
          runInAction(() => {
            this.a = 1000;
          }),
          1000
        );
      }
    

    通过该工具函数,可以将所有对observable值的操作放在一个回调里,而不是命名各种各样的action。

    最后,Mobx提供的一个工具函数,其原理redux-saga,使用ES6的generator来实现异步操作,可以彻底摆脱action的干扰。

    
    @asyncAction
      changeA() {
        this.a = 0;
        const data = yield Promise.resolve(1)
        this.a = data;
      }
    

    Mobx原理分析

    autorun

    Mobx的核心就是通过observable观察某一个变量,当该变量产生变化时,对应的autorun内的回调函数就会发生变化。

    
    const Mobx = require("mobx");
    const { observable, autorun } = Mobx;
    const ob = observable({ a: 1, b: 1 });
    autorun(() => {
      console.log("ob.b:", ob.b);
    });
    
    ob.b = 2;
    

    执行该代码会发现,log了两遍ob.b的值。其实从这个就能猜到,Mobx是通过代理变量的getter和setter来实现的变量更新功能。首先先代理变量的getter函数,然后通过预执行一遍autorun中回调,从而触发getter函数,来实现观察值的收集,依次来代理setter。之后只要setter触发便执行收集好的回调就ok了。
    具体源码如下:

    function autorun(view, opts){
        reaction = new Reaction(name, function () {
               this.track(reactionRunner);
        }, opts.onError);
       function reactionRunner() {
            view(reaction);
        }
    }

    autorun的核心就是这一段,这里view就是autorun里的回调函数。具体到track函数,比较关键到代码是:

    Reaction.prototype.track = function (fn) {
        var result = trackDerivedFunction(this, fn, undefined);
    }

    trackDerivedFunction函数中会执行autorun里的回调函数,紧接着会触发obserable中代理的函数:

    function generateObservablePropConfig(propName) {
        return (observablePropertyConfigs[propName] ||
            (observablePropertyConfigs[propName] = {
                configurable: true,
                enumerable: true,
                get: function () {
                    return this.$mobx.read(this, propName);
                },
                set: function (v) {
                    this.$mobx.write(this, propName, v);
                }
            }));
    }

    在get中会将回调与其绑定,之后更改了obserable中的值时,都会触发这里的set,然后随即触发绑定的函数。

    Mobx的一些坑

    通过autorun的实现原理可以发现,会出现很多我们想象中应该触发,但是没有触发的场景,例如:

    1. 无法收集新增的属性

    
    const Mobx = require("mobx");
    const { observable, autorun } = Mobx;
    let ob = observable({ a: 1, b: 1 });
    autorun(() => {
      if(ob.c){
        console.log("ob.c:", ob.c);
      }
    });
    ob.c = 1
    

    对于该问题,可以通过extendObservable(target, props)方法来实现。

    
    const Mobx = require("mobx");
    const { observable, autorun, computed, extendObservable } = Mobx;
    var numbers = observable({ a: 1, b: 2 });
    extendObservable(numbers, { c: 1 });
    autorun(() => console.log(numbers.c));
    numbers.c = 3;
    
    // 1
    
    // 3
    

    extendObservable该API会可以为对象新增加observal属性。

    当然,如果你对变量的entry增删非常关心,应该使用Map数据结构而不是Object。

    2. 回调函数若依赖外部环境,则无法进行收集

    
    const Mobx = require("mobx");
    const { observable, autorun } = Mobx;
    let ob = observable({ a: 1, b: 1 });
    let x = 0;
    autorun(() => {
      if(x == 1){
        console.log("ob.c:", ob.b);
      }
    });
    x = 1;
    ob.b = 2;
    

    很好理解,autorun的回调函数在预执行的时候无法到达ob.b那一行代码,所以收集不到。

    参考链接:

    1. www.zhihu.com/question/52…
    2. taobaofed.org/blog/2016/0…
    3.  Mobx.js.org/index.html

    查看原文

    传统React使用的数据管理库为Redux。Redux要解决的问题是统一数据流,数据流完全可控并可追踪。要实现该目标,便需要进行相关的约束。Redux由此引出了dispatch action reducer等概念,对state的概念进行强约束。然而对于一些项目来说,太过强,便失去了灵活性。Mobx...

    赞 12 收藏 10 评论 1

    不可能的是 赞了文章 · 2月14日

    也来说说touch事件与点击穿透问题

    前言

    做过移动端H5页面的同学肯定知道,移动端web的事件模型不同于PC页面的事件。看了一些关于touch事件的文章,我想再来回顾下touch事件的原理,为什么通过touch可以触发click事件,touch事件是不是万能的以及它可能存在的问题。

    touch事件的来源

    PC网页上的大部分操作都是用鼠标的,即响应的是鼠标事件,包括mousedownmouseupmousemoveclick事件。一次点击行为,事件的触发过程为:mousedown -> mouseup -> click 三步。

    手机上没有鼠标,所以就用触摸事件去实现类似的功能。touch事件包含touchstarttouchmovetouchend,注意手机上并没有tap事件。手指触发触摸事件的过程为:touchstart -> touchmove -> touchend

    手机上没有鼠标,但不代表手机不能响应mouse事件(其实是借助touch去触发mouse事件)。有人在PC和手机上对事件做了对比实验,以说明手机对touch事件相应速度快于mouse事件。

    19161138-7c39b72bc6c048738962c042d1df766f.png

    可以看到在手机上,当我们手触碰屏幕时,要过300ms左右才会触发mousedown事件,所以click事件在手机上看起来就像慢半拍一样。

    touch事件中可以获取以下参数

    参数 含义
    touches 屏幕中每根手指信息列表
    targetTouches 和touches类似,把同一节点的手指信息过滤掉
    changedTouches 响应当前事件的每根手指的信息列表

    tap是怎么来的

    用过Zepto或KISSY等移动端js库的人肯定对tap事件不陌生,我们做PC页面时绑定click,相应地手机页面就绑定tap。但原生的touch事件本身是没有tap的,js库里提供的tap事件都是模拟出来的。

    我们在上面看到,手机上响应 click 事件会有300ms的延迟,那么这300ms到底是干嘛了?浏览器在 touchend 后会等待约300ms,原因是判断用户是否有双击(double tap)行为。如果没有 tap 行为,则触发 click 事件,而双击过程中就不适合触发 click 事件了。由此可以看出 click 事件触发代表一轮触摸事件的结束。

    既然说tap事件是模拟出来的,我们可以看下Zepto对 singleTap 事件的处理。见源码 136-143 行,可以看出在 touchend 响应 250ms 无操作后,则触发singleTap。

    点击穿透的场景

    有了以上的基础,我们就可以理解为什么会出现点击穿透现象了。我们经常会看到“弹窗/浮层”这种东西,我做个了个demo。

    20151004_01.jpg

    整个容器里有一个底层元素的div,和一个弹出层div,为了让弹出层有模态框的效果,我又加了一个遮罩层。

    <div class="container">
        <div id="underLayer">底层元素</div>
    
        <div id="popupLayer">
            <div class="layer-title">弹出层</div>
            <div class="layer-action">
                <button class="btn" id="closePopup">关闭</button>
            </div>
        </div>
    </div>
    <div id="bgMask"></div>
    

    然后为底层元素绑定 click 事件,而弹出层的关闭按钮绑定 tap 事件。

    $('#closePopup').on('tap', function(e){
        $('#popupLayer').hide();
        $('#bgMask').hide();
    });
    
    $('#underLayer').on('click', function(){
        alert('underLayer clicked');
    });
    

    点击关闭按钮,touchend首先触发tap,弹出层和遮罩就被隐藏了。touchend后继续等待300ms发现没有其他行为了,则继续触发click,由于这时弹出层已经消失,所以当前click事件的target就在底层元素上,于是就alert内容。整个事件触发过程为 touchend -> tap -> click。

    而由于click事件的滞后性(300ms),在这300ms内上层元素隐藏或消失了,下层同样位置的DOM元素触发了click事件(如果是input框则会触发focus事件),看起来就像点击的target“穿透”到下层去了。

    完整demo请用chrome手机模拟器查看,或直接扫描二维码在手机上查看。

    结合Zepto源码的解释

    zepto中的 tap 通过兼听绑定在 document 上的 touch 事件来完成 tap 事件的模拟的,是通过事件冒泡实现的。在点击完成时(touchstart / touchend)的 tap 事件需要冒泡到 document 上才会触发。而在冒泡到 document 之前,手指接触和离开屏幕(touchstart / touchend)是会触发 click 事件的。

    因为 click 事件有延迟(大概是300ms,为了实现safari的双击事件的设计),所以在执行完 tap 事件之后,弹出层立马就隐藏了,此时 click 事件还在延迟的 300ms 之中。当 300ms 到来的时候,click 到的其实是隐藏元素下方的元素。

    如果正下方的元素有绑定 click 事件,此时便会触发,如果没有绑定 click 事件的话就当没发生。如果正下方的是 input 输入框(或是 select / radio / checkbox),点击默认 focus 而弹出输入键盘,也就出现了上面的“点透”现象。

    穿透的解决办法

    1. 遮挡

    由于 click 事件的滞后性,在这段时间内原来点击的元素消失了,于是便“穿透”了。因此我们顺着这个思路就想到,可以给元素的消失做一个fade效果,类似jQuery里的fadeOut,并设置动画duration大于300ms,这样当延迟的 click 触发时,就不会“穿透”到下方的元素了。

    同样的道理,不用延时动画,我们还可以动态地在触摸位置生成一个透明的元素,这样当上层元素消失而延迟的click来到时,它点击到的是那个透明的元素,也不会“穿透”到底下。在一定的timeout后再将生成的透明元素移除。具体可见demo

    2. pointer-events

    pointer-events是CSS3中的属性,它有很多取值,有用的主要是autonone,其他属性值为SVG服务。

    取值 含义
    auto 效果和没有定义 pointer-events 属性相同,鼠标不会穿透当前层。
    none 元素不再是鼠标事件的目标,鼠标不再监听当前层而去监听下面的层中的元素。但是如果它的子元素设置了pointer-events为其它值,比如auto,鼠标还是会监听这个子元素的。

    关于使用 pointer-events 后的事件冒泡,有人做了个实验,见代码

    因此解决“穿透”的办法就很简单,demo如下

    $('#closePopup').on('tap', function(e){
        $('#popupLayer').hide();
        $('#bgMask').hide();
    
        $('#underLayer').css('pointer-events', 'none');
    
        setTimeout(function(){
            $('#underLayer').css('pointer-events', 'auto');
        }, 400);
    });
    

    3. fastclick

    使用fastclick库,其实现思路是,取消 click 事件(参看源码 164-173 行),用 touchend 模拟快速点击行为(参看源码 521-610 行)。

    FastClick.attach(document.body);
    

    从此所有点击事件都使用click,不会出现“穿透”的问题,并且没有300ms的延迟。解决穿透的demo

    有人(叶小钗)对事件机制做了详细的剖析,循循善诱,并剖析了fastclick的源码以自己模拟事件的创建。请看这篇文章,看完后一定会对移动端的事件有更深的了解

    参考资料

    本文最早发表在我的个人博客上,转载请保留出处jsorz.cn/blog/2015/1…

    查看原文

    做过移动端H5页面的同学肯定知道,移动端web的事件模型不同于PC页面的事件。看了一些关于touch事件的文章,我想再来回顾下touch事件的原理,为什么通过touch可以触发click事件,touch事件是不是万能的以及它可能存在的问题。

    赞 50 收藏 194 评论 9

    不可能的是 赞了文章 · 1月30日

    说说动画卡顿的解决方案

    CSS3 动画卡顿解决方案

    前端时间用animation实现H5页面中首页动画过渡,很简单的一个效果,首页加载一个客服头像,先放大,停留700ms后再缩小至顶部。代码如下

    <!DOCTYPE html>
    <html>
    <head lang="zh-cn">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=1" >
        <script type="text/javascript" data-original="http://apps.bdimg.com/libs/jquery/2.1.1/jquery.min.js"></script>
        <title>首页加载动画</title>
        <head>
            <style>
                .welcome-main{
                    display: none;
                    padding-bottom: 40px;
                }
                .top-info{
                    width: 100%;
                    position: absolute;
                    left: 0;
                    top: 93px;
                }
                .wec-img{
                    width: 175px;
                    height: 175px;
                    position: relative;
                    padding: 23px;
                    box-sizing: border-box;
                    margin: 0 auto;
                }
                .wec-img:before{
                    content: '';
                    position: absolute;
                    left: 0;
                    top: 0;
                    width: 100%;
                    height: 100%;
                    background: url("./images/kf-welcome-loading.png");
                    background-size: 100%;
                }
                .wec-img .img-con{
                    width: 100%;
                    height: 100%;
                    border-radius: 50%;
                    /*box-sizing: border-box;*/
                    background: url("./images/kf_1.jpg");
                    background-size: 100%;
                    padding: 1px;
                }
                .wec-img .img-con img{
                    width: 100%;
                    height: 100%;
                    border-radius: 50%;
                }
                .loaded .wec-img{
                    -webkit-transform-origin: center top;
                }               
                .loading.welcome-main{
                    display: block;
                }
                .loading .wec-img{
                    -webkit-animation:fadeIn .3s  ease both;
                }
                .loading .wec-img:before{
                    -webkit-animation:rotate .6s .2s linear both;
                }
                .loaded .top-info{
                    -webkit-animation:mainpadding 1s 0s ease both;
                }
                .loaded .wec-img{
                    -webkit-animation:imgSmall 1s 0s ease both;
                }
                @-webkit-keyframes mainpadding{
                    0%{
                        -webkit-transform:translateY(0)
                    }
                    100%{
                        -webkit-transform:translateY(-87px)
                    }
                }
                @-webkit-keyframes imgSmall{
                    0%{
                        width: 175px;
                        height: 175px;
                        padding: 23px;
                
                    }
                    100%{
                        width: 60px;
                        height: 60px;
                        padding: 0;
                
                    }
                }
                @-webkit-keyframes fadeIn{
                    0%{opacity:0;-webkit-transform:scale(.3)}
                    100%{opacity:1;-webkit-transform:scale(1)}
                }
                @-webkit-keyframes rotate{
                    0%{opacity:0;-webkit-transform:rotate(0deg);}
                    50%{opacity:1;-webkit-transform:rotate(180deg);}
                    100%{opacity:0;-webkit-transform:rotate(360deg);}
                }
              </style>
            <body>
                <div class="welcome-main">
                    <div class="top-info">
                        <div class="wec-img"><p class="img-con"><img data-original="" alt=""></p></div>
                    </div>
                </div>
                <script>
                    $('.welcome-main').addClass('loading');
                    setTimeout(function(){
                        $('.hi.fst').removeClass('loading');
                        $('.welcome-main').addClass('loaded');
                    },700);
                
                </script>
            </body>
        </html>
        

    在chrome上测试ok,但在提测给QA的时候发现部分机型,如华为,系统4.2,oppo系统5.1的出现卡顿情况。

    百思不得其解,后来参考文章深入浏览器理解CSS animations 和 transitions的性能问题一文,将图片缩放中动画元素改成transform,如下

      @-webkit-keyframes imgSmall{
        0%{
            -webkit-transform:scale(1);
        }
        100%{
            -webkit-transform:scale(.465);
        }
      }
    

    果然啊,卡顿问题解决了。

    文章深入浏览器理解CSS animations 和 transitions的性能问题是这么解释的,现代的浏览器通常会有两个重要的执行线程,这2个线程协同工作来渲染一个网页:主线程和合成线程。

    一般情况下,主线程负责:运行JavaScript;计算HTML 元素的 CSS 样式;页面的布局;将元素绘制到一个或多个位图中;将这些位图交给合成线程。

    相应地,合成线程负责:通过 GPU将位图绘制到屏幕上;通知主线程更新页面中可见或即将变成可见的部分的位图;计算出页面中哪部分是可见的;计算出当你在滚动页面时哪部分是即将变成可见的;当你滚动页面时将相应位置的元素移动到可视区域。

    假设我们要一个元素的height从 100 px 变成 200 px,就像这样:

    div {
        height: 100px;
        transition: height 1s linear;
    }
    
    div:hover {
        height: 200px;
    }
    

    主线程和合成线程将按照下面的流程图执行相应的操作。注意在橘黄色方框的操作可能会比较耗时,在蓝色框中的操作是比较快速的。

    Alt text

    而使用transform:scale实现

    div {
        transform: scale(0.5);
        transition: transform 1s linear;
    }
    
    div:hover {
        transform: scale(1.0);
    }
    

    此时流程如下:

    Alt text

    也就是说,使用transform,浏览器只需要一次生成这个元素的位图,并在动画开始的时候将它提交给GPU去处理 。之后,浏览器不需要再做任何布局、 绘制以及提交位图的操作。从而,浏览器可以充分利用 GPU 的特长去快速地将位图绘制在不同的位置、执行旋转或缩放处理。

    为了从数量级上去证实这个理论,我打开chrome的Timeline查看页面FPS

    图片描述

    其中,当用height做动画元素时,在切换过程的FPS只有44,我们知道每秒60帧是最适合人眼的交互,小于60,人眼能明显感觉到,这就是为什么卡顿的原因。

    图片描述

    rendering和painting所花的时间如下:

    图片描述

    再来看看用transform:scale

    图片描述

    FPS达到66,且rendering和painting时间减少了3倍。

    到此为止问题是解决了,隔了几天,看到一篇解决Chrome动画”卡顿”的办法,发现还能通过开启硬件加速的方式优化动画,于是又试了一遍。

    webkit-transform: translate3d(0,0,0);
    -moz-transform: translate3d(0,0,0);
    -ms-transform: translate3d(0,0,0);
    -o-transform: translate3d(0,0,0);
    transform: translate3d(0,0,0);
    

    惊人的事情发生了,FPS达到72:

    图片描述

    图片描述

    总结解决CSS3动画卡顿方案

    1. 尽量使用transform当成动画熟悉,避免使用height,width,margin,padding等;

    2. 要求较高时,可以开启浏览器开启GPU硬件加速。

    参考文章

    1. 深入浏览器理解CSS animations 和 transitions的性能问题

    2. 解决Chrome动画”卡顿”的办法

    查看原文

    前端时间用animation实现H5页面中首页动画过渡,很简单的一个效果,首页加载一个客服头像,先放大,停留700ms后再缩小至顶部。代码如下

    赞 15 收藏 79 评论 4

    不可能的是 赞了文章 · 1月21日

    使用css时,可能会出错的两个地方

    本文首发于公众号:符合预期的CoyPan

    写在前面

    css大家都很熟悉了,这里就不多介绍了。本文主要介绍一下两个在日常的工作中可能会出错的地方。

    margin-top 与 padding-top

    这两个属性大家都很熟悉了,margin-top表示外部的上边距,padding-top表示内部的上边距。

    取值可以是一个具体的值或者一个百分比,如:

    margin-top: 10px;
    margin-top: 10%;
    
    padding-top: 20px;
    margin-top: 20%;

    当取值为具体的值时,没有什么好说的。当取值为百分比时,需要特别注意:百分比不是相对于父元素的高度的,而是相对于父元素的宽度的

    w3c标准如下:

    clipboard.png

    clipboard.png

    直接看例子:

    clipboard.png

    用处:可以用来在页面中显示 固定宽高比的图片

    注意:heighttop的百分比取值,总是相对于父元素的高度

    这里提一下,w3cSchool中文站中,关于margtin-top的描述是错误的。地址在这里:www.w3school.com.cn/css/pr_marg…

    clipboard.png

    position: fixed

    一提到position:fixed,自然而然就会想到:相对于浏览器窗口进行定位

    但其实这是不准确的。如果说父元素设置了transform,那么设置了position:fixed的元素将相对于父元素定位,否则,相对于浏览器窗口进行定位。

    w3c的官方标准如下:

    clipboard.png

    看例子:

    clipboard.png

    .parent加上transform:translateY(0)以后,

    clipboard.png

    总结

    • padding-topmargin-toppadding-bottommargin-bottom取值为百分比时,是相对于父元素的宽度
    • position:fixed,相对于浏览器窗口定位。例外:父代元素中,有元素设置了transform,则postion:fixed相对于设置了transform的父元素定位。

    写在后面

    本文总结了平时css开发中需要稍微注意一下的,可能会出错的两个问题。符合预期。


    欢迎关注我的公众号: 符合预期的CoyPan
    这里只有干货,符合你的预期
    图片描述

    查看原文

    这两个属性大家都很熟悉了,margin-top表示外部的上边距,padding-top表示内部的上边距。

    赞 43 收藏 25 评论 8

    不可能的是 赞了文章 · 1月21日

    宇宙最强vscode教程(基础篇)

    本文主要介绍vscode在工作中常用的快捷键及插件,目标在于提高工作效率

    本文的快捷键是基于mac的,windows下的快捷键放在括号里 Cmd+Shift+P(win Ctrl+Shift+P)

    [TOC]

    零、快速入门

    有经验的可以跳过快速入门或者大致浏览一遍

    1. 命令面板

    命令面板是vscode快捷键的主要交互界面,可以使用f1或者Cmd+Shift+P(win Ctrl+Shift+P)打开。

    在命令面板中你可以输入命令进行搜索(中英文都可以),然后执行。

    命名面板中可以执行各种命令,包括编辑器自带的功能和插件提供的功能。

    所以一定要记住它的快捷键Cmd+Shift+P

    image-20190120143658078

    2. 界面介绍

    刚上手使用vscode时,建议要先把它当做一个文件编辑器(可以打字然后保存),等到有了一定经验再去熟悉那些快捷键

    先来熟悉一下界面及快捷命令(不用记)

    3. 在命令行中使用vscode

    如果你是 Windows用户,安装并重启系统后,你就可以在命令行中使用 code 或者 code-insiders了,如果你希望立刻而不是等待重启后使用,可以将 VS Code 的安装目录添加到系统环境变量 PATH

    如果你是mac用户,安装后打开命名面板Cmd+Shift+P,搜索shell命令,点击在PAth中安装code命令,然后重启终端就ok了

    image-20190120144757840

    最基础的使用就是使用code命令打开文件或文件夹

    code 文件夹地址,vscode 就会在新窗口中打开该文件夹

    如果你希望在已经打开的窗口打开文件,可以使用-r参数

    vscode命令还有其他功能,比如文件比较,打开文件跳转到指定的行和列,如有需要自行百度:bowing_woman:

    注意:

    在继续看文章之前记住记住打开命令面板的快捷键Cmd+shift+P(win下是Ctrl+shift+p)

    一、代码编辑

    windows下的快捷键放在括号里

    光标的移动

    基础

    1. 移动到行首 Cmd+左方向键 (win Home)
    2. 移动到行尾 Cmd+右方向键 (win End)
    3. 移动到文档的开头和末尾 Cmd+上下方向键 (win Ctrl+Home/End)
    4. 在花括号{}左边右边之间跳转 Cmd+Shift+ (win Ctrl+Shift+)

    进阶

    1. 回到上一个光标的位置,Cmd+U(win Ctrl+U) 非常有用,有时候vue文件,你改了html,需要去下面改js,改完js又需要回去,这时候Cmd+U直接回
    2. 在不同的文件之间回到上一个光标的位置 Control+- (win 没测试,不知道),你改了a文件,改了b文件之后想回到a文件继续编辑,mac使用controls+-

    文本选择

    1. 你只需要多按一个shift键就可以在光标移动的时候选中文本
    2. 选中单词 Cmd+D 下面要讲的多光标也会讲到Cmd+D
    3. 对于代码块的选择没有快捷键,可以使用cmd+shift+p打开命令面板,输入选择括号所有内容,待会说下如何添加快捷键

    1

    删除

    1. 你可以选中了代码之后再删除,再按Backpack(是backpack吗)或者delete删除,但是那样做太low了
    2. 所以,最Geek的删除方式是Cmd+Shift+K (win Ctrl+Shift+K),想删多少删多少,当前你可以使用ctrl+x剪切,效果一样的

    2

    代码移动

    • Option+上下方向键(win Alt+上下)

    3

    • 代码移动的同时按住shift就可以实现代码复制 Option+Shift+上下3

    添加注释

    注释有两种形式,单行注释和块注释(在js中,单行注释//,块注释/**/)

    • 单行注释 Cmd+/ (win Ctrl +/)
    • 块注释 Option+Shift+A

    注意:不同语言使用的注释不同

    二、代码格式

    代码格式化

    • 对整个文档进行格式化:Option+Shift+F (win Alt+Shift+F),vscode会根据你使用的语言,使用不同的插件进行格式化,记得要下载相应格式化的插件
    • 对选中代码进行格式化: Cmd+K Cmk+F win(Ctrl+K Ctrl+F)

    代码缩进

    • 真个文档进行缩进调节,使用Cmd+Shift+P打开命令面板,输入缩进,然后选择相应的命令
    • 选中代码缩进调节:Cmd+] Cmd+[ 分别是减小和增加缩进(win 下不知道,自行百度)

    三、一些小技巧

    • 调整字符的大小写,选中,然后在命令面板输入转化为大写或者转化为小写

    • 合并代码行,多行代码合并为一行,Cmd+J(win下未绑定)

    • 行排序,将代码行按照字母顺序进行排序,无快捷键,调出命令面板,输入按升序排序或者按降序排序

    四、多光标特性

    使用鼠标:

    按住Option(win Alt),然后用鼠标点,鼠标点在哪里哪里就会出现一个光标

    注意:有的mac电脑上是按住Cmd,然后用鼠标点才可以

    快捷命令

    1. Cmd+D (win Ctrl+D) 第一次按下时,它会选中光标附近的单词;第二次按下时,它会找到这个单词第二次出现的位置,创建一个新的光标,并且选中它。(注:cmd-k cmd-d 跳过当前的选择)

    2. Option+Shift+i (win Alt+Shift+i) 首先你要选中多行代码,然后按Option+Shift+i,这样做的结果是:每一行后面都会多出来一个光标

    撤销多光标

    • 使用Esc 撤销多光标
    • 鼠标点一下撤销

    五、快速跳转(文件、行、符号)

    快速打开文件

    Cmd+P (win Ctrl+P)输入你要打开的文件名,回车打开

    这里有个小技巧,选中你要打开的文件后,按Cmd+Enter,就会在一个新的编辑器窗口打开(窗口管理,见下文)

    在tab不同的文件间切换,cmd+shift+[]

    行跳转

    加入浏览器报了个错,错误在53行,如何快速跳转到53行

    Ctrl+g 输入行号

    如果你想跳转到某个文件的某一行,你只需要先按下 “Cmd + P”,输入文件名,然后在这之后加上 “:”和指定行号即可。

    符号跳转

    符号可以是文件名、函数名,可以是css的类名

    Cmd+Shift+O(win Ctrl+Shift+o) 输入你要跳转的符号,回车进行跳转

    win下输入Ctrl+T,可以在不同文件的符号间进行搜索跳转

    定义(definition)和实现(implementation)处

    f12跳到函数的定义处

    Cmd+f12(win Ctrl+f12)跳转到函数的实现处

    引用跳转

    很多时候,除了要知道一个函数或者类的定义和实现以外,你可能还希望知道它们被谁引用了,以及在哪里被引用了。这时你只需要将光标移动到函数或者类上面,然后按下 Shift + F12,VS Code 就会打开一个引用列表和一个内嵌的编辑器。在这个引用列表里,你选中某个引用,VS Code 就会把这个引用附近的代码展示在这个内嵌的编辑器里。

    六、代码重构

    当我们想修改一个函数或者变量的名字时候,我们只需把光标放到函数或者变量名上,然后按下 F2,这样这个函数或者变量出现的地方就都会被修改。

    查看原文

    本文主要介绍vscode在工作中常用的快捷键及插件,目标在于提高工作效率本文的快捷键是基于mac的,windows下的快捷键放在括号里 Cmd+Shift+P(win Ctrl+Shift+P)

    赞 296 收藏 222 评论 8

    不可能的是 赞了文章 · 1月20日

    从前端角度理解缓存

    缓存的概念分很多种,本次讨论的主要就是前端缓存中的Http缓存。

    缓存是怎么回事

    前端发送请求主要经历以下三个过程,请求->处理->响应。如果有多次请求就需要重复执行这个过程。

    重复请求的过程

    以下是一个重复请求的流程图:

    重复请求

    从以上的流程图可以看书,如果用户重复请求同一资源的话,会对服务器资源造成浪费,服务器重复读取资源,发送给浏览器后浏览器重复下载,造成不必要的等待与消耗。

    缓存读取的过程

    缓存读取就是浏览器在向服务器请求资源之前,先查询一下本地缓存中是否存在需要的资源,如果存在,那便优先从缓存中读取。当缓存不存在或者过期,再向服务器发送请求。

    缓存读取

    如何开启Http缓存并对缓存进行设置,是本次讨论的关键。

    缓存的类型

    浏览器有如下常见的几个字段:

    1. expires: 设置缓存过期的时间
    2. private: 客户端可以缓存
    3. public: 客户端和代理服务器都可缓存
    4. max-age=xxx: 缓存的内容将在 xxx 秒后失效
    5. no-cache: 需要使用对比缓存来验证缓存数据
    6. no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发
    7. last-modified: 内容上次被修改的时间
    8. Etag: 文件的特殊标识

    强制缓存和协商缓存

    缓存方法可以分为强制缓存与协商缓存。

    从字面理解,强制缓存的方式简单粗暴,给cache设置了过期时间,超过这个时间之后cache过期需要重新请求。上述字段中的expirescache-control中的max-age都属于强制缓存。

    协商缓存根据一系列条件来判断是否可以使用缓存。

    强制缓存优先级高于协商缓存

    强制缓存

    expires

    expires给浏览器设置了一个绝对时间,当浏览器时间超过这个绝对时间之后,重新向服务器发送请求。

    Expires: Fri, 04 Jan 2019 12:00:00 GMT

    这个方法简单直接,直接设定一个绝对的时间 (当前时间+缓存时间)。但是也存在隐患,例如浏览器当前时间是可以进行更改的,更改之后expires设置的绝对时间相对不准确,cache可能会出现长久不过期或者很快就过期的情况。

    cache-control: max-age

    为了解决expires存在的问题,Http1.1版本中提出了cache-control: max-age,该字段与expires的缓存思路相同,都是设置了一个过期时间,不同的是max-age设置的是相对缓存时间开始往后多久,因此不存在受日期不准确情况的影响。

    但是强制缓存存在一个问题,该缓存方式优先级高,如果在过期时间内缓存的资源在服务器上更新了,客服端不能及时获取最新的资源。

    协商缓存

    协商缓存解决了无法及时获取更新资源的问题。以下两组字段,都可以对资源做标识,由服务器做分析,如果未进行更新,那返回304状态码,从缓存中读取资源,否则重新请求资源。

    last-modify

    last-modify告知了客户端上次修改该资源的时间,

    Last-Modified: Wed, 02 Jan 2019 03:06:03 GMT

    浏览器将这个值记录在if-modify-since中(浏览器自动记录了该字段信息),下一次请求相同资源时,与服务器返回的last-modify进行比对,如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。

    last-modify以秒为单位进行更新,如果小于该单位高频进行更新的话,不适合采用该方法。

    ETag

    ETag是对资源的特殊标识

    Etag: W/"e563df87b65299122770e0a84ada084f"

    请求该资源成功之后,将返回的ETag存入if-none-match字段中(浏览器自动记录了该字段信息),同样在请求资源时传递给服务器,服务器查询该编码对应的资源有无更新,无更新返回304状态,更新返回200并重新请求。

    以下有个小例子,查询书籍更新:

    当书籍信息查询之后,再次查询,服务器根据资源的ETag查询得知该资源没有进行更新,返回304状态码。

    书籍信息(旧)

    更新返回的数据信息,再次查询,返回200状态码,重新进行请求:

    书籍信息(新)

    从返回的Request Headers可以看出,再次请求时,浏览器自动发送了If-Modified-SinceIf-None-Match两个字段,浏览器根据这两个字段中(If-None-Match 优先级大于 If-Modified-Since)来判断是否修改了资源。

    image

    ETag如何计算

    ETag是针对某个文件的特殊标识,服务器默认采用SHA256算法生成。也可以采用其他方式,保证编码的唯一性即可。

    缓存的优先级

    根据上文优缺点的比对,可以得出以下的优先级顺序:

    Cache-Control > Expires > ETag > Last-Modified

    如果资源需要用到强制缓存,Cache-Control相对更加安全,协商缓存中利用ETag查询更新更加全面。

    缓存的判断流程

    图片来源:浏览器缓存机制详解

    缓存存储在哪

    disk cache

    disk cache为存储在硬盘中的缓存,存储在硬盘中的资源相对稳定,不会随着tab或浏览器的关闭而消失,可以用来存储大型的,需长久使用的资源。

    当硬盘中的资源被加载时,内存中也存储了该资源,当下次改资源被调用时,会优先从memory cache中读取,加快资源的获取。

    memory cache

    memory cache即存储在内存中的缓存,内存中的内容会随着tab的关闭而释放。

    当接口状态返回304时,资源默认存储在memory cache中,当页面关闭后,重新打开需要再次请求。

    这两种存储方式的区别可以参考该回答

    When you visit a URL in Chrome, the HTML and the other assets(like images) on the page are stored locally in a memory and a disk cache. Chrome will use the memory cache first because it is much faster, but it will also store the page in a disk cache in case you quit your browser or it crashes, because the disk cache is persistent.

    当您访问chrome中的URL时,页面上的HTML和其他资产(如图像)将本地存储在内存和磁盘缓存中。Chrome将首先使用内存缓存,因为它的速度快得多,但它也会将页面存储在磁盘缓存中,以防您退出浏览器或它崩溃,因为磁盘缓存是持久的。

    为什么有的资源一会from disk cache,一会from memory cache

    三级缓存原理

    1. 先去内存看,如果有,直接加载
    2. 如果内存没有,择取硬盘获取,如果有直接加载
    3. 如果硬盘也没有,那么就进行网络请求
    4. 加载到的资源缓存到硬盘和内存,下次请求可以快速从内存中获取到

    为什么有的请求状态码返回200,有的返回304

    200 from memory cache

    不访问服务器,直接读缓存,从内存中读取缓存。此时的数据时缓存到内存中的,当关闭进程后,也就是浏览器关闭以后,数据将不存在。

    但是这种方式只能缓存派生资源。

    200 from disk cache

    不访问服务器,直接读缓存,从磁盘中读取缓存,当关闭进程时,数据还是存在。

    这种方式也只能缓存派生资源

    304 Not Modified

    访问服务器,发现数据没有更新,服务器返回此状态码。然后从缓存中读取数据。

    薄荷应用

    举一个简单的小🌰,以薄荷的减肥群页面为讨论对象,查看一下资源加载的情况:

    薄荷图片缓存

    这些图片都是从硬盘中读取,因为没有在内存中获取到响应的资源,当我们刷新页面时,这个资源因为从硬盘中读取时,也存储到了内存中,再次获取就是从内存中获取了:
    薄荷图片缓存2

    当我们没有关闭页面时,内存中的资源始终存在,重新打开则内存释放。

    CDN缓存

    CDN边缘节点缓存策略因服务商不同而不同,但一般都会遵循http标准协议,通过http响应头中的Cache-control: max-age的字段来设置CDN边缘节点数据缓存时间。

    当客户端向CDN节点请求数据时,CDN节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;否则,CDN节点就会向源站发出回源请求,从源站拉取最新数据,更新本地缓存,并将最新数据返回给客户端。

    如何合理应用缓存

    强制缓存优先级最高,并且资源的改动在缓存有效期内都不会对缓存产生影响,因此该方法适用于大型且不易修改的的资源文件,例如第三方CSS、JS文件或图片资源,文件后可以加上hash进行版本的区分。建议将此类大型资源存入disk cache,因为存在硬盘中的文件资源不易丢失。

    协商缓存灵活性高,适用于数据的缓存,根据上述方法的对比,采用Etag标识进行对比灵活度最高,并考虑将数据存入内存中,因为内存加载速最快,并且数据体积小,不会占用大量内存资源。

    广而告之

    本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

    欢迎讨论,点个赞再走吧 。◕‿◕。 ~

    查看原文

    前端发送请求主要经历以下三个过程,请求->处理->响应。如果有多次请求就需要重复执行这个过程。

    赞 57 收藏 49 评论 0

    不可能的是 赞了文章 · 1月17日

    让老板虎躯一震的前端技术,KPI杀手

    本文由云+社区发表

    作者:思衍Jax

    img

    天下武功,唯 (wei) 快(fu) 不(bu) 破(po)。

    随着近几年的前端技术的高速发展,越来越多的团队使用 React、Vue 等 SPA 框架作为其主要的技术栈。以 React 应用为例,从性能角度,其最重要的指标可能就是首屏渲染所花费的时间了。那么今天,我们要给大家分享的一个把优化做到极致的故事。

    我们的目标是让 H5 的页面也能够拥有 Native 般的体验,如果你还在寻求什么技术能够让老板虎躯一震(拯救你的KPI),那么这篇文章或许能够帮助到你。

    企鹅辅导课程详情页是什么

    img企鹅辅导详情页

    课程详情页是腾讯旗下企鹅辅导 APP 中最重要页面之一,也是流量最大的页面之一,所以它的打开速度也是至关重要的。

    这是一个使用 React 编写的 H5 页面,运行于多端,包括: 企鹅辅导APP手机 QQ手机浏览器

    架构演变

    纯异步渲染

    我们知道当前主流的 SPA 的应用的默认渲染方式都是这样的:

    img

    在这种情况下,从加载页面到用户看到页面(首屏渲染所花费的时间)就是上图中灰色边框区域所包括的时间。

    这是最慢的一种方式,就算 CGI 够快,最少要花费 1S2S 左右的时间了。

    接着我们简单优化一下:

    • 把静态资源缓存起来,这样下次用户打开的时候就不用从网络请求了。
    • 步拉取 CGI 这个动作是否可以提前呢?我们可以在请求 HTML 之后,先通过一段 JS 脚本去请求 CGI 数据,后面第 步的时候,就可以直接拿到数据了,这就是 CGI 预加载

      怎么做到呢?我们的方案是统一封装 Request 请求工具,在用 Webpack 打包的时候,会往页面顶部注入一段 预加载 CGI 的 JS 代码,维护一个CGI 与 DATA 对应 MAP,后面发请求的时候,先去 MAP 里取值,如果有值的话直接拿出来,没有的话则发起HTTP 请求。(具体请查阅我们团队开源的 Preload 工具)

    img

    这种模式还有一些其他的优化的方法:

    • 在 HTML 内实现 Loading 态或者骨架屏;
    • 去掉外联 css;
    • 使用动态 polyfill;
    • 使用 SplitChunksPlugin 拆分公共代码;
    • 正确地使用 Webpack 4.0 的 Tree Shaking;
    • 使用动态 import,切分页面代码,减小首屏 JS 体积;
    • 编译到 ES2015+,提高代码运行效率,减小体积;
    • 使用 lazyload 和 placeholder 提升加载体验。

    效果如下图所示:

    img异步渲染

    直出同构

    在异步的模式下,除了上述优化,我们还在端内(企鹅辅导 APP、手机 QQ)内做了离线包缓存(腾讯手Q方面独立研发出来的针对手机端优化的方案,简而言之就是将静态资源缓存在手机 APP 内),经过我们的数据测试,首屏渲染大概能够达到秒开(1s左右) 的效果。

    img-w300

    但对有着性能极致追求的我们来说,肯定是不会满意的。

    继续优化,最容易、最大众的套路肯定就是直出(服务端渲染)了。

    img

    现在直出的方案已经有很多很多种,这里也不多做介绍了,如果您想了解更多关于服务端渲染的方案,请参考这篇文章。

    直出针对首屏时间的优化效果是非常明显的,经过我们的测试,数据大概能够提升25%左右。

    直出之后的效果如下图:

    img直出同构

    可以看到对于首屏来说,没有了【加载中...】的等待时间,视觉体验提升了不少。

    PWA 直出

    imgPWA

    针对上述、常见的直出应用来说,我们能够优化的点在哪里呢?让我们来详细分析一波,这也是今天我们要给大家分享的重点。

    首先看看直出应用各个环节的耗时表 (本地环境 2018款 iMac):

    过程名称 过程花费
    Node 内 CGI 拉取 300 ms
    RenderToString 20 ms
    网络耗时 10 ms
    前端HTML渲染 30 ms

    从上面的表中我们看出,直出渲染的耗时的大头还是在 CGI 接口的拉取上。

    我们现在提出两个问题

    • CGI 接口的数据是否可以缓存 ?
    • HTML 又是否可以缓存 ?
    一、接口的动静分离

    img动态信息

    这个页面的接口数据中,有一些数据,是实时变动的, 比如:当前还剩多少个名额、此时此刻课程的价格、用户是否购买过这个课程等。

    这些数据的特性决定了这个数据接口不能够被缓存。(假设将其缓存,那么就会存在可能用户进来看到当前还剩下10个名额,其实课程已经卖光了的情况)

    为了这个时间耗时的大头,我们做了CGI接口的动静分离

    将与用户态、当前时间没有关联的数据(比如课程标题课程上课的时间试听模块的地址等)放在一个接口(静态接口),其他变化的数据放在另一个接口(动态接口)。

    那么可以使用静态的接口来做服务端渲染,好处是第一比较快(少了动态的信息,而且后台也可以做缓存),第二 Node 直出可以做缓存了。

    二、直出 Redis 缓存

    这样我们就可以将那部分静态的、不会经常变动的数据用来直出 HTML,然后将这个 HTML 文件缓存到 Redis 中

    客户端请求此网页,Node 端接受到请求之后,先去 Redis 里拿缓存的 HTML,如果 Redis 缓存没有命中,则拉取静态的 CGI 接口渲染出 HTML存入 Redis。

    客户端拿到 HTML 之后,会立刻渲染,然后再用 JS 去请求动态的数据,渲染到相应的地方。

    img

    做完之后我们可以看到优化效果的提升是非常非常明显的:

    img

    直接从 262ms 提升到了 16ms !(本地环境),简直飞一般的感觉,妈妈再也不用担心领导看耗时了。

    三、PWA 直出缓存
    关于什么是 PWA ,以及如何使用,请移步这篇文章。

    做了 Node 端直出的 HTML 缓存之后,我们接着优化,接着思考,是否可以在客户端也缓存 HTML,这样连网络延时这部分消耗也省掉呢。

    答案就是使用 PWA 在客户端做离线缓存,将我们直出的 HTML 缓存在客户端,每次用户请求的时候,直接从 PWA 离线缓存里取出对应的直出页面(HTML)响应给用户,响应之后紧接着请求 Node 服务更新本地的 PWA 缓存。(如下图所示)

    img

    核心代码:

    self.addEventListener("fetch", event => {  
     // TODO other logic (maybe fetch filter)
    
      // core logic
      event.respondWith(
        caches.open(cacheName).then(function(cache) {
          return cache.match(cacheCourseUrl).then(function(response) {
            var fetchPromise = fetch(cacheCourseUrl).then(function(
              networkResponse
            ) {
              if (networkResponse.status === 200) {
                cache.put(cacheCourseUrl, networkResponse.clone());
              }
              return networkResponse;
            });
            return response || fetchPromise;
          });
        })
      );
    });

    废话不多说,先看效果对比 (左 PWA 直出;右 离线包):

    imgduibi

    从上图可以看出,使用了 PWA 直出缓存之后,首屏渲染基本是毫秒开,可以说与 Native 并肩了。

    经过我们的数据测试,使用 PWA 直出缓存,首屏渲染的时间最好可以到400ms左右级别:

    img

    PWA 直出细节优化

    一、防页面跳动

    因为对接口进行了动静分离,使用静态接口直出页面,然后在客户端拉取动态数据渲染完。这就可能会导致页面的抖动(比如详情页中的试听模块,是在客户端渲染的)。

    img

    因为高度改变了,视觉上就会出现抖动(具体可以参考上面章节直出时候的 GIF 截图)。

    要去掉页面抖动的情况,就必须保证容器的高度在直出时候已经存在了

    比如这个试听模块,其实这个封面图和试听按钮是可以在服务端渲染出来的,而后面的 Video 模块则必须要在客户度渲染(腾讯云 Tcplayer)。

    所以这里可以拆分成:(试听封面 + 按钮 + 时间)服务端渲染 + 底层 Video(客户端渲染)。

    有些需要在客户端计算高度的容器(表现为常放在 ComponentDidMount 里计算),如果它们依赖客户端环境(比如依赖当前系统是安卓还是 IOS),就导致他们肯定不能放在服务端直接渲染出来,这又怎么办呢?

    这里我们的做法,是将这些计算放在 HTML body 之前,通过内联的脚本嵌入,计算出当前环境,给 body 加上一个特定的类(class),然后在这个特定的类下面的元素,就可以通过 css 给予特定的样式。比如下面代码:

    /*
     * 因为在不同的手机 APP 环境内,页面的 padding 是不一样的。
     * 我们要在页面渲染完之前加上相应的 padding 
     */
    var REGEXP_FUDAO_APP = /EducationApp/;
    if (
      typeof navigator !== "undefined" &&
      REGEXP_FUDAO_APP.test(navigator.userAgent)
    ) {
      if (/Android/i.test(navigator.userAgent)) {
        document.body.classList.add("androidFudaoApp");
      } else if (/iPhone|iPad|iPod|iOS/i.test(navigator.userAgent)) {
        if (window.screen.width === 375 && window.screen.height === 812) {
          document.body.classList.add("iphoneXFudaoApp");
        } else {
          document.body.classList.add("iosFudaoApp");
        }
      }
    }
    .androidFudaoApp .tt {
      padding-top: 48px;
      background-position-y: 84px;
    }
    
    .iphoneXFudaoApp .tt {
      padding-top: 88px;
      background-position-y: 124px;
    }
    
    .iosFudaoApp .tt {
      padding-top: 64px;
      background-position-y: 100px;
    }

    然后把这段代码通过构建插入到页面 body 之前。

    img-w500

    防抖动优化效果如下 (左优化完,右未优化):

    imgduibi_doudong

    二、冷启动预加载

    虽然我们做了 PWA 离线缓存,但是对于冷启动来说,客户端里面的 PWA 缓存还是没有的,这样就会导致初次点击页面,渲染速度相对慢一点。

    这里我们可以在 APP 启动的时候,用一个预加载的脚本最大限度的拉取用户可能访问的页面。

    核心代码如下:

    // 预加载页面时, PWA 预缓存课程详情页面的直出
    function prefetchCache(fetchUrl) {
        fetch("https://you preFetch Cgi")
          .then(data => {
            return data.json();
          })
          .then(res => {
            const { courseInfo = [] } = res.result || {};
            courseInfo.forEach(item => {
              if (item.cid) {
                caches.open(cacheName).then(function(cache) {
                  fetch(`${courseURL}?course_id=${item.cid}`).then(function(
                    networkResponse
                  ) {
                    if (networkResponse.status === 200) {
                      cache.put(
                        `${courseURL}?course_id=${item.cid}`,
                        networkResponse.clone()
                      );
                    }
                    // return networkResponse;
                  });
                });
              }
            });
          })
          .catch(err => {
            // To monitor err
          });
    }

    PWA 直出遗留问题

    一、兼容性问题

    随着 PWA 技术的发展,现今大部分手机以及 PC 环境已经支持对 PWA 进行了支持。经过我们的测试发现:安卓基本上都是支持的,IOS 需要11.3以上才支持。

    Service Workers 兼容性图

    img

    二、IOS 渲染问题

    很多的经验告诉我们,外联的 script 标签要放在 body 的后面,因为它会阻塞页面的 DOM 渲染。

    经过测试发现,IOS 的 WebView (UIWebView)渲染机制并不会上述一样,而是要等到后面的 JS 执行完之后才渲染页面,如果是这样,我们的直出渲染优化就没有效果了(因为 HTML 并不在最开始渲染),这里可以使用 script 标签的 asyncdefer 属性来达到异步渲染的作用。

    升级 WkWebView 之后,情况得到改善,渲染正常。

    附录

    参考资料

    此文已由作者授权腾讯云+社区在各渠道发布

    原文:企鹅辅导课程详情页毫秒开的秘密 - PWA 直出

    获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

    查看原文

    随着近几年的前端技术的高速发展,越来越多的团队使用 React、Vue 等 SPA 框架作为其主要的技术栈。以 React 应用为例,从性能角度,其最重要的指标可能就是首屏渲染所花费的时间了。那么今天,我们要给大家分享的一个把优化做到极致的故事。

    赞 72 收藏 50 评论 2

    查看全部 个人动态

    认证与成就

    • 获得 126 次点赞
    • 获得 10 枚徽章 获得 1 枚金徽章, 获得 4 枚银徽章, 获得 5 枚铜徽章

    发起 0 场讲座

    擅长技能 编辑

    开源项目 & 著作 编辑

    (゚∀゚ ) 暂时没有

    注册于 2017年08月12日个人主页被 963 人浏览

    分享扩散: ••• 换一批

    推荐关注

    × Close

    我要该,理由是:

      返回重选 取消 提交
      产品
      热门问答
      热门专栏
      热门讲堂
      最新活动
      圈子
      找工作
      移动客户端
      资源
      每周精选
      用户排行榜
      徽章
      帮助中心
      声望与权限
      社区服务中心
      开发手册
      商务
      人才服务
      企业培训
      活动策划
      广告投放
      区块链解决方案
      合作联系
      关于
      关于我们
      加入我们
      联系我们
      关注
      产品技术日志
      社区运营日志
      市场运营日志
      团队日志
      社区访谈
      条款
      服务条款
      内容许可

      扫一扫下载 App

      Copyright © 2011-2019 SegmentFault. 当前呈现版本 19.02.27
      浙ICP备 15005796号-2   浙公网安备 33010602002000号 杭州堆栈科技有限公司版权所有

      CDN 存储服务由 又拍云 赞助提供

      移动版 桌面版

      回顶部