Code Monkey home page Code Monkey logo

blog's People

Contributors

liuyang0623 avatar webproblem 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

blog's Issues

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

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

题目

如何实现 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.prototype.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

总结

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

参考

移动端手势库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 的源码,学习到裁剪图片的原理和实现方案,平时在开发过程中,其实只要清楚了实现思路和原理,就能够方便的实现具体的功能。

从underscore源码看数组乱序

image

前言

前段时间看到过一道实现数组乱序的面试题,也在《如何轻松拿到淘宝前端 offer》一文中看到作者提到数组乱序的问题,竟然还涉及到 v8 引擎方面的知识,正好近期在拜读 underscore 的源码,自己也没有了解过这方面的知识点,在此就总结下关于数组乱序的知识。

面试题

看到的面试题大致是这样的:var arr = ['a', 'b', 'c', 'd', 'c']; 实现数组乱序

首先,通过题目可以看出,可能存在一个小陷阱,数组中存在两个 'c' 元素,我想这并不是面试官或者出题人无意打错字,可能是想顺便考察下面试者对数组去重的掌握情况。当然了,这个小细节也可以忽略直接进入实现乱序功能。

对于有代码洁癖的开发人员来说,是不允许自己的程序中出现相同的元素数组值的,所以我们先把数组去重好了。

arr = [...new Set(arr)]; // ["a", "b", "c", "d"]

去重之后,就可以思考如何实现乱序了。所谓乱序就是打乱原数组元素之间的位置,使之与原数组之间的结构不一致。一开始想的方案是随意交换两个相邻元素的位置使得与原数组不一致就好了。

function shuffle(array) {
    array = array.concat();
    var rand = Math.floor(Math.random() * Number(array.length));
    var temp = array[rand];
    if(array[rand - 1] !== void 0) {
        array[rand] = array[rand - 1];
    	array[rand - 1] = temp;
    }else if(arr[rand + 1] !== void 0) {
        array[rand] = array[rand + 1];
    	array[rand + 1] = temp;
    }
    return array;
}

但后来一想,发现这只是达到了部分乱序的效果而已,要如何才能达到全部元素随机乱序的效果呢?

sort + Math.random

常见的一种实现方案就是使用数组的 sort 排序方法,原理就是 sort 的 compareFunction 匿名函数的返回值小于 0 则升序排列,大于 0 则降序排列,由于 Math.random() 得到的是 0-1 区间的数,Math.random() - 0.5 有 50% 的概率大于 0,50% 的概率小于 0。

function shuffle(arr) {
    return arr.concat().sort(function() {
        return Math.random() - 0.5;
    })
}

但是这并不是最好的解决方案,sort 排序存在的问题就是元素位置概率不均,某个元素排序后位置不变或者调换在相邻位置的概率更高,测试如下:

var count = new Array(7).fill(0);

for(var i=0; i<10000; i++) {
    var arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
    arr = shuffle(arr);
    count[arr.indexOf('a')]++;
}

console.log(count); // 一次随机乱序的结果 => [3033, 2927, 1899, 1143, 585, 284, 129]

测试进行了 10000 次的乱序排序,可以看出 'a' 元素出现在第一个位置的次数远高于出现在其他位置的次数,也就是说本应当出现在每个位置的次数应该是差不多的,实际上却差别很大,这种差异大或者不公平的结果的出现,说明这种方案并不是一个很好的解决方案。

v8 对 sort 的实现

想要知道为什么 sort 排序会出现元素分布不均的情况,就要了解底层的实现方式。v8 源码中对 sort 的实现,出于性能考虑,对于短数组(数组长度小于等于10)使用的是插入排序,长数组(数组长度大于10)使用的是快速排序和插入排序的混合排序。v8源码地址

image

插入排序的源码:

function InsertionSort(a, from, to) {
	for (var i = from + 1; i < to; i++) {
		var element = a[i];
		for (var j = i - 1; j >= from; j--) {
			var tmp = a[j];
			var order = comparefn(tmp, element);
			if (order > 0) {
				a[j + 1] = tmp;
			} else {
				break;
			}
		}
		a[j + 1] = element;
	}
};

简单来说就是将第一个元素视为有序序列,遍历数组,将之后的元素依次插入这个构建的有序序列中。其中 comparefn() 是关键, comparefn 部分:

comparefn

具体分析 sort 的分布不均的情况,可参考冴羽的JavaScript专题之乱序

关于 underscore 源码的解读,解读地址: Underscore.analysis.js

Fisher–Yates shuffle

那么有没有更好的实现方案呢?答案是肯定是有的。先来看看 underscore 源码对数组乱序的实现。

PS:自己研读 underscore 源码时加的注释。

  /**
   * 得到一个随机乱序的集合副本
   * var arr = ['a', 'b', 'c', 'd'];
   * _.shuffle(arr); // 一次随机的结果 => ["b", "c", "d", "a"]
   */
  _.shuffle = function(obj) {
    // 传入 Infinity 参数是为了一次性返回乱序的结果,如果不传,每次执行都会返回一个单一的随机项
    return _.sample(obj, Infinity);
  };

  /**
   * 从集合中产生一个随机样本
   * 原理是遍历对象元素,将当前元素与另一个随机元素调换位置,直到遍历结束
   * 数组乱序讲解参考文章:
   * https://github.com/mqyqingfeng/Blog/issues/51
   * https://github.com/hanzichi/underscore-analysis/issues/15
   * @param obj    需要乱序的对象
   * @param n      返回的随机元素个数,如果值为空,会返回一个单一的随机项
   * @param guard  暂没发现这个参数的作用
   */
  _.sample = function(obj, n, guard) {
    // 返回单一的随机项
    if (n == null || guard) {
      if (!isArrayLike(obj)) obj = _.values(obj);
      return obj[_.random(obj.length - 1)];
    }
    var sample = isArrayLike(obj) ? _.clone(obj) : _.values(obj);
    var length = getLength(sample);
    // 防止 n 的值为负数,且 n 的值不能超过对象元素的数量,确保下面的 for 循环正常遍历
    n = Math.max(Math.min(n, length), 0);
    var last = length - 1;
    for (var index = 0; index < n; index++) {
      // 获取随机调换位置元素的下标
      var rand = _.random(index, last);
      var temp = sample[index];
      sample[index] = sample[rand];
      sample[rand] = temp;
    }
    return sample.slice(0, n);
  };

