ac黄

ac黄 查看完整档案

广州编辑华南农业大学  |  信息管理与信息系统 编辑咪哒  |  前端工程师 编辑 blog.acwong.org 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

ac黄 收藏了文章 · 2018-03-08

面试的信心来源于过硬的基础

在过去的一年很多人不满于公司没有福利、人际关系不好相处、没有发展前途的境遇等等,想着在开年来换一份工作来重新开始自己,那么 你 准备好了吗?

下面是本人整理的一份面试材料,本想自己用的,但是新年第一天 公司突然给了我个惊喜,涨工资了!!!

1、 viewport

    <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
    // width    设置viewport宽度,为一个正整数,或字符串‘device-width’
    // device-width  设备宽度
    // height   设置viewport高度,一般设置了宽度,会自动解析出高度,可以不用设置
    // initial-scale    默认缩放比例(初始缩放比例),为一个数字,可以带小数
    // minimum-scale    允许用户最小缩放比例,为一个数字,可以带小数
    // maximum-scale    允许用户最大缩放比例,为一个数字,可以带小数
    // user-scalable    是否允许手动缩放

延伸 提问

怎样处理 移动端 1px 被 渲染成 2px 问题


    1 局部处理
        meta标签中的 viewport属性 ,initial-scale 设置为 1 
        rem 按照设计稿标准走,外加利用transfrome 的scale(0.5) 缩小一倍即可;
    2 全局处理
        meta标签中的 viewport属性 ,initial-scale 设置为 0.5
        rem 按照设计稿标准走即可

2、跨域的几种方式

首先了解下浏览器的同源策略
同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

那么怎样解决跨域问题的呢?

    1 通过jsonp跨域
        1.)原生实现:
         <script>
            var script = document.createElement('script');
            script.type = 'text/javascript';
        
            // 传参并指定回调执行函数为onBack
            script.src = 'http://www.....:8080/login?user=admin&callback=onBack';
            document.head.appendChild(script);
        
            // 回调执行函数
            function onBack(res) {
                alert(JSON.stringify(res));
            }
         </script>
    2、 document.domain + iframe跨域  
        此方案仅限主域相同,子域不同的跨域应用场景。
        1.)父窗口:(http://www.domain.com/a.html)

            <iframe id="iframe" data-original="http://child.domain.com/b.html"></iframe>
            <script>
                document.domain = 'domain.com';
                var user = 'admin';
            </script>
            2.)子窗口:(http://child.domain.com/b.html)
            
            <script>
                document.domain = 'domain.com';
                // 获取父窗口中变量
                alert('get js data from parent ---> ' + window.parent.user);
            </script>

        弊端:请看下面渲染加载优化

    3、 nginx代理跨域
    4、 nodejs中间件代理跨域
    5、 后端在头部信息里面设置安全域名
    
    更多跨域的具体内容请看  https://segmentfault.com/a/1190000011145364
    

3、 渲染优化

    1.禁止使用iframe(阻塞父文档onload事件);
        *iframe会阻塞主页面的Onload事件;
        *搜索引擎的检索程序无法解读这种页面,不利于SEO;
        *iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。

        使用iframe之前需要考虑这两个缺点。如果需要使用iframe,最好是通过javascript
        动态给iframe添加src属性值,这样可以绕开以上两个问题。

    2.禁止使用gif图片实现loading效果(降低CPU消耗,提升渲染性能);
    3、使用CSS3代码代替JS动画(尽可能避免重绘重排以及回流);
    4、对于一些小图标,可以使用base64位编码,以减少网络请求。但不建议大图使用,比较耗费CPU;
            小图标优势在于:
                1.减少HTTP请求;
                2.避免文件跨域;
                3.修改及时生效;

    5、页面头部的<style></style> 会阻塞页面;(因为 Renderer进程中 JS线程和渲染线程是互斥的);
    6、页面头部<script</script> 会阻塞页面;(因为 Renderer进程中 JS线程和渲染线程是互斥的);
    7、页面中空的 href 和 src 会阻塞页面其他资源的加载 (阻塞下载进程);
    
    8、网页Gzip,CDN托管,data缓存 ,图片服务器;
    9、前端模板 JS+数据,减少由于HTML标签导致的带宽浪费,前端用变量保存AJAX请求结果,每次操作本地变量,不用请求,减少请求次数
    10、用innerHTML代替DOM操作,减少DOM操作次数,优化javascript性能。
    11、当需要设置的样式很多时设置className而不是直接操作style。
    12、少用全局变量、缓存DOM节点查找的结果。减少IO读取操作。
    13、避免使用CSS Expression(css表达式)又称Dynamic properties(动态属性)。
    14、图片预加载,将样式表放在顶部,将脚本放在底部  加上时间戳。

    15、 避免在页面的主体布局中使用table,table要等其中的内容完全下载之后才会显示出来,显示比div+css布局慢。
        对普通的网站有一个统一的思路,就是尽量向前端优化、减少数据库操作、减少磁盘IO。
            向前端优化指的是,在不影响功能和体验的情况下,能在浏览器执行的不要在服务端执行,
            能在缓存服务器上直接返回的不要到应用服务器,程序能直接取得的结果不要到外部取得,
            本机内能取得的数据不要到远程取,内存能取到的不要到磁盘取,缓存中有的不要去数据库查询。
            减少数据库操作指减少更新次数、缓存结果减少查询次数、将数据库执行的操作尽可能的让你的程序完成(例如join查询),
            减少磁盘IO指尽量不使用文件系统作为缓存、减少读写文件次数等。程序优化永远要优化慢的部分,换语言是无法“优化”的。
            

4、事件的各个阶段

1:捕获阶段 ---> 2:目标阶段 ---> 3:冒泡阶段
document   ---> target目标 ----> document

由此,addEventListener的第三个参数设置为true和false的区别已经非常清晰了:

true表示该元素在事件的“捕获阶段”(由外往内传递时)响应事件;

false表示该元素在事件的“冒泡阶段”(由内向外传递时)响应事件。

5、let var const

let 允许你声明一个作用域被限制在块级中的变量、语句或者表达式
    let绑定不受变量提升的约束,这意味着let声明不会被提升到当前
    该变量处于从块开始到初始化处理的“暂存死区”。

var 声明变量的作用域限制在其声明位置的上下文中,而非声明变量总是全局的
    由于变量声明(以及其他声明)总是在任意代码执行之前处理的,所以在代码中的任意位置声明变量总是等效于在代码开头声明
    
const 声明创建一个值的只读引用 (即指针)
    这里就要介绍下 JS 常用类型 
    String、Number、Boolean、Array、Object、Null、Undefined
    其中基本类型 有 Undefined、Null、Boolean、Number、String,保存在栈中;
    复合类型 有 Array、Object ,保存在堆中;
    
    基本数据当值发生改变时,那么其对应的指针也将发生改变,故造成 const申明基本数据类型时,
    再将其值改变时,将会造成报错, 例如 const a = 3 ; a = 5 时 将会报错;
    但是如果是复合类型时,如果只改变复合类型的其中某个Value项时, 将还是正常使用;

6、箭头函数

    语法比函数表达式更短,并且不绑定自己的this,arguments,super或 new.target。这些函数表达式最适合用于非方法函数,并且它们不能用作构造函数。
    

7、快速的让一个数组乱序

    var arr = [1,2,3,4,5,6,7,8,9,10];
    arr.sort(function(){
        return Math.random() - 0.5;
    })
    console.log(arr);

此处解释:(语言组织能力不足,请勿吐槽)

首先: 当return 的值

    小于 0 ,那么 a 会被排列到 b 之前;
    等于 0 , a 和 b 的相对位置不变;
    大于 0 , b 会被排列到 a 之前;

这里你会 发现起始 的时候数组是正序排列,每当进行一次排列的时候, 都会先随机一个随机数 
(注意这里的每一次排列 指 每一个红框指一次排列, 共9次排列 , 一次排列中可能存在多次比较);

当一次排列的 随机数大于0.5 时 将会进行第二次比较, 当第二次随机数 仍然大于0.5 时 ,
    将会再 进行一次比较, 直到 随机数大于0.5 或者排列到第一位;

当一次排列的 随机数 小于0.5时 当前比较的两项 索引将不会改变 ,继续下一次 的排列;

8、字体font-family

    @ 宋体      SimSun
    @ 黑体      SimHei
    @ 微信雅黑   Microsoft Yahei
    @ 微软正黑体 Microsoft JhengHei
    @ 新宋体    NSimSun
    @ 新细明体  MingLiU
    @ 细明体    MingLiU
    @ 标楷体    DFKai-SB
    @ 仿宋     FangSong
    @ 楷体     KaiTi
    @ 仿宋_GB2312  FangSong_GB2312
    @ 楷体_GB2312  KaiTi_GB2312  
    @
    @ 说明:中文字体多数使用宋体、雅黑,英文用Helvetica
    
    body { font-family: Microsoft Yahei,SimSun,Helvetica; } 

9、可能用到的meta标签

    
    <!-- 设置缩放 -->
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, minimal-ui" />
    <!-- 可隐藏地址栏,仅针对IOS的Safari(注:IOS7.0版本以后,safari上已看不到效果) -->
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <!-- 仅针对IOS的Safari顶端状态条的样式(可选default/black/black-translucent ) -->
    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
    <!-- IOS中禁用将数字识别为电话号码/忽略Android平台中对邮箱地址的识别 -->
    <meta name="format-detection"content="telephone=no, email=no" />

    其他meta标签
    <!-- 启用360浏览器的极速模式(webkit) -->
    <meta name="renderer" content="webkit">
    <!-- 避免IE使用兼容模式 -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <!-- 针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,比如黑莓 -->
    <meta name="HandheldFriendly" content="true">
    <!-- 微软的老式浏览器 -->
    <meta name="MobileOptimized" content="320">
    <!-- uc强制竖屏 -->
    <meta name="screen-orientation" content="portrait">
    <!-- QQ强制竖屏 -->
    <meta name="x5-orientation" content="portrait">
    <!-- UC强制全屏 -->
    <meta name="full-screen" content="yes">
    <!-- QQ强制全屏 -->
    <meta name="x5-fullscreen" content="true">
    <!-- UC应用模式 -->
    <meta name="browsermode" content="application">
    <!-- QQ应用模式 -->
    <meta name="x5-page-mode" content="app">
    <!-- windows phone 点击无高光 -->
    <meta name="msapplication-tap-highlight" content="no">

10、消除transition闪屏

    .css {
        -webkit-transform-style: preserve-3d;
        -webkit-backface-visibility: hidden;
        -webkit-perspective: 1000;
    }
    过渡动画(在没有启动硬件加速的情况下)会出现抖动的现象, 以上的 解决方案只是改变 视角 来启动硬件加速的一种方式;
    启动硬件加速的 另外一种方式: 
        .css {
            -webkit-transform: translate3d(0,0,0);
            -moz-transform: translate3d(0,0,0);
            -ms-transform: translate3d(0,0,0);
            transform: translate3d(0,0,0);
        }
    
    启动硬件加速
    最常用的方式:translate3d、translateZ、transform

    opacity属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)

    will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,
    其它属性并不会变成复合层),

    弊端: 硬件加速会导致 CPU性能占用量过大,电池电量消耗加大 ;因此 尽量避免泛滥使用硬件加速。

