Giter VIP home page Giter VIP logo

awsome-front-end-xmind's People

Contributors

bailinlin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

awsome-front-end-xmind's Issues

移动端手势库AlloyFinger源码分析

简介

AlloyFinger 是由腾讯前端团队 AlloyTeam 出品的一个小巧轻量级的移动端手势库,整个手势库的代码不超过400行,却支持绝大多数的手势操作,能够满足日常的开发需求。AlloyFinger传送门: AlloyFinger

JavaScript 移动端触摸事件

手机移动端浏览器提供了4种触摸事件:touchstart,touchmove,touchend,touchcancel,分别对应的是手指触点刚接触屏幕时触发事件,手指触点在屏幕上移动时触发事件,手指触点移开屏幕时触发事件以及被系统中断时触发事件(按 Home 键返回主屏等操作)。

这里要说明下,移动端浏览器也支持部分 PC 端带有的事件,比如 click 事件。但是在移动端上,click 事件会存在延时触发的情况,大概延时300ms。

移动端300ms延时触发 click 事件

在移动端为什么click事件会存在延时触发的情况呢?究其原因,是因为苹果公司在早期发布iphone的时候,采用了双击缩放网页的设计。当用户手指点击一次屏幕时,浏览器不能立即判定用户操作是单击操作还是双击操作,而是延迟了300ms,以判断用户是否再次点击了屏幕,如果300ms之内没有再次点击屏幕就判定为单击事件,才会去触发click事件。

源码分析

AlloyTeam 团队为 AlloyFinger 打造了多个能够适用不同技术栈中的手势库版本,能够方便的使用在 React 框架,Vue框架以及原生JS中。不同场景下的手势库版本的实现思路都是一样的,所以这里只分析了原生JS的实现思路。

如何使用

AlloyFinger 的使用方式非常简单,源码中暴露出了一个全局的 AlloyFinger 构造函数对象,使用方式如下,返回值是一个 AlloyFinger 实例对象。

// element 是需要手势操作的DOM元素,值可以是DOM对象也可以是元素选择器。
// options 是一个对象,包含了需要的手势操作函数。
var af = new AlloyFinger(element, options);

var af = new AlloyFinger(element, {
    tap: function() {
        //do something...
    }
});

有了 AlloyFinger 实例对象后,你还可以通过绑定自定义事件的方式使用手势库

//绑定手势事件
af.on('tap', function() {
    //do something...
});
//解绑手势事件
af.off('tap', function() {
    //do something...
});
//销毁实例
af.destroy();

整体架构

源码架构

AlloyFinger 构造函数

首先,先定义了一个 AlloyFinger 构造函数,里面做了很多操作,事件的监听回调,变量值的初始化,将手势操作作为订阅者添加到订阅列表中。在这部分源码中,会初始化很多关于手指触点的水平坐标和垂直坐标的存储变量,刚开始看的时候会觉得代码比较的混乱,所以笔者把这部分的变量捋一遍梳理了出来,便于清晰的阅读源码。

this.x1: 存储在刚开始触摸时第一个手指触点的X坐标位置
this.y1: 存储在刚开始触摸时第一个手指触点的Y坐标位置
this.preV.x: 存储第一个手指触点与第二个手指触点之间的水平间距
this.preV.y: 存储第一个手指触点与第二个手指触点之间的垂直间距
this.x2: 存储在移动操作时第一个手指触点的X坐标位置
this.y2: 存储在移动操作时第一个手指触点的Y坐标位置
this.sx2: 存储在移动操作时第二个手指触点的X坐标位置
this.sy2: 存储在移动操作时第二个手指触点的Y坐标位置

整个的源码解读都放置在我的github上,几乎每一行都有自己的注解,感兴趣的话可以点击这里:传送门

源码都是精简干练的,多看优秀的源码还是对自己的技术有帮助的,可能看完了之后会思考自己怎么去DIY一个手势库呢?想要自己怎么去DIY一个手势库,必须得先了解各个手势操作的实现思路,有思路了之后才能动手写代码。

具体实现

  • tap点击

tap的本质其实就是touchend,但是在具体实现的时候必须做下限制,当前只存在一个手指触点,且touchstart的时候手指触点和touchend时手指触点的X轴Y轴的偏差不能小于30,这样才能判定当前的操作是tap操作。

var len = evt.touches.length;
if(len < 1) {
    if ((this.x2 && Math.abs(this.x1 - this.x2) <= 30) ||
                (this.y2 && Math.abs(this.y1 - this.y2) <= 30)) {
                    //我是tap操作,do something...
    }
}
  • doubleTap

doubleTap双击操作的实现思路大致是这样的,得先判断一段时间内是否有两次touchstart操作,并且两次touchstart都是快速完成的,不然会被认为是长按操作了,还有一点就是两次触点的位置的X轴Y轴的偏差不能小于30。

//存储手指按下触摸操作的时间戳
this.now = null;
//存储上一次手指触点触摸的时间戳
this.last = null;
//用于存储手指触摸操作时的水平坐标和垂直坐标(如果是多指触摸操作,则记录的是第一个手指触摸的位置)
this.preTapPosition = { x: null, y: null };
//是否为双击操作
this.isDoubleTap = false;
...
function start() {
    this.now = Date.now();
    if (this.preTapPosition.x !== null) {
        //如果手指连续触摸操作之间的时间间隔小于250毫秒,且手指连续触摸操作之间的触点位置水平坐标小于30,垂直坐标小于30,那么就判定该操作为双击操作
        this.isDoubleTap = (this.delta > 0 && this.delta <= 250 && Math.abs(this.preTapPosition.x - this.x1) < 30 && Math.abs(this.preTapPosition.y - this.y1) < 30);
    }
    this.preTapPosition.x = this.x1;
    this.preTapPosition.y = this.y1;
    this.last = this.now;
}
function end() {
    if (this.isDoubleTap) {
        //我是doubleTap操作,do something...
    }
}
  • swipe

swipe滑过操作具体的实现思路是touchstart的手指触点的坐标和touchend时候手指触点的坐标x、y方向偏移要大于30,且还要判断是往哪个方向滑动。

//判定swipe滑动的方向
_swipeDirection: function (x1, x2, y1, y2) {
    return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')
}

更多的手势操作源码分析可以参考我的github上的源码分析,传送门

AlloyFinger 手势库还用在了一个小巧的移动端裁剪图片工具上,下次还可以分析一波裁剪工具 AlloyCrop 的源码,学习到裁剪图片的原理和实现方案,平时在开发过程中,其实只要清楚了实现思路和原理,就能够方便的实现具体的功能。

javascript 实现高仿 growingIO

前言

埋点,是网站分析的一种常用的数据采集方法。我们主要用来采集用户行为数据(例如页面访问路径,点击了什么元素)进行数据分析,从而让运营同学更加合理的安排运营计划。现在市面上有很多第三方埋点服务商,百度统计,友盟,growingIO 等大家应该都不太陌生,大多情况下大家都只是使用,最近我研究了下 web 埋点,你要不要了解下。

现有埋点三大类型

用户行为分析是一个大系统,一个典型的数据平台。由用户数据采集,用户行为建模分析,可视化报表展示几个模块构成。现有的埋点采集方案可以大致被分为三种,手动埋点,可视化埋点,无埋点

  1. 手动埋点
    手动代码埋点比较常见,需要调用埋点的业务方在需要采集数据的地方调用埋点的方法。优点是流量可控,业务方可以根据需要在任意地点任意场景进行数据采集,采集信息也完全由业务方来控制。这样的有点也带来了一些弊端,需要业务方来写死方法,如果采集方案变了,业务方也需要重新修改代码,重新发布。
  2. 可视化埋点
    可是化埋点是近今年的埋点趋势,很多大厂自己的数据埋点部门也都开始做这块。优点是业务方工作量少,缺点则是技术上推广和实现起来有点难(业务方前端代码规范是个大前提)。阿里的活动页很多都是运营通过可视化的界面拖拽配置实现,这些活动控件元素都带有唯一标识。通过埋点配置后台,将元素与要采集事件关联起来,可以自动生成埋点代码嵌入到页面中。
  3. 无埋点
    无埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据,优点是前端只要加载埋点脚本。缺点是流量和采集的数据过于庞大,服务器性能压力山大,主流的 GrowingIO 就是这种实现方案。

我们暂时放弃可视化埋点的实现,在 手动埋点无埋点 上进行了尝试,为了便于描述,下文我会称采集脚本为 SDK。

思考几个问题

埋点开发需要考虑很多内容,贯穿着不轻易动手写代码的原则,我们在开发前先思考下面这几个问题

  1. 我们要采集什么内容,进行哪些采集接口的约定
  2. 业务方通过什么方式来调用我们的采集脚本
  3. 手动埋点:SDK 需要封装一个方法给业务方进行调用,传参方式业务方可控
  4. 无埋点:考虑到数据量对于服务器的压力,我们需要对无埋点进行开关配置,可以配置进行哪些元素进行无埋点采集
  5. 用户标识:游客用户和登录用户的采集数据怎么进行区分关联
  6. 设备Id:用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样,怎么实现
  7. 单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异
  8. 混合应用:app 与 h5 的混合应用我们要怎么进行通讯

我们要采集什么内容,进行哪些采集接口的约定

第一期我们先实现对 PV(即页面浏览量或点击量) 、UV(一天内同个访客多次访问) 、点击量、用户的访问路径的基础指标的采集。精细化分析的流量转化需要和业务相关,需要和数据分析方做约定,我们预留扩展。所以我们的采集接口需要进行以下的约定

{
    "header":{ // HTTP 头部
        "X-Device-Id":" 550e8400-e29b-41d4-a716-446655440000", //设备ID,用来区分用户设备
        "X-Source-Url":"https://www.baidu.com/", //源地址,关联用户的整个操作流程,用于用户行为路径分析,例如登录,到首页,进入商品详情,退出这一整个完整的路径
        "X-Current-Url":"", //当前地址,用户行为发生的页面
        "X-User-Id":"",//用户ID,统计登录用户行为
    },
    "body":[{ // HTTP Body体
        "PageSessionID":"", //页面标识ID,用来区分页面事件,例如加载和离开我们会发两个事件,这个标识可以让我们知道这个事件是发生在一个页面上
        "Event":"loaded", //事件类型,区分用户行为事件
        "PageTitle":  "埋点测试页",  //页面标题,直观看到用户访问页面
        "CurrentTime":  “1517798922201”,  //事件发生的时间
        "ExtraInfo":  {
         }    //扩展字段,对具体业务分析的传参
    }]
}

以上就是我们现在约定好了的通用的事件采集的接口,所传的参数基本上会根据采集事件的不同而发生变化。但是在用户的整一个访问行为中,用户的设备是不会变化的,如果你想采集设备信息可以重新约定一个接口,在整个采集开始之前发送设备信息,这样可以避免在事件采集接口上重复采集固定数据。

{
    "header":{ // HTTP 头部
          "X-Device-Id"  :"550e8400-e29b-41d4-a716-446655440000"  ,      //  设备id
    },
    "body":{ // HTTP Body体
              "DeviceType":  "web" ,   //设备类型
             "ScreenWide"  :  768 , //  屏幕宽
             "ScreenHigh":  1366 , //  屏幕高
             "Language":    "zh-cn"  //语言
    }
}

业务方通过什么方式来调用我们的采集脚本

埋点应该让调用的业务方,尽可能少有工作量,最好是什么都不用做,😁,但是实现起来有点难额。我们采用的方案是让业务方在代码里通过 script 脚本来引用我们的 SDK ,业务方只要配置一些需要的参数进行埋点定制(👆我们讲到过的无埋点的流量控制),然后什么都不做就可以进行基础数据的采集。

(function() {
			var collect = document.createElement('script');
			collect.type = 'text/javascript';
			collect.async = true;
			collect.src =  'http://collect.trc.com/index.js';
			var s = document.getElementsByTagName('script')[0];
			s.parentNode.insertBefore(collect, s);
	})();