underscore 实现的大致思路就是遍历数组元素时,将当前元素随机与后面的另一个数组元素的位置进行互换,直到遍历结束。

这种实现思路就是所谓的 Fisher–Yates shuffle 算法,且时间复杂度为 O(n) ,性能上是最优的。

按照这个思路再次实现如下:

function shuffle(arr) {
    var rand, temp;
    for(var i=0; i<arr.length; i++) {
        rand = i + Math.floor(Math.random() * (arr.length - i));
        temp = arr[i];
        arr[i] = arr[rand];
        arr[rand] = temp;
    }
    return arr;
}

再来测试这个方案的效果:

var count = new Array(7).fill(0);

for(var i=0; i<10000; i++) {
    var arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
    arr = shuffle(arr);
    count[arr.indexOf('b')]++;
}
console.log(count); // 一次随机乱序排序的结果 => [1403, 1413, 1448, 1464, 1451, 1456, 1365]

可以看到 ‘b’ 元素出现在各个位置的次数大致是均匀的,达到了真正的乱序效果。

其他实现方案

还有一种实现思路,可能不是最优的方案。 大致思路就是随机挑选数组中的任意一个元素存放到新数组中,且该元素从原数组中移出,重复这样的操作直到原数组中的元素为空,新数组就是乱序后的结果。

function shuffle(arr) {
    var newArr = [], rand;
    arr = arr.concat();
    while(arr.length) {
        rand = Math.floor(Math.random() * arr.length);
        newArr.push(arr[rand]);
        arr.splice(rand, 1);
    }
    return newArr;
}

总结

  • 常见的 sort + Math.random 方式乱序其实也是属于局部乱序,并没有达到所有元素都打乱顺序的效果。原因是因为 v8 底层对于短数组使用的是插入排序,长数组使用的是混合排序。
  • Fisher–Yates shuffle 算法应该是最优的实现方案,原理是遍历数组,将当前元素与在它后面的任意一个元素交换位置,直到遍历结束。

参考

JS浏览器事件循环机制

先来明白些概念性内容。

进程、线程

  • 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。

  • 线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。

浏览器内核

  • 浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。

  • 浏览器内核有多种线程在工作。

    • GUI 渲染线程:

      • 负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程。
      • 和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。
    • JS 引擎线程:

      • 单线程工作,负责解析运行 JavaScript 脚本。
      • 和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
    • 事件触发线程:

      • 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
    • 定时器触发线程:

      • 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
      • 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。
    • http 请求线程:

      • http 请求的时候会开启一条请求线程。

      • 请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。

        image

JavaScript 引擎是单线程

JavaScript 引擎是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。

HTML5 中提出了 Web-Worker API,主要是为了解决页面阻塞问题,但是并没有改变 JavaScript 是单线程的本质。了解 Web-Worker

JavaScript 事件循环机制

JavaScript 事件循环机制分为浏览器和 Node 事件循环机制,两者的实现技术不一样,浏览器 Event Loop 是 HTML 中定义的规范,Node Event Loop 是由 libuv 库实现。这里主要讲的是浏览器部分。

Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。

  • JS 调用栈

    JS 调用栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。

  • 同步任务、异步任务

    JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。

  • Event Loop

    调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。

image

image

  • 定时器

    定时器会开启一条定时器触发线程来触发计时,定时器会在等待了指定的时间后将事件放入到任务队列中等待读取到主线程执行。

    定时器指定的延时毫秒数其实并不准确,因为定时器只是在到了指定的时间时将事件放入到任务队列中,必须要等到同步的任务和现有的任务队列中的事件全部执行完成之后,才会去读取定时器的事件到主线程执行,中间可能会存在耗时比较久的任务,那么就不可能保证在指定的时间执行。

  • 宏任务(macro-task)、微任务(micro-task)

    除了广义的同步任务和异步任务,JavaScript 单线程中的任务可以细分为宏任务和微任务。

    macro-task包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。

    micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver。

        console.log(1);
        setTimeout(function() {
            console.log(2);
        })
        var promise = new Promise(function(resolve, reject) {
            console.log(3);
            resolve();
        })
        promise.then(function() {
            console.log(4);
        })
        console.log(5);

    示例中,setTimeout 和 Promise被称为任务源,来自不同的任务源注册的回调函数会被放入到不同的任务队列中。

    有了宏任务和微任务的概念后,那 JS 的执行顺序是怎样的?是宏任务先还是微任务先?

    第一次事件循环中,JavaScript 引擎会把整个 script 代码当成一个宏任务执行,执行完成之后,再检测本次循环中是否寻在微任务,存在的话就依次从微任务的任务队列中读取执行完所有的微任务,再读取宏任务的任务队列中的任务执行,再执行所有的微任务,如此循环。JS 的执行顺序就是每次事件循环中的宏任务-微任务。

    • 上面的示例中,第一次事件循环,整段代码作为宏任务进入主线程执行。
    • 遇到了 setTimeout ,就会等到过了指定的时间后将回调函数放入到宏任务的任务队列中。
    • 遇到 Promise,将 then 函数放入到微任务的任务队列中。
    • 整个事件循环完成之后,会去检测微任务的任务队列中是否存在任务,存在就执行。
    • 第一次的循环结果打印为: 1,3,5,4。
    • 接着再到宏任务的任务队列中按顺序取出一个宏任务到栈中让主线程执行,那么在这次循环中的宏任务就是 setTimeout 注册的回调函数,执行完这个回调函数,发现在这次循环中并不存在微任务,就准备进行下一次事件循环。
    • 检测到宏任务队列中已经没有了要执行的任务,那么就结束事件循环。
    • 最终的结果就是 1,3,5,4,2。

参考

自律计划

受 ScarSu 小姐姐的自律计划的启发,同时也意思到自己的不自律,执行力不够,所以也试着开始养成自律的习惯,希望能够坚持下去。

据说我们要养成一个习惯,至少需要21天的坚持。 所以这里制定的自律计划的时间范围是 2018.11.19-2018.12.16,大概是一个月左右的时间,如果能坚持下去,后面的自律计划会适当调整。

每日打卡项

  • 早起
  • 早餐
  • 学习
  • 运动
  • 每日总结整理
  • 控制睡前刷手机
  • 睡觉