11、android 4.x bug

    1.三星 Galaxy S4中自带浏览器不支持border-radius缩写
    2.同时设置border-radius和背景色的时候,背景色会溢出到圆角以外部分
    3.部分手机(如三星),a链接支持鼠标:visited事件,也就是说链接访问后文字变为紫色
    4.android无法同时播放多音频audio
    5.oppo 的border-radius 会失效

12、JS 判断设备来源

    function deviceType(){
        var ua = navigator.userAgent;
        var agent = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];    
        for(var i=0; i<len,len = agent.length; i++){
            if(ua.indexOf(agent[i])>0){         
                break;
            }
        }
    }
    deviceType();
    window.addEventListener('resize', function(){
        deviceType();
    })


    微信的 有些不太一样
    function isWeixin(){
        var ua = navigator.userAgent.toLowerCase();
        if(ua.match(/MicroMessenger/i)=='micromessenger'){
            return true;
        }else{
            return false;
        }
    }

13、audio元素和video元素在ios和andriod中无法自动播放

    
    原因: 因为各大浏览器都为了节省流量,做出了优化,在用户没有行为动作时(交互)不予许自动播放;

    /音频,写法一
    <audio data-original="music/bg.mp3" autoplay loop controls>你的浏览器还不支持哦</audio>
    
    //音频,写法二
    <audio controls="controls"> 
        <source data-original="music/bg.ogg" type="audio/ogg"></source>
        <source data-original="music/bg.mp3" type="audio/mpeg"></source>
        优先播放音乐bg.ogg,不支持在播放bg.mp3
    </audio>
    
    //JS绑定自动播放(操作window时,播放音乐)
    $(window).one('touchstart', function(){
        music.play();
    })
    
    //微信下兼容处理
    document.addEventListener("WeixinJSBridgeReady", function () {
        music.play();
    }, false);
    
    //小结
    //1.audio元素的autoplay属性在IOS及Android上无法使用,在PC端正常;
    //2.audio元素没有设置controls时,在IOS及Android会占据空间大小,而在PC端Chrome是不会占据任何空间;
    //3.注意不要遗漏微信的兼容处理需要引用微信JS;

14、css实现单行文本溢出显示 ...

直接上效果:相对于多行文本溢出做处理, 单行要简单多,且更容易理解。

实现方法

overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
当然还需要加宽度width属来兼容部分浏览。

15、实现多行文本溢出显示...

效果:

实现方法:

display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;

适用范围:

因使用了WebKit的CSS扩展属性,该方法适用于WebKit浏览器及移动端;

注:

1、-webkit-line-clamp用来限制在一个块元素显示的文本的行数。 为了实现该效果,它需要组合其他的WebKit属性。常见结合属性:
2、display: -webkit-box; 必须结合的属性 ,将对象作为弹性伸缩盒子模型显示 。
3、-webkit-box-orient 必须结合的属性 ,设置或检索伸缩盒对象的子元素的排列方式 。

如果你觉着这样还不够美观, 那么就接着往下看:

效果:

这样 是不是你想要的呢?

实现方法:

div {
    position: relative;
    line-height: 20px;
    max-height: 40px;
    overflow: hidden;
}