//用户自定义要进行无埋点采集的元素,如果不进行无埋点采集,可以不配置
 var _XT = [];
_XT.push(['Target','div']);

手动埋点:SDK

如果业务方需要采集更多业务定制的数据,可以调用我们暴露出的方法进行采集

//自定义事件
sdk.dispatch('customEvent',{extraInfo:'自定义事件的额外信息'})

游客与用户关联

我们使用 userId 来做用户标识,同一个设备的用户,从游客用户切换到登录用户,如果我们要把他们关联起来,需要有一个设备Id 做关联

web 设备Id

用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样。web 变量存储,我们第一时间想到的就是 cookie,sessionStorage,localStorage,但是这3种存储方式都和访问资源的域名相关。我们总不能每次访问一个网站就新建一个设备指纹吧,所以我们需要通过一个方法来跨域共享设备指纹

我们想到的方案是,通过嵌套 iframe 加载一个静态页面,在 iframe 上加载的域名上存储设备id,通过跨域共享变量获取设备id,共享变量的原理是采用了iframe 的 contentWindow通讯,通过 postMessage 获取事件状态,调用封装好的回调函数进行数据处理具体的实现方式

//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id,
    collect.setIframe = function () {
        var that = this
        var iframe = document.createElement('iframe')
        iframe.id = "frame",
        iframe.src = 'http://collectiframe.trc.com' // 配置域名代理,目的是让开发测试生产环境代码一致
        iframe.style.display='none' //iframe 设置的目的是用来生成固定的设备id,不展示
        document.body.appendChild(iframe)

        iframe.onload = function () {
                iframe.contentWindow.postMessage('loaded','*');
        }

        //监听message事件,iframe 加载完成,获取设备id ,进行相关的数据采集
        helper.on(window,"message",function(event){
            that.deviceId = event.data.deviceId

            if(event.data && event.data.type == 'loaded'){
                that.sendDevice(that.getDevice(), that.deviceUrl);
                setTimeout(function () {
                    that.send(that.beforeload)
                    that.send(that.loaded)
                },1000)
            }
        })
    }

iframe 与 SDK 通讯

function receiveMessageFromIndex ( event ) {
    getDeviceInfo() // 获取设备信息
    var data =  {
            deviceId: _deviceId,
            type:event.data
    }

    event.source.postMessage(data, '*'); // 将设备信息发送给 SDK
}