早起

一日之计在于晨,坚持早起同时也意味着能坚持按时睡觉。早起空闲出来的时间可以做很多事情,学习,运动,思考等。

早餐

平时周末有不吃早餐的习惯,其实这样长期对身体不好,所以要坚持吃早餐且要有营养。

学习

学习又分为软技能和专业技能,软技能包括很多方面,主要是对自己认知和思维上的提升,包括学英语,看书,听电台等一些有营养的提升方式,专业技能指的是工作方面的提升。保证每天至少有3小时的时间花在学习上。

运动

坚持运动,锻炼身体是很有必要的,每天抽出1小时的时间来运动,包括慢跑或快走等。

每日总结整理

之前就有过这样的动作,但很多都是断断续续的,并没有坚持每天去梳理总结。所以要养成每天总结的习惯,反思自己不断进步,同时也记录生活的而点滴。

控制睡前刷手机

睡前玩手机容易影响睡眠,睡前应该保持一个舒适的状态,所以睡前半小时不能玩手机。

睡觉

要做到早起,就必须要有规律的按时入睡,才能保证一天良好的状态。

作息计划

早起

早上 6:50 起床

7:20-8:20 学习

早餐

9:00 之前完成

学习

早上1小时,19:00-20:00 1小时,21:30-22:10,其他碎片化时间

运动

20:10-21:00,方式主要是慢跑或者快走

每日总结整理

22:30 之前完成

睡觉

23:00 之前手机关机,23:20 之前入睡

打卡表(日更)

日期 早起 早餐 学习 运动 每日总结整理 控制睡前刷手机 睡觉
2018.11.19 完成 完成 完成 未完成 完成 未完成 完成
2018.11.20 完成 完成 未完成 完成 完成 未完成 未完成
2018.11.21
2018.11.22
2018.11.23
2018.11.24
2018.11.25
2018.11.26
2018.11.27
2018.11.28
2018.11.29
2018.11.30
2018.12.01
2018.12.02
2018.12.03
2018.12.04
2018.12.05
2018.12.06
2018.12.07
2018.12.08
2018.12.09
2018.12.10
2018.12.11
2018.12.12
2018.12.13
2018.12.14
2018.12.15
2018.12.16

Vue源码系列——Vue 的初始化

Vue源码系列--初始化

最近在看 Vue 的源码架构,打算在公司组织 Vue 源码的分享会,所以准备做一系列关于 Vue 源码的技术输出。

目录结构

先来大致看下 vue 目录结构,这里只列出 src 目录下的文件结构。

├── src
	├── compiler -------------------------------- 编译器代码,将 template 模板编译成 render 函数
	├── core ------------------------------------ 核心代码,主要包括响应式原理,vdom,全局 API 等
	├── platforms ------------------------------- 平台代码
		├── web --------------------------------- web 端代码
		├── weex -------------------------------- 移动端混合开发代码
	├── server ----------------------------------- ssr 服务端渲染
	├── sfc -------------------------------------- .vue 单文件组件解析
	├── shared ----------------------------------- 通用代码,定义的一些工具函数和常量

Vue 的入口文件

分析 Vue 源码首先要找到 Vue 的入口文件,从入口开始,一步步深入了解实现原理。在跟目录下的 package.json 文件中找到构建配置。

  "scripts": {
    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
    "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
    "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
    "dev:test": "karma start test/unit/karma.dev.config.js",
    "dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
    "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
    "dev:weex": "rollup -w -c scripts/config.js --environment TARGET:weex-framework",
    "dev:weex:factory": "rollup -w -c scripts/config.js --environment TARGET:weex-factory",
    "dev:weex:compiler": "rollup -w -c scripts/config.js --environment TARGET:weex-compiler ",
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
    "test": "npm run lint && flow check && npm run test:types && npm run test:cover && npm run test:e2e -- --env phantomjs && npm run test:ssr && npm run test:weex",
    "test:unit": "karma start test/unit/karma.unit.config.js",
    "test:cover": "karma start test/unit/karma.cover.config.js",
    "test:e2e": "npm run build -- web-full-prod,web-server-basic-renderer && node test/e2e/runner.js",
    "test:weex": "npm run build:weex && jasmine JASMINE_CONFIG_PATH=test/weex/jasmine.js",
    "test:ssr": "npm run build:ssr && jasmine JASMINE_CONFIG_PATH=test/ssr/jasmine.js",
    "test:sauce": "npm run sauce -- 0 && npm run sauce -- 1 && npm run sauce -- 2",
    "test:types": "tsc -p ./types/test/tsconfig.json",
    "lint": "eslint src scripts test",
    "flow": "flow check",
    "sauce": "karma start test/unit/karma.sauce.config.js",
    "bench:ssr": "npm run build:ssr && node benchmarks/ssr/renderToString.js && node benchmarks/ssr/renderToStream.js",
    "release": "bash scripts/release.sh",
    "release:weex": "bash scripts/release-weex.sh",
    "release:note": "node scripts/gen-release-note.js",
    "commit": "git-cz"
  }

可以看到,针对不同模块的构建输出设置了不同的构建脚本,这里只看 dev 脚本的,也就是运行 npm run dev。运行脚本的时候,会找到 scripts/config.js 执行,然后在 config.js 中找到 web-full-dev 的配置。

可以看到,构建时的入口文件是 web/entry-runtime-with-compiler.js 。这里的 web 其实是配置的相对路径的别名,相关的配置都写在 scripts/alias.js 中。

顺着路口文件一路找下去,可以看到 Vue 被定义在 src/core/instance/index.js 中。

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

/**
 * Vue 构造器
 * @param {*} options
 */
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

这个文件的主要作用是定义了一个 Vue 构造器,里面通过执行 _init 函数进行初始化,这也说明了 Vue 必须通过 new 关键字来初始化。Vue 构造器接收的 options 就是在 new Vue 时传入的配置项。

new Vue 的过程

通过内部定义的 Vue 构造器可以看到,new Vue 时,内部会通过调用 _init 函数进行初始化。_init 函数是挂载在 Vue 原型上的方法,代码定义在 initMixin 中,也就是 src/core/instance/init.js。