div:after {
    content: "..."; position: absolute; bottom: 0; right: 0; padding-left: 40px;
    background: -webkit-linear-gradient(left, transparent, #fff 55%);
    background: -o-linear-gradient(right, transparent, #fff 55%);
    background: -moz-linear-gradient(right, transparent, #fff 55%);
    background: linear-gradient(to right, transparent, #fff 55%);
}

不要只顾着吃,要注意胃口,此方法有个弊端 那就是 【未超出行的情况下也会出现省略号】 ,这样会不会很挫!!! 没办法,只能结合JS 进行优化该方法了。

注:


1、将height设置为line-height的整数倍,防止超出的文字露出。
2、给p::after添加渐变背景可避免文字只显示一半。
3、由于ie6-7不显示content内容,所以要添加标签兼容ie6-7(如:<span>…<span/>);兼容ie8需要将::after替换成:after。

16、让图文不可复制

这点应该大家 都很熟悉了, 某些时候【你懂的】为了快捷搜索答案,可是打死也不让你复制

-webkit-user-select: none; 
-ms-user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
user-select: none;

那有些网页为了尊重原创,复制的文本 都会被加上一段来源说明,是如何做到的呢?问的好! 等的就是你这个问题 -_- 。

大致思路:


1、答案区域监听copy事件,并阻止这个事件的默认行为。
2、获取选中的内容(window.getSelection())加上版权信息,然后设置到剪切板(clipboarddata.setData())。

17、盒子垂直水平居中

这个问题好像面试必问的吔!反正我是必问的,哈哈!!! 其实无关多少种实现思路,只要你能实现就可以!

提供4种方法

1、定位 盒子宽高已知, position: absolute; left: 50%; top: 50%; margin-left:-自身一半宽度; margin-top: -自身一半高度;

2、table-cell布局 父级 display: table-cell; vertical-align: middle;  子级 margin: 0 auto;

3、定位 + transform ; 适用于 子盒子 宽高不定时; (这里是本人常用方法)
    
    position: relative / absolute;
    /*top和left偏移各为50%*/
       top: 50%;
       left: 50%;
    /*translate(-50%,-50%) 偏移自身的宽和高的-50%*/
    transform: translate(-50%, -50%); 注意这里启动了3D硬件加速哦 会增加耗电量的 (至于何是3D加速 请看浏览器进程与线程篇)

4、flex 布局
    父级: 
        /*flex 布局*/
        display: flex;
        /*实现垂直居中*/
        align-items: center;
        /*实现水平居中*/
        justify-content: center;

再加一种水平方向上居中 :margin-left : 50% ; transform: translateX(-50%);

18、改变placeholder的字体颜色大小

其实这个方法也就在PC端可以,真机上屁用都没有,当时我就哭了。 但 还是贴出来吧

input::-webkit-input-placeholder { 
    /* WebKit browsers */ 
    font-size:14px;
    color: #333;
} 
input::-moz-placeholder { 
    /* Mozilla Firefox 19+ */ 
    font-size:14px;
    color: #333;
} 
input:-ms-input-placeholder { 
    /* Internet Explorer 10+ */ 
    font-size:14px;
    color: #333;
}

19、最快捷的数组求最大值

    var arr = [ 1,5,1,7,5,9];
    Math.max(...arr)  // 9 

20、更短的数组去重写法

    [...new Set([2,"12",2,12,1,2,1,6,12,13,6])]
    
    // [2, "12", 12, 1, 6, 13]

21、 vue 父子组件嵌套时,组件内部的各个生命周期钩子触发先后顺序

首先 我们可以把 子组件当做function函数来看待,当父组件 import 子组件的时候, 就当是声明了 并加载了这个函数,
在调用的时候才会去执行这个函数(子组件)。那么父子组件中的各个声明周期钩子触发的先后顺序是怎样的呢?
如下图:

下图带222 的 是为子组件,所以一次顺序是为 先创建父组件,然后才穿件子组件,当子组件创建完成并且实体dom挂载完成后父组件才挂载完成

注:资源来源于自己长期收集整理而来,如有和其他网站和论坛相同部分,在此抱歉!

查看原文

ac黄 赞了文章 · 2018-03-08

浏览器前端优化

本文转载自:众成翻译
译者:网络埋伏纪事
链接:http://www.zcfy.cc/article/2847
原文:https://hackernoon.com/optimising-the-front-end-for-the-browser-f2f51a29c572#.81dkyz4uu

优化全都是与速度和满意度有关。

  • 从用户体验的角度,我们希望前端提供可以快速加载和执行的网页。

  • 而从开发者体验的角度,我们希望前端是快速、简单而规范的。

这不仅会给我们带来快乐的用户和快乐的开发者,而且由于 Google 偏向于优化,SEO 排名也会显著提高。

如果你已经花费了大量时间来改善你网站的 Google Pagespeed Insights分数,那么这将有助于揭示这一切实际上意味着什么,以及我们必须为优化前端所采取的大量策略。

背景

最近我的整个团队有机会花一些时间加快把我们提出的升级变为代码库,可能是用 React。这确实让我思考起了我们该如何创建前端。很快,我意识到浏览器将是我们的方法中的一个重要因素,同时也是我们知识中的大瓶颈。

方法

首先

我们不能控制浏览器或者改变它的行为方式,但是我们可以理解它的工作原理,这样就可以优化我们提供的负载。

幸运的是,浏览器行为的基础原理是相当稳定而且文档齐全的,并且在相当长一段时间内肯定不会发生显著变化。

所以这至少给了我们一个目标。

其次

另一方面,代码、技术栈、架构和模式是我们可以控制的东西。它们更灵活,变化的更快,并给我们这一边提供了更多选择。

因此

我决定从外向内着手,搞清楚我们代码的最终结果应该是什么样的,然后形成编写代码的意见。在这第一篇博文中,我们打算专注于对于浏览器来说我们需要了解的一切。

浏览器都做了什么

下面我们来建立一些知识。如下是我们希望浏览器要运行的一些简单 HTML:

<!DOCTYPE html>
<html>
  <head>
    <title>The "Click the button" page</title>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="styles.css" />
  </head>
  
  <body>
    <h1>
      Click the button.
    </h1>
    
    <button type="button">Click me</button>
    
    <script>
      var button = document.querySelector("button");
      button.style.fontWeight = "bold";
      button.addEventListener("click", function () {
        alert("Well done.");
      });
    </script>
  </body>
</html>

浏览器如何渲染网页

当浏览器接收到 HTML 时,就会解析它,将其分解为浏览器所能理解的词汇,而这个词汇由于HTML5 DOM 规范定义,在所有浏览器中是保持一致的。然后浏览器通过一系列步骤来构造和渲染页面。如下是一个很高层次的概述:

  1. 使用 HTML 创建文档对象模型(DOM)。

  2. 使用 CSS 创建 CSS 对象模型(CSSOM)。

  3. 基于 DOM 和 CSSOM 执行脚本(Script)

  4. 合并 DOM 和 CSSOM 形成渲染树(Render Tree)。

  5. 使用渲染树布局(Layout)所有元素的大小和位置。

  6. 绘制(Paint)所有元素。

步骤一 — HTML

浏览器开始从上到下读取标记,并且通过将它分解成节点,来创建 DOM 。

HTML 加载优化策略

  • 样式在顶部,脚本在底部

虽然这个规则有例外和细微差别,不过总体思路是尽可能早地加载样式,尽可能晚地加载脚本。原因是脚本执行之前,需要 HTML 和 CSS 解析完成,因此,样式尽可能的往顶部放,这样在底部脚本开始编译和执行之前,样式有足够的时间完成计算。

下面我们进一步研究如何在优化的同时做细微调整。

  • 最小化和压缩

这适用于我们提交的所有内容,包括 HTML、CSS、JavaScript、图片和其它资源。

最小化是移除所有多余的字符,包括空格、注释、多余的分号等等。

压缩(比如 GZip)是将代码或者资源中重复的数据替换为一个指向原始实例的指针,大大压缩下载文件的大小,并且是在客户端解压文件。

双管齐下的话,可以潜在将负载降低 80% 到 90%。比如:光 bootstrap 就节省了 87% 的负载

  • 可访问性

虽然可访问性不会让页面的下载变得更快,但是会大大提高残障人士的满意度。要确保是给所有人提供的!给元素加上 aria 标签,给图片提供 alt 文本,以及所有其它好东西

使用像 WAVE 这样的工具确认在哪些地方可以改善可访问性。

步骤二 — CSS

当浏览器发现任何与节点相关的样式时(即外部样式表、内部样式表或行内样式),就立即停止渲染 DOM ,并用这些节点来创建 CSSOM。这就是人们称 CSS 阻塞渲染的原因。这里是不同类型样式的一些优缺点

//外部样式
<link rel="stylesheet" href="styles.css">

// 内部样式
<style>
  h1 {
    font-size: 18px;
  }
</style>

// 行内样式
<button style="background-color: blue;">Click me</button>

CSSOM 节点的创建与 DOM 节点的创建类似,随后,两者会被合并。这就是现在它们的样子:

CSSOM 的构建会阻塞页面的渲染,因此我们想在树中尽可能早地加载样式,让它们尽可能轻便,并且在有效的地方延迟加载它们。

CSS 加载优化策略

media 属性指定要加载样式必须满足的条件,比如:是最大还是最小分辨率?是面向屏幕阅读器吗?

桌面是很强大,但是移动设备不是,所以我们想给移动设备尽可能最轻的负载。我们可以假设先只提供移动端样式,然后对桌面端样式放一个媒体条件。虽然这样做不会阻止桌面端样式下载,不过会阻止它阻塞页面加载和使用宝贵的资源。

// 这个 css 在所有情况都会下载,并阻塞页面的渲染。
// media="all" 是默认值,并且与不声明任何 media 属性一样。
<link rel="stylesheet" href="mobile-styles.css" media="all">

// 在移动端,这个 css 会在后台下载,而且不会中断页面加载。
<link rel="stylesheet" href="desktop-styles.css" media="min-width: 590px">

// CSS 中只为打印视图计算的媒体查询
<style>
  @media print {
    img {
      display: none;
    }
  }
</style>
  • 延迟加载 CSS

如果我们有一些样式可以等到首屏有价值的内容渲染完成后,再加载和计算,比如出现在首屏以下的,或者页面变成响应式之后不那么重要的东西。我们可以把样式加载写在脚本中,用脚本等待页面加载完成,然后再插入样式。

<html>
  <head>
    <link rel="stylesheet" href="main.css">
  </head>
  
  <body>
    <div class="main">
      折叠内容之上的重要部分。
    </div>
    
    <div class="secondary">
      折叠内容之下。页面加载之后,向下滚动才会看到的东西。
    </div>
    
    <script>
      window.onload = function () {
        // 加载 secondary.css
      }
    </script>
  </body>
</html>

这里有一个如何实现这个的例子,还有另一个例子

  • 较小的特殊性

链在一起的元素越多,自然要传输的数据就越多,因而会增大 CSS 文件,这是一个明显的缺点。不过这样做还有一个客户端计算的损耗,即会把样式计算为较高的特殊性。

// 更具体的选择器 == 糟糕
.header .nav .menu .link a.navItem {
  font-size: 18px;
}

// 较不具体的选择器 == 好
a.navItem {
  font-size: 18px;
}
  • 只加载需要的样式

这听起来可能有点愚蠢或者装模作样,不过如果你已经从事前端工作多年的话,就会知道 CSS 的一个最大问题是删除东西的不可预测性。设计的时候它就是被下了不断增长这样的诅咒。

要尽可能多削减 CSS ,可以使用类似uncss)包这样的工具,或者如果想有一个网上的替代品,那就到处找找,还是有挺多选择的。

步骤三 — JavaScript

然后,浏览器会不断创建 DOM / CSSOM 节点,直到发现任何 JavaScript 节点,即外部或者行内的脚本。

// 外部脚本
`<script data-original="app.js">`</script>

// 内部脚本
<script>
  alert("Oh, hello");
</script>

由于脚本可能需要访问或操作之前的 HTML 或样式,我们必须等待它们构建完成。

因此浏览器必须停止解析节点,完成创建 CSSOM,执行脚本,然后再继续。这就是人们称 JavaScript 阻塞解析器的原因。

浏览器有种称为'预加载扫描器'的东西,它会扫描 DOM 的脚本,并开始预加载脚本,不过脚本只会在先前的 CSS 节点已经构建完成后,才会依次执行。

假如这就是我们的脚本:

var button = document.querySelector("button");
button.style.fontWeight = "bold";
button.addEventListener("click", function () {
  alert("Well done.");
});

那么这就是我们的 DOM 和 CSSOM 的效果:

JavaScript 加载优化策略

优化脚本是我们可以做的最重要的事情之一,同时也是大多数网站做得最糟糕的事情之一。

  • 异步加载脚本

通过在脚本上使用 async 属性,可以通知浏览器继续,用另一个低优先级的线程下载这个脚本,而不要阻塞其余页面的加载。一旦脚本下载完成,就会执行。

`<script data-original="async-script.js" async>`</script>

这意味着这段脚本可以随时执行,这就导致了两个明显的问题。首先,它可以在页面加载后执行很长时间,所以如果依靠它为用户体验做一些事情,那么可能会给用户一个不是最佳的体验。其次,如果它刚好在页面完成加载之前执行,我们就没法预测它会访问正确的 DOM/CSSOM 元素,并且可能会中断执行。

async 适用于不影响 DOM 或 CSSOM 的脚本,而且尤其适用于与 HTML 和 CSS 代码无关,对用户体验无影响的外部脚本,比如分析或者跟踪脚本。不过如果你发现了任何好的使用案例,那就用它。

  • 延迟加载脚本

deferasync 非常相似,不会阻塞页面加载,但会等到 HTML 完成解析后再执行,并且会按出现的次序执行。

这对于会作用于渲染树上的脚本来说,确实是一个好的选择。不过对于加载折叠内容之上的页面,或者需要之前的脚本已经运行的脚本来说,并不重要。

`<script data-original="defer-script.js" defer>`</script>

这里是使用 defer 策略的另一个好选择,或者也可以使用 addEventListener。如果想了解更多,请从这里开始阅读。

// 所有对象都在 DOM 中,并且所有图像、脚本、link 和子帧都完成了加载。
window.onload = function () {
};

// 在 DOM 准备好时调用,在图像和其它外部内容准备好之前
document.onload = function () {
};

// JQuery 的方式
$(document).ready(function () {
});

不幸的是 asyncdefer 对于行内脚本不起作用,因为只要有行内脚本,浏览器默认就会编译和执行它们。当脚本内嵌在 HTML 中时,就会立即运行,通过在外部资源上使用上述两个属性,我们只是把脚本抽取出来,或者延迟把脚本发布到 DOM/CSSOM。

  • 操作之前克隆节点

当且仅当对 DOM 执行多次修改时看到了不必要的行为时,就试试这招。

先克隆整个 DOM 节点,对克隆的节点做修改,然后用它来替换原始节点,这样可能效率更高。因为这样就避免了多次重画,降低了 CPU 和内存消耗。这样做还可以避免更改页面时的抖动和无样式内容的闪烁(Flash of Unstyled Content,FOUC)。

// 通过克隆,高效操作一个节点

var element = document.querySelector(".my-node");
var elementClone = element.cloneNode(true); // (true) 也克隆子节点, (false) 不会

elementClone.textContent = "I've been manipulated...";
elementClone.children[0].textContent = "...efficiently!";
elementClone.style.backgroundColor = "green";

element.parentNode.replaceChild(elementClone, element);

请注意,克隆的时候并没有克隆事件监听器。有时这实际上刚好是我们想要的。过去我们已经用过这种方法来重置不调用命名函数时的事件监听器,而且那时也没有 jQuery 的 .on().off() 方法可用。

  • Preload/Prefetch/Prerender/Preconnect

这些属性基本上也实现了它们所做的承诺,而且都棒极了。不过,这些属性都是相当新,还没被浏览器普遍支持,也就是说对我们大多数人来说它们实际上不是真正的竞争者。不过,如果你有空的话,可以看看这里这里

步骤四 — 渲染树

一旦所有节点已被读取,DOM 和 CSSOM 准备好合并,浏览器就会构建渲染树。如果我们把节点当成单词,把对象模型当成句子,那么渲染树便是整个页面。现在浏览器已经有了渲染页面所需的所有东西。

步骤五 — 布局

然后我们进入布局阶段,确定页面上所有元素的大小和位置。

步骤六 — 绘制

最终我们进入绘制阶段,真正地光栅化屏幕上的像素,为用户绘制页面。

整个过程通常会在几秒或者十分之一秒内发生。我们的任务是让它做得更快。

如果 JavaScript 事件改变了页面的某个部分,就会导致渲染树的重绘,并且迫使我们再次经历布局和绘制。现在浏览器足够智能,会仅进行部分重画。不过我们不能指望靠这就能高效或者高性能。

话虽如此,不过很显然 JavaScript 主要是在客户端基于事件,而且我们想让它来操作 DOM,所以它就得做到这一点。我们只是必须限制它的不良影响。

至此你已经足够认识到要感谢 Tali Garsiel 的演讲。这是 2012 年的演讲,但是信息依然是真实的。她在此主题上的综合论文可以在这里读到

如果你喜欢迄今为止所读过的内容,但仍然渴望知道更多的底层技术性的东西,那么所有知识的权威就是HTML5 规范

我们差不多搞定了,不过请和我多待段时间!现在我们来探讨为什么需要知道上面的所有知识。

浏览器如何发起网络请求

本节中,我们将理解如何才能高效地把渲染页面所需的数据传输给浏览器。

当浏览器请求一个 URL 时,服务器会响应一些 HTML。我们将从超级小的开始,慢慢增加复杂性。

假如这就是我们页面的 HTML:

<!DOCTYPE html>
<html>
  <head>
    <title>The "Click the button" page</title>
  </head>
  
  <body>
    <h1>
      Button under construction...
    </h1>
  </body>
</html>

我们需要学习一个新术语,关键渲染路径(Critical Rendering Path,CRP),就是浏览器渲染页面所需的步数。如下就是现在我们的 CRP 示意图看起来的样子:

浏览器发起一个 GET 请求,在我们页面(现在还没有 CSS 及 JavaScript)所需的 1kb HTML 响应回来之前,它一直是空闲的。接收到响应之后,它就能创建 DOM,并渲染页面。

关键路径长度

三个 CRP 指标的第一个是路径长度。我们想让这个指标尽可能低。

浏览器用一次往返来获取渲染页面所需的 HTML,而这就是它所需的一切。因此我们的关键路径长度是 1,很完美。

下面我们上一个档次,加点内部样式和内部 JavaScript。

<!DOCTYPE html>
<html>
  <head>
    <title>The "Click the button" page</title>
    <style>
      button {
        color: white;
        background-color: blue;
      }
    </style>
  </head>
  
  <body>
    <button type="button">Click me</button>
    
    <script>
      var button = document.querySelector("button");
      button.addEventListener("click", function () {
        alert("Well done.");
      });
    </script>
  </body>
</html>

如果我们检查一下 CRP 示意图,应该能看到有两点变化。

新增了两步,创建 CSSOM执行脚本。这是因为我们的 HTML 有内部样式和内部脚本需要计算。不过,由于没有发起外部请求,关键路径长度没变。

但是注意,渲染没那么快了。而且我们的 HTML 大小增加到了 2kb,所以某些地方还是受了影响。

关键字节数

那就是三个指标之二,关键字节数出现的地方。这个指标用来衡量渲染页面需要传送多少字节数。并非页面会下载的所有字节,而是只是实际渲染页面,并把它响应给用户所需的字节。

不用说,我们也想减少这个数。

如果你认为这就不错了,谁还需要外部资源啊,那就大错特错了。虽然这看起来很诱人,但是它在规模上是不可行的。在现实中,如果我的团队要通过内部或者行内方式给页面之一提供所需的一切,页面就会变得很大。而浏览器不是为了处理这样的负载而创建的。

看看这篇关于像 React 推荐的那样内联所有样式时,页面加载效果的有趣文章。DOM 变大了四倍,挂载花了两倍的时间,到可以响应多花了 50% 的时间。相当不能接受。

还要考虑一个事实,就是外部资源是可以被缓存的,因此在回访页面,或者访问用相同资源(比如 my-global.css)的其它页面时,浏览器就用发起网络请求,而是用其缓存的版本,从而为我们赢得更大的胜利。

所以下面我们更进一步,对样式和脚本使用外部资源。注意这里我们有一个外部 CSS 文件、一个外部 JavaScript 文件和一个外部 asyncJavaScript 文件。

<!DOCTYPE html>
<html>
  <head>
    <title>The "Click the button" page</title>
    <link rel="stylesheet" href="styles.css" media="all">
    `<script type="text/javascript" data-original="analytics.js" async>`</script>  // async
  </head>
  
  <body>
    <button type="button">Click me</button>
    
    `<script type="text/javascript" data-original="app.js">`</script>
  </body>
</html>

如下是现在 CRP 示意图看起来的样子:

浏览器得到页面,创建 DOM,一发现任何外部资源,预加载扫描器就开始介入。继续,开始下载 HTML 中所找到的所有外部资源。CSS 和 JavaScript 有较高的优先级,其它资源次之。

它挑出我们的 styles.cssapp.js,开辟另一条关键路径去获取它们。不过不会挑出 analytics.js,因为我们给它加了 async属性。浏览器依然会用另一个低优先级的线程下载它,不过因为它不会阻塞页面渲染,所以也与关键路径无关。这正是 Google 自己的优化算法对网站进行排名的方式。

关键文件

最后,是我们最后一个 CRP 指标,关键文件,也就是浏览器渲染页面需要下载的文件总数。在例三中,HTML 文件本身、CSS 和 JavaScript 文件都是关键文件。async 的脚本不算。当然,文件越少越好。

回到关键路径长度

现在你可能会认为这肯定就是最长的关键路径吧?我的意思是说要渲染页面,我们只需要下载 HTML、CSS 和 JavaScript,而且只需要两个往返就可以搞定。

HTTP1 文件限制

不过,生活依然没那么简单。拜 HTTP1 协议所赐,我们的浏览器一次从一个域名并发下载的最大文件数是有限制的。范围从 2(很老的浏览器)到 10(Edge)或者 6(Chrome)。

你可以从这里查看用户浏览器请求你的域名时的最大并发文件数。

你可以并且应该通过把一些资源放在影子域名上,来绕开这个限制,从而最大限度地提高优化潜力。

警告:不要把关键的 CSS 放到根域名之外的其他地方,DNS 查找和延迟都会抵消这样做时所带来的任何可能的好处。

HTTP2

如果网站是 HTTP2,并且用户的浏览器也是兼容的,那么你就可以完全避开这个限制。不过,这种好事并不常见。

可以在这里测试你网站的 HTTP2。

TCP 往返限制

另一个敌人逼近了!

任何一次往返可传输的最大数据量是 14kb,对于包括所有 HTML、CSS 和脚本在内的所有网络请求都是如此。这来自于防止网络拥堵和丢包的一个 TCP 规范

如果一次请求中,我们的 HTML 或者任何累积的资源超过了 14kb,那么就需要多做一次往返来获取它们。所以,是的,这些大的资源确实会给 CRP 添加很多路径。

大招

现在将我们的大网页倾巢而出。

<!DOCTYPE html>
<html>
  <head>
    <title>The "Click the button" page</title>
    <link rel="stylesheet" href="styles.css">     // 14kb
    <link rel="stylesheet" href="main.css">       // 2kb
    <link rel="stylesheet" href="secondary.css">  // 2kb
    <link rel="stylesheet" href="framework.css">  // 2kb
    `<script type="text/javascript" data-original="app.js">`</script>  // 2kb
  </head>
  
  <body>
    <button type="button">Click me</button>
    
    `<script type="text/javascript" data-original="modules.js">`</script> // 2kb
    `<script type="text/javascript" data-original="analytics.js">`</script> // 2kb
    `<script type="text/javascript" data-original="modernizr.js">`</script>  // 2kb
  </body>
</html>

现在我知道一个按钮就有很多 CSS 和 JavaScript,但是它是一个很重要的按钮,它对我们来说意义重大。所以就不要评判,好吗?

整个页面被很好地最小化和压缩到 2kb,远低于 14kb 的限制,所以我们又回到正好一次 CRP 往返了,而浏览器开始忠实地用一个关键文件,即我们的 HTML,来创建 DOM。

CRP 指标:长度 1,文件数 1,字节数 2kb

浏览器发现了一个 CSS 文件,而预加载扫描器识别出所有外部资源(CSS 和 JavaScript),并发送一个请求开始下载它们。但是等一等,第一个 CSS 文件是 14kb,超出了一次往返的最大负载,所以它本身就是一个 CRP。

CRP 指标:长度 2,文件数 2,字节数 16kb

然后它继续下载资源。余下的资源低于 14kb,所以可以在一次往返中搞定。不过由于总共有 7 个资源,而且我们的网站还没启用 HTTP2,而且用的是 Chrome,所以这次往返只能下载 6 个文件。

CRP 指标:长度 3,文件数 8,字节数 28kb

现在我们终于可以下载完最终文件,并开始渲染 DOM了。

CRP 指标:长度 4,文件数 9,字节数 30kb

我们的 CRP 总共有 30kb 的关键资源,在 9 个关键文件和 4 个关键路径中。有了这个信息,以及一些关于连接中延迟的知识,我们实际上就可以开始对给定用户的页面性能进行真正准确的估计了。

浏览器网络优化策略

  • Pagespeed Insights

使用Insights 来确定性能问题。Chrome DevTools 中还有个 audit标签。

  • 善用 Chrome 开发者工具

DevTools 如此惊人。我们为它单独写一整本书,不过这里已经有不少资源可以帮助你。这里)有一篇开始解释网络资源的文章值得一读。

  • 在好的环境中开发,在糟糕的环境中测试

你当然想在你的 1tb SSD、32G 内存的 Macbook Pro 上开发,不过对于性能测试,应该转到 Chrome 中的 network 标签下,模拟低带宽、节流 CPU 连接,从而真正得到一些有用的信息。

  • 合并资源/文件

在上面的 CRP 示意图中,我省略了一些你不需要知道的东西。不过基本上,每接收到一个外部 CSS 和 JavaScript 文件后,浏览器都会构建 CSSOM,并执行脚本。所以,尽管你可以在一次往返中传递几个文件,它们每个也都会让浏览器浪费宝贵的时间和资源,所以最好还是将文件合并在一起,消除不必要的加载。

  • 在 head 部分为首屏设置内部样式

是让 CSS 和 JavaScript 内部化或者内联,以防止获取外部资源,还是相反,让资源变成外部资源,这样就可以缓存,从而让 DOM 保持轻量,二者并非非黑即白。

但是有一个很好的观点是对首屏关键内容设置内部样式,可以避免在首次有意义的渲染时获取资源。

  • 最小化/压缩图片

这很简单、基础,有很多选择可以这样做,选一个你最喜欢的即可。

  • 延迟加载图片直到页面加载后

用一些简单的原生 JavaScript,你就可以延迟加载出现在折叠部分之下或者对首次用户响应状态不重要的图片。这里有一些不错的策略。

  • 异步加载字体

字体加载的代价非常高,如果可以的话,你应该使用带回退的 web 字体,然后逐步渲染字体和图标。这看起来可能不咋样,不过另一个选择是如果字体还没有加载,页面加载时就完全没有文字,这被称为不可见文本的闪烁(Flash Of Invisible Text,FOIT)。

  • 是否真正需要 JavaScript/CSS?

你需要吗?请回答我!是否有原生 HTML 元素可以产生用脚本一样的行为?是否可以用行内样式或图标而不是内部/外部资源?比如,内联一个 SVG

  • CDN

内容分发网络(CDN)可用于为用户提供物理上更近和更低延迟的位置,从而降低加载时间。

    • *

现在你开心惨了,已经知道了足够多的东西,可以从这里走出去,自己探索有关这个主题的更多东西了。我推荐参加这个免费的 Udacity 课程,并且阅读Google 自己的 优化文档

如果你渴望更底层的知识,那么这本免费电子书《高性能浏览器网络》是个开始的好地方。

总结

关键渲染路径是最重要的,它让网站优化有规律可循。需要关注的 3 个指标是:

1 — 关键字节数

2 — 关键文件数

3 — 关键路径数

这里我所写的应该足以让你掌握基础知识,并帮助你解释 Google Pagespeed Insights对你的性能有什么看法。

最佳实践的应用将伴随着良好的 DOM 结构、网络优化和可用于减少 CRP 指标的各种策略的结合。让用户更高兴,让 Google 的搜索引擎更高兴。

在任何企业级网站中,这将是一项艰巨的任务,但是你必须迟早做到这一点,所以不要再承担更多的技术性债务,并开始投资于坚实的性能优化策略。

感谢你阅读至此,如果你真的做到了。衷心希望能帮到你,有任何反馈或者纠正,请给我发消息。

在本博客的下一部分中,我希望给出一个真实世界的例子,说明如何在我自己的团队的大量代码库中实现所有这些原则。我们在一个域上拥有超过 2000 个可管理内容的页面,并且每天为数十万个页面浏览量提供服务,因此这将很有趣。

不过这可能还需要段时间,我现在需要休息一下。

查看原文

赞 28 收藏 155 评论 2

ac黄 发布了文章 · 2017-09-25

ES6/ES7 三点式 —— 扩展运算符与剩余操作符

ES6 标准提供给 JavaScript 开发者许多简化代码的新特性,今天要介绍的扩展运算符就是非常常用的一种。可以使你的代码更加简洁优雅。

扩展运算符

扩展运算符以三个点的形式出现 ... 可以将数组或者对象里面的值展开。

const a = [1, 2, 3]

console.log(a) // 1 2 3

const b = {a: 1, b: 2}

console.log({c: 3, ...b}) // {a: 1, b: 2, c: 3}

扩展运算符的应用

接下来看看扩展运算符的常见应用。

1.复制数组和复制对象

const a = [1, 2, 3]
const b = [...a]

// 相当于 b = a.slice()

console.log(a) // [1, 2, 3]
console.log(b) // [1, 2, 3]
console.log(a === b) // false
const a = {a: 1, b: 2}
const b = {...a}

console.log(a) // {a: 1, b: 2}
console.log(b) // {a: 1, b: 2}
console.log(a === b) // false

// 相当于 const b = Object.assign({}, a)

要注意复制时候只会进行浅复制。

2.合并数组和合并对象

const a = [1, 2, 3]
const b = [4, 5]

console.log([...a, ...b]) // [1, 2, 3, 4, 5]

// 相当于 a.concat(b)
const a = {a: 1, b: 2}
const b = {b: 3, c: 4}

console.log({...a, ...b, c: 5}) // {a: 1, b: 3, c: 5}

// 相当于 Object.assign(a, b, {c: 5})

3.类数组对象数组化

前端开发当中经常会遇到一些类数组对象,如:function 的 arguments,document.querySelectorAll 等,通常会将这些类数组对象转换为数组的形式使其可以利用数组原型对象的方法。

const divs = document.querySelectorAll('divs')

// divs.push 会报错

// slice 方式

divs = [].slice.call(divs)

// 使用扩展运算符

divs = [...divs]

4.代替 apply 方法

function sum(x, y, z) {
  console.log(x + y + z)
}

const args = [1, 2, 3]
// apply 方式

fn.apply(null, args)

// 扩展运算符方式

fn(...args)

剩余操作符

另外一种以三个点 ... 形式出现的是剩余操作符,与扩展操作符相反,剩余操作符将多个值收集为一个变量,而扩展操作符是将一个数组扩展成多个值。

// 配合 ES6 解构的新特性
const [a, ...b] = [1, 2, 3]

console.log(a) // 1
console.log(b) // [2, 3]

最后再看一个例子,在日常开发当中非常常见,而且同时利用到扩展操作符和剩余操作符,在 React 开发当中常常会利用一些组件库,为了业务需要我们会将一些组件库提供的组件封装成业务组件方便开发。

import { Button } from 'antd'  // 组件库提供的组件

function MyButton({ isDanger, children, ...others }) {
  return (
    <div>
      {isDanger ? 
        <Button {...others} size="large" type="danger">{children}</Button> :
        <Button {...others} size="small" type="primary">{children}</Button>
      }
    </div>
  )
}

本文同步于我的个人博客 http://blog.acwong.org/2017/09/23/es6-destructuring-assignment/

查看原文

赞 10 收藏 14 评论 1

ac黄 赞了回答 · 2017-08-31

为什么dom调用attr会出错?

你在jQ对象后边加了[2]返回的就是原生DOM接口了,压根儿没.attr()这么个方法。

可以这么写:

$('input').eq(2).prop('checked', true);

要注意的两点:

  1. .eq()方法以0为起始基数,值为负时表示倒数;
  2. checked是个布尔属性,布尔属性只要存在即为true,也就是说这个属性你写在标签里,不管是:

    1. <input checked>
    2. <input checked="true">
    3. <input checked="checked">

      的哪一种,值都是true,所以不要用.attr,要用.prop()

关注 5 回答 5

ac黄 回答了问题 · 2017-07-31

解决JS字符串截取问题,怎么去除字符串两头的指定字符,只要中间的某部分?

var a = /^\/Upload\/Image\/(\S+)\.jpg$/
a.exec('/Upload/Image/f62488ceebe5442ea7454312ae4676eb.jpg')[1]

关注 11 回答 8

ac黄 关注了用户 · 2017-06-30

Leechikit @leechikit

关注 61

ac黄 赞了文章 · 2017-06-29

Flexbox布局的正确使用姿势

在项目中,我们还会大量使用到flexbox的新旧属性,但大多数人一般只会写新属性,旧属性交由autoprefixer处理,但其实完成同样功能的新旧属性表现形式却不尽相同。还有部分人只使用“万能”的flex:number属性为伸缩项目分配空间,但有些特殊情景却无法满足,此文为此梳理了flexbox的新旧属性区别和分配空间的原理,为大家用flexbox布局的项目通通渠。

Flexbox兼容性

PC端的兼容性
flexbox-pc

移动端的兼容性
flexbox-wap

如上图,为了兼容IE10-11和Android4.3-,UC,我们仍需要使用Flexbox的旧属性。

Flexbox新旧属性

Flexbox的新属性提供了很多旧版本没有的功能,但是目前Android4.x和UC仍有一定市场占有率需要兼容,因此目前只使用新旧属性都有的功能。
能实现相同功能的Flexbox新旧属性如下表:
flexbox-attribute

但想象是美好的,现实是残酷的,新旧属性里有那么几个顽固分子并不能乖乖的表现的一样,总有那么一点不同。
下面我们来看看是哪些新旧属性有不同:

flex-direction:row-reverse vs box-orient:horizontal;box-direction:reverse

相同点:改变主轴方向和伸缩项目的排列顺序;在ltr下伸缩项目从右到左排列。
不同点:

flex-direction:row-reverse:第一个伸缩项目向主轴起点对齐
row-reverse

box-orient:horizontal;box-direction:reverse:最后一个伸缩项目向主轴终点对齐
row-reverse2

flex-direction:column-reverse vs box-orient:vertical;box-direction:reverse

相同点:改变主轴方向和伸缩项目的排列顺序;在ltr下伸缩项目从下到上排列。
不同点:
flex-direction:column-reverse:第一个伸缩项目向主轴起点对齐。
column-reverse

box-orient:vertical;box-direction:reverse:最后一个伸缩项目向主轴终点对齐。
column-reverse2

oreder:integer vs box-ordinal-group:integer

相同点:定义伸缩项目显示顺序。
不同点:
oreder:integer:默认值为0;可以为负值。
box-ordinal-group:integer:默认值为1;取值大于1。

flex-grow:number vs box-flex:number

相同点:定义伸缩项目的扩展因素。
不同点:box-flex:number同时定义了伸缩项目的缩小因素。

flex-shrink:number vs box-flex:number

相同点:定义伸缩项目的缩小因素。
不同点:box-flex:number同时定义了伸缩项目的扩展因素。

Flexbox分配空间原理

影响Flexbox布局分配空间的属性有三个,分别是flex-growflex-shrinkflex-basis

  • flex-grow:当伸缩项目在主轴方向的总宽度 < 伸缩容器,伸缩项目根据扩展因素分配伸缩容器的剩余空间。

  • flex-shrink:当伸缩项目在主轴方向的总宽度 > 伸缩容器,伸缩项目根据缩小因素分配总宽度超出伸缩容器的空间。

  • flex-basis:伸缩基础,在进行计算剩余空间或超出空间前,给伸缩项目重新设置一个宽度,然后再计算。

我们先来看看如何计算计算拉伸后的伸缩项目宽度,先简单明了的给个公式,再通过栗子来验证。

伸缩项目扩展宽度 = (项目容器宽度 - 项目宽度或项目设置的flex-basis总和) * 对应的flex-grow比例

拉伸后伸缩项目宽度 = 原伸缩项目宽度 + 扩展宽度

.flexbox-wrap{
    width:550px;
    display: flex;
}
.flexbox-item{
    &:nth-child(1){
        width:60px;
    }
    &:nth-child(2){
        width:70px;
    }
    &:nth-child(3){
        flex-basis:80px;
    }
    &:nth-child(4){
        flex-basis:90px;
    }
    &:nth-child(5){
         flex-basis:100px;
    }
}
@for $i from 1 through 5 {
    .flexbox-item:nth-child(#{$i}){
        flex-grow: $i;
        background-color: rgba(35 * (6-$i), 20 * $i, 35 * $i,1);
    }
}

flex-grow

我们来计算一下上面栗子中第一个伸缩项目拉伸后的宽度。
对应着公式一步步计算:

// 项目容器宽度
container = 550
// 项目宽度或项目设置的flex-basis总和
itemSum = 60 + 70 + 80 + 90 + 100 = 400
// 第一个伸缩项目对应的flex-grow比例
flexRatio = 1 / ( 1 + 2 + 3 + 4 + 5 ) = 1/15
// 第一个伸缩项目扩展宽度
extendWidth = ( 550 - 400 ) * 1/15 = 10
// 第一个伸缩项目拉伸后的宽度
itemWidth = 60 + 10 = 70

计算后得到第一个伸缩项目拉伸后的宽度是70px,我们通过chrome上的盒子模型来看看是否正确
flex-grow-box

chrome计算的结果和我们计算的结果是一致的。

根据拉伸的计算公式是不是很容易就能推演出压缩的计算公式呢?

伸缩项目缩小宽度 = (项目宽度或项目设置的flex-basis总和 - 项目容器宽度) * 对应的flex-shrink比例

压缩后伸缩项目宽度 = 原伸缩项目宽度 - 缩小宽度

继续用个栗子来验证公式是否正确

.flexbox-wrap{
    width:250px;
    display: flex;
}
.flexbox-item{
    &:nth-child(1){
        width:60px;
    }
    &:nth-child(2){
        width:70px;
    }
    &:nth-child(3){
        flex-basis:80px;
    }
    &:nth-child(4){
        flex-basis:90px;
    }
    &:nth-child(5){
         flex-basis:100px;
    }
}
@for $i from 1 through 5 {
    .flexbox-item:nth-child(#{$i}){
        flex-shrink: $i;
        background-color: rgba(35 * (6-$i), 20 * $i, 35 * $i,1);
    }
}

flex-shrink

我们来计算一下上面栗子中第一个伸缩项目压缩后的宽度。
对应着公式一步步计算:

// 项目容器宽度
container = 250
// 项目宽度或项目设置的flex-basis总和
itemSum = 60 + 70 + 80 + 90 + 100 = 400
// 第一个伸缩项目对应的flex-shrink比例
flexRatio = 1 / ( 1 + 2 + 3 + 4 + 5 ) = 1/15
// 第一个伸缩项目缩小宽度
extendWidth = ( 400 - 250 ) * 1/15 = 10
// 第一个伸缩项目压缩后的宽度
itemWidth = 60 - 10 = 50

计算后得到第一个伸缩项目压缩后的宽度是50px,我们通过chrome上的盒子模型来看看是否正确
flex-shrink-box

chrome计算的结果和我们计算的结果不一样。
gangga

伸缩项目压缩的计算方式和拉伸的不一样,是因为压缩会有极端情况,我们把第一个伸缩项目的flex-shrink修改为10,此时缩小宽度为( 400 - 250 ) * ( 10 / 24) = 62.5,缩小的宽度比原宽度要大,计算的压缩后的宽度变成了负数。

为了避免这种极端情况,计算缩小比例是要考虑伸缩项目的原宽度。

正确的公式是这样的

伸缩项目缩小宽度 = (项目宽度或项目设置的flex-basis总和 - 项目容器宽度) (对应的flex-shrink 项目宽度或项目设置的flex-basis比例)

压缩后伸缩项目宽度 = 原伸缩项目宽度 - 缩小宽度

对应着公式一步步计算:

// 项目容器宽度
container = 250
// 项目宽度或项目设置的flex-basis总和
itemSum = 60 + 70 + 80 + 90 + 100 = 400
// 第一个伸缩项目对应的flex-shrink比例
flexRatio = (1*60) / (1*60+2*70+3*80+4*90+5*100) = 6/130
// 第一个伸缩项目缩小宽度
extendWidth = ( 400 - 250 ) * 6/130 ≈ 6.922
// 第一个伸缩项目压缩后的宽度
itemWidth = 60 - 6.922 = 53.078

计算后得到第一个伸缩项目压缩后的宽度是53.078px,和chrome上的盒子模型是一样的。

Flexbox属性缩写陷阱

上面介绍的flex-growflex-shrinkflex-basis有一个缩写的写法flex

flex: flex-grow [flex-shrink] [flex-basis]

flex各种缩写的值

  • flex: initial == flex: 0 1 auto

  • flex: none == flex: 0 0 auto

  • flex: auto == flex: 1 1 auto

  • flex: number == flex: number 1 0%

在实际项目中,会直接写使用缩写的flex来给伸缩项目分配空间,但是使用缩写属性会留下一些陷阱,导致表现的结果不尽如人意。

分别使用flexflex-grow来把伸缩项目拉伸填满容器,看看表现的差异。

首先看看使用flex-grow拉伸伸缩项目的效果

.flexbox-wrap{
    width:550px;
    display: flex;
}
.flexbox-item{
    flex-grow:1;
    &:nth-child(1){
        width:60px;
    }
    &:nth-child(2){
        width:70px;
    }
    &:nth-child(3){
        width:80px;
    }
    &:nth-child(4){
        width:90px;
    }
    &:nth-child(5){
         width:100px;
    }
}
@for $i from 1 through 5 {
    .flexbox-item:nth-child(#{$i}){
        background-color: rgba(35 * (6-$i), 20 * $i, 35 * $i,1);
    }
}

每个伸缩项目在原宽度上拉伸相同的宽度
flex-grow1

通过上面的计算拉伸后的伸缩项目宽度,可以计算第一个伸缩项目拉伸后的宽度

// 项目容器宽度
container = 550
// 项目宽度或项目设置的flex-basis总和
itemSum = 60 + 70 + 80 + 90 + 100 = 400
// 第一个伸缩项目对应的flex-grow比例
flexRatio = 1 / ( 1 + 1 + 1 + 1 + 1 ) = 1/5
// 第一个伸缩项目扩展宽度
extendWidth = ( 550 - 400 ) * 1/5 = 30
// 第一个伸缩项目拉伸后的宽度
itemWidth = 60 + 30 = 90

flex-grow1-box

然后我们把flex-grow:1替换成flex:1,下面是表现的效果,伸缩项目拉伸后的宽度变成一样了。
flex1

从chrome的盒子模型可看到伸缩项目拉伸后宽度变成了110px,伸缩容器等分了容器的宽度。
flex1-box

flex:1展开后是flex:1 1 0%flex-grow:1相当于flex:1 1 auto,两者的区别在于flex-basis的值不同。flex:1为项目宽度重新设置了宽度为0,所以可分配空间为整个容器,从公式计算上可以更直观理解:

// 项目容器宽度
container = 550
// 项目宽度或项目设置的flex-basis总和
itemSum = 0 + 0 + 0 + 0 + 0 = 0
// 第一个伸缩项目对应的flex-grow比例
flexRatio = 1 / ( 1 + 1 + 1 + 1 + 1 ) = 1/5
// 第一个伸缩项目扩展宽度
extendWidth = ( 550 - 0 ) * 1/5 = 110
// 第一个伸缩项目拉伸后的宽度
itemWidth = 0 + 110 = 110

需要注意的Flexbox特性

无效属性

  • column-*在伸缩容器无效

  • float和clear在伸缩项目无效

  • vertical-align在伸缩项目无效

  • ::first-line and ::first-letter在伸缩容器无效

伸缩容器中的非空字符文本节点也是伸缩项目

<div class="flexbox-wrap">
    <span class="flexbox-item">1</span>
    <span class="flexbox-item">2</span>
    我是个假文本
    <span class="flexbox-item">3</span>
    <span class="flexbox-item">4</span>
    <span class="flexbox-item">5</span>
</div>

textelement

margin折叠

  • 伸缩容器和伸缩项目的margin不会折叠

  • 伸缩项目间的margin不会折叠

旧版Flexbox的BUG

伸缩项目为行内元素要加display:block;或display:flex

欢迎关注:Leechikit
原文链接:segmentfault.com

到此本文结束,欢迎提问和指正。
写原创文章不易,若本文对你有帮助,请点赞、推荐和关注作者支持。

查看原文

赞 10 收藏 124 评论 3

ac黄 关注了专栏 · 2017-06-28

每天一点canvas动画

编码需要乐趣

关注 1004

ac黄 发布了文章 · 2017-06-27

JavaScript 异步编程的四种方式

异步编程是每个使用 JavaScript 编程的人都会遇到的问题,无论是前端的 ajax 请求,或是 node 的各种异步 API。本文就来总结一下常见的四种处理异步编程的方法。

回调函数

使用回调函数是最常见的一种形式,下面来举几个例子:

// jQuery ajax
$.get('test.html', data => {
  $('#result').html(data)
})
// node 异步读取文件
const fs = require('fs')

fs.readFile('/etc/passwd', (err, data) => {
  if (err) {
    throw err
  }
  console.log(data)
})

回调函数非常容易理解,就是定义函数的时候将另一个函数(回调函数)作为参数传入定义的函数当中,当异步操作执行完毕后在执行该回调函数,从而可以确保接下来的操作在异步操作之后执行。

回调函数的缺点在于当需要执行多个异步操作的时候会将多个回调函数嵌套在一起,组成代码结构上混乱,被称为回调地狱(callback hell)。

func1(data0, data1 => {
  func2(data2, data3 => {
    func3(data3, data4 => data4)
  })
})

Promise

Promise 利用一种链式调用的方法来组织异步代码,可以将原来以回调函数形式调用的代码改为链式调用。

// jQuery ajax promise 方式
$.get('test.html')
  .then(data => $(data))
  .then($data => $data.find('#link').val('href'))
  .then(href => console.log(href))

自己定义一个 Promise 形式的函数在 ES6 当中也非常简单:

function ready() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('ready')
    }, 3000)
  })
}