//监听message事件
if(window.addEventListener){
        window.addEventListener("message", receiveMessageFromIndex, false);
}else{
        window.attachEvent("onmessage", receiveMessageFromIndex, false)

如果你想知道可以看我的另一篇博客 web 浏览器指纹跨域共享

单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异

我们知道单页面应用都是无刷新的页面加载,所以我们在页面跳转的处理和我们的普通的页面会有所不同。单页面应用的路由插件运用了 window 自带的无刷新修改用户浏览记录的方法,pushState 和 replaceState。

window 的 history 对象 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录,所以我们只要改写 history 的方法,在方法执行前执行我们的采集方法就能实现对单页面应用的页面跳转事件的采集了

 // 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入我们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法

    collect = {}
    collect.onPushStateCallback : function(){}  // 自定义的采集方法

    (function(history){
        var replaceState = history.replaceState;   // 存储原生 replaceState
        history.replaceState = function(state, param) {     // 改写 replaceState
           var url = arguments[2];
           if (typeof collect.onPushStateCallback == "function") {
                 collect.onPushStateCallback({state: state, param: param, url: url});   //自定义的采集行为方法
           }
           return replaceState.apply(history, arguments);    // 调用原生的 replaceState
        };
     })(window.history);

这块介绍起来也比较的复杂,如果你想了解更多,可以看我的另一篇博客你需要知道的单页面路由实现原理

混合应用:app 与 h5 的混合应用我们要怎么进行通讯

现在大部分的应用都不是纯原生的应用, app 与 h5 的混合的应用是现在的一种主流。

纯 web 数据采集我们考虑到前端存储数据容易丢失,我们在每一次事件触发的时候都用采集接口传输采集到的数据。考虑到现在很多用户的手机会有流量管家的软件监控,如果在 App 中 h5 还是采集到数据就传输给服务端,很有可能会让流量管家检测到,给用户报警,从而使得用户不再信任你的 App , 所以我们在用户操作的时候将数据传给 app 端,存储到 app。用户切换应用到后台的时候,通过 app 端的 SDK 打包传输到服务器,我们给 app 提供的方法封装了一个适配器

// app 与 h5 混合应用,直接将数信息发给 app
collect.saveEvent = function (jsonString) {

    collect.dcpDeviceType && setTimeout(function () {
        if(collect.dcpDeviceType=='android'){
            android.saveEvent(jsonString)
        } else {
            window.webkit && window.webkit.messageHandlers ? window.webkit.messageHandlers.nativeBridge.postMessage(jsonString) : window.postBridgeMessage(jsonString)
        }

    },1000)
	}

实现思路

通过上面几个问题的思考,我们对埋点的实现大致已经有了一些想法,我们使用思维导图来还原下我们即将要做的事情,图片记得放大看哦,太小了可能看不清。

  1. 我们需要暴露给业务方调用的方法

暴露方法
2. 我们需要处理的事件类型
监听事件
3. SDK 的基本实现思路
实现逻辑

我们来看下几个核心代码的实现

工具方法

我们定义了几个工具方法,提高开发的幸福指数 😝

    var helper = {};

    // 生成一个唯一的标识,pageSessionId (用这个变量来关联开始加载、加载完成、离开页面的事件,计算出页面加菜时间,停留时间)
    helper.uuid = function(){}

    // 元素绑定事件监听,兼容浏览器到IE8
    helper.on = function(){}

    //元素移除事件监听的适配器函数,兼容浏览器到IE8
    helper.remove = function(){}

    //将json转为字符串,事件传输的参数类型转化
    helper.changeJSON2Query = function(){}

    //将相对路径解析成文档全路径
    helper.normalize = function(){}

采集逻辑

    var collect = {
        deviceUrl:'http://collect.trc.com/rest/collect/device/h5/v1',
        eventUrl:'http://collect.trc.com/rest/collect/event/h5/v1',
        isuploadUrl:'http://collect.trc.com/rest/collect/isupload/app/v1',
        parmas:{ ExtraInfo:{} },
        device:{}
    };

    //获取埋点配置
    collect.setParames = function(){}

    //更新访问路径及页面信息
    collect.updatePageInfo = function(){}

    //获取事件参数
    collect.getParames = function(){}

    //获取设备信息
    collect.getDevice = function(){}

    //事件采集
    collect.send = function(){}

    //设备采集
    collect.sendDevice = function(){}

    //判断才否采集,埋点采集的开关
    collect.isupload = function(){

        1. 判断是否采集,不采集就注销事件监听(项目中区分游客身份和用户身份的采集情况,这个方法会被判断两次)
        2. 采集则判断是否已经采集过
            a.已经采集过不做任何操作
            b.没有采集过添加事件监听
        3. 判断是 混合应用还是纯 web 应用
            a.如果是web 应用,调用 collect.setIframe 设置 iframe
            b.如果是混合应用 将开始加载和加载完成事件传输给 app
    }

    //点击事件处理函数
    collect.clickHandler = function(){}

    //离开页面的事件处理函数
    collect.beforeUnloadHandler = function(){}

    //页面回退事件处理函数
    collect.onPopStateHandler = function(){}

    //系统事件初始化,注册离开事件,浏览器后退事件
    collect.event = function(){}

    //获取记录开始加载数据信息
    collect.getBeforeload = function(){}

    //存储加载完成,获取设备类型,记录加载完成信息
    collect.onload = function(){

        1. 判断cookie是否有存设备类型信息,有表示混合应用
        2. 采集加载完成时间等信息
        3. 调用 collect.isupload 判断是否进行采集
    }

    //web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id
    collect.setIframe = function(){}

    //app 与 h5 混合应用,直接将数信息发给 app,判断设备类型做原生方法适配器
    collect.saveEvent = function(){}

    //采集自定义事件类型
    collect.dispatch = function(){}

    //将参数 userId 存入sessionStorage
    collect.storeUserId = function(){}

    //采集H5信息,如果是混合应用,将采集到的信息发送给 app 端
    collect.saveEventInfo = function(){}

    //页面初始化调用方法
    collect.init = function(){

        1. 获取开始加载的采集信息
        2. 获取 SDK 配置信息,设备信息
        3. 改写 history 两个方法,单页面应用页面跳转前调用我们自己的方法
        4. 页面加载完成,调用 collect.onload 方法

    }


    collect.init(); // 初始化

    //暴露给业务方调用的方法
    return {
        dispatch:collect.dispatch,
        storeUserId:collect.storeUserId,
    }

扩展

👆就是我这段时间研究的成果了,代码的篇幅比较长,就不放在博客里了,感兴趣的同学可以加我微信进行交流,或则在文章下面留言,也欢迎大家给我提意见,帮忙优化 😝。

web 浏览器指纹跨域共享

你需要知道的单页面路由实现原理

数据埋点是什么?设置埋点的意义是什么?

数据采集与埋点

美团点评前端无痕埋点实践

如何清楚易懂的解释“UV和PV"的定义

你需要知道的单页面路由实现原理

前言

最近开发的埋点项目,需要记录用户行为轨迹即用户页面访问顺序。需要在页面跳转的时候,记录用户访问的信息(比如 url ,请求头部等),非单页面应用可以给 window 对象加上一个 beforeunload 事件,在页面离开时触发采集开关,但是现在很多业务是单页面应用,用户切换地址的时候,是无刷新的局部更新,没有办法触发 beforeunload。所以单页面应用的路由插件一定运用了 window 自带的,无刷新修改用户浏览记录的方法,pushState 和 replaceState。

pushState 和 replaceState 了解一下

history 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录

history 对象的详细信息已经有很多很好很详细的介绍文献,这里不再做总结,我们引用阮老师的教程介绍,history对象 -- JavaScript 标准参考教程(alpha)

history.pushState

history.pushState方法接受三个参数,依次为:

state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。

title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。

url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
假定当前网址是example.com/1.html,我们使用pushState方法在浏览记录(history对象)中添加一个新记录。

var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');

添加上面这个新记录后,浏览器地址栏立刻显示 example.com/2.html,但并不会跳转到 2.html,甚至也不会检查2.html 是否存在,它只是成为浏览历史中的最新记录。这时,你在地址栏输入一个新的地址(比如访问 google.com ),然后点击了倒退按钮,页面的 URL 将显示 2.html;你再点击一次倒退按钮,URL 将显示 1.html。

总之,pushState 方法不会触发页面刷新,只是导致 history 对象发生变化,地址栏会有反应。

如果 pushState 的 url参数,设置了一个新的锚点值(即hash),并不会触发 hashchange 事件。如果设置了一个跨域网址,则会报错。

// 报错
history.pushState(null, null, 'https://twitter.com/hello');

上面代码中,pushState想要插入一个跨域的网址,导致报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上。

history.replaceState

history.replaceState 方法的参数与 pushState 方法一模一样,区别是它修改浏览历史中当前纪录,假定当前网页是 example.com/example.html。

history.pushState({page: 1}, 'title 1', '?page=1');
history.pushState({page: 2}, 'title 2', '?page=2');
history.replaceState({page: 3}, 'title 3', '?page=3');

history.back()
// url显示为http://example.com/example.html?page=1

history.back()
// url显示为http://example.com/example.html

history.go(2)
// url显示为http://example.com/example.html?page=3

单页面应用用户访问轨迹埋点

开发过单页面应用的同学,一定比较清楚,单页面应用的路由切换是无感知的,不会重新进行 http 请求去获取页面,而是通过改变页面渲染视图来实现。所以他的实现原理一定也是通过原生的 pushState 或则 replaceState 来实现的。所以在页面跳转的时候一定会调用 pushState 或则 replaceState ,要记录用户的跳转信息,我们只要拦截 pushState 和 replaceState,在执行默行为先执行我们的方法就能够采集到用户的跳转信息了

// 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入我们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法

collect = {}

collect.onPushStateCallback : function(){}  // 自定义的采集方法

(function(history){
    var replaceState = history.replaceState;   // 存储原生 replaceState
    history.replaceState = function(state, param) {     // 改写 replaceState
       var url = arguments[2];
       if (typeof collect.onPushStateCallback == "function") {
             collect.onPushStateCallback({state: state, param: param, url: url});   //自定义的采集行为方法
       }
       return replaceState.apply(history, arguments);    // 调用原生的 replaceState
    };
 })(window.history);

vue-router 的路由实现

既然知道了这个原理,我们来看下 vue-router 的实现,我们打开 vue-router 项目地址,把项目克隆下来,或则直接在 github 上预览,在 Vue 开发的项目里,我们通过 router.push('home') 来实现页面的跳转,所以我们检索下,push 方法的实现

查找push方法的实现

我们检索到了 20 个 js 文件,😂,一般到这个时候,我们会放弃源码阅读,那么我们今天的文章就到这结束,谢谢大家!

开个玩笑,源码阅读不能这么粗糙,我们找到 src 目录,点开 index.js 文件,看到 history对象的定义和 mode 参数有关

if (!inBrowser) {
  mode = 'abstract'
}
this.mode = mode

switch (mode) {
  case 'history':
    this.history = new HTML5History(this, options.base)
    break
  case 'hash':
    this.history = new HashHistory(this, options.base, this.fallback)
    break
  case 'abstract':
    this.history = new AbstractHistory(this, options.base)
    break
  default:
    if (process.env.NODE_ENV !== 'production') {
      assert(false, `invalid mode: ${mode}`)
    }
}

看到 history 对象的实例与配置的 mode 有关,vue-router 通过3中方式实现了路由切换。与我们今天讲的内容相匹配的是 HTML5History 的实现方案,其他的将不再文章中做扩展,若果你感兴趣想要了解,可以看文章后面的扩展阅读

我们来看 vue-router 中的 HTML5History 源码:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    pushState(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    replaceState(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

// src/util/push-state.js
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      history.replaceState({ key: _key }, '', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}

在使用 Vue 开发的过程中,我们一定用到过 push 和 replace 来改变路由,和视图。

router 实例调用的 push 实际是 history 的方法,通过 mode 来确定匹配 history 的实现方案,从代码中我们看到,push 调用了 src/util/push-state.js 中被改写过的 pushState 的方法,改写过的方法会根据传入的参数 replace?: boolean 来进行判断调用 pushState 还是 replaceState ,同时做了错误捕获,如果,history 无刷新修改访问路径失败,则调用 window.location.replace(url) ,有刷新的切换用户访问地址 ,同理 pushState 也是这样。这里的 transitionTo 方法主要的作用是做视图的跟新及路由跳转监测,如果 url 没有变化(访问地址切换失败的情况),在 transitionTo 方法内部还会调用一个 ensureURL 方法,来修改 url。 transitionTo 方法中应用的父方法比较多,这里不做长篇赘述,具体代码分析可以关注后我以后的文章

模拟单页面路由

通过上面的学习,我们知道了,单页面应用路由的实现原理,我们也尝试去实现一个。在做管理系统的时候,我们通常会在页面的左侧放置一个固定的导航 sidebar,页面的右侧放与之匹配的内容 main 。点击导航时,我们只希望内容进行更新,如果刷新了整个页面,到时导航和通用的头部底部也进行重绘重排的话,十分浪费资源,体验也会不好。这个时候,我们就能用到我们今天学习到的内容,通过使用 HTML5 的 pushState 方法和 replaceState 方法来实现,

思路:首先绑定 click 事件。当用户点击一个链接时,通过 preventDefault 函数防止默认的行为(页面跳转),同时读取链接的地址(如果有 jQuery,可以写成$(this).attr('href')),把这个地址通过pushState塞入浏览器历史记录中,再利用 AJAX 技术拉取(如果有 jQuery,可以使用$.get方法)这个地址中真正的内容,同时替换当前网页的内容。

为了处理用户前进、后退,我们监听 popstate 事件。当用户点击前进或后退按钮时,浏览器地址自动被转换成相应的地址,同时popstate事件发生。在事件处理函数中,我们根据当前的地址抓取相应的内容,然后利用 AJAX 拉取这个地址的真正内容,呈现,即可。

最后,整个过程是不会改变页面标题的,可以通过直接对 document.title 赋值来更改页面标题。

扩展

好了,我们今天通过多个方面来讲了 pushState 方法和 replaceState 的应用,你应该对这个两个方法能有一个比较深刻的印象,如果想要了解更多,你可以参考以下链接

history对象 -- JavaScript 标准参考教程(alpha)

从vue-router看前端路由的两种实现

精读《你不知道的 javascript(上卷)》

前言

《你不知道的 javascript》是一个前端学习必读的系列,让不求甚解的JavaScript开发者迎难而上,深入语言内部,弄清楚JavaScript每一个零部件的用途。本书介绍了该系列的两个主题:“作用域和闭包”以及“this和对象原型”。这两块也是值得我们反复去学习琢磨的两块只是内容,今天我们用思维导图的方式来精读一遍。(思维导图图片可能有点小,记得点开看,你会有所收获)

第一部分 作用域和闭包

作用域是什么

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对 变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。赋值操作符会导致 LHS 查询。 的赋值操作。 =操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。
JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像 var a = 2 这样的声 明会被分解成两个独立的步骤:

  1. 首先, var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
  2. 接下来, a = 2 会查询(LHS 查询)变量 a 并对其进行赋值。

LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所 需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层 楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。

不成功的RHS引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛 出 ReferenceError 异常(严格模式下)。

词法作用域

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段 基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它 们进行查找。

JavaScript 中有两个机制可以“欺骗”词法作用域: eval(..) 和 with 。 前者可以对一段包 含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在 运行时)。后者本质上是通过将一个对象的引用 当作 作用域来处理,将对象的属性当作作 用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都 将 导致代码运行变慢。 不要使用它们。

函数作用域和块作用域

函数是 JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会 在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。

但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域, 也可以属于某个代码块(通常指 { .. } 内部)。

从 ES3 开始, try/catch 结构在 catch 分句中具有块作用域。在 ES6 中引入了 let 关键字( var 关键字的表亲), 用来在任意代码块中声明变量。 if(..) { let a = 2; } 会声明一个劫持了 if 的 { .. } 块的变量,并且将变量添加到这个块 中。

有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开 发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。

提升

我们习惯将 var a = 2; 看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a 和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。

这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前 首先 进行处理。 可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。

要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引 起很多危险的问题!

作用域闭包

闭包就好像从 JavaScript 中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人 才能够到达那里。但实际上它只是一个标准,显然就是关于如何在函数作为值按需传递的 词法环境中书写代码的。

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。

如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循 环中。但同时闭包也是一个非常强大的工具,可以用多种形式来实现 模块 等模式。模块有两个主要特征:

(1)为创建内部作用域而调用了一个包装函数;
(2)包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭 包。

现在我们会发现代码中到处都有闭包存在,并且我们能够识别闭包然后用它来做一些有用 的事!

第二部分 this 和对象原型

this 全面解析

如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  1. 由 new 调用?绑定到新创建的对象。

  2. 由 call 或者 apply (或者 bind )调用?绑定到指定的对象。

  3. 由上下文对象调用?绑定到那个上下文对象。

  4. 默认:在严格模式下绑定到 undefined ,否则绑定到全局对象。

一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑 定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null) ,以保护全局对象。ES6中的箭头函数并不会使用四条标准的绑定规则, 而是根据当前的词法作用域来决定 this ,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样。

对象

JavaScript 中的对象有字面形式(比如 var a = { .. } )和构造形式(比如 var a = new Array(..) )。字面形式更常用,不过有时候构造形式可以提供更多选项。

许多人都以为“JavaScript 中万物都是对象”,这是错误的。对象是 6 个(或者是 7 个,取 决于你的观点)基础类型之一。对象有包括 function 在内的子类型,不同子类型具有不同 的行为,比如内部标签 [object Array] 表示这是对象的子类型数组。

对象就是键 / 值对的集合。可以通过 .propName 或者 ["propName"] 语法来获取属性值。访 问属性时, 引擎实际上会调用内部的默认 [[Get]] 操作(在设置属性值时是 [[Put]] ), [[Get]] 操作会检查对象本身是否包含这个属性,如果没找到的话还会查找 [[Prototype]] 链(参见第 5 章)。

属性的特性可以通过属性描述符来控制,比如 writable 和 configurable 。此外,可以使用 Object.preventExtensions(..) 、 Object.seal(..) 和 Object.freeze(..) 来设置对象(及其 属性)的不可变性级别。

属性不一定包含值——它们可能是具备 getter/setter 的“访问描述符”。此外,属性可以是 可枚举或者不可枚举的,这决定了它们是否会出现在 for..in 循环中。

你可以使用 ES6 的 for..of 语法来遍历数据结构(数组、对象, 等等)中的值, for..of 会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。

混合对象"类"

类是一种设计模式。 许多语言提供了对于面向类软件设计的原生语法。 JavaScript 也有类 似的语法,但是和其他语言中的类完全不同。

类意味着复制。

传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类 中。

多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父 类,但是本质上引用的其实是复制的结果。

JavaScript 并不会(像类那样)自动创建对象的副本。

混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆 弱的语法,比如显式伪多态( OtherObj.methodName.call(this, ...) ),这会让代码更加难 懂并且难以维护。

此外, 显式混入实际上无法完全模拟类的复制行为, 因为对象(和函数!别忘了函数也 是对象)只能复制引用, 无法复制被引用的对象或者函数本身。 忽视这一点会导致许多 问题。

总地来说,在 JavaScript 中模拟类是得不偿失的,虽然能解决当前的问题,但是可能会埋下更多的隐患。

原型

如果要访问对象中并不存在的一个属性, [[Get]] 操作(参见第 3 章)就会查找对象内部 [[Prototype]] 关联的对象。这个关联关系实际上定义了一条“原型链”(有点像嵌套的作用域链),在查找属性时会对它进行遍历。

所有普通对象都有内置的 Object.prototype ,指向原型链的顶端(比如说全局作用域),如 果在原型链中找不到指定的属性就会停止。 toString() 、 valueOf() 和其他一些通用的功能 都存在于 Object.prototype 对象上,因此语言中所有的对象都可以使用它们。

关联两个对象最常用的方法是使用 new 关键词进行函数调用, 在调用的 章)中会创建一个关联其他对象的新对象。4个步骤(第2章)中会创建一个关联其他对象的新对象。

使用 new 调用函数时会把新对象的 .prototype 属性关联到“其他对象”。带 new 的函数调用 通常被称为“构造函数调用”,尽管它们实际上和传统面向类语言中的 类构造函数 不一样。

JavaScript 是 中的机制有一个核心区别, 那就是不会进行复制, 对象之间是通过内部的

虽然这些 机制和传统面向类语言中的“类初始化”和“类继承”很相似, 但是 javascript 机制和传统面向对象类语言中的“类初始化”和“类继承”很相似但是 javascript 中的机制有一个核心区别,就是不会进行复制,对象之间是通过内部的 [[Prototype]] 链关联的。

出于各种原因,以“继承”结尾的术语(包括“原型继承”)和其他面向对象的术语都无 法帮助你理解 JavaScript 的 真实 机制(不仅仅是限制我们的思维模式)。

相比之下,“委托”是一个更合适的术语,因为对象之间的关系不是 复制 而是委托。

行为委托

在软件架构中你可以 选择是否 使用类和继承设计模式。大多数开发者理所当然地认为类是 唯一(合适)的代码组织方式,但是本章中我们看到了另一种更少见但是更强大的设计模式: 行为委托 。