// 挂载到 Vue 原型上
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    // 性能分析
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    // 合并配置项
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化生命周期
    initLifecycle(vm)
    // 初始化事件
    initEvents(vm)
    // 初始化渲染
    initRender(vm)
    // beforeCreate 生命周期钩子函数阶段
    callHook(vm, 'beforeCreate')
    // 初始化 injections
    initInjections(vm) // resolve injections before data/props
    // 初始化 props,methods,data 等
    initState(vm)
    // 初始化 provide
    initProvide(vm) // resolve provide after data/props
    // created 生命周期钩子函数阶段
    callHook(vm, 'created')

    /* istanbul ignore if */
    // 性能分析
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    // 挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

_init 函数的作用主要是合并初始化配置项,初始化事件监听,初始化渲染等操作。可以看到的是,initState 函数也就是初始化配置项是在 beforeCreate 钩子函数阶段后,created 钩子函数阶段前完成的,所以在 beforeCreate 钩子函数阶段是不能访问和操作数据的。

合并配置选项

// 合并配置项
if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
} else {
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )
}

这段代码就是用来合并配置选项的。这里只考虑根实例的初始化,组件实例的初始化暂不考虑,也就是 new Vue 初始化。所以代码会进入到 else 代码块,执行 mergeOptions 函数进行合并。mergeOptions 函数接受 3 个参数,第一个参数执行一个函数并得到返回值,其值就相当于是 Vue.options。第二个参数就是传入的配置项,第三个参数是 Vue 实例。Vue.options 定义在 src/core/global-api/index.js 中。

首先给 Vue.options 赋值了一个空对象,然后通过遍历 ASSET_TYPES 数组给 Vue.options 添加属性。ASSET_TYPES 定义在 src/shared/constants.js 中。

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

然后再执行拷贝函数将 KeepAlive 组件赋值给 components 属性值。最终 Vue.options 的值如下:

Vue.options = {
    components: {
        KeepAlive
    },
    directives: {},
    filters: {}
}

到此就明白了 mergeOptions 函数的参数类型了。接下来看下 mergeOptions 函数是怎么合并配置选项的。

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  // 规范化 props
  normalizeProps(child, vm)
  // 规范化 inject
  normalizeInject(child, vm)
  // 规范化 directives
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }
  
  // 合并配置项代码
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

mergeOptions 函数首先会对一些配置项进行规范化,比如 props,inject 依赖注入,directives 自定义指令。拿props 为例,这个属性是用来父组件向子组件传参用的,其值的写法有很多种。

// 第 1 种:数组形式
props: ['size', 'disabled']

// 第 2 种
props: {
    size: String,
    disabled: Boolean    
}

// 第 3 种
props: {
    size: {
        type: String   
    },
    disabled: {
        type: Boolean
    }    
}

对于第 1 种和第 2 种写法,在 Vue 内部会统一规范化成第 3 种形式的写法。规范化函数如下:

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name

  // props 是数组或对象的情况
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

对选项的值规范化后,就开始进入到选项合并阶段了,首先会遍历 Vue 内部提供的默认配置项并自行 mergeField 函数,再接着遍历传入的配置项执行 mergeField 函数。mergeField 函数时关键,函数内部会针对不同的配置项进行相应的合并处理。

// 合并配置项代码
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options

strats 是定义的一个空对象,Vue 内部会将配置项合并函数作为属性挂载到这个对象上,包括 data 选项合并函数,生命周期选项合并函数,watch,props,methods 等选项合并函数。Vue 的配置项合并规则在官方文档中解释得很清楚了,合并规则就是:

  • 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
  • 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
  • 值为对象的选项,例如 methodscomponents 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

整个 options.js 的作用都是用来合并配置项的。

Vue 的挂载

合并配置项完成后,接着就会进行一些初始化。

// 初始化生命周期
initLifecycle(vm)
// 初始化事件监听
initEvents(vm)
// 初始化渲染
initRender(vm)
// beforeCreate 生命周期钩子函数阶段
callHook(vm, 'beforeCreate')
// 初始化 injections
initInjections(vm) // resolve injections before data/props
// 初始化 props,methods,data 等配置项
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// created 生命周期钩子函数阶段
callHook(vm, 'created')

需要注意的是,对于 props,methods,data 等配置项的初始化是发生在 beforeCreate 钩子函数之后, created 钩子函数之前的,所以,在 beforeCreate 钩子函数阶段是不能访问好操作数据的,必须是至少在 created 钩子函数阶段才能访问和操作。

完成了上面的初始化之后,就会进入到挂载阶段。

// 挂载
if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

$mount 函数定义在 src\platforms\web\entry-runtime-with-compiler.js。

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

整个函数的作用都是在把模板编译成 render 函数,再把 VDOM 转换成真实的 DOM 元素放入到 document 文档中。这里有一个需要知道的点就是,如果配置项中没有定义 render 配置项,就会选取 template 配置项作为模板编译成 render 函数,如果 template 配置项也不存在,那就直接选取 外部 HTML 作为模板进行编译。优先级是 ender 函数选项-template选项-外部HTML。最后会通过调用定义在 src\core\instance\lifecycle.js 中的 mountComponent 函数转换成真实 DOM。

在 mountComponent 函数内部,会进入 beforeMount 钩子函数阶段和 mounted 钩子函数阶段,需要注意的是,vue 的挂载完成是在 beforeMount 钩子函数之后和 mounted 钩子函数之前发生的,所以 vm.$el 至少是在 mounted 钩子函数阶段才能访问到。

Vue 生命周期

  • 在 beforeCreate 阶段也就是实例初始化但是还没创建完成的时候,数据监测和事件初始化还没完成,不能对任何数据访问操作。
  • created 阶段,实例创建完成,数据监测和事件初始化也都完成,可以访问和操作 data 数据,但是在这一步,$el 还获取不到,这一步可以开始一些数据请求的操作。
  • 实例创建完成后,就进入到挂载阶段,在挂载阶段就可以访问 $el 的值了。要说明的是进入挂载阶段前,会先判断传入的配置项中是否有 el 属性,如果有才会继续初始化,如果没有初始化会暂停,等到执行了 vm.$mount(el) 后才会继续。在 beforeMount 阶段,内部会找到 template 配置项作为模板进行编译成 render 函数,如果没有template 配置项,则使用外部 HTML 作为模板,如果有直接配置 render 函数,则直接使用配置的 render 函数,优先级是:render 函数选项-template选项-外部HTML。并且在 beforeMount 阶段,$el 获取的值还是双大括号的 vue 语法,只是个占位符,内容还没有被替换掉。
  • 当执行完 render function之后,到了mounted阶段,el被创建的vm.$el替换掉,挂载完成。到这里为止,初始化过程中会触发上面这些钩子函数,更新和销毁钩子函数都需要手动触发的。
  • 当修改 data 选项中的数据时,会触发 beforeUpdate 和 updated,虚拟 DOM 按照 DIFF 算法重新渲染和打补丁,最后 DOM 更新完成后进入到 updated 阶段。
  • beforeDestory 是在vue实例销毁之前调用,在这里,实例仍然可以访问操作,一般在这一步中进行:销毁定时器、解绑全局事件、销毁插件对象等操作。
  • destoryed 在实例销毁后调用,所有指令都将会解绑,事件监听移除,子实例也会销毁。