ready().then(ready => console.log(`${ready} go!`))

在 node 8.0 以上的版本还可以利用 util.promisify 方法将回调形式的函数变为 Promise 形式。

const util = require('util')
const fs = require('fs')

const readPromise = util.promisify(fs.readFile)

readPromise('test.txt').then(data => console.log(data.toString()))

想详细了解 Promise 可以阅读拙作谈谈 ES6 的 Promise 对象

Generators

node 的著名开发者 TJ 利用 ES6 新特性生成器(Generators)开发了一个异步控制工具 co

如果不了解 Generators 可以看看以下的文章:

利用 co 可以将异步代码的写法写成类似同步代码的形式:

const util = require('util')
const fs = require('fs')
const co = require('co')

const readFile = util.promisify(fs.readFile)

co(function* () {
  const txt = yield readFile('file1.txt', 'utf8')
  console.log(txt)
  const txt2 = yield readFile('file2.txt', 'utf8')
  console.log(txt2)
})

使用 Generators 的好似显然易见,可以使异步代码写得非常清晰,缺点就是要另外引入相关的库来利用该特性。

Async/Await

node7.6 以上的版本引入了一个 ES7 的新特性 Async/Await 是专门用于控制异步代码。先看一个例子:

const util = require('util')
const fs = require('fs')