行为委托认为对象之间是兄弟关系, 互相委托, 而不是父类和子类的关系。 JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。也就是说,我们可以选择在 JavaScript 中努 力实现类机制(参见第 4 和第 5 章),也可以拥抱更自然的 [[Prototype]] 委托机制。

当你只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。

对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把 它们抽象成类。对象关联可以用基于 [[Prototype]] 的行为委托非常自然地实现。

扩展

思维导图能比较清晰的还原整本书的知识结构体系,如果你还没用看过这本书,可以按照这个思维导图的思路快速预习一遍,提高学习效率。学习新事物总容易遗忘,我比较喜欢在看书的时候用思维导图做些记录,便于自己后期复习,如果你已经看过了这本书,也建议你收藏复习。如果你有神马建议或则想法,欢迎留言或加我微信交流:646321933

你不知道的javascript上卷第二部分在线文档

你不知道的 javascript(上卷)PDF 下载地址

Webpack4+Babel7+ES6兼容IE8

前阵子重构了一个挺有意思的项目,是一个基于浏览器环境的数据采集sdk。公司各个产品的前端页面中都嵌入了这个sdk,用于采集用户的行为数据,上传到公司的大数据平台,为后续的运营决策分析提供数据支撑。

笔者接手这个项目的时候,前任开发者已经把功能都写差不多了。唯一需要做的就是做下模块化拆分和代码规范,以便后续的开发维护。模块化拆分用webpack,代码规范用eslint。既然要重构,那就顺手用es6重写吧。callback也不要了,全换成promise,async、await也用起来,反正怎么爽怎么写。

问PM浏览器最低兼容到哪个版本,PM说兼容公司各个产品所兼容的最低版本就行。和公司各个产品的前端负责人沟通后发现,居然有兼容IE8的,真是我了个fk。

google了一下Webpack+Babel+ES6兼容IE8,果然坑很多。试了好几篇博客给出的方案,都跑不通。也没怎么研究具体哪里有问题,因为那些解决方案里面的webpack和babel都是旧版的,跑通了也不高兴用。笔者分析了那些博客中提出的几个关键性问题,然后参考webpack和babel最新的官方文档,总结出一套最新的Webpack4+Babel7+ES6兼容IE8的方案。

ES6兼容IE8需要解决四个问题

语法支持

IE浏览器不支持ES6的语法,只在IE10、IE11中支持了部分ES6的API,所以在IE浏览器中使用ES6需要把ES6的代码编译成ES5才能执行。方法也很简单,就是用babel-loader。这部分没什么坑,所以我也就不细说了。给个网站,大家可以自行查看ES5、ES6在各浏览器版本中的支持情况

https://kangax.github.io/compat-table/es6/

ES3保留关键字

如果在IE8下通过object.propertyName的方式使用ES3中的保留关键字(比如default、class、catch),就会报错

SCRIPT1048: 缺少标识符

webpack有一款loader插件es3ify-loader专门用来处理ES3的关键字兼容问题。这个插件的作用就是把这些保留字给你加上引号,使用字符串的形式引用。

// 编译前
function(t) { return t.default; }

// 编译后
function(t) { return t["default"]; }

然而,笔者亲身实践后发现,UglifyJS本来就已经提供了对IE浏览器的支持,不需要额外引入es3ify-loader。webpack默认的UglifyJS配置不支持ie8,需要手动配下。

{
  mode: 'production',
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        uglifyOptions: {
          ie8: true
        }
      })
    ]
  }
}

执行环境

解决了前面两个问题只能保证语法上不报错,但使用ES6中的API(比如Promise)还是会报错。另外,IE8对ES5的API支持也很差,只支持了少量的API,有些API还只是支持部分功能(比如Object.defineProperty)。所以,要在IE8中完美运行ES6的代码,不仅需要填充ES6的API,还要填充ES5的API。

babel为此提供了两种解决方案:@babel/polyfill@babel/runtime。具体使用方法官方文档已经写的很详细了,笔者就不赘述了。想了解两者之间的差别的同学可以看下大搜车墨白同学的文章,babel-polyfill VS babel-runtime

这里纠正墨白同学文中的一个错误,就是@babel/polyfill现在已经支持按需加载,准确的说也不能算是错误,因为墨白同学在写这篇文章的时候还不支持按需加载。具体方法我就不细说了,文档里都有,配置下browserlist@babel/preset-envuseBuiltsIns属性就可以了。

我只说下我在实际开发过程中碰到的坑。

虽然@babel/polyfill@babel/runtime都支持按需加载,但都只能识别出业务代码中使用到的缺失的API,如果第三方库有用到这些缺失的API,babel不能识别出来,自然也就不能填充进来。比如regenerator-runtime中用到的Object.createArray.prototype.forEach。解决办法是,在入口文件处手动引入缺失的API。

模块化加载

笔者原来是想用ES6的模块化加载方案,因为这样可以利用webpack的tree shaking,移除冗余代码,使打包出来的文件体积更小。但在IE8下测试发现Object.defineProperty会报错'Accessors not supported!'。报错代码如下

if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported!');

我用@babel/plugin-transform-modules-commonjs转成commonjs加载就可以把这个坑绕过去,但同时也意味着放弃了tree shaking

总结

package.json

{
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/plugin-transform-runtime": "^7.2.0",
    "@babel/preset-env": "^7.1.0",
    "@babel/runtime": "^7.3.4",
    "babel-loader": "^8.0.4",
    "uglifyjs-webpack-plugin": "^2.0.1",
    "webpack": "^4.20.2",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.9",
    "webpack-merge": "^4.1.4"
  }
}

webpack配置

{
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-env'
            ],
            plugins: [
              [
                '@babel/plugin-transform-runtime'
              ],
              [
                // 笔者为了兼容IE8才用了这个插件,代价是不能tree shaking
                // 没有IE8兼容需求的同学可以把这个插件去掉
                '@babel/plugin-transform-modules-commonjs'
              ]
            ]
          }
        }
      }
    ]
  },
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        sourceMap: true,
        uglifyOptions: {
          ie8: true,
        }
      })
    ]
  }
}

入口文件按需引入缺失的API

require('core-js/fn/object/define-property')
require('core-js/fn/object/create')
require('core-js/fn/object/assign')
require('core-js/fn/array/for-each')
require('core-js/fn/array/index-of')
require('core-js/fn/function/bind')
require('core-js/fn/promise')

最后附上文章开头提到的sdk源码,笔者已将公司业务相关代码去除,将通用部分开源。https://github.com/xtTech/dc-sdk-js

从一道面试题认识函数柯里化

最近在整理面试资源的时候,发现一道有意思的题目,所以就记录下来。

题目

如何实现 multi(2)(3)(4)=24?

首先来分析下这道题,实现一个 multi 函数并依次传入参数执行,得到最终的结果。通过题目很容易得到的结论是,把传入的参数相乘就能够得到需要的结果,也就是 2X3X4 = 24。

简单的实现

那么如何实现 multi 函数去计算出结果值呢?脑海中首先浮现的解决方案是,闭包。

function multi(a) {
    return function(b) {
        return function(c) {
            return a * b * c;
        }
    }
}

利用闭包的原则,multi 函数执行的时候,返回 multi 函数中的内部函数,再次执行的时候其实执行的是这个内部函数,这个内部函数中接着又嵌套了一个内部函数,用于计算最终结果并返回。

闭包实现

单纯从题面来说,似乎是已经实现了想要的结果,但仔细一想就会发现存在问题。

上面的实现方案存在的缺陷:

  • 代码不够优雅,实现步骤需要一层一层的嵌套函数。
  • 可扩展性差,假如是要实现 multi(2)(3)(4)...(n) 这样的功能,那就得嵌套 n 层函数。

那么有没有更好的解决方案,答案是,使用函数式编程中的函数柯里化实现。

函数柯里化

在函数式编程中,函数是一等公民。那么函数柯里化是怎样的呢?

函数柯里化指的是将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数且返回结果的新函数的技术。

函数柯里化的主要作用和特点就是参数复用、提前返回和延迟执行。

例如:封装兼容现代浏览器和 IE 浏览器的事件监听的方法,正常情况下封装是这样的。

var addEvent = function(el, type, fn, capture) {
    if(window.addEventListener) {
        el.addEventListener(type, function(e) {
            fn.call(el, e);
        }, capture);
    }else {
        el.attachEvent('on' + type, function(e) {
            fn.call(el, e);
        })
    }
}

该封装的方法存在的不足是,每次写监听事件的时候调用 addEvent 函数,都会进行 if else 的兼容性判断。事实上在代码中只需要执行一次兼容性判断就可以了,后续的事件监听就不需要再去判断兼容性了。那么怎么用函数柯里化优化这个封装函数。

var addEvent = (function() {
    if(window.addEventListener) {
        return function(el, type, fn, capture) {
            el.addEventListener(type, function(e) {
                fn.call(el, e);
            }, capture);
        }
    }else {
        return function(ele, type, fn) {
            el.attachEvent('on' + type, function(e) {
                fn.call(el, e);
            })
        }
    }
})()

js 引擎在执行该段代码的时候就会进行兼容性判断,并且返回需要使用的事件监听封装函数。这里使用了函数柯里化的两个特点:提前返回和延迟执行。

柯里化另一个典型的应用场景就是 bind 函数的实现。使用了函数柯里化的两个特点:参数复用和提前返回。

Function.prototype.bind = function(){
	var fn = this;
	var args = Array.prototye.slice.call(arguments);
	var context = args.shift();

	return function(){
		return fn.apply(context, args.concat(Array.prototype.slice.call(arguments)));
	};
};

柯里化的实现

那么如何通过函数柯里化实现面试题的功能呢?

通用版

function curry(fn) {
    var args = Array.prototype.slice.call(arguments, 1);
	return function() {
		var newArgs = args.concat(Array.prototype.slice.call(arguments));
        return fn.apply(this, newArgs);
    }
}

curry 函数的第一个参数是要动态创建柯里化的函数,余下的参数存储在 args 变量中。

执行 curry 函数返回的函数接收新的参数与 args 变量存储的参数合并,并把合并的参数传入给柯里化了的函数。

function multiFn(a, b, c) {
    return a * b * c;
}
var multi = curry(multiFn);
multi(2,3,4);

结果:

image

虽然得到的结果是一样的,但是很容易发现存在问题,就是代码相对于之前的闭包实现方式较复杂,而且执行方式也不是题目要求的那样 multi(2)(3)(4)。那么下面就来改进这版代码。

改进版

就题目而言,是需要执行三次函数调用,那么针对柯里化后的函数,如果传入的参数没有 3 个的话,就继续执行 curry 函数接收参数,如果参数达到 3 个,就执行柯里化了的函数。

function curry(fn, args) {
    var length = fn.length;
    var args = args || [];
    return function(){
        newArgs = args.concat(Array.prototype.slice.call(arguments));
        if(newArgs.length < length){
            return curry.call(this,fn,newArgs);
        }else{
            return fn.apply(this,newArgs);
        }
    }
}
function multiFn(a, b, c) {
    return a * b * c;
}
var multi = curry(multiFn);
multi(2)(3)(4);
multi(2,3,4);
multi(2)(3,4);
multi(2,3)(4);

image

可以看到,通过改进版的柯里化函数,已经将题目定的实现方式扩展到好几种了。这种实现方案的代码扩展性就比较强了,但是还是有点不足,就是必须事先知道求值的参数个数,那能不能让代码更灵活点,达到随意传参的效果,例如: multi(2)(3)(4),multi(5)(6)(7)(8)(9) 这样的。

优化版

function multi() {
    var args = Array.prototype.slice.call(arguments);
	var fn = function() {
		var newArgs = args.concat(Array.prototype.slice.call(arguments));
        return multi.apply(this, newArgs);
    }
    fn.toString = function() {
        return args.reduce(function(a, b) {
            return a * b;
        })
    }
    return fn;
}