总结

new Vue的时候,内部会调用 _init 函数进行初始化,主要是初始化事件监听,初始化渲染,初始化配置项,实例挂载等操作。初始化的时候,会执行 beforeCreate, created, beforeMount, mounted 生命周期钩子函数。

从underscore源码看如何实现map函数(二)

在上篇文章中,遗留了两个问题:

  • arguments 存在性能问题
  • call 比 apply 速度更快

本篇文章将会对这两个问题进行详细的分析。

arguments 存在性能问题

arguments 是存在于函数内部用于存储传递给函数的参数的类数组对象,在函数被调用时创建。arguments 是类数组对象,只拥有 length 属性,但是可以在函数内部转换为数组。

function test() {
    var args = Array.prototype.slice.call(arguments);
    console.log(args); // ["白展堂", "吕秀才"]
    // var args2 = [].slice.call(arguments);
    // console.log(args2);
}
test('白展堂', '吕秀才');

但是对参数使用 slice 会阻止某些 JavaScript 引擎中的优化,引发性能问题。所以将 arguments 对象转换为数组应该采用如下方法:

var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));
var args = Array.from(arguments);
var args = [...arguments];

我们来测试下,性能差距究竟有多大。

function sum() {
	var args = Array.prototype.slice.call(arguments);
	return args;
}
function sum1() {  
    var args = Array.apply(null, arguments); 
	return args;
}
function leaksArguments(number) {
	var t = new Date;
	for(var i=0; i<100000; i++) {
		if(!number) {
			sum(1,2,3,4,5,6);
		}else {
			sum1(1,2,3,4,5,6);
		}
	}
	console.log(`耗时:${new Date - t}`);
}

简单写了一个测试用例,分别得出两种将 arguments 对象转换成数组的方法的耗时情况,执行 leaksArguments 函数时不传参数使用的是 Array.prototype.slice.call 方式转换,传参使用的是 Array.apply 方式转换。测试结果如下:

image

可以看到,性能提升了2倍多,且随着传递给函数的参数越多,性能耗时差距就越大。这也说明,JavaScript 引擎在访问 arguments 时会消耗性能。