const readFile = util.promisify(fs.readFile)

async function readFiles () {
  const txt = await readFile('file1.txt', 'utf8')
  console.log(txt)
  const txt2 = await readFile('file2.txt', 'utf8')
  console.log(txt2)
})

首先要使用 async 关键字定义一个包含异步代码的函数,在 Promise 形式的异步函数前面使用 await 关键字就可以将异步写成同步操作的形式。

看上去与 Generators 控制方式相差不大,但是 Async/Await 是原生用于控制异步,所以是比较推荐使用的。

错误处理

最后来探讨下四种异步控制方法的错误处理。

回调函数

回调函数错误处理非常简单,就是在回调函数中同时回传错误信息:

const fs = require('fs')

fs.readFile('file.txt', (err, data) => {
  if (err) {
    throw err
  }
  console.log(data)
})

Promise

Promise 在 then 方法之后使用一个 catch 方案来捕捉错误信息:

const fs = require('fs')
const readFile = util.promisify(fs.readFile)

readFile('file.txt')
  .then(data => console.log(data))
  .catch(err => console.log(err))

Generators 和 Async/Await

Generators 和 Async/Await 比较类似,可以有两种方式,第一种使用 Promise 的 catch 方法,第二种用 trycatch 关键字。

Promise catch

const fs = require('fs')
const co = require('co')
const readFile = util.promisify(fs.readFile)