image

这样的解决方案就可以灵活的使用了。不足的是返回值是 Function 类型。

image

总结

  • 就题目本身而言,是存在多种实现方式的,只要理解并充分利用闭包的强大。
  • 可能在实际应用场景中,很少使用函数柯里化的解决方案,但是了解认识函数柯里化对自身的提升还是有帮助的。
  • 理解闭包和函数柯里化之后,如果在面试中遇到类似的题型,应该就可以迎刃而解了。

后记

本着学习和总结的态度写的技术输出,文中有任何错误和问题,请大家指出。更多的技术输出可以查看我的 github博客

整理了一些前端的学习资源,希望能够帮助到有需要的人,地址: 学习资源汇总

参考

精读《高性能 javascript》

前言

本期我来给大家推荐的书是《高性能JavaScript》,在这本书中我们能够了解 javascript 开发过程中的性能瓶颈,如何提升各方面的性能,包括代码的加载、运行、DOM交互、页面生存周期等。同样我们今天还是用思维导图的方式来精读一遍。(思维导图图片可能有点小,记得点开看,你会有所收获)

加载和执行


管理浏览器中的 JavaScript 代码是个棘手的问题,因为代码执行阻塞了其他浏览器处理过程,诸如用户 界面绘制。每次遇到<script>标签,页面必须停下来等待代码下载(如果是外部的)并执行,然后再继续处 理页面其他部分。但是,有几种方法可以减少 JavaScript 对性能的影响:

  1. 将所有<script>标签放置在页面的底部,紧靠 body 关闭标签</body>的上方。此法可以保证页面在脚本 运行之前完成解析。
  2. 将脚本成组打包。页面的<script>标签越少,页面的加载速度就越快,响应也更加迅速。不论外部脚本 文件还是内联代码都是如此。

有几种方法可以使用非阻塞方式下载 JavaScript:

  • <script>标签添加 defer 属性(只适用于 Internet Explorer 和 Firefox 3.5 以上版本)
  • 动态创建<script>元素,用它下载并执行代码
  • 用 XHR 对象下载代码,并注入到页面中

通过使用上述策略,你可以极大提高那些大量使用 JavaScript 代码的网页应用的实际性能。

数据存取

在 JavaScript 中,数据存储位置可以对代码整体性能产生重要影响。有四种数据访问类型:直接量,变 量,数组项,对象成员。它们有不同的性能考虑。

直接量和局部变量访问速度非常快,数组项和对象成员需要更长时间。局部变量比域外变量快,因为它位于作用域链的第一个对象中。变量在作用域链中的位置越深,访问所需 的时间就越长。全局变量总是最慢的,因为它们总是位于作用域链的最后一环。避免使用 with 表达式,因为它改变了运行期上下文的作用域链。而且应当小心对待 try-catch 表达式的 catch 子句,因为它具有同样效果。嵌套对象成员会造成重大性能影响,尽量少用。

一个属性或方法在原形链中的位置越深,访问它的速度就越慢。一般来说,你可以通过这种方法提高 JavaScript 代码的性能:将经常使用的对象成员,数组项,和域外变 量存入局部变量中。然后,访问局部变量的速度会快于那些原始变量。通过使用这些策略,你可以极大地提高那些需要大量 JavaScript 代码的网页应用的实际性能。

DOM 编程


DOM 访问和操作是现代网页应用中很重要的一部分。但每次你通过桥梁从 ECMAScript 岛到达 DOM 岛 时,都会被收取“过桥费”。为减少 DOM 编程中的性能损失,请牢记以下几点:

最小化 DOM 访问,在 JavaScript 端做尽可能多的事情。在反复访问的地方使用局部变量存放 DOM 引用.小心地处理 HTML 集合,因为他们表现出“存在性”,总是对底层文档重新查询。将集合的 length 属性缓 存到一个变量中,在迭代中使用这个变量。如果经常操作这个集合,可以将集合拷贝到数组中。

如果可能的话,使用速度更快的 API,诸如 querySelectorAll()和 firstElementChild。注意重绘和重排版;批量修改风格,离线操作 DOM 树,缓存并减少对布局信息的访问。动画中使用绝对坐标,使用拖放代理。使用事件托管技术最小化事件句柄数量。

算法和流程控制


正如其他编程语言,代码的写法和算法选用影响 JavaScript 的运行时间。与其他编程语言不同的是, JavaScript 可用资源有限,所以优化技术更为重要。

for,while,do-while 循环的性能特性相似,谁也不比谁更快或更慢。除非你要迭代遍历一个属性未知的对象,否则不要使用 for-in 循环。改善循环性能的最好办法是减少每次迭代中的运算量,并减少循环迭代次数。

一般来说,switch 总是比 if-else 更快,但并不总是最好的解决方法。当判断条件较多时,查表法比 if-else 或者 switch 更快。

浏览器的调用栈尺寸限制了递归算法在 JavaScript 中的应用;栈溢出错误导致其他代码也不能正常执行。如果你遇到一个栈溢出错误,将方法修改为一个迭代算法或者使用制表法可以避免重复工作。

运行的代码总量越大,使用这些策略所带来的性能提升就越明显。

字符串和正则表达式


密集的字符串操作和粗浅地编写正则表达式可能是主要性能障碍,但本章中的建议可帮助您避免常见缺陷。当连接数量巨大或尺寸巨大的字符串时,数组联合是 IE7 和它的早期版本上唯一具有合理性能的方法。如果你不关心 IE7 和它的早期版本,数组联合是连接字符串最慢的方法之一。使用简单的+和+=取而代之, 可避免(产生)不必要的中间字符串。

回溯既是正则表达式匹配功能基本的组成部分,又是正则表达式影响效率的常见原因。回溯失控发生在正则表达式本应很快发现匹配的地方,因为某些特殊的匹配字符串动作,导致运行缓慢 甚至浏览器崩溃。避免此问题的技术包括:使相邻字元互斥,避免嵌套量词对一个字符串的相同部分多次 匹配,通过重复利用前瞻操作的原子特性去除不必要的回溯。

提高正则表达式效率的各种技术手段,帮助正则表达式更快地找到匹配,以及在非匹配位置上花费更少 时间(见《更多提高正则表达式效率的方法》)。正则表达式并不总是完成工作的最佳工具,尤其当你只是搜索一个文本字符串时。

虽然有很多方法来修整一个字符串,使用两个简单的正则表达式(一个用于去除头部空格,另一个用于 去除尾部空格)提供了一个简洁、跨浏览器的方法,适用于不同内容和长度的字符串。从字符串末尾开始 循环查找第一个非空格字符,或者在一个混合应用中将此技术与正则表达式结合起来,提供了一个很好的 替代方案,它很少受到字符串整体长度的影响。

快速响应用户界面


JavaScript 和用户界面更新在同一个进程内运行,同一时刻只有其中一个可以运行。这意味着当 JavaScript 代码正在运行时,用户界面不能响应输入,反之亦然。有效地管理 UI 线程就是要确保 JavaScript 不能运行 太长时间,以免影响用户体验。最后,请牢记如下几点:

  1. JavaScript 运行时间不应该超过 100 毫秒。过长的运行时间导致 UI 更新出现可察觉的延迟,从而对整体 用户体验产生负面影响。
  2. JavaScript 运行期间,浏览器响应用户交互的行为存在差异。无论如何,JavaScript 长时间运行将导致用 户体验混乱和脱节。
  3. 定时器可用于安排代码推迟执行,它使得你可以将长运行脚本分解成一系列较小的任务。

网页工人线程是新式浏览器才支持的特性,它允许你在 UI 线程之外运行 JavaScript 代码而避免锁定 UI。网页应用程序越复杂,积极主动地管理 UI 线程就越显得重要。没有什么 JavaScript 代码可以重要到允 许影响用户体验的程度。

Ajax


高性能 Ajax 包括:知道你项目的具体需求,选择正确的数据格式和与之相配的传输技术。

作为数据格式,纯文本和 HTML 是高度限制的,但它们可节省客户端的 CPU 周期。XML 被广泛应用 普遍支持,但它非常冗长且解析缓慢。JSON 是轻量级的,解析迅速(作为本地代码而不是字符串),交 互性与 XML 相当。字符分隔的自定义格式非常轻量,在大量数据集解析时速度最快,但需要编写额外的 程序在服务器端构造格式,并在客户端解析。

当从页面域请求数据时,XHR 提供最完善的控制和灵活性,尽管它将所有传入数据视为一个字符串, 这有可能降低解析速度。另一方面,动态脚本标签插入技术允许跨域请求和本地运行 JavaScript 和 JSON, 虽然它的接口不够安全,而且不能读取信息头或响应报文代码。多部分 XHR 可减少请求的数量,可在一次响应中处理不同的文件类型,尽管它不能缓存收到的响应报文。当发送数据时,图像灯标是最简单和最 有效的方法。XHR 也可用 POST 方法发送大量数据。

除这些格式和传输技术之外,还有一些准则有助于进一步提高 Ajax 的速度:

  1. 减少请求数量,可通过 JavaScript 和 CSS 文件打包,或者使用 MXHR。
  2. 缩短页面的加载时间,在页面其它内容加载之后,使用 Ajax 获取少量重要文件。
  3. 确保代码错误不要直接显示给用户,并在服务器端处理错误。
  4. 学会何时使用一个健壮的 Ajax 库,何时编写自己的底层 Ajax 代码。

Ajax 是提升你网站潜在性能之最大的改进区域之一,因为很多网站大量使用异步请求,又因为它提供 了许多不相关问题的解决方案,这些问题诸如,需要加载太多资源。对 XHR 的创造性应用是如此的与众 不同,它不是呆滞不友好的界面,而是响应迅速且高效的代名词;它不会引起用户的憎恨,谁见了它都会 爱上它。

编程实践

JavaScript 提出了一些独特的性能挑战,关系到你组织代码的方法。网页应用变得越来越高级,包含的 JavaScript 代码越来越多,出现了一些模式和反模式。请牢记以下编程经验:

  1. 通过避免使用 eval_r()和 Function()构造器避免二次评估。此外,给 setTimeout()和 setInterval()传递函数参 数而不是字符串参数。
  2. 创建新对象和数组时使用对象直接量和数组直接量。它们比非直接量形式创建和初始化更快。
  3. 避免重复进行相同工作。当需要检测浏览器时,使用延迟加载或条件预加载。
  4. 当执行数学远算时,考虑使用位操作,它直接在数字底层进行操作。
  5. 原生方法总是比 JavaScript 写的东西要快。尽量使用原生方法。

构建并部署高性能 javascript 应用

开发和部署过程对基于 JavaScript 的应用程序可以产生巨大影响,最重要的几个步骤如下:

  1. 合并 JavaScript 文件,减少 HTTP 请求的数量
  2. 使用 YUI 压缩器紧凑处理 JavaScript 文件
  3. 以压缩形式提供 JavaScript 文件(gzip 编码)
  4. 通过设置 HTTP 响应报文头使 JavaScript 文件可缓存,通过向文件名附加时间戳解决缓存问题
  5. 使用内容传递网络(CDN)提供 JavaScript 文件,CDN 不仅可以提高性能,它还可以为你管理压缩和缓 存

所有这些步骤应当自动完成,不论是使用公开的开发工具诸如 Apache Ant,还是使用自定义的开发工具 以实现特定需求。如果你使这些开发工具为你服务,你可以极大改善那些大量使用 JavaScript 代码的网页 应用或网站的性能。

工具