那么如何安全地使用 arguments 呢?

  • 只使用 arguments.length 属性。
  • 只是用 arguments[i] ,需要始终为 arguments 的合法整型索引,且不允许越界 。
  • 除了 .length[i],不要直接使用 arguments
  • 严格来说用 fn.apply(y, arguments) 是没问题的,但除此之外都不行(例如 .slice)。 Function#apply 是特别的存在。
  • 请注意,给函数添加属性值(例如 fn.$inject = ...)和绑定函数(即 Function#bind 的结果)会生成隐藏类,因此此时使用 #apply 不安全。

如果按照上面的方式正确使用 arguments 对象,就不必担心使用 arguments 导致性能消耗问题。

call 比 apply 速度更快

call 和 apply 实现的功能都是一样的,都是为了改变函数在运行时的上下文,只是接收的参数方式不同,call 方法从第二个参数开始是一系列参数列表,而 apply 方法则是把参数放在数组里。

为了证明 call 的速度比 apply 更快,来写一个简单的测试。

var foo = {
    color: 'blue'
}
function bar(name) {
    return name + this.color;
}
function test(number) {
	var t = new Date;
	for(var i=0; i< 100000; i++) {
		if(!number) {
			bar.call(foo, '天空的颜色');
		}else {
			bar.apply(foo, ['天空的颜色']);
		}
	}
	console.log(`${!number?'call':'apply'}耗时:${new Date - t}`);
}

测试结果如下:

image

当然,可以推荐在一个性能测试网站 jsperf 上进行性能测试。下面是几个简单的测试结果。

image

image

想要知道 call 和 apply 在性能上为什么会有差异,就得知道 call 和 apply 在执行过程中发生了什么。简单来说就是 call 和 apply,最终都是调用一个叫做 [[Call]] 的内部函数 ,apply 方法执行过程中对参数的处理更为复杂,需要进行检测数组参数和格式化等步骤,而 call 方式的参数原本就是按照顺序排列的参数列表,处理步骤更为简洁。 关于具体的执行步骤可参见 stackoverflow 的回答

需要知道的是,关于 call 和 apply 性能差异问题也只是存在于 ES6 之前,随着 ECMAScript 语言和 JavaScript 解释器性能不断增强,call 和 apply 性能大致一样了。call 和 apply 是存在于不同场景下的,我们应该更加注重两个函数在实际应用场景中如何选择合适的方式来实现需求的效果。

参考

从underscore源码看如何判断两个对象相等

首先要清楚 JavaScript 中的相等分为宽松相等(==)和严格相等(===)。宽松相等在比较值的时候会先进行类型的隐式转换,严格相等下如果比较值的类型不一致,那么就判定比较值不全等。如果比较值是引用类型,宽松相等和严格相等就不能直接判断出值是否相等了(引用类型浅拷贝比较值除外,也就是比较值指向的是同一引用地址),原因是对于任意两个不同的非原始对象,即便他们有相同的结构,都会计算得到 false 。

var num = 1;
var str = '1';
console.log(num == str); // true
console.log(num === str); // false

var obj1 = {name: '白展堂'};
var obj2 = {name: '白展堂'};
var obj3 = obj1;
console.log(obj1 == obj2); // false
console.log(obj1 === obj2); // false
console.log(obj1 == obj3); // true
console.log(obj1 === obj3); // true

var arr1 = [1];
var arr2 = [1];
console.log(arr1 == arr2); // false
console.log(arr1 === arr2); // false

JSON.stringify

如何判断对象是否相等?

一种解决方案就是使用 JSON.stringify 序列化成字符串再做比较。

var obj1 = {name: '白展堂', age: 25};
var obj2 = {name: '白展堂', age: 25};
JSON.stringify(obj1) === JSON.stringify(obj2); // true

var arr1 = ['a', 'b', 'c', 'd'];
var arr2 = ['a', 'b', 'c', 'd'];
JSON.stringify(arr1) === JSON.stringify(arr2); // true

这种方案看似可以判断出对象是否相等,但是会不会存在问题呢?看过 underscore 源码的都知道,isEqual 函数的实现有多复杂,很多种情况显然不是通过 JSON.stringify 序列化就能解决的。

先来分析下 JSON.stringify 方案存在的问题,假设比较对象中的属性值存在 RegExp 对象,判定结果是怎样的呢?

function eq(a, b) {
    return JSON.stringify(a) === JSON.stringify(b);
}
var obj1 = {name: '白展堂', reg: /test1/i};
var obj2 = {name: '白展堂', reg: /test2/i};
eq(obj1, obj2); // true

结果为 true,也就是说 obj1 和 obj2 序列化的字符串是一致的。

var obj1 = {name: '白展堂', reg: /test1/i};
var obj2 = {name: '白展堂', reg: /test2/i};
JSON.stringify(obj1); // "{"name":"白展堂","reg":{}}"
JSON.stringify(obj2); // "{"name":"白展堂","reg":{}}"

可以看到,JSON.stringify 将 RegExp 对象序列化成了 '{}',也就是说 JSON.stringify 序列化对于某些情况会存在问题,比如 undefined 和 Function 函数在序列化过程中会被忽略。

function test() {}
JSON.stringify(undefined) === JSON.stringify(test); // true

_.isEqual

那么如何完美的判断对象或值相等?现在来看看 underscore 中的 isEqual 函数是如何针对不同的比较值进行处理的。

区分 +0 与 -0 之间的差异

ECMAScript 中,认为 0 与 -0 是相等的,其实不然。

1 / 0 // Infinity
1 / -0 // -Infinity
1 / 0 === 1 / -0 // false

原因是因为 JavaScript 中的 Number 是64位双精度浮点数,采用了IEEE_754 浮点数表示法,这是一种二进制表示法,按照这个标准,最高位是符号位(0 代表正,1 代表负),剩下的用于表示大小。而对于零这个边界值 ,1000(-0) 和 0000(0)都是表示 0 ,这才有了正负零的区别。

那么如何区分 0 与 -0?

function eq(a, b) {
    // 比较值a,b相等且值不是0和-0的情况
    if(a === b) {
        return a !== 0 || 1 / a === 1 / b; 
    }
    return false;
}
eq(0, 0); // true
eq(0, -0); // false

判断值是否为 NaN

判断某个值是否为 NaN 时不能直接比较这个值是否等于 NaN,因为 ECMAScript 中 NaN 不等于自身,可以使用原生函数 Number.isNaN() 或 isNaN()。

var a = NaN;
a === NaN; // false
isNaN(a); // true

那么自己如何实现判断 NaN 值的方法?利用 NaN 不等于自身的原理。

function eq(a, b) {
    if(a !== a) return b !== b; 
}
eq(NaN, NaN); //true
eq(NaN, 'test'); // false

隐式类型转换

对于 RegExp,String,Number,Boolean 等类型的值,假设一个比较值是字面量赋值,另一个比较值的通过构造函数生成的,ECMAScript 会认为两个值并不相等。

var s1 = 'test';
var s2 = new String('test');
console.log(s1 === s2); // false
typeof s1; // 'string'
typeof s2; // 'object'

var n1 = 100;
var n2 = new Number(100);
console.log(n1 === n2); // false
typeof n1; // 'number'
typeof n2; // 'object'

原因是因为字面量赋值的变量和构造函数生成的变量之间的类型不同,前面说过,严格相等下不同类型的值是不全等的,那么如何处理这种情况?答案是对比较值进行隐式转换。

image

递归遍历

对于 toString() 是 Array 和 Object 类型的比较值,则循环遍历里面的元素或属性进行比较,只有length 属性值相等且里面的元素或属性都相等的情况下,就说明两个比较值是相等的了。存在一种情况就是比较值里的元素或者属性值是一个嵌套的对象,这就需要使用递归遍历。

image

PS: underscore 源码中的 _.isEqual 源码注释地址: 源码注释

参考

从underscore源码看如何实现map函数(一)

前言

经常会看到这样的面试题,让面试者手动实现一个 map 函数之类的,嗯,貌似并没有什么实际意义。但是对于知识探索的步伐不能停止,现在就来分析下如何实现 map 函数。

PS: 关于 underscore 源码解读注释,详见:underscore 源码解读

Array.prototype.map

先来了解下原生 map 函数。

map 函数用于对数组元素进行迭代遍历,返回一个新函数并不影响原函数的值。map 函数接受一个 callback 函数以及执行上下文参数,callback 函数带有三个参数,分别是迭代的当前值,迭代当前值的索引下标以及迭代数组自身。map 函数会给数组中的每一个元素按照顺序执行一次 callback 函数。

var arr = [1,2,3];
var newArr = arr.map(function(item, index){
    if(index == 1) return item * 3;
    return item;
})
console.log(newArr); // [1, 6, 3]

实现

for 循环

实现思路其实挺简单,使用 for 循环对原数组进行遍历,每个元素都执行一遍回调函数,同时将值赋值给一个新数组,遍历结束将新数组返回。

将自定义的 _map 函数依附在 Array 的原型上,省去了对迭代数组类型的检查等步骤。

Array.prototype._map = function(iteratee, context) {
    var arr = this;
    var newArr = [];
    for(var i=0; i<arr.length; i++) {
        newArr[i] = iteratee.call(context, arr[i], i, arr);
    }
    return newArr;
}

测试如下:

var arr = [1,2,3];
var newArr = arr._map(function(item, index){
    if(index == 1) return item * 3;
    return item;
})
console.log(newArr); // [1, 6, 3]

好吧,其实重点不在于自己如何实现 map 函数,而是解读 underscore 中是如何实现 map 函数的。

underscore 中的 map 函数

_.map 相对于 Array.prototype.map 来说,功能更加完善和健壮。 _.map 源码:

  /**
   * @param obj 对象
   * @param iteratee 迭代回调
   * @param context 执行上下文
   * _.map 的强大之处在于 iteratee 迭代回调的参数可以是函数,对象,字符串,甚至不传参
   * _.map 会根据不同类型的 iteratee 参数进行不同的处理
   * _.map([1,2,3], function(num){ return num * 3; }); // [3, 6, 9]
   * _.map([{name: 'Kevin'}, {name: 'Daisy'}], 'name'); // ["Kevin", "Daisy"]
   */
  _.map = _.collect = function(obj, iteratee, context) {
    // 针对不同类型的 iteratee 进行处理
    iteratee = cb(iteratee, context);
    var keys = !isArrayLike(obj) && _.keys(obj),
        length = (keys || obj).length,
        results = Array(length);
    for (var index = 0; index < length; index++) {
      var currentKey = keys ? keys[index] : index;
      results[index] = iteratee(obj[currentKey], currentKey, obj);
    }
    return results;
  };

可以看到,_.map 接受 3 个参数,分别是迭代对象,迭代回调和执行上下文。iteratee 迭代回调在函数内部进行了特殊处理,为什么要这么做,原因是因为iteratee 迭代回调的参数可以是函数,对象,字符串,甚至不传参。

// 传入一个函数
_.map([1,2,3], function(num){ return num * 3; }); // [3, 6, 9]

// 什么也不传
_.map([1,2,3]); // [1, 2, 3]

// 传入一个对象
_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true]