co(function* () {
  const data = yield readFile('file.txt').catch(err => console.log(err))
})
const fs = require('fs')
const co = require('co')
const readFile = util.promisify(fs.readFile)

async function testRead() {
  const data = await readFile('file.txt').catch(err => console.log(err))
}

try/catch

const fs = require('fs')
const co = require('co')
const readFile = util.promisify(fs.readFile)

co(function* () {
  try {
    const data = yield readFile('file.txt')
  } catch(err) {
    console.log(err)
  }
})
const fs = require('fs')
const readFile = util.promisify(fs.readFile)

async function testRead() {
  try {
    const data = await readFile('file.txt')
  } catch(err) {
    console.log(data)
  }
}

感谢您的阅读,有不足之处请为我指出。

参考

  1. 谈谈 ES6 的 Promise 对象

  2. 深入浅出ES6(三):生成器 Generators

  3. 深入浅出ES6(十一):生成器 Generators,续篇

本文同步于我的个人博客 http://blog.acwong.org/2017/06/24/javascript-async-programming/

查看原文

赞 27 收藏 120 评论 0

ac黄 关注了问题 · 2016-08-30

怎么判断键盘上Tab键切换到的div

一个注册页面,有一两个输入框采用自己的代码模拟下拉框,其他的input输入框可以用键盘上Tab键切换聚焦到,但是这个模拟的下拉框就无法判断是否聚焦,请教各路大神如何判断出?
图片描述

关注 3 回答 2

认证与成就

  • 获得 193 次点赞
  • 获得 12 枚徽章 获得 1 枚金徽章, 获得 3 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-03-29
个人主页被 1.1k 人浏览