当网页或应用程序变慢时,分析网上传来的资源,分析脚本的运行性能,使你能够集中精力在那些需要 努力优化的地方。使用网络分析器找出加载脚本和其它页面资源的瓶颈所在,这有助于决定哪些脚本需要延迟加载,或者 进行进一步分析。传统的智慧告诉我们应尽量减少 HTTP 请求的数量,尽量延迟加载脚本以使页面渲染速度更快,向用户 提供更好的整体体验。使用性能分析器找出脚本运行时速度慢的部分,检查每个函数所花费的时间,以及函数被调用的次数, 通过调用栈自身提供的一些线索来找出哪些地方应当努力优化。虽然花费时间和调用次数通常是数据中最有价值的点,还是应当仔细察看函数的调用过程,可能发现其 它优化方法。这些工具在那些现代代码所要运行的编程环境中不再神秘。在开始优化工作之前使用它们,确保开发时 间用在解决问题的刀刃上。

😊往期的读书笔记 && 技术文章

为了系统的串联前端知识,我平时喜欢用思维导图来记录读书笔记,我在 github 建了仓库放这些思维导图的原件,和读书笔记。如果你也喜欢用思维导图的方式来记录读书笔记,也欢迎和我一同维护这个仓库,欢迎留言或则微信(646321933)与我交流

精读《你不知道的 javascript(上卷)》

精读《你不知道的javascript》中卷

精读《深入浅出Node.js》

精读《图解HTTP》

javascript 垃圾回收算法

你需要知道的单页面路由实现原理

javascript 实现高仿 growingIO

新鲜出炉的8月前端面试题

思维导图下载地址

如何机制地回答浏览器兼容性问题

前言

有过面试经验的同学应该都被问过浏览器兼容性的问题,对于面试官的问题,常常猝不及防,因为通常他们都是这么问的。"来谈谈浏览器兼容的问题吧","你对浏览器的兼容性有了解过吗",那么如何才是我们正确回答这个问题的姿势呢。

虽然面试官的问题十分的笼统,浏览器的兼容性无非还是样式兼容性(css),交互兼容性(javascript),浏览器 hack 三个方面。

样式兼容性(css)方面

  1. 因为历史原因,不同的浏览器样式存在差异,可以通过 Normalize.css 抹平差异,也可以定制自己的 reset.css,例如通过通配符选择器,全局重置样式

     * { margin: 0; padding: 0; }
    
  2. 在CSS3还没有成为真正的标准时,浏览器厂商就开始支持这些属性的使用了。CSS3样式语法还存在波动时,浏览器厂商提供了针对浏览器的前缀,直到现在还是有部分的属性需要加上浏览器前缀。在开发过程中我们一般通过IDE开发插件、css 预处理器以及前端自动化构建工程帮我们处理。

    浏览器内核与前缀的对应关系如下

    内核 主要代表的浏览器 前缀
    Trident IE浏览器 -ms
    Gecko Firefox -moz
    Presto Opera -o
    Webkit Chrome和Safari -webkit
  3. 在还原设计稿的时候我们常常会需要用到透明属性,所以解决 IE9 以下浏览器不能使用 opacit。

     opacity: 0.5;
     filter: alpha(opacity = 50); //IE6-IE8我们习惯使用filter滤镜属性来进行实现
     filter: progid:DXImageTransform.Microsoft.Alpha(style = 0, opacity = 50); //IE4-IE9都支持滤镜写法progid:DXImageTransform.Microsoft.Alpha(Opacity=xx)
    

交互兼容性(javascript)

  1. 事件兼容的问题,我们通常需要会封装一个适配器的方法,过滤事件句柄绑定、移除、冒泡阻止以及默认事件行为处理

     var  helper = {}
    
     //绑定事件
     helper.on = function(target, type, handler) {
     	if(target.addEventListener) {
     		target.addEventListener(type, handler, false);
     	} else {
     		target.attachEvent("on" + type,
     			function(event) {
     				return handler.call(target, event);
     		    }, false);
     	}
     };
    
     //取消事件监听
     helper.remove = function(target, type, handler) {
     	if(target.removeEventListener) {
     		target.removeEventListener(type, handler);
     	} else {
     		target.detachEvent("on" + type,
     	    function(event) {
     			return handler.call(target, event);
     		}, true);
         }
     };
    
  2. new Date()构造函数使用,'2018-07-05'是无法被各个浏览器中,使用new Date(str)来正确生成日期对象的。 正确的用法是'2018/07/05'.

  3. 获取 scrollTop 通过 document.documentElement.scrollTop 兼容非chrome浏览器

     var scrollTop = document.documentElement.scrollTop||document.body.scrollTop;
    

浏览器 hack

  1. 快速判断 IE 浏览器版本

     <!--[if IE 8]> ie8 <![endif]-->
     
     <!--[if IE 9]> *气的 ie9 浏览器 <![endif]-->
    
  2. 判断是否是 Safari 浏览器

     /* Safari */
     var isSafari = /a/.__proto__=='//';
    
  3. 判断是否是 Chrome 浏览器

     /* Chrome */
     var isChrome = Boolean(window.chrome);
    

身段不能掉,我们是个有逼格的前端

“什么?你们公司要兼容IE6,我们今天的面试就到这里为止吧,告辞”。

扩展阅读

如何处理CSS3属性前缀_Autoprefixer

CSS透明opacity和IE各版本透明度滤镜filter的最准确用法

往期文章

精读《你不知道的 javascript(上卷)》

精读《你不知道的javascript》中卷

精读《深入浅出Node.js》

javascript 垃圾回收算法

精读《图解HTTP》

思维导图下载地址

精读《你不知道的javascript》中卷

前言

《你不知道的 javascript》是一个前端学习必读的系列,让不求甚解的JavaScript开发者迎难而上,深入语言内部,弄清楚JavaScript每一个零部件的用途。本书《你不知道的javascript》中卷介绍了该系列的两个主题:“类型和语法”以及“异步与性能”。这两块也是值得我们反复去学习琢磨的两块只是内容,今天我们用思维导图的方式来精读一遍。(思维导图图片可能有点小,记得点开看,你会有所收获)

第一部分 作用域和闭包

类型


JavaScript 有 七 种 内 置 类 型: null 、 undefined 、 boolean 、 number 、 string 、 object 和 symbol ,可以使用 typeof 运算符来查看。

变量没有类型,但它们持有的值有类型。类型定义了值的行为特征。

很 多 开 发 人 员 将 undefined 和 undeclared 混 为 一 谈, 但 在 JavaScript 中 它 们 是 两 码 事。 undefined 是值的一种。 undeclared 则表示变量还没有被声明过。

遗憾的是, JavaScript 却将它们混为一谈, 在我们试图访问 "undeclared" 变量时这样报 错:ReferenceError: a is not defined, 并 且 typeof 对 undefined 和 undeclared 变 量 都 返 回 "undefined" 。

然而,通过 typeof 的安全防范机制(阻止报错)来检查 undeclared 变量,有时是个不错的办法。


JavaScript 中的数组是通过数字索引的一组任意类型的值。字符串和数组类似,但是它们的 行为特征不同, 在将字符作为数组来处理时需要特别小心。 JavaScript 中的数字包括“整 数”和“浮点型”。

基本类型中定义了几个特殊的值。

null 类型只有一个值 null , undefined 类型也只有一个值 undefined 。 所有变量在赋值之 前默认值都是 undefined 。 void 运算符返回 undefined 。

数 字 类 型 有 几 个 特 殊 值, 包 括 NaN ( 意 指“not a number” , 更 确 切 地 说 是“invalid number” )、 +Infinity 、 -Infinity 和 -0 。

简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递, 而复合值(对象等) 通过引用复制来赋值 / 传递。 JavaScript 中的引用和其他语言中的引用 / 指针不同,它们不 能指向别的变量 / 引用,只能指向值。

原生函数

JavaScript