// 传入一个字符串
_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], 'name'); // ["Kevin", "Daisy"]

先来分析下 _.map 函数内部是如何针对不同类型的 iteratee 进行处理的。

cb

cb 函数源码如下(PS: 所有的注释都是个人见解):

var cb = function(value, context, argCount) {
    // 是否使用自定义的 iteratee 迭代器,外部可以自定义 iteratee 迭代器
    if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
    // 处理不传入 iteratee 迭代器的情况,直接返回迭代集合
    // _.map([1,2,3]); // [1,2,3]
    if (value == null) return _.identity;
    // 优化 iteratee 迭代器是函数的情况
    if (_.isFunction(value)) return optimizeCb(value, context, argCount);
    // 处理 iteratee 迭代器是对象的情况
    if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
    // 其他情况的处理,数组或者基本数据类型的情况
    return _.property(value);
};

cb 函数内部针对 value 类型(也就是 iteratee 迭代器)的不同做了相应的处理。

underscore 中允许我们自定义 _.iteratee 函数的,也就是可以自定义迭代回调。

if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);

正常情况下,这个判断语句应该为 false,因为在 underscore 内部中已经定义了 _.iteratee 就是与 builtinIteratee 相等。

_.iteratee = builtinIteratee = function(value, context) {
    return cb(value, context, Infinity);
};

这样做的目的是为了区分是否有自定义 _.iteratee 函数,如果有重写了 _.iteratee 函数,就使用自定义的函数。

那么为什么会允许我们去修改 _.iteratee 函数呢?试想如果场景中只是需要 _.map 函数的 iteratee 参数是函数的话,就用该函数处理数组元素,如果不是函数,就直接返回当前元素,而不是将 iteratee 进行针对性处理。

_.iteratee = function(value, context) {
    if(typeof value === 'function') {
        return function(...rest) {
            return value.call(context, ...rest)
        };
    }
    return function(value) {
        return value;
    }
}

测试如下:

_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], 'name');

image

需要注意的是,很多迭代函数都依赖于 _.iteratee 函数,所以要谨慎使用自定义 _.iteratee。

当然了,如果没有 iteratee 迭代器的情况下,也是直接返回迭代集合。

正常使用情况下,传入的 iteratee 迭代器应该都会是函数的,为了提升性能,在 cb 函数内部针对 iteratee 迭代器是函数的情况做了性能处理,也就是 optimizeCb 函数。

optimizeCb

optimizeCb 函数源码如下:

  /**
   * 优化迭代器回调
   * @param func 迭代器回调 
   * @param context 执行上下文
   * @param argCount 指定迭代器回调接受参数个数
   */
  var optimizeCb = function(func, context, argCount) {
    // 如果没有传入上下文,直接返回
    if (context === void 0) return func;
    // 根据指定接受参数进行处理
    switch (argCount) {
      case 1: return function(value) {
        // value: 当前迭代元素
        return func.call(context, value);
      };
      // The 2-parameter case has been omitted only because no current consumers
      // made use of it.
      case null:
      case 3: return function(value, index, collection) {
        // value: 当前迭代元素,index: 迭代元素索引,collection: 迭代集合
        return func.call(context, value, index, collection);
      };
      case 4: return function(accumulator, value, index, collection) {
        // accumulator: 累加器,value: 当前迭代元素,index: 迭代元素索引,collection: 迭代集合
        return func.call(context, accumulator, value, index, collection);
      };
    }
    // 当指定迭代器回调接受参数的个数超过4个,就用 arguments 代替
    // 为什么不直接使用这段代码而是在上面根据 argCount 处理接受的参数
    // 1. arguments 存在性能问题
    // 2. call 比 apply 速度更快
    return function() {
      return func.apply(context, arguments);
    };
  };

optimizeCb 函数内部主要是针对 iteratee 迭代器接受的参数进行性能优化。当指定迭代器回调接受参数的个数超过4个,就用 arguments 代替。为什么要这样处理?原因是因为 arguments 存在性能问题,且 call 比 apply 速度更快。具体分析会在下一篇给出解释,这里不做过多的分析。

_.matcher

回到前面对 iteratee 迭代器类型做处理的话题,如果 iteratee 迭代器是对象的情况,又该如何处理?也就是这样:

_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true]

在 cb 函数内部使用了 _.matcher 函数处理这种情况,来分析下 _.matcher 函数都做了哪些事情。 _.matcher 源码如下:

  /**
   * 传入一个属性对象,返回一个属性检测函数,检测对象是否具有指定属性
   * var matcher = _.matcher({name: '白展堂'});
    var obj = {name: '白展堂', age: 25};
    matcher(obj); // true
   */
  _.matcher = _.matches = function(attrs) {
    // 合并复制对象,attrs 必须是 Objdect 类型
    // arrts 的值为空或者其他数据类型,都能保证 attrs 是 Object 类型
    attrs = _.extendOwn({}, attrs);
    // 返回属性检测函数
    return function(obj) {
      // 检测 obj 对象是否具有指定属性 attrs
      return _.isMatch(obj, attrs);
    };
  };