为基本数据类型值提供了封装对象,称为原生函数(如 String 、 Number 、 Boolean 等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如: String#trim() 和 Array#concat(..) )。

对于简单标量基本类型值,比如 "abc" ,如果要访问它的 length 属性或 String.prototype 方法, JavaScript 引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实现对这些属性和方法的访问。

强制类型转换

本章介绍了 JavaScript 的数据类型之间的转换,即强制类型转换:包括显式和隐式。

强制类型转换常常为人诟病, 但实际上很多时候它们是非常有用的。 作为有使命感的 JavaScript 开发人员,我们有必要深入了解强制类型转换,这样就能取其精华,去其糟粕。

显式强制类型转换明确告诉我们哪里发生了类型转换, 有助于提高代码可读性和可维 护性。

隐式强制类型转换则没有那么明显,是其他操作的副作用。感觉上好像是显式强制类型转 换的反面,实际上隐式强制类型转换也有助于提高代码的可读性。

在处理强制类型转换的时候要十分小心,尤其是隐式强制类型转换。在编码的时候,要知 其然,还要知其所以然,并努力让代码清晰易读。

语法


JavaScript 语法规则中的许多细节需要我们多花点时间和精力来了解。 从长远来看, 这有 助于更深入地掌握这门语言。

语句和表达式在英语中都能找到类比——语句就像英语中的句子,而表达式就像短语。表 达式可以是简单独立的,否则可能会产生副作用。

JavaScript 语法规则之上是语义规则(也称作上下文)。例如, { } 在不同情况下的意思不 尽相同,可以是语句块、对象常量、解构赋值(ES6)或者命名函数参数(ES6)。

JavaScript 详细定义了运算符的优先级(运算符执行的先后顺序)和关联(多个运算符的 组合方式)。只要熟练掌握了这些规则,就能对如何合理地运用它们作出自己的判断。

ASI(自动分号插入)是 JavaScript 引擎的代码解析纠错机制, 它会在需要的地方自动插 入分号来纠正解析错误。问题在于这是否意味着大多数的分号都不是必要的(可以省略), 或者由于分号缺失导致的错误是否都可以交给 JavaScript 引擎来处理。

JavaScript 中有很多错误类型, 分为两大类:早期错误(编译时错误, 无法被捕获)和运 行时错误(可以通过 try..catch 来捕获)。所有语法错误都是早期错误,程序有语法错误 则无法运行。

函数参数和命名参数之间的关系非常微妙。尤其是 arguments 数组,它的抽象泄漏给我们 挖了不少坑。因此,尽量不要使用 arguments ,如果非用不可,也切勿同时使用 arguments 和其对应的命名参数。

finally 中代码的处理顺序需要特别注意。它们有时能派上很大用场,但也容易引起困惑, 特别是在和带标签的代码块混用时。总之,使用 finally 旨在让代码更加简洁易读,切忌 弄巧成拙。

switch 相对于 if..else if.. 来说更为简洁。需要注意的一点是,如果对其理解得不够透 彻,稍不注意就很容易出错。

第二部分 this 和对象原型

异步:现在与将来


实际上, JavaScript 程序总是至少分为两个块:第一块 现在 运行;下一块 将来 运行, 以响 应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访 问,所以对状态的修改都是在之前累积的修改之上进行的。

一旦有事件需要运行, 事件循环就会运行, 直到队列清空。 事件循环的每一轮称为一个 tick。 用户交互、IO 和定时器会向事件队列中加入事件。

任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一 个或多个后续事件。

并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时 在运行(尽管在任意时刻只处理一个事件)。

通常需要对这些并发执行的“进程”(有别于操作系统中的进程概念)进行某种形式的交 互协调,比如需要确保执行顺序或者需要防止竞态出现。这些“进程”也可以通过把自身 分割为更小的块,以便其他“进程”插入进来。

回调

回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。

第一,大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流 程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码 是坏代码,会导致坏 bug。

我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。

第二,也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三 方(通常是不受你控制的第三方工具!)来调用你代码中的 continuation。 这种控制转移导 致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。

可以发明一些特定逻辑来解决这些信任问题,但是其难度高于应有的水平,可能会产生更 笨重、更难维护的代码,并且缺少足够的保护,其中的损害要直到你受到 bug 的影响才会 被发现。

我们需要一个通用的方案来解决这些信任问题。不管我们创建多少回调,这一方案都应可 以复用,且没有重复代码的开销。

我们需要比回调更好的机制。到目前为止,回调提供了很好的服务,但是未来的 JavaScript 需要更高级、功能更强大的异步模式。本书接下来的几章会深入探讨这些新型技术。

Promise

Promise 非常好,请使用。它们解决了我们因只用回调的代码而备受困扰的控制反转问题。

它们并没有摈弃回调,只是把回调的安排转交给了一个位于我们和其他工具之间的可信任 的中介机制。

Promise 链也开始提供(尽管并不完美)以顺序的方式表达异步流的一个更好的方法,这 有助于我们的大脑更好地计划和维护异步 JavaScript 代码。我们将在第 4 章看到针对这个 问题的一种更好的解决方案!

生成器


生成器是 ES6 的一个新的函数类型, 它并不像普通函数那样总是运行到结束。 取而代之 的是, 生成器可以在运行当中(完全保持其状态)暂停, 并且将来再从暂停的地方恢复 运行。

这种交替的暂停和恢复是合作性的而不是抢占式的,这意味着生成器具有独一无二的能力 来暂停自身,这是通过关键字 yield 实现的。不过,只有控制生成器的迭代器具有恢复生 成器的能力(通过 next(..) )。

yield / next(..) 这一对不只是一种控制机制,实际上也是一种双向消息传递机制。 yield .. 表 达式本质上是暂停下来等待某个值,接下来的 next(..) 调用会向被暂停的 yield 表达式传回 一个值(或者是隐式的 undefined )。

在异步控制流程方面,生成器的关键优点是:生成器内部的代码是以自然的同步 / 顺序方 式表达任务的一系列步骤。其技巧在于,我们把可能的异步隐藏在了关键字 yield 的后面, 把异步移动到控制生成器的迭代器的代码部分。

换句话说,生成器为异步代码保持了顺序、同步、阻塞的代码模式,这使得大脑可以更自 然地追踪代码,解决了基于回调的异步的两个关键缺陷之一。

程序性能

本部分的前四章都是基于这样一个前提:异步编码模式使我们能够编写更高效的代码,通 常能够带来非常大的改进。但是,异步特性只能让你走这么远,因为它本质上还是绑定在 一个单事件循环线程上。

因此,在这一章里,我们介绍了几种能够进一步提高性能的程序级别的机制。

Web Worker 让你可以在独立的线程运行一个 JavaScript 文件(即程序),使用异步事件在 线程之间传递消息。 它们非常适用于把长时间的或资源密集型的任务卸载到不同的线程 中,以提高主 UI 线程的响应性。

SIMD 打算把 CPU 级的并行数学运算映射到 JavaScript API, 以获得高性能的数据并行运算,比如在大数据集上的数字处理。

最后, asm.js 描述了 JavaScript 的一个很小的子集, 它避免了 JavaScript 难以优化的部分 (比如垃圾收集和强制类型转换),并且让 JavaScript 引擎识别并通过激进的优化运行这样 的代码。可以手工编写 asm.js, 但是会极端费力且容易出错,类似于手写汇编语言(这也 是其名字的由来)。实际上, asm.js 也是高度优化的程序语言交叉编译的一个很好的目标, 比如 Emscripten 把 C/C++ 转换成 JavaScript(https://github.com/kripken/emscripten/wiki) 。

JavaScript 还有一些更加激进的思路已经进入非常早期的讨论, 尽管本章并没有明确包含 这些内容,比如近似的直接多线程功能(而不是藏在数据结构 API 后面)。不管这些最终 会不会实现,还是我们将只能看到更多的并行特性偷偷加入 JavaScript, 但确实可以预见, 未来 JavaScript 在程序级别将获得更加优化的性能。

性能测试与调优

对一段代码进行有效的性能测试,特别是与同样代码的另外一个选择对比来看看哪种方案 更快,需要认真注意细节。

与其打造你自己的统计有效的性能测试逻辑,不如直接使用 Benchmark.js 库,它已经为你 实现了这些。但是,编写测试要小心,因为我们很容易就会构造一个看似有效实际却有缺 陷的测试,即使是微小的差异也可能扭曲结果,使其完全不可靠。

从尽可能多的环境中得到尽可能多的测试结果以消除硬件/ 设备的偏差, 这一点很重要。 jsPerf.com 是很好的网站,用于众包性能测试运行。

遗憾的是,很多常用的性能测试执迷于无关紧要的微观性能细节,比如 x++ 对比 ++x 。编 写好的测试意味着理解如何关注大局, 比如关键路径上的优化以及避免落入类似不同的 JavaScript 实现细节这样的陷阱中。

尾调用优化是 ES6 要求的一种优化方法。它使 JavaScript 中原本不可能的一些递归模式变 得实际。 TCO 允许一个函数在结尾处调用另外一个函数来执行,不需要任何额外资源。这意味着,对递归算法来说,引擎不再需要限制栈深度。

扩展

思维导图能比较清晰的还原整本书的知识结构体系,如果你还没用看过这本书,可以按照这个思维导图的思路快速预习一遍,提高学习效率。学习新事物总容易遗忘,我比较喜欢在看书的时候用思维导图做些记录,便于自己后期复习,如果你已经看过了这本书,也建议你收藏复习。如果你有神马建议或则想法,欢迎留言或加我微信交流:646321933,备注技术交流

精读《你不知道的javascript》上卷

精读《深入浅出Node.js》

思维导图下载地址

你不知道的 javascript(中卷)PDF 下载地址

精读《图解HTTP》

前言

作为一个前端,如果能够深刻理解 HTTP 通信,能够让我们在日常开发工作中快速定位问题。所以我十分建议大家去读一下《图解HTTP》和《HTTP权威指南》。权威指南讲解的十分详细,内容也十分的全面,但是这本书的厚度也让很多同学望而却步。推荐大家从 《图解HTTP》 这本书开始学习,这本书虽然没有权威指南详细,但涵盖了很多我们日常开发需要的知识点,理解他,能让我们的开发效率事半功倍。

《图解HTTP》这本书对互联网基盘—— HTTP 协议进行了全面系统的介绍。作者从 HTTP 发展史开始,严谨地剖析了 HTTP 协议的结构,列举很多常见通信场景及实战案例,最后延伸到Web安全、最新技术动向等方面。通过书中大量生动形象的通信图例,我们能够更全面地理解 HTTP 通信过程中客户端与服务器之间的交互情况。在读这本书的过程中我收获颇多,并用思维导图的方式记录下来,方便自己后期温故。如果你刚好没有读过,可以当做度这本书的预习阅读,相信你也会有所收获(思维导图图片可能有点小,记得点开看)

了解 HTTP 协议访问 Web

这一章介绍了 Web 及网络协议的基础,http 通讯被拆分成四层,应用层,传输层,网络层和链路层,每层只要考虑分派给自己的任务,不需要弄清整个协议链路的细节。应用层使我们肉眼能看见的,我们常用的 http 协议和 FTP 协议就处在这一层,往下就是传输层,TCP/IP 协议工作的地方(我们的IP是IP地址,和这里的IP 协议有做区分),再往下是网络层,传输层建立连接之后,网络层负责将数据包的传输(数据包是网络传输的最小单位)。最后是链路层,用来连接网络配件的部分,举个栗子就是我们常说的网卡啊,光纤啊。在一个完整的数据传输中,客户端会按照应用层,传输层,网络层,链路层的顺序进行进行处理,每一层都会加一个首部,服务器端在接收的时候按照链路层,网路层,传输层,应用层的顺序去移除首部。

我们常说的3次握手就是 TCP 协议采用的策略,为了传输方便,TCP 将大数据分割成以报文为单位的数据包,IP 协议负责把数据包发送出去。在发送的过程中常常需要经过多个路由器的中转,这个时候会运用 ARP 协议来查找下一个路由器的地址。

通常用户的习惯会去访问域名而不是IP地址,将域名解析成对应的 IP 就需要用到 DNS 协议域名解析的服务。

简单的 HTTP 协议

日常开发中我们感知不到 http 协议的底层实现,我们所了解的 HTTP 协议总是由客户端发起,服务端接收。我们关注到的请求,常常是请求的URI,协议版本,头部信息,及内容实体,我们常使用的响应信息则包括了响应状态,响应内容。

我们常常会使用不通的 http 方法来执行不同的操作。我们常使用 GET 来获取资源,使用 POST 传输实体主题,使用 PUT 传输文件,使用 DELETE 删除文件,使用 OPTIONS 询问支持的方法(常常在跨域的场景中使用),使用TRACE 获取访问路径,使用 CONNECT 用隧道协议链接代理。

http 协议是一种无状态协议,不会去记录上一次访问状态,这使得当我们要做类似于登录这样的公能的时候,需要通过 cookie 来进行状态的管理。

HTTP 报文内的 HTTP 信息


通常HTTP报文(用于HTTP协议交互的信息)的结构包括,请求行,状态行,首部字段等,从 HTTP 的报文中,我们可以获得很多信息。在 MIME 扩展中会使用一种称为多部分对象集合的方法,来容纳多份不同的数据类型, 在 HTTP 报文中使用多部分对象时,需要在首部字段上加上 Content-type 。

通过设置首部字段,来达到获取部分内容范围请求(请求资源中断后,不需要重新开始请求),将传输内容编码的目的,来提高加载效率。

有的时候不同的场景需要我们获取不同的内容,就比如页面的中英文切换的功能,在HTTP请求中,我们通过设置 accept 类的请求头字段实现,也就是内容协商的方式,返回最合适的内容。协商方式分为,服务器驱动协商,客户端驱动协商,透明协商。

返回结果的 HTTP 状态

在HTTP通讯中,通过转态码,告知客户端的请求状态。状态码大致可以被分为 5 大类,1xx 表示接受的请求正在处理,2xx 表示请求正常处理完毕,3xx 表示需要进行附加操作以完成请求,4xx 表示客户端无法处理请求,5xx 表示服务器处理出错。

在这5大类请求中,我们常用的十几种状态码,需要我们重点去掌握,详细介绍看👆的思维导图。

与 HTTP 协作的 Web 服务器

通过使用虚拟机,可以实现单台主机多个域名的站点部署。在实际通讯中,我们常常使用,代理,网关,隧道协助请求转发,安全通信。

为了提高网站性能,我们会使用缓存方式来提高站点请求速率,通常是通过设置代理服务器缓存和客户端缓存来实现。需要注意的是,缓存都会设置一个过期时间,站点内容更新时,缓存内容也需要及时更新。

HTTP 头部

在请求中,HTTP 报文由请求方法,URI,HTTP版本,HTTP 首部字段等部分构成。在响应中,HTTP 的报文有 HTTP 版本,状态码,HTTP首部字段3部分构成。首部信息尤为重要,我们可以通过首部字段的设置来传递请求信息,类比于缓存控制,报文创建时间,是否压缩编码,是否支持跨域等。

确保 Web 安全的 HTTPS


HTTP 简单灵活的设置也造就了他的缺点。1. 通信使用明文,内容可能被窃听。2、不验证通讯方省份,因此有可能会遭遇伪装。3、无法证明报文的完整性,所有有可能被篡改。HTTPS 其实也不算是一个全新的协议,HTTPS = HTTP + 加密 + 认证 + 完整性保护。HTTPS 完善了 HTTP 的相关缺点,在 HTTP 的部分通信接口采用 SSL 和 TLS 协议替代,使用了数字证书认证机构和其他相关机关颁发的公开秘钥证书

何为认证


HTTP 的认证方式通常是 BASIC认证(基本认证),DIGEST认证(摘要认证),SSL 客户端认证,FormBase 认证(基于表单认证)。BASIC 认证和 DIGEST 认证通过首部字段 Authorization 确认认证信息,SSL 借由 HTTPS 客户端证书完成认证方式。 表单认证则是依赖于 cookie 保存用户的登录状态。

基于 HTTP 的功能追加协议

SPDY 在TCP/IP 的应用层与运输层之间通过新加会话层的形式运作,使用 SPDY 使得 HTTP 协议的功能得到了扩展,实现了多路复用流,赋予请求优先级,压缩 HTTP 首部,推送功能,服务器提醒功能。

使用 WebSocket 进行全双工通信,只要建立了链接,客户端和服务器都能都主动得向对方发送信息。和 HTTP 相比,减少了每次建立连接的开销,减少了通信首部信息。

HTTP/2.0 改善了使用 web 时的速度体验。

构建 Web 内容的技术

web 应用开发语言的三剑客,HTML,CSS,javascript 。书出版的有点久所以书中没有提到,现在火爆的 web 构建技术,Vue,React 等流行前端框架。XML,JSON 数据发布语言,其中 JSON 比较常用,日常开发都离不了他。

web 的攻击技术


在 Web 应用中,从浏览器那接收到的 HTTP 请求的全部内容,都可以在客户端自由的变更、篡改,在HTTP请求报文中加载攻击代码,就能发起对 Web 应用的攻击。Web 应用的攻击模式主要是主动攻击和被动攻击。

常见的攻击方式有,跨站脚本攻击,SQL 注入攻击,HTTP 首部注入攻击。这些攻击造成的影响见👆思维导图。通常简单的 HTTP 协议本身不会存在安全性的问题,协议本身也不会成为攻击对象,黑客利用的常常是因为设计和设置上的缺陷,会话管理疏忽等引发的安全漏洞进行攻击。

😊往期的读书笔记

《图解HTTP》这本书读完啦,下一期我们来读读《HTTP权威指南》,我在 github 建了仓库放这些思维导图的原件,如果觉得图片不清晰,同学们可以去 github 上下载查阅。如果你也喜欢用思维导图的方式来记录读书笔记,也欢迎和我一同维护这个仓库,欢迎留言或则微信(646321933)与我交流

精读《你不知道的 javascript(上卷)》

精读《你不知道的javascript》中卷

精读《深入浅出Node.js》

javascript 垃圾回收算法

思维导图下载地址

《图解HTTP》 PDF下载地址

javascript 垃圾回收算法

我们通常理解的 javascript 垃圾回收机制都停留在表面,"会释放不被引用变量内存",最近在读《深入浅出node.js》的书,详细了解了下 v8 垃圾回收的算法,记录了一些学习笔记。

敲黑板:v8引擎的垃圾回收算法

V8的垃圾回收策略主要基于分代式垃圾回收机制,现代的垃圾回收算法中按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同分代的内存施以更高效的算法。在V8中,主要将内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象, 老生代中的对象为存活时间较长或常驻内存的对象。

Scavenge算法

在分代的基础上,新生代中的对象主要通过Scavenge算法进行垃圾回收,在Scavenge的具体 实现中,主要采用了Cheney算法

Cheney 算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,每一部分空间称为 semispace。在这两个 semispace 空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的 semispace 空间称为 From 空间,处于闲置状态的空间称为 To 空间。当我们分配对象时,先是在 From 空间中进行分配。当开始进行垃圾回收时,会检查 From 空间中的存活对象,这 些存活对象将被复制到 To 空间中,而非存活对象占用的空间将会被释放。完成复制后,From 空 间和To空间的角色发生对换。 简而言之, 在垃圾回收的过程中, 就是通过将存活对象在两个 semispace 空间之间进行复制。

Mark-Sweep & Mark-Compact

Scavenge算法通过牺牲空间换时间的算法非常适合生命周期短的新生代,但是,当一个对象经过多次复制,生命周期较长的时候或则To空间不足的时候,对象会被分配到进入到老生代中,需要采用新的算法进行垃圾回收。

Mark-Sweep 并不将内存空间划分为两半,所以不存在浪费一半空间的行为。与 Scavenge 复制活着的对象不同, Mark-Sweep 在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。可以看出,Scavenge 中只复制活着的对象,而 Mark-Sweep 只清理死亡对象。

Mark-Sweep 在进行一次标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。Mark-Compact 对象在标记为死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存

增量标记

为了避免出现 JavaScript 应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的 3 种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为“全停顿",长时间的"全停顿"垃圾回收会让用户感受到明显的卡顿,带来体验的影响。以1.5 GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。这是垃圾回收中引起JavaScript线程暂停执行的时间,在 这样的时间花销下,应用的性能和响应能力都会直线下降。

为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记(incremental marking),也就是拆分为许多小“步进”,每做完一“步进” 就让 JavaScript 应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成

扩展阅读

JavaScript 内存泄漏教程

精读《深入浅出Node.js》

《深入浅出Node.js》PDF

精读《深入浅出Node.js》

前言

如果你想要深入学习Node,那你不能错过《深入浅出Node.js》这本书,它从不同的视角介绍了 Node 内在的特点和结构。由首章Node 介绍为索引,涉及Node 的各个方面,主要内容包含模块机制的揭示、异步I/O 实现原理的展现、异步编程的探讨、内存控制的介绍、二进制数据Buffer 的细节、Node 中的网络编程基础、Node 中的Web 开发、进程间的消息传递、Node 测试以及通过Node 构建产品需要的注意事项。最后的附录介绍了Node 的安装、调试、编码规范和NPM 仓库等事宜。在读这本书的过程中我收获颇多,并用思维导图的方式记录下来,方便自己后期温故。如果你刚好没有读过,可以当做度这本书的预习阅读,相信你也会有所收获(思维导图图片可能有点小,记得点开看)

第1章 Node简介

第2章 模块机制


CommonJS提出的规范均十分简单,但是现实意义却十分强大。Node通过模块规范,组织了 自身的原生模块,弥补JavaScript弱结构性的问题,形成了稳定的结构,并向外提供服务。NPM 通过对包规范的支持,有效地组织了第三方模块,这使得项目开发中的依赖问题得到很好的解决, 并有效提供了分享和传播的平台,借助第三方开源力量,使得Node第三方模块的发展速度前所未 有,这对于其他后端JavaScript语言实现而言是从未有过的。从一定的角度上讲,CommonJS规范 帮助Node形成了它的骨骼。只有茁壮的根,才能培养出茂盛的枝叶,并成长为参天大树。正是这 些底层的规范和实践,使得Node有序地发展着,摆脱掉过去JavaScript纷乱和被误解的局面,进 而进化成良性的生态系统。

第3章 异步I/O


本章介绍了异步I/O和另一些非I/O的异步方法。可以看出,事件循环是异步实现的核心,它 与浏览器中的执行模型基本保持了一致。而像古老的Rhino,尽管是较早就能在服务器端运行的 JavaScript运行时,但是执行模型并不像浏览器采用事件驱动,而是像其他语言一般采用同步I/O 作为主要模型,这造成它在性能上无所发挥。Node正是依靠构建了一套完善的高性能异步I/O框 架,打破了JavaScript在服务器端止步不前的局面。

第4章 异步编程


在接触Node的过程中,很多人粗略地接触了几个回调函数之后就放弃了。尽管异步编程略微 艰难,但是并非一无是处,一旦习惯,就显得自然。从社区和过往的经验而言,JavaScript异步编 程的难题已经基本解决,无论是通过事件,还是通过Promise/Deferred模式,或者流程控制库。相 信在掌握以上技巧之后,异步编程不是难事,习惯异步编程之后,将会收获许多值得享受的编程 体验。

本章主要介绍了主流的几种异步编程解决方案,这是目前JavaScript中主要使用的方案。但对 于其他语言而言,还有协程(coroutine)等方式。但是由于Node基于V8的原因,在目前EMCAScript5 的实现下还不支持协程。这些标准和规范还在制定中,所以暂时不作介绍。未来的V8如果支持 Generator,也将在Node中能直接使用。

最后,因为人们总是习惯性地以线性的方式进行思考,以致异步编程相对较为难以掌握。这 个世界以异步运行的本质是不会因为大家线性思维的惯性而改变。就像日出月落不会因为你的心 情而改变其自有的运行轨迹。

第5章 内存控制

Node将JavaScript的主要应用场景扩展到了服务器端,相应要考虑的细节也与浏览器端不同, 需要更严谨地为每一份资源作出安排。总的来说,内存在Node中不能随心所欲地使用,但也不是 完全不擅长。本章介绍了内存的各种限制,希望读者可以在使用中规避禁忌,与生态系统中的各 种软件搭配,发挥Node的长处。

第6章 理解Buffer


体验过JavaScript友好的字符串操作后,有些开发者可能会形成思维定势,将Buffer当做字 符串来理解。但字符串与Buffer之间有实质上的差异,即Buffer是二进制数据,字符串与Buffer 之间存在编码关系。因此,理解Buffer的诸多细节十分必要,对于如何高效处理二进制数据十 分有用。

第7章 网络编程


Node基于事件驱动和非阻塞设计,在分布式环境中尤其能发挥出它的特长,基于事件驱动可 以实现与大量的客户端进行连接,非阻塞设计则让它可以更好地提升网络的响应吞吐。Node提供 了相对底层的网络调用,以及基于事件的编程接口,使得开发者在这些模块上十分轻松地构建网 络应用。下一章我们将在本章的基础上探讨具体的Web应用。

第8章 构建Web应用


本章涉及的内容较为丰富,在Web应用的整个构建过程中,从处理请求到响应请求的整个过 程都有原理性阐述,整理本章细节就可以完成一个功能完备的Web开发框架。过去的各种Web技 术,随着框架和库的成型,开发者往往迷糊地知道应用框架和库,却不知道细节的实现,这好比 没有地图却在野地里行进。本章的内容希望能为Node开发者带来地图似的启发,在开发Web应用 时能够心有轮廓,明了细微。

现在知名和成熟的Web框架有Connect、Express等,本章中的内容在这些框架中都有实现, 因为行文的原因,本章中的代码实现得较为粗糙,实际使用请使用这些成熟的框架。

第9章 玩转进程

尽管Node从单线程的角度来讲它有够脆弱的:既不能充分利用多核CPU资源,稳定性也无 法得到保障。但是群体的力量是强大的,通过简单的主从模式,就可以将应用的质量提升一个 档次。在实际的复杂业务中,我们可能要启动很多子进程来处理任务,结构甚至远比主从模式 复杂,但是每个子进程应当是简单到只做好一件事,然后通过进程间通信技术将它们连接起来 即可。这符合Unix的设计理念,每个进程只做一件事,并做好一件事,将复杂分解为简单,将 简单组合成强大。

尽管通过 child_process 模块可以大幅提升Node的稳定性,但是一旦主进程出现问题, 所 有子进程将会失去管理。在Node的进程管理之外,还需要用监听进程数量或监听日志的方式确 保整个系统的稳定性,即使主进程出错退出,也能及时得到监控警报,使得开发者可以及时处 理故障。

第10章 测试

测试是应用或者系统最重要的质量保证手段。有单元测试实践的项目,必然对代码的粒度和 层次都掌握得较好。单元测试能够保证项目每个局部的正确性,也能够在项目迭代过程中很好地 监督和反馈迭代质量。如果没有单元测试,就如同黑夜里没有秉烛的行走。

对于性能,在编码过程中一定存在部分感性认知,与实际情况有部分偏差,而性能测试则能 很好地斧正这种差异。

第11章 产品化


一般而言,决定用一项技术进行产品开发时,只有最早期是与这门技术完全相关的。随着时 间的迁移,要解决的已经不是原来的问题了,一门技术只能在一定层面上发挥出它的优势来。用 Node也是一样,随着开发的进展、涉及层面的增多,我们看到在产品的角度要解决的问题依然是 大部分技术都要解决的问题。我们希望读者能够将Node纳入到新的层面上进行考虑,使它更适应 产品,在产品中发挥出更大的优势来。

扩展

思维导图能比较清晰的还原整本书的知识结构体系,如果你还没用看过这本书,可以按照这个思维导图的思路快速预习一遍,提高学习效率。学习新事物总容易遗忘,我比较喜欢在看书的时候用思维导图做些记录,便于自己后期复习,如果你已经看过了这本书,也建议你收藏复习。如果你有神马建议或则想法,欢迎留言或加我微信交流:646321933

《深入浅出Node.js》PDF

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.