_.matcher 的主要作用就是检测 obj 对象是否具有指定属性 attrs,例如:

var matcher = _.matcher({name: '白展堂'});
var obj = {name: '白展堂', age: 25};
var obj2 = {name: '吕秀才', age: 25};

matcher(obj); // true
matcher(obj2); // false

具体的检测是使用了 _.isMatch 函数, _.isMatch 源码如下:

  /**
   * 检测对象中是否包含指定属性
   * var obj = {name: '白展堂', age: 25}; 
   * var attrs = {name: '白展堂'}; 
   * _.isMatch(obj, attrs); // true
   */
  _.isMatch = function(object, attrs) {
    var keys = _.keys(attrs), length = keys.length;
    if (object == null) return !length;
    var obj = Object(object);
    for (var i = 0; i < length; i++) {
      var key = keys[i];
      if (attrs[key] !== obj[key] || !(key in obj)) return false;
    }
    return true;
  };

核心部分就梳理清楚了,回到 _.map 函数,可以看到,也是使用了 for 循环来实现 map 功能,和我们自己实现了思路一致,有一点不同的是, _.map 函数的第一个参数,不仅限于数组,还可以是对象和字符串。

_.map('name'); // ["n", "a", "m", "e"]

_.map({name: '白展堂', age: 25}); // ["白展堂", 25]

在 _.map 函数内部,对类数组的对象也进行了处理。

遗留问题

到这里就梳理清楚了在 underscore 中是如何实现 map 函数的,以及优化性能方案。可以说在 underscore 中每行代码都很精炼,值得反复揣摩。

同时在梳理过程中,遗留了两个问题:

  • arguments 存在性能问题
  • call 比 apply 速度更快

这两个问题将会在下一篇中进行详细的分析。

参考

前端 MVC/MVP/MVVM 模式

前言

之前有碰到过一些关于对 MVC/MVP/MVVM 模式理解的面试题,以及它们之间的异同,这里就做下简单的笔记。

由于软件架构设计模式的知识点理解起来会比较吃力,网上也很少有较权威的讲解文章,所以这里的笔记可能会存在知识点比较浅显或者错误的理解,待以后有深刻的理解后再修正,这里只记录当前对 MV* 模式的理解。

需要注意的是,这种 MV* 模式和设计模式是有区别的。MV* 模式是一种管理与组织代码的学问,其本质是一种软件开发的模型。而设计模式是在解决一类问题的基础上总结出来的解决方案,是具体写代码的方式。

且前后端的 MV* 模式是不相同的,不能混为一谈。以 MVC 模式为例区分前后端之间的区别如下:

前后端MVC

MVC/MVP/MVVM 是什么?之间的异同又是什么?

MVC

image

MVC 分为 3 个模块,Model(数据层),View(视图层),Controller(控制器)。模块之间的依赖关系如下:

  • Model: 对外暴露函数调用接口和事件接口,不依赖 Controller和 View。
  • View: 对外暴露用户触发事件接口,并监听 Model 数据变化触发的事件,依赖于 Model。
  • Controller: 监听 View 的用户事件,并对 Model 的接口了如指掌,依赖于 Model和 View。

数据流回路流程如下:

  • 用户与 View 交互,触发用户事件
  • Controller 监听到用户事件,调用 Model 接口,改变 Model 层的数据
  • Model 层数据变化触发相应的事件,将新的数据传递给 View 层,View 做出改变,用户得到反馈

MVP

MVP

MVP 分为 3 个模块,Model,View,Presenter。模块之间的依赖关系如下:

  • Model: 对外暴露函数调用接口和事件接口,不依赖 Presenter 和 View。
  • View: 对外暴露函数调用接口和用户触发事件接口,不依赖 Presenter 和 View。
  • Presenter: 监听 View 和 Model 的事件,并对它们的接口了如指掌,所以依赖于 Model 和 View。

数据流回路流程如下:

  • 用户与 View 交互,触发用户事件
  • Presenter 监听到用户事件,调用 Model 接口,改变 Model 层的数据
  • Model 层数据变化触发相应的事件,事件被 Presenter 层监听到,调用 View 暴露函数调用接口,View 做出改变,用户得到反馈

MVVM

MVVM

MVVM 分为 3 个模块,Model,View,ViewModel 。模块之间的依赖关系如下:

  • Model: 对外暴露函数调用接口和事件接口,不依赖 ViewModel 和 View。
  • View: 监听用户交互事件,然后调用 ViewModel 的响应逻辑,同时将自己的显示状态与 ViewModel 的状态数据绑定在一起,所以依赖于 ViewModel。
  • ViewModel: 监听 Model 的事件,并对 Model 的接口了如指掌,依赖于 Model。同时向 View 暴露响应逻辑的调用接口,以及所有的状态数据,并不依赖于 View。

数据流回路流程如下:

  • 用户与 View 交互,触发用户事件
  • View 层调用起 ViewModel 层的响应逻辑的接口
  • ViewModel 层的响应逻辑处理完后,调用 Model 接口,改变 Model 层的数据
  • Model 层数据变化触发相应的事件,被 ViewModel 监听到,并更新 ViewModel 的数据状态
  • ViewModel 层的数据状态的改变会引起 View 的状态改变,View 做出改变,用户得到反馈

总结

  • MVC: Controller 作为 View 层和 Model 层之间的连接点,连接 View -> Model 之间的通信,Model 层的数据更新后会通知 View 层的视图更新并反馈给用户。View 和 Model 之间的强耦合度会加大调试时的难度。
  • MVP: Presenter 承接起了 View 和 Model 之间的双向通信,View 与 Model 不发生联系,降低了耦合度且方便单元测试。
  • MVVM: ViewModel 中构建了一组状态数据,作为 View 状态的抽象,通过双向数据绑定使 ViewModel 中的状态数据与 View 的显示状态保持一致,这样 View 的显示状态变化会自动更新 ViewModel 的状态数据,ViewModel 状态数据的变化也会自动同步 View 的显示状态。

参考

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.