linzx


  • 首页

  • 归档

  • 标签

  • 搜索

记一次对webpack打包后的代码的失败探究

发表于 2018-06-14 | 分类于 javascript | 阅读次数
| 字数统计 3,704 | 阅读时长 15

记得4月新出webpack4,上个月刚好没什么事情,用webpack4又重新去搭了一遍自己的项目。在搭项目的途中,忽然对webpack模块化之后的代码起了兴趣,于是想搞清楚我们引入的文件到底是怎么运行的。

1、基本版——单入口引入一个js文件

所谓的基本版,就是我只引入了一个test.js,代码只有一行var a = 1。打包之后,发现生成的文件main.js并没有多少代码,只有90行不到。

截取出真正执行的代码就更加少了,只有下面4行。我们接下去就从这几行代码中看下打包出来的文件的执行流程是怎么样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(function(modules) {
//新建一个对象,记录导入了哪些模块
var installedModules = {};
// The require function 核心执行方法
function __webpack_require__(moduleId){/*内容暂时省略*/}
// expose the modules object (__webpack_modules__) 记录传入的modules作为私有属性
__webpack_require__.m = modules;
// expose the module cache 缓存对象,记录了导入哪些模块
__webpack_require__.c = installedModules;
// Load entry module and return exports 默认将传入的数组第一个元素作为参数传入,这个s应该是start的意思了
return __webpack_require__(__webpack_require__.s = 0);
})([(function(module, exports, __webpack_require__) {
/* 0 */
var a = 1;
/***/ })
/******/ ])

首先很明显,整个文件是个自执行函数。传入了一个数组参数modules。

这个自执行函数内部一开始新建了一个对象installedModules,用来记录打包了哪些模块。

然后新建了函数__webpack_require__,可以说整个自执行函数最核心的就是__webpack_require__。__webpack_require__有许多私有属性,其中就有刚刚新建的installedModules。

最后自执行函数return了__webpack_require__,并传入了一个参数0。因为__webpack_require__的传参变量名称叫做moduleId,那么传参传进来的也就是模块id*。所以我大胆猜测这个0可能是某个模块的id。

这时候我瞄到下面有一行注释/* 0 */。可以发现webpack会在每一个模块导入的时候,会在打包模块的顶部写上一个id的注释。那么刚才那个0就能解释了,就是我们引入的那个模块,由于是第一个模块,所以它的id是0。

那么当传入了moduleId之后,__webpack_require__内部发生了什么?

webpack_require解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function __webpack_require__(moduleId) {
// Check if module is in cache
// 检查缓存对象中是否有这个id,判断是否首次引入
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache) 添加到.c缓存里面
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function 执行通过moduleId获取到的函数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
// 表示module对象里面的模块加载了
module.l = true;
// Return the exports of the module
return module.exports;
}

首先通过moduleId判断这个模块是否引入过。如果已经引入过的话,则直接返回。否则installedModules去记录下这次引入。这样子如果别的文件也要引入这个模块的话,避免去重复执行相同的代码。

然后通过modules[moduleId].call去执行了引入的JS文件。

看完这个函数之后,大家可以发现其实webpack打包之后的文件并没有什么很复杂的内容嘛。当然这很大一部分原因是因为我们的场景太简单了,那么接下来就增加一点复杂性。

2、升级版——单入口引入多个文件

接下来我修改一下webpack入口,单个入口同时下引入三个个文件

1
entry: [path.resolve(__dirname, '../src/test.js'),path.resolve(__dirname, '../src/test2.js'),path.resolve(__dirname, '../src/test3.js')],

三个文件的内容分别为var a = 1,var b = 2,var c = 3。接下来我们可以看看打包之后的代码

打包之后的文件main.js核心内容并没有发生变化,和上面一模一样。但是这个自执行函数传入的参数却发生了变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function(modules) {
/*这部分内容省略,和前面一模一样*/
})([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
__webpack_require__(1);
__webpack_require__(2);
module.exports = __webpack_require__(3);
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
var a = 1;
/***/ }),
/* 2 */
/***/ (function(module, exports, __webpack_require__) {
var b = 2;
/***/ })
/* 3 */
/***/ (function(module, exports, __webpack_require__) {
var c = 3;
/***/ })
/******/ ]);

前面说过,自执行函数默认将传入的参数数组的第一个元素传入__webpack_require__执行代码。

我们可以看一下传入第一个参数的内容,在上一章中是我们引入的文件内容var a = 1,但是这里却不是了。而是按模块引入顺序执行函数__webpack_require__(1),__webpack_require__(2),__webpack_require__(3),通过__webpack_require__函数去执行了我们引入的代码。

大家可以先想一下这里的1,2,3是怎么来的,为什么可以函数调用的时候,直接传参1,2,3

不过到这里还不明白,module.exports到底起了什么作用,如果起作用,为什么又只取最后一个呢?

3.升级版——多入口,多文件引入方式

因为好奇如果多入口多文件是怎么样的,接下去我又将入口改了一下,变成了下面这样

1
2
3
4
entry: {
index1: [path.resolve(__dirname, '../src/test1.js')],
index2: [path.resolve(__dirname, '../src/test2.js'),path.resolve(__dirname, '../src/test3.js')],
},

打包生成了index1.js和index2.js。发现index1.js和第一章讲的一样,index2.js和第二个文件一样。并没有什么让我很意外的东西。

4、进阶版——引入公共模块

在前面的打包文件中,我们发现每个模块id似乎是和引入顺序有关的。而在我们日常开发环境中,必然会引入各种公共文件,那么webpack会怎么处理这些id呢

于是我们在配置文件中新增了webpack.optimize.SplitChunksPlugin插件。

在webpack2和3版本中是webpack.optimize.CommonsChunkPlugin插件。但是在webpack4进行了一次优化改进,想要了解的可以看一下这篇文章webpack4:代码分割CommonChunkPlugin的寿终正寝。所以这里的代码将是使用webpack4打包出来的。

然后修改一下配置文件中的入口,我们开了两个入口,并且两个入口都引入了test3.js这个文件

1
2
3
4
entry: {
index1: [path.resolve(__dirname, '../src/test.js'),path.resolve(__dirname, '../src/test3.js')],
index2: [path.resolve(__dirname, '../src/test2.js'),path.resolve(__dirname, '../src/test3.js')],
},

可以看到,打包后生成了3个文件。

1
2
3
<script type="text/javascript" src="scripts/bundle.4474bdd2169853ce33a7.js"></script>
<script type="text/javascript" src="scripts/index1.4474bdd2169853ce33a7.js"></script>
<script type="text/javascript" src="scripts/index2.4474bdd2169853ce33a7.js"></script>

首先bundle.js(文件名自己定义的)很明显是一个公共文件,里面应该有我们提取test3.js出来的内容。打开文件后,发现里面的代码并不多,只有下面几行。

1
2
3
4
5
6
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[2],{
/***/ 2:
/***/ (function(module, exports, __webpack_require__) {
var c = 1;
/***/ })
}]);

单纯看文件内容,我们大概能推测出几点:

  • window全局环境下有一个名为webpackJsonp的数组
  • 数组的第一个元素仍然是数组,记录了数字2,应该是这个模块的id
  • 数组第二个元素是一个记录了形式为{模块id:模块内容}的对象。
  • 对象中的模块内容就是我们test3.js,被一个匿名函数包裹

在webpack2中,采用的是{文件路径:模块内容}的对象形式。不过在升级到webpack3中优化采用了数字形式,为了方便提取公共模块。

注意到一点,这个文件中的2并不像之前一样作为注释的形式存在了,而是作为属性名。但是它为什么直接就将这个模块id命名为2呢,目前来看,应该是这个模块是第二个引入的。带着这个想法,我接下去看了打包出来的index1.js文件

截取出了真正执行并且有用的代码出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// index1.js
(function(modules) { // webpackBootstrap
// install a JSONP callback for chunk loading
function webpackJsonpCallback(){
/*暂时省略内容*/
return checkDeferredModules
}
function checkDeferredModules(){/*暂时省略内容*/}
// The module cache
var installedModules = {};
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
0: 0
};
var deferredModules = []; //
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++){
webpackJsonpCallback(jsonpArray[i]);
}
var parentJsonpFunction = oldJsonpFunction;
// add entry module to deferred list
deferredModules.push([0,2]);
// run deferred modules when ready
return checkDeferredModules();
})([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
__webpack_require__(1);
module.exports = __webpack_require__(2);
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
var a = 1;
/***/ })
/******/ ]);

在引入webpack.optimize.SplitChunksPlugin之后,核心代码在原来基础上新增了两个函数webpackJsonpCallback和checkDeferredModules。然后在原来的installedModules基础上,多了一个installedModules,用来记录了模块的运行状态;一个deferredModules,暂时不知道干嘛,看名字像是存储待执行的模块,等到后面用到时再看。

此外,还有这个自执行函数最后一行代码调用形式不再像之前一样。之前是通过调用__webpack_require__(0),现在则变成了checkDeferredModules。那么我们便顺着它现在的调用顺序再去分析一下现在的代码。

在分析了不同之后,接下来就按照运行顺序来查看代码,首先能看到一个熟悉的变量名字webpackJsonp。没错,就是刚才bundle.js中暴露到全局的那个数组。由于在html中先引入了bundle.js文件,所以我们可以直接从全局变量中获取到这个数组。

前面已经简单分析过window["webpackJsonp"]了,就不细究了。接下来这个数组进行了一次for循环,将数组中的每一个元素传参给了方法webpackJsonpCallback。而在这里的演示中,传入就是我们bundle.js中一个包含模块信息的数组[[2],{2:fn}}]。

接下来就看webpackJsonpCallback如何处理传进来的参数了

webpackJsonpCallback简析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/******/ function webpackJsonpCallback(data) {
/******/ var chunkIds = data[0]; // 模块id
/******/ var moreModules = data[1]; // 提取出来的公共模块,也就是文件内容
/******/ var executeModules = data[2]; // 需要执行的模块,但演示中没有
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [];
/******/
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(installedChunks[chunkId]) {
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(parentJsonpFunction) parentJsonpFunction(data);
/******/
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/
/******/ // add entry modules from loaded chunk to deferred list
/******/ deferredModules.push.apply(deferredModules, executeModules || []);
/******/
/******/ // run deferred modules when all chunks ready
/******/ return checkDeferredModules();
/******/ };

这个函数中主要干了两件事情,分别是在那两个for循环中。

一是在installedChunks对象记录引入的公共模块id,并且将这个模块标为已经导入的状态0。

1
installedChunks[chunkId] = 0;

然后在另一个for循环中,设置传参数组modules的数据。我们公共模块的id是2,那么便设置modules数组中索引为2的位置为引入的公共模块函数。

1
2
modules[moduleId] = moreModules[moduleId];
//这段代码在我们的例子中等同于 modules[2] = (function(){/*test3.js公共模块中的代码*/})

其实当看到这段代码时,心里就有个疑问了。因为index1.js中设置modulesp[2]这个操作并不是一个push操作,如果说数组索引为2的位置已经有内容了呢?暂时保留着心中的疑问,继续走下去。心中隐隐感觉到这个打包后的代码其实并不是一个独立的产物了。

我们知道modules是传进来的一个数组参数,在第二个章节中可以看到,我们会在最后执行函数__webpack_require__(0),然后依顺序去执行所有引入模块。

不过这次却和以前不一样了,可以看到webpackJsonpCallback最后返回的代码是checkDeferredModules。前面也说了整个自执行函数最后返回的函数也是checkDeferredModules,可以说它替代了__webpack_require__(0)。接下去就去看看checkDeferredModules发生了什么

checkDeferredModules简析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/******/ function checkDeferredModules() {
/******/ var result;
/******/ for(var i = 0; i < deferredModules.length; i++) {
/******/ var deferredModule = deferredModules[i];
/******/ var fulfilled = true;
/******/ for(var j = 1; j < deferredModule.length; j++) {
/******/ var depId = deferredModule[j];
/******/ if(installedChunks[depId] !== 0) fulfilled = false;
/******/ }
/******/ if(fulfilled) {
/******/ deferredModules.splice(i--, 1);
/******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
/******/ }
/******/ }
/******/ return result;
/******/ }

这个函数关键点似乎是在deferredModules,但是我们刚才webpackJsonpCallback唯一涉及到这个的只有这么一句,并且executeModules其实是没有内容的,所以可以说是空数组。

1
deferredModules.push.apply(deferredModules, executeModules || []);

既然没有内容,那么webpackJsonpCallback就只能结束函数了。回到主线程,发现下面马上是两句代码,得,又绕回来了。

1
2
3
4
// add entry module to deferred list
deferredModules.push([0,2]);
// run deferred modules when ready
return checkDeferredModules();

不过现在就有deferredModules这个数组终于有内容了,一次for循环下来,最后去执行我们模块的代码仍然是这一句

1
result = __webpack_require__(__webpack_require__.s = deferredModule[0]);

很熟悉,有木有,最后还是回到了__webpack_require__,然后就是熟悉的流程了

1
2
__webpack_require__(1);
module.exports = __webpack_require__(2);

但是当我看到这个内容竟然有这行代码时__webpack_require__(2);还是有点崩溃的。为什么?因为它代码明确直接执行了__webpack_require__(2)。但是2这个模块id是通过在全局属性webpackJsonp获得的,代码不应该明确知道的啊。

我原来以为的运行过程是,每个js文件通过全局变量webpackJsonp获得到公共模块id,然后push到自执行函数传参数组modules。那么等到真正执行的时候,会按照for循环依次执行数组内的每个函数。它不会知道有1,2这种明确的id的。

为什么我会这么想呢?因为我一开始认为每个js文件都是独立的,想交互只能通过全局变量来。既然是独立的,我自然不知道公共模块id是2事实上,webpackJsonp的确是验证了我的想法。

可惜结果跟我想象的完全不一样,在index1.js直接指定执行哪些模块。这只能说明一个事情,其实webpack内部已经将所有的代码顺序都确定好了,而不是在js文件中通过代码来确定的。事实上,当我去查看index2.js文件时,更加确定了我的想法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/******/ (function(modules) {/*内容和index1.js一样*/})
/************************************************************************/
/******/ ([
/* 0 */,
/* 1 */,
/* 2 */,
/* 3 */
/***/ (function(module, exports, __webpack_require__) {
__webpack_require__(4);
module.exports = __webpack_require__(2);
/***/ }),
/* 4 */
/***/ (function(module, exports, __webpack_require__) {
var b = 2;
/***/ })
/******/ ]);
//# sourceMappingURL=index2.19eeab4e90ee99ee1ce4.js.map

仔细查看自执行函数的传参数组,发现它的第0,1,2位都是undefined。我们知道这几个数字其实就是每个模块本身的Id。而这几个id恰恰就是index1.js和bundle.js中的模块。理论上来说在浏览器下运行,index2.js应该无法得知的,但是事实却完全相反。

走到这一步,我对webpack打包后的代码也没有特别大的欲望了,webpack内部实现才是更重要的了。好了,不说了,我先去看了,等我搞明白了,再回来写续集。

未命名

发表于 2018-06-13 | 阅读次数
| 字数统计 330 | 阅读时长 1

不得不吐槽一句,vue的源码竟然是用ts语言写的。可怜我等小前端看的迷迷糊糊的,要不是有一点python的基础,很多东西真的是完全看不懂哎。最后才想起可以把ts编译成js文件

vue的compiler模块

直接找到compiler模块下的入口index.js文件,根据它的代码,我们能猜出几个有用的信息(猜的,一步步来,错了再回来改)

1
2
3
4
5
6
7
8
9
10
exports.createCompiler = create_compiler_1.createCompilerCreator(function baseCompile(template, options) {
var ast = index_1.parse(template.trim(), options);
optimizer_1.optimize(ast, options);
var code = index_2.generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
};
});

  1. 通过parse方法,可以得到一颗AST树
  2. 猜测render方法,应该是个渲染函数,将我们得到的AST树渲染成html
  3. staticRenderFns好像是一个数据渲染函数集合(因为最后有个s)

前端传统的AST,即抽象语法树,是把我们的js代码进行编译给浏览器理解。现在比如babel的编译,jsx语法的编译其实模仿的都是这种思想。

vue的shared模块

这个模块没有什么内容,一个utils文件主要给其他模块提供各种各样的配置函数

vue的core模块

observer

数据监听模块,我们的双向绑定就是其中之一

###

eventEmitter的实现

发表于 2018-04-26 | 分类于 javascript | 阅读次数
| 字数统计 3,626 | 阅读时长 14

半个月前看到一篇文章将eventEmitter,看完之后心血来潮自己写了一个。晚上睡觉前忽然想到还可以尝试实现vue中emitter。于是,故事就这么开始了。

1、实现一个eventEmiiter

1.1、整体架构

我们先看一张图,看下EventEmitter需要实现哪些功能
image

可以看到一个EventEmitter类的内容其实并不杂乱。根据这张图来看,我们大致可以分为以下几个模块。

  1. EventEmitter初始属性

    • _events //存储所有的监听器
    • _maxListeners setMaxListeners getMaxListeners
  2. addEventListener模块

    • addListener
    • prependListener
    • once
    • prependOnceListener
    • on
  3. emit模块

    • emitNone
    • emitOne emitTwo emitThree
    • emitMany
    • error 事件
  4. removeEventListener模块

    • removeListener
    • removeAllListeners
  5. listeners,eventNames

    • listeners //获取一个监听器下的所有事件
    • eventNames //获取有哪些监听器
  6. 工具函数和兼容性函数

    • spliceOne
    • arrayClone
    • objectCreatePolyfill
    • objectKeysPolyfill
    • functionBindPolyfill

基本上按照上面这个顺序,就可以写出来一个基本的的eventEmitter的类了。

推荐大家可以先自己尝试写一写,这样子等下看成熟库的源码可以得到的收获更多。

然后去网上找了一个成熟库的源码进行对比,果然发现了一些问题需要改善😳。

点这里。写完之后,可以看下看EventEmitter类源码怎么实现的。

  • 自己为了节省代码行数,单个事件和多个事件都用了Array去存储。其实作为库,节约的十几行代码和性能比起来,还是后者更重要
  • 没有考虑emit几个参数的情况,不同情况的处理有助于提高性能
  • 没有考虑限制一个类可以绑定的最大事件数。因为如果数目一多话,容易造成内存泄露.
  • 函数缺少对参数的判断。缺少防御性代码

1.2、简单分析一下部分代码

具体代码就不分析了😂😂,稍微对主线讲解一下吧。因为源码并不复杂,沉下心花个半小时肯定能全部看懂。

作者一开始新建一个_events对象,这个对象会在后期存取我们监听器。然后设定了一个监听器允许的最大事件,避免内存泄露的可能性。

1
2
3
4
5
6
7
8
function EventEmitter() {
if (!this._events || !Object.prototype.hasOwnProperty.call(this, '_events')) {
this._events = objectCreate(null);
this._eventsCount = 0;
}
this._maxListeners = this._maxListeners || undefined;
}

然后添加事件,在真实场景中,我们会在html中获取需要添加哪些监听器type,和对应的方法listener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function _addListener(target, type, listener, prepend) {
var m;
var events;
var existing;
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
events = target._events;
if (!events) {
events = target._events = objectCreate(null);
target._eventsCount = 0;
} else {
if (events.newListener) {
target.emit('newListener', type,
listener.listener ? listener.listener : listener);
events = target._events;
}
existing = events[type];
}
if (!existing) {
existing = events[type] = listener;
++target._eventsCount;
} else {
if (typeof existing === 'function') {
existing = events[type] =
prepend ? [listener, existing] : [existing, listener];
} else {
if (prepend) {
existing.unshift(listener);
} else {
existing.push(listener);
}
}
}
return target;
}

当我们事件添加完毕之后,则是通过emit进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
EventEmitter.prototype.emit = function emit(type) {
var er, handler, len, args, i, events;
events = this._events;
if (events)
doError = (doError && events.error == null);
else if (!doError)
return false;
handler = events[type];
if (!handler)
return false;
if (isFn) handler.call(self);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self);
}
}
return true;
};

主线代码就这样,更多细节我是真的推荐大家全看源码的。因为这400多行的代码真的不复杂。反倒是源码中间还是有很多细节可以值得细细品味的。

1.2.1、为什么使用Object.create(null)

我们可以看到网上许多库(比如vue)都是使用Object.create(null)来创建对象,而不是使用{}来新建对象。这是为什么呢🤔?

Object.create()这个API我就不介绍了,不清楚的推荐上MDN先了解一下

我们可以先在chrome的控制台上打印Object.create({})创建的对象是什么样子的:
image

可以看到新创建出来的对象继承了Object原型链上所有的方法。

我们可以再看一下使用Object.create(null)创建出来的对象:
image

没有任何属性,显示No properties。

区别很明显,我们获得了一个非常纯净的对象。那么问题来了,这样对象有什么好处呢🤔?

首先我们需要知道无论是var a = {}还是Object.create({}),他们返回的对象都是继承自Object的原型,而原型是可以修改的。但是假如有别的库或者开发者在页面上修改了Object的原型,那么你也会继承下来修改后的原型方法,这个可能就不是我们想要的了。

随手在一个csdn的网页控制台写个例子,没想到就出现这个问题
image

而如果我们自己在每个库开头新建一个干净的对象,我们可以自己改写这个对象的原型方法进行复用和继承,既不会影响他人,也不会被他人影响。

1.2.2、比原生splice效率还高的函数

在源码中看到了这么一段代码,作者亲自打了注释说1.5倍速度快于原生。

1
2
3
4
5
6
// About 1.5x faster than the two-arg version of Array#splice().
function spliceOne(list, index) {
for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
list[i] = list[k];
list.pop();
}

splice的效率慢我是知道,但是作者说1.5倍快我就要亲自试验下了。

看他方法,缺陷应该是数组长度越长,所需时间越长;下标越靠近开始,所需时间越长。于是我用了不同长度的数组,不同下标去进行反复测试100次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 测试的代码是这么写的,如果不合理请指教
var arr = [];for(let i = 0;i < 50000;i++){arr.push(i)}
console.time();
spliceOne(arr,1)
// arr.splice(1,1)
// arr.splice(49995,1)
// spliceOne(arr,49995)
console.timeEnd()
//在数据长度是5的情况下,下标为1,splice效率快33%
//在数据长度是500的情况下,下标为1,splice效率快75%
//在数据长度是50000的情况下,下标为1,splice效率快95%
//在数据长度是5的情况下,下标为4,spliceOne效率快20%
//在数据长度是500的情况下,下标为45,spliceOne效率快50%
//在数据长度是50000的情况下,下标为49995,spliceOne效率快50%

因为源码是针对node.js的,不知道是不是浏览器内部对splice做过优化。作者的方法在特定情况下的确是做到了更快,还是很厉害的。👍👍👍🤕

1.2.3、多个emit方法

源码作者专门为emit不同数量参数写了不同的方法,有emitNone,emitOne,emitTwp,emitThree,emitMany。

如果按照我来写,最多也就分成emitNone和emitMany两个方法。但是作者应该是为了更高的效率,尽可能减少for循环这种代码。这也是我这种不怎么写库的人迟钝的地方。节约的十几行代码在压缩之后,重要性是低于性能上的损耗的。

2、简单实现vue中的EventEmitter

在写完EventEmitter之后,仍然感觉特别单调。然后睡觉的时候忽然在想,是不是可以正好将自己写好这个类套进到vue里面呢?有了实际场景,就知道自己写的东西到底能干什么,有什么问题。不然空有理论也是没有任何进步的。

之前网上也有很多文章解析了vue如何实现双向绑定。事实上在编译html的过程中实现了的不仅仅是数据双向绑定,添加事件监听器也是这一过程做的。只是网上关于事件监听的文章却几乎没有。

2.1、自己尝试实现一个vue中的EventEmitter

按照我一开始的想法,应该是先编译HTML获取所有的属性,判断出哪些属性是绑定事件,哪些是数据绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div id="app" :data="data" @click="test" @change="test2">test内容</div>
</>
<script>
var vue = {
methods: {
test(){alert(123)},
test2(){console.log(456)}
}
}
var onRE = /^@|^v-on:/;
function compile(node) {
var reg = /\{\{(.*)\}\}/;
//节点类型为元素
if(node.nodeType === 1){
var attr = node.attributes;
for(var i = 0;i < attr.length; i ++){
console.log(attr[i])
}
}
}
compile(window.app)
</script>

于是自己先写出了第一段代码,希望依靠原生node的方法attributes去获取DOM元素上所有的属性

但是等到获取之后,才发现获取到的每个属性attr[i]竟然是一个神奇的对象类型[object Attr],表现形式是@click=test。虽然表现很像是字符串,但是个NamedNodeMap。靠根本不知道怎么用嘛😂😂😂

去网上找了资料之后,才知道他是怎么获取key和value的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var onRE = /^@/;
function compile(node) {
var reg = /\{\{(.*)\}\}/;
//节点类型为元素
if(node.nodeType === 1){
var attr = node.attributes;
for(var i = 0;i < attr.length; i ++){
if(onRE.test(attr[i].nodeName)){
var value = attr[i].nodeValue;
}
}
}
}
compile(window.app)

只是文章里面说DOM4规定中已经不推荐使用这个属性了😢ㄟ( ▔, ▔ )ㄏ。想了想放弃了,还是乖乖去看了一下vue的源码是怎么实现的吧。

2.2、vue源码实现一个EventEmitter

因为想着vue肯定也是先编译HTML,所以直接找到了源码中的html-parse模块。

vue先定义了一个parseHTML的方法,传进来需要编译的html模板,也就是我们的template。然后通过一个属性正则表达式一步步去match出模板字符串内的所有属性,最后返回了一个包含所有属性的数组attrs。

然后vue会对得到的数组attrs进行遍历判断,这个属性是v-for?还是change?还是src等等。当获取到的属性为@click或者v-on:click这种事件之后,然后通过方法addHandler去添加事件监听器。我们也就可以在开发中使用emit了。

当然vue中间还会有很多操作。比如会接着将这个属性数组以及tag传入到一个createASTElement函数里面进行生成一棵AST树渲染成真实的dom等等。只不过这并不是我们本篇文章需要讨论的内容了

我们接下去就按照vue的流程来实现绑定事件。首先我们定义好我们的html内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div id="app" :data="data" @click="test" @change="test2">test内容</div>
</>
<script>
var vue = {
data(){
return {data:1}
}
methods: {
test(){alert(123)},
test2(){console.log(456)}
}
}
</script>

在我们就要开始进行编译之前,我们准备好所有需要用到的正则,新建好一个eventEmitter类

1
2
3
4
5
6
7
8
9
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var ncname = '[a-zA-Z_][\\w\\-\\.]*';
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
var startTagClose = /^\s*(\/?)>/;
var onRE = /^@|^v-on:/;
var eventEmitter = new EventEmitter()
const app = document.getElementById("app")

然后开始写我们的编译函数。前面已经说了,我们传进模板,然后依据正则一步步match出所有的属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function compiler(html){
html = trim(html) //因为模板换行有空格,所以需要先去除开头的空格
let index = html.match(startTagOpen)[0].length
html = html.substring(index)
const match = {
attrs: [],
attrList: []
}
let end, attr;
//编译完整个html标签
//如果多层dom,vue有循环,但是测试就不搞那么复杂了
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
match.attrs.push(attr)
index = attr.length
html = html.substring(attr[0].length)
}
return match
}

解释一下编译过程吧。先根据开头标签的正则,找到需要编译的html。然后截取出除开始标签<div的剩余字符串。

接下来继续对字符串判断。依靠属性正则表达式,判断这段html标签内有没有属性,如果有的话,从字符串中截取(类似于splice效果)出来。

继续不断循环字符串,直到遇到闭合标签/div>为止。然后结束编译,返回数组。

编译完成后我们已经获取到了模板里面所有的属性,但是现在存储起来的属性表现形式是一个match出来的数组,并不是一个方便开发者使用的map形式。所以接下来我们要处理一下我们获得的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function processAttrs(match){
let l = match.attrs.length
for (var i = 0; i < l; i++) {
var args = match.attrs[i];
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
if (args[0].indexOf('""') === -1) {
if (args[3] === '') { delete args[3]; }
if (args[4] === '') { delete args[4]; }
if (args[5] === '') { delete args[5]; }
}
var value = args[3] || args[4] || args[5] || '';
match.attrList[i] = {
name: args[1],
value: value
};
}
return match
}

通过这一步,我们已经获取到了一个attrList,并且存储起来了一个个表现为map形式的属性。然后我们要遍历这些属性,判断哪些是需要绑定的方法,哪些使我们不需要的属性。如果是需要绑定的方法,我们通过addHandler函数来添加事件监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function processHandler(match){
let attrList = match.attrList, l = attrList.length
let name, value
for(let i = 0; i < l; i ++){
name = attrList[i].name
value = attrList[i].value
if(onRE.test(name)){
name = name.replace(onRE, '');
addHandle(vue, name, value, false);
}
}
}
function addHandle(target, name, value, prepend){
let handler = target.methods[value]
eventEmitter.addListener(name, handler, prepend)
eventEmitter.emit("click")
}

走到这里整个流程就已经结束了。接下去每次进入页面去进行初始化编译就好了。

1
2
3
4
5
function parseHTML(html){
const match = compiler(html)
processAttrs(match)
processHandler(match)
}

如果想尝试触发我们之前绑定的事件,在vue中是子组件向父组件触发。这里就不搞父子组件这么麻烦了。我们可以直接在JS里面调用emit来进行验证

1
eventEmitter.emit("click")

game over 😊

文章结束了,日常总结一下吧。实现整个eventEmitter的代码其实并不复杂,尤其在源码非常简洁的情况下,基本上认真看个十几分钟就能明白整个轮廓。然后我没有仔细看vue中的实现是怎么样的,不过我猜测应该相似度很高。

后面看vue提取属性还是花了更多的时间,原来还以为可以自己通过attribute属性来实现的。没想到最后还是参考了vue,再看的途中,也明白了vue编译html的整个过程,以及每个过程实现了哪些内容。

其实看源码可以学到的东西都很多,最直接的就是知道怎么实现一个功能。此外呢?其实此外是是更多的。比如编码习惯,比如防御性代码怎么写,比如结尾处理代码怎么写,比这不就看到有方法比原生API的效率还快。这些都是看源码的乐趣所在。

看完之后,以后妈妈再也不用担心面试考eventEmitter了

实现一个自定义滚动条

发表于 2018-02-24 | 分类于 javascript | 阅读次数
| 字数统计 2,931 | 阅读时长 11

因为最近在写项目的UI库,遇到自定义滚动条这一个槛还是卡了我挺久的,主要卡在了如何自动监听内容变化并更新滚动条高度。市面上基本所有的滚动条插件都没有实现这一点,最后面扒了element的源码才最终解决。本文主要讲的也是这个。

首先,我们先把需要实现的功能先确定下来。

  • 鼠标左键点击可以拖动
  • 鼠标滑轮滚动
  • 内容发生变化,自动更新滚动条长度
  • 提供开发者一个滚动回调的接口

前面两点依靠原生滚动条其实比较简单,但是在第三点上实在是卡了我好久,想了好久都没有想出来。最后还是看了element源码才实现成功。

接下去我会以垂直滚动条为例(水平滚动条基本同理),实现一个自定义的滚动条出来。我争取把其中原理细节讲清楚。

1、搭建好基本的样式框架

开始我们先把HTML和样式写好

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="scrollbar">
<div class="scrollbar-content">
<ul class="box">
<li>11111</li><li>11111</li><li>11111</li><li>11111</li>
<li>11111</li><li>11111</li><li>11111</li><li>11111</li>
<li>11111</li><li>11111</li><li>11111</li><li>11111</li>
<li>11111</li><li>11111</li><li>11111</li><li>11111</li>
</ul>
</div>
<div class='scrollbar-bar'>
<div ref="thumb" class="scrollbar-thumb"></div>
</div>
</div>

第一步的HTML和CSS

滚动条的框架如上面所示,接下午我会以简称wrap,bar,thumb进行简称

  • wrap :内容区域包裹框
  • bar : 包裹区域中自定义滚动条的滚动框
  • thumb :自定义滚动条

开始之前要大家可以先记住一点,我们并不是不用原生滚动条,实际上我们所有的操作都需要依靠原生滚动条才能实现。只不过它隐藏在了暗处,而让UI更好看的自定义滚动条出现在明处。

1.1计算出滚动条的宽度。

第一步我们先将原生的滚动条隐藏掉。但是这里涉及到第一个问题,那就是不同浏览器的下滚动条宽度是不一样的。我们需要准确的知道,如果wrap产生了滚动条,那它的宽度是多少。

先写一个获取到区域内滚动条的宽度(scrollWidth)的回调函数getScrollWidth,获取到滚动条高度之后,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function getScrollWidth(){
const outer = document.createElement("div");
outer.className = "el-scrollbar__wrap";
outer.style.width = '100px';
outer.style.visibility = "hidden";
outer.style.position = "absolute";
outer.style.top = "-9999px";
document.body.appendChild(outer);
const widthNoScroll = outer.offsetWidth;
outer.style.overflow = "scroll";
const inner = document.createElement("div");
inner.style.width = "100%";
outer.appendChild(inner);
const widthWithScroll = inner.offsetWidth;
outer.parentNode.removeChild(outer);
scrollBarWidth = widthNoScroll - widthWithScroll;
return scrollBarWidth;
}

获取到滚动条的宽度scrollBarWidth之后,通过再来设置wrap的css样式,通过marginRight将滚动条移动到视线之外

1
2
wrap.style.overflow = scroll;
wrap.style.marginRight = -scrollWidth + "px";

1.2计算出滚动条的高度。

第二步我们需要计算出滚动条的高度。计算方法也很简单,元素高度scrollHieght/内容高度clientHeight,得出来的就是滚动条所占的百分比。

因为内容高度经常变更,我们可以写一个更新滚动条高度的回调函数updateThumb,方便后期随时调用。

1
2
3
4
function updateThumb(){
let heightPercentage = (wrap.clientHeight * 100 / wrap.scrollHeight);
thumb.style.height = heightPercentage + "%";
}

到了这一步,基本上一个滚动条的基本样式已经出来了。接下去我们要实现它的使用功能。

查看第一步的成果

2、添加滚动条滑动功能

到这里我们已经可以看到成型的滚动条的UI界面了,但是仍然缺少滚动和拖动的功能。关键点是在于如何去监听滚动条的变化。

2.1滚轮滑动

还记得文章开头说过,我们所有功能的实现都依赖隐藏起来的原生滚动条。如果大家理解了我上面说的话,那么问题就简单了。当我们开始滑动滚轮的时候,隐藏在暗处的原生滚动条也会同时滚动,此时便会触发原生滚动条的scroll事件。

这里可以再详细说明下。只要元素的scrollTop发生变化,就必然会触发scroll事件。所以我们操作滚轮,其实本质上是改变元素的scrollTop。

所以我们只需要写一个相应的回调函数handleScroll,在每次触发回调的时候,实时修改我们自定义滚动条的样式就行了。

1
2
3
4
5
6
function handleScroll(){
this.moveY = (wrap.scrollTop *100 / wrap.clientHeight);
//通过计算出来的百分比,然后对滚动条执行translate移动
thumb.style.transform = "translateY" + moveY;
},
wrap.addEventListener('scroll',handleScroll);

查看滚轮滑动效果

2.2点击滚动框,滚动条及内容移动到相应位置

接下去我们实现第二个功能。当我们点击滚动框的一个位置时,滚动条也会跳到这个位置,同时内容位置也会发生改变。

第一步先获得点击的y坐标,然后计算出和滚动框bar顶部的距离,通过这段距离占滚动框bar的百分比去获取scrollTop值。

1
2
3
4
5
6
7
8
9
10
11
12
function clickTrackHandle(e){
//获得点击位置与滚动框顶部之间的距离
const offset = Math.abs(e.target.getBoundingClientRect().top - e.clientY)
//让点击位置处于滚动条的中间
const thumbHalf = thumb.offsetHeight / 2;
//计算出滚动条在滚动框的百分比位置
const thumbPositionPercentage = (offset - thumbHalf) * 100 / wrap.offsetHeight;
//通过改变scrollTop来操作。所有操作滚动条的最后一步都是通过handleScroll来实现
wrap.scrollTop = (thumbPositionPercentage * wrap.scrollHeight / 100);
}
bar.addEventListener("click",clickTrackHandle);

只要scrollTop值发生变化就会触发我们上一步写的回调。

查看点击滚动框的效果

2.3拖动滚动条,移动内容

接下来我们再去实现手动拖拽滚动条去实现移动内容,这个知识点就是拖拽的知识点,不过在看源码的时候发现element的习惯很好,他是在当你点击滚动条的时候绑定拖拽,然后松开的时候取消绑定。

1
2
3
4
5
6
7
8
function mouseMoveDocumentHandler(){}; //实时记录滚动条位置的拖拽函数
//当点击滚动条时
document.addEventListener("mousedown",mouseMoveDocumentHandler);
document.onselectstart = false; //同时阻止选中
//当松开滚动条时
document.removeEventListener("mousedown",mouseMoveDocumentHandler);
document.onselectstart = null; //同时阻止选中

因为这一块代码比较多,就不贴文章里,大家可以直接链接里看就是了。
基本上到了这一步,已经实现了市面上自定义滚动条的效果了。下面就是解决卡了我的好久的实时更新问题了。

查看自定义滚动条的效果

3、实现滚动条随内容实时更新

第二章讲的主要都是实现滚动条功能,这一章讲的是纠结😖我很久的功能。

因为滚动条的高度并不是我们一开始能够确定的,它需要在dom内容渲染出来之后才能确定。而且有时候随着内容的变化,还需要实时改变滚动条的高度。再看了市面上的滚动条之后,发现基本都没有满足这一功能。

事实上缺少了这一点,使用起来是缺少视觉交互的。举个例子,加入一个原来有滚动条的元素因为内容减少导致了滚动条小时,但是自定义滚动条因为没有检测到变化仍然存在,那就会给用户造成困扰。

我不希望每次更新内容都要通过加一步回调函数来更新一下滚动条,而是希望它自己实时更新。在网上没有找到答案之后,最终去翻了element源码,研究了好久,总算找到了想要的答案。

关键点就在于我能前面之前说的那一句话——如果我们改变元素的scrollTop,是会触发scroll事件。

大家想象一个情景,如果滚动条永远出现在最底部,比如下图
image

那么只要我内容发生了一点变化,滚动条必然会变长或者变短。那么在滚动条长度变化时,scrollTop自然发生了改变(滚动条消失则scrollTop变为0),那么就会触发scroll的回调函数,那么我们就自动监测到了啊😊。

在明白了这一点后,却又冒出来一个问题。正常情况下,滚动条不可能出现在最底部啊,那怎么办呢?

element选择了自己造一个置于底部的滚动条来满足自己需求。我也学习者element的方法进行实现。

最终实现查看效果点这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
const ul = document.getElementById("ul");
const resizeTrigger = document.createElement("div");
resizeTrigger.className = "resize-triggers";
resizeTrigger.innerHTML = '<div class="expand-trigger"><div><div></div></div></div>';
ul.appendChild(resizeTrigger);
const resetTrigger = function (element) {
const trigger = element.__resizeTrigger__;
const expand = trigger.firstElementChild;
const expandChild = expand.firstElementChild;
expandChild.style.height = expand.offsetHeight + 1 + 'px';
expand.scrollTop = expand.scrollHeight;
};
ul.addEventListener("scroll",function(){
resetTrigger(this);
},true)
</script>

ul是我们包裹内容的DOM元素。

配合着css来看,第一段JS我们创建出了resizeTrigger这个div,并且我们将他的height:100%。这样子如果内容发生变化,resizeTrigger永远和父元素ul同时改变高度。这里设置成高度100%非常重要,这样子才能主动同步到内容的变化。

注意到resizeTrigger里面还有有一个父子元素expand和expandChild。在第二段JS的resetTrigger函数中。然后设置expandChild的高度超过父元素expand的高度,促使expand产生滚动条。然后我们再将滚动条的scrollTop设置为最大,这样子滚动条就会出现在滚动区域resizeTrigger的最底部了。

现在我们做到了将滚动条设置在了最底部,所以只要内容发生了变化,那么滚动条的scrollTop必然也会发生变化。

最后一段代码就是scroll的监听。当监听到scrollTop值发生变化时,触发相应的回调函数。

所以这块代码最后的逻辑其实是这样的。内容改变 –> ul高度改变–> resizeTrigger高度改变 –> expand滚动条的scrollTop发生变化 –> 触发scroll的回调函数,在函数里面调整再次调整滚动条的高度,保证滚动条高度正确。

通过这三段代码,我们也基本实现了自动监听内容变化来更新滚动条。

简单画了个配图来帮助理解逻辑
image

image

通过两个小蓝框产生的滚动条来帮助监听内容变化

4、实现组件化,方便开发者使用

经过以上3大步基本上是可以实现一个自定义的滚动条的。上面的代码是面向原生js的。在我们的项目里面,实现第4点是通过封装成一个scrollbar的的组件,在项目里面进行使用。

这一条要求因为不同框架实现方式都不一样,所以就不详细贴代码了,不过最终原理肯定还是一样的。因为自己项目用的是一个Vue框架,所以是个Vue组件,有需求可以自己去看。

没有写过写Vue组件的可以看看这一篇,少踩些坑

查看scrollbar组件


好了文章就到此结束了,在看人家源码的过程中也学到了许多。比如使用JSX来编写组件;scroll监听其实就是判断scrollTop;比如通过自己造滚动条的方法监听scrollTop来实现自动更新。最后通过写文章,对一些新的知识点理解还是加深了许多。

《浪潮之巅》读后感

发表于 2018-01-29 | 分类于 《浪潮之巅》 | 阅读次数
| 字数统计 3,017 | 阅读时长 10

先简单的写一下每一章读完后的简单读后感,然后简单的总结一下吧

第1章 帝国的余晖—AT T公司

1.一个公司的长远发展不应该为眼前的利益所耽误。在华尔街的影响之下,不得不接连为满足上市公司的利益不断拆分,合并,最后消亡。

2.没有跟上时代的趋势,在传统电话与移动电话交接的关键期,拥有充足资金和先进技术,但是却因为业务的拆分直接被时代快速拉下来了。

3.一个想要长久发展的公司必然需要一个强势为公司长远考虑的领导人。

第2章 蓝色巨人—IBM公司

1.IBM作为一个走过百年的公司,拥有许多传统并且占有垄断优势的业务,才可以不断的去尝试新的方向。

2.新兴创业公司如果贸然学习传统公司多方向出击,反而会容易失去原有的优势,得不偿失。所以第三点↓。

3.公司需要不断深耕自己的核心领域,加强优势。

第3 章 “水果”公司的复兴—苹果公司

1.不断积累自己的优势,准确的把握未来的趋势,明确自己的核心竞争力,不断创新出用户所需要的,并且将其做到极致

2.死而复生的苹果公司和乔布斯

第4 章 计算机工业的生态链

1.摩尔定律。必须不断的加快自己的研发进度,以保证公司不会因为没有及时跟上下一代产品导致市场和利润率下降。

2.安迪-比尔定律。硬件的不断更新会推动软件的不断创新。为了更多的软件创新,反过来会逼迫硬件不断发展来满足使用软件的需求。

3.反摩尔定律。逼迫公司不断加大研发来保证自家产品可以跟上市场的速度。

第5 章 奔腾的芯— 英特尔公司

1.明确所有计算机公司的共同需求之后,选择其中一块领域进行深耕,不断加强自己的垄断优势。(当然需要明确自己的方向是符合时代趋势的,不然也是扯淡。

2.在自己的发展过程中,也许小方向有问题,不过如果能把快速所有对手都干掉的话,也足够自己来纠正方向(指令集之争)。忽然想到了现在的烧钱抢市场。

第6 章 IT 领域的罗马帝国— 微软公司

1.成长期的腾讯和成长期的微软非常相似,希望能够占领所有的市场,看见什么就做什么,并且依靠操作系统的地利优势,往往能够快读打败对手。

2.早期依托地利优势打败网景的浏览器,成功握住了互联网2.0的入口。但是因为没有重视入口,在近年已经被chrome打败了。

3.一旦有了稳定的盈利点,并且市场足够大的话,那么只要随着时间发展,必然有巨大的利润。

4.在微机时代依托操作系统的优势,成功站在了浪潮之巅。但是在后来的软件之争中却不断失败退守。

5.把握住计算机的核心,依靠地利优势,快速在初期占领了很大的软件市场。

6.老业务在公司占有主导权的时候,会阻碍新业务的发展。比如办公软件部门的强势就很难使公司有资金去做MSN这一类的免费商业模式。

第7 章 纯软件公司的先驱— 甲骨文公司

1.以点带面,把握住一个巨头没有重点关注的市场,快速拥有核心优势,击败主要竞争对手,做到垄断市场。感觉和今日头条很像,一开始依靠新闻领域,占领二三线市场,今天的今日头条手上已经有了多个巨大流量入口的APP。

第8 章 互联网的金门大桥— 思科公司

1.公司创立之初,便拥有巨大的技术优势,同时所做的事情既是符合时代趋势,还没有竞争对手,这样的公司能不站在浪潮之巅吗?。

2.不过当这三点优势都没有了之后,成长便慢慢趋于停滞了。

3.自己资助内部人创业,帮助他们的同时,既是消灭了隐藏的对手,又是壮大了自己。

第9 章 英名不朽—杨致远、费罗和雅虎公司

1.有着甲骨文的侵略性,但是又因为没有有远见的领导人。不断并购,盲目扩张合并,结果两个不好的公司合并,反而离第一名越来越远。

2.华尔街的压力对一个公司长远的发展实在是影响太大了

3.业务太多太杂,在面对更加专注的公司而不断惨败。

第10 章 硅谷的见证人— 惠普公司

1.强烈的证明了两家差的公司合并,只会将问题越来越多。

2.公司所代表的趋势,决定了你会吸引什么样的人才。

第11 章 没落的贵族— 摩托罗拉公司

1.当一家公司的先天优势与未来趋势相反的时候,真的很难下决定放弃自己的优势扑向一个大家都是起点的领域。

2.一家公司不够专注,太多的页面,多线战争反而使其在每一个领域都没有优势,最后便是全面溃败。

3.再次强调一位好的领导人的重要性。

第12 章 硅谷的另一面

1.成王败寇

2.嗜血的地方

3.机会均等

第13 章 短暂的春秋—与机会失之交臂的公司

在人类命运降临的伟大瞬间,市民的一切美德——小心、顺从、勤勉、谨慎,都无济于事。它始终只要求天才人物,并且将他造就成不朽的形象。命运鄙视地把畏首畏尾的人拒之门外。命运——这世上的另一位神,只愿意用热烈的双臂把勇敢者高高举起。

1.太阳公司

2.Novell 公司

3.网景公司

4.RealNetworks

第14 章 幕后的英雄— 风险投资

对于想找投资的新创业公司,红杉资本有一些基本要求——公司的业务要能几句话就讲清楚。红杉资本的投资人会给你一张名片,看你能不能在名片背面的一点点地方把你想做的事情写清楚。

1.风投的起源

2.风投的结构

3.风投的过程

4.投资的决策和公司的估价

5.风险投资就是投人

6.风投的角色

7.著名的风投公司

第15章 信息产业的规律性

1.70–20–10律。老大拥有最多的市场,最大的利润,它能够决定趋势的走向,是市场规则的制定者和解释着。老二,老三虽然偶尔能扑起一朵朵的小浪花,但是已经很难翻身了。

2.诺威格定律。当一个公司在市场占有主导地位(50%以上),因为已经无法做到翻一番,所以需要不断去开创新的财路,才能保证公司长盛不衰。

3.基因决定定律。深耕系统,大型机和服务的IBM很难在PC市场上成功。因为老业务在公司占有主导权的时候,会阻碍新业务的发展

4.有时候,公司产品的周边从业者反而会比公司更加捍卫自己的产品,并不断打压别的挑战者。因为这是在维护自己的饭碗。可以去培育用户,有点像小米之家。

第16 章 硅谷的摇篮—斯坦福大学

二战后,帮助斯坦福大学解决财政危机的是它的一位教授弗里德里克·特曼,他后来被称为“硅谷之父”。他仔细研究了斯坦福夫妇的遗嘱,发现里面没有限制大学出租土地,于是他兴奋地声称找到了解决问题的秘密武器——建立斯坦福科技园。

1.充满传奇的大学

2.硅谷的支柱

3.纽曼加洪堡的教育模式

4.创业的孵化器

第17章 科技公司的吹鼓手—投资银行

华尔街的贪婪既会捧起,也会扼杀一个科技新星。
1.华尔街和美国的金融体系

2.著名的投资公司

3.科技公司的上市过程

4.成也萧何,败也萧何

5.华尔街与微软、雅虎和Google 的三国演义

金融危机来临前的征兆,比如很多不需要融资的投资公司也上市融资。因为那些银行家比我们看的更远。

第18 章 挑战者— Google 公司

1.如果是三等工程师,那么谷歌根本不需要。

2.绝代双骄。优秀的领导人。

3.一开始就有长远目光的打算。

4.早期在与人竞争的时候,显得更加关注。比如和雅虎在竞争搜索,不说谷歌做的怎么样,先看看雅虎做的怎么样吧。

5.在招人方面精益求精,瑞士军刀的品质

6.与其担心对手在自己的优势领域抢占市场,不如主动出击,将战线推到对方的优势领域。

第19 章 成功的转基因—诺基亚、3M、GE 公司

1.放弃现有的优势,押注虚无缥缈的未来是很难的。

第20 章 印钞机— 最佳的商业模式

1.弄一个很低的门槛,可以帮你过滤出有真正需求的人。比如只是付费的一块钱门槛。

2.戴尔的“虚拟工厂”。怎么样做到成本最低,利润最高。就是尽可能的压缩产品线所有环节,使资金做到收回最快,利用率最高。

第21 章 互联网2.0

1.维基百科,youtube,facebook的例子都证明了一点,在互联网2.0时代,每个人都是互联网的参与者。他们既是内容的提供者,也是内容的消费者。

2浏览器成为了人们上网最快,最方便的方式,成为了互联网2.0的入口,,满足了人们快速上网的基本需求。那么下一个入口在哪?

第21 章 金融风暴的冲击

1.优胜劣汰。金融危机可以帮助好的公司以极低的成本收购到那些泡沫公司。使那些差的公司无法维持自己日益没落的统治地位。

2.金融危机后可以过滤出真正好的公司,既是危机,也是遍地黄金的机会。


浏览了一遍下来,发现新兴公司如果想要快速发展,如果符合以下三点,拥有技术优势,符合时代趋势,还没有竞争对手,那么往往可以在开头占有巨大的便宜。

但是要成为一个伟大的公司,初期的发展就换成了另外三句话,快速拥有核心优势,击败主要竞争对手,做到垄断市场。然后就是不断滚雪球,尝试多个相关方向的发展。

如果这时候再有一个英明的领导人,可以保证公司在10年内不会被人超越。

如果还能紧紧把握时代的趋势,这个就真的太难了,做到的公司最后才真的能够站在浪潮之巅。

一步步实现一个简单的router插件

发表于 2018-01-13 | 分类于 javascript | 阅读次数
| 字数统计 877 | 阅读时长 4

单页应用在随着框架的诞生已经是大趋势,自己平时在项目中也已经用了不少vur-router的代码了。用久了自然对其中的实现好奇了起来,于是想着自己能不能做出一个简单的vue-router。一开始扒了源码研究,实在是看不懂。于是从一篇篇文章开始慢慢补充起自己的基础。

一、写一个最简单的前端路由

如果使用过vue-router的人应该知道,Router插件是通过切换hash值来切换页面的。而浏览器则提供了一个hashchange的回调方法监听的hash值的变化。我们可以就这个原生API实现一个简单的前端路由。

之所以用这个API作为我们第一个出场嘉宾,其好处是可以兼容低版本的IE8浏览器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<a href="#/">home</a>
<a href="#/router1">router1</a>
<a href="#/router2">router2</a>
<div id="content"></div>
--------
class Router {
constructor(router = {}){
this.router = router;
this.init();
}
refresh(){
let url = window.location.hash.slice(1) || "/";
if(this.router[url]){
this.router[url]();
}else{
throw new Error("It isn't a router name that registered");
}
}
init(){
window.addEventListener("hashchange",this.refresh.bind(this),false);
}
}
function changeContent(text){
document.getElementById("content").innerText = text;
}
let router = new Router({
"/"(){
changeContent('home')
},
'/router1'(){
changeContent('router1')
},
'/router1'(){
changeContent('router2')
}
})

当浏览器监听到hash值发生改变之后,自动调用对应路由提供的回调,这样子就出现了一个简单的单页路由demo。

二、实现一个简单的H5版本的前端路由

随着HTML5的发布,我们则多了一个新的选择history。

浏览器原生的history对象为我们提供了一些新的方法如下,具体意思基本上也可以做

1
2
3
4
5
history.go(); //加载 history 列表中的某个具体页面。
history.back(); //加载 history 列表中的前一个 URL,等同于history.go(-1)。
history.foward(); //加载 history 列表中的下一个 URL,等同于history.go(1)。
history.pushState(state,title,url); //添加一条页面地址记录
history.replaceState(state,title,url); //更新当前的历史地址记录

这里再额外说一句pushState方法和replaceState的区别,从网上找了一张图来表示

image

pushState()是在history栈中添加一个地址并同时跳往该地址,replaceState()是直接替换当前地址为新地址。

每当你使用pushState()或者replaceState()方法时,会触发popstate事件的回调

然后我们以这两个方法为基础,重新写一个Router的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<a href="#/">home</a>
<a href="#/router1">router1</a>
<a href="#/router2">router2</a>
<div id="content"></div>
--------
class Router{
constructor(router = {}){
this.router = router;
this.init();
}
route(path,cb){
this.router[path] = cb || function () {
throw new Error("please give a callback");
};
}
refresh(state){
let url = state.path || "/";
if(this.router[url]){
this.router[url]();
}else{
throw new Error("It's not register router");
}
}
route(path,cb){
this.router[path] = cb || function () {
throw new Error("please give a callback");
};
}
init(){
let _this = this;
window.addEventListener("popstate",function (event) {
this.refresh(event.state || {});
}.bind(this))
document.querySelectorAll("a").forEach(a => {
a.addEventListener("click",function (e) {
e.preventDefault();
let path = link.slice(1) || "/";
let link = a.getAttribute("href");
_this.refresh({path : path} || {});
if(e.target.getAttribute("type") === 'replace'){
window.history.replaceState({'path':path},path,e.target.hre;
} else {
window.history.pushState({'path' : path},path,e.target.href);
}
})
},false);
//首次进入路由
let path = window.location.hash.slice(1) || '/';
this.refresh({path : path} || {});
}
}
function changeContent(text){
document.getElementById('content').innerHTML = text;
}
let router = new Router({
'/'(){
changeContent("home");
},
'/router1'(){
changeContent("router1");
},
'/router2'(){
changeContent("router2");
}
})
router.route('/router3',function(){
changeContent('路由3');
})

三、实现一个简单的vue-router版路由

因为vue-router是基于Vue的插件,所以很多可以发现源码很多地方,都拥有很典型的Vue特色。

第一步跟着文件中的index.js入手,大致可以找出几个初始函数来createMatcher,createRouterMap,

replace方法到模板编译

发表于 2017-12-29 | 分类于 javascript | 阅读次数
| 字数统计 782 | 阅读时长 3

本文章主要是自己用于熟悉正则的几个方法,并没有如何写正则表达式的内容,如果希望学习写正则匹配的,我也不太会(手动捂脸)

首先让我们先熟悉一个最常见的API——replace()方法

replace不会修改原字符串

语法和参数

1
2
str.replace(regexp|substr, newSubStr|function)

参数名称 参数含义
regexp 一个RegExp 对象或者字符串。该正则所匹配的内容会被第二个参数的返回值替换掉。
substr 一个要被 newSubStr 替换的字符串。其被视为一整个字符串,而不是一个正则表达式。仅仅是第一个匹配会被替换。
newSubStr 用于替换掉第一个参数在原字符串中的匹配部分的字符串。该字符串中可以内插一些特殊的变量名。参考下面的使用字符串作为参数。
function 一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。参考下面的指定一个函数作为参数。

先看一个最简单的例子

1
2
3
4
let a = "abcd1234".replace("1234","e"); //将字符串"1234"替换为字符"e"
let b = "1234abcd1234".replace(/\d+/g,"e"); //将数字替换为字符"e"
console.log(a); //abcde ,用字符串匹配没想到怎么全局替换,知道的可以告诉一下
console.log(b); //eabcde

但是很多时候纯粹的替换字符的场景比较少,很多时候我们希望将获得到的值经过操作然后替换上去,所以replace()方法也很聪明的提供了回调函数。接下来我们来熟悉下回调函数的参数

先看语法

1
str.replace(regexp, function(match,p1,offset,string){})

再看参数含义

参数名 参数含义
match 匹配的子串,即字符串中那一些符合匹配规则的子字符串(可以理解成“abcd”中的是“bc”)
p1 第一个匹配到的内容
offset 匹配到的子字符串在原字符串中索引。(比如,如果原字符串是“abcd”,匹配到的子字符串是“bc”,那么这个参数将是1)
string 被匹配的原字符串。

其实如果大家之前有用过match这个API的话,就能发现他的形参全部是以match为基础进行实现的

这个时候再来一个简单版的小demo对着看,大家基本上就能明白了

1
2
3
4
5
6
7
8
9
10
11
"strss".replace(/s+/g, function(match,offset,string){
console.log(match)
console.log(offset)
console.log(string)
});
// s
// 0
// strss
// ss
// 3
// strss

有没有一种ES5数组回调参数的既视感

不过回调函数提供的参数还赋予了更强大的功能,这里我们先暂时略过

发现还是避不过正则表达式匹配的内容

我们可以先在熟悉语法的情况下,一步一步从最基本的方法使用来认识这个方法

因为我们想在最终的替换中进一步转变匹配结果,所以我们必须使用一个函数。

我的圣诞老人

发表于 2017-12-25 | 分类于 javascript | 阅读次数
| 字数统计 769 | 阅读时长 3

我不知道大家是什么时候知道圣诞节的,也不知道很多人会不会有和我一样的童年经历。

在我很小很小的时候,也许是幼儿园,也许是学前班,也许是记忆开始的时候,不知道从哪里听到了圣诞老人的传说。只要将一只空袜子放在床头,圣诞老人会在袜子里放上你最想要的礼物。

不知道刚听到这个传说的时候,自己是怎么样的心情,喜悦?迷惑?还是好奇。只记得自己小心翼翼的挑了一只最大的袜子,抚平了放在头边,然后带着期待的心情进入了梦乡。

现在已经想不起来自己第一次收到了什么礼物,一盒水彩笔?还是什么?

但还能回忆出醒来之后睁大眼睛找袜子的我,拿着礼物在床上蹦蹦跳跳的我,告诉爸妈圣诞老人来了,最后拿起礼物飞奔出去炫耀的我。你知道吗,圣诞老人来我家送礼物啦!

之后几年,自己依然和以前一样准备好了空袜子,也开始好奇圣诞老人到底是什么样子。只知道电视里的他是一个穿着红衣服,有的白胡子的老爷爷,他会从家里的烟囱爬进来,然后从大麻袋中挑出你最想要的礼物。

于是我不断在那一晚提醒着自己,不要睡着,要一直守着,要亲眼看看圣诞老人是什么样子的。可是孩子的好奇终究抵不过睡意,虽然第二天仍然收到了礼物,但还是没有看到老爷爷的样子。

只不过对孩子而言,只要有礼物就好了,圣诞老人到底是什么模样,他是不是从烟囱爬进来又有什么关系呢。

后来,直到有一年我再也没有在袜子边上看见礼物的时候,哭闹着打爸妈的时候,圣诞老人的故事在我身上就结束了。

懂事以后,自然知道了圣诞老人是假的。只是真的感谢我的圣诞老人,在我的童年里满足了一个爱幻想的小孩子,当别的小孩在说“圣诞老人就是你妈妈的时候”,“世界上没有圣诞老人”时,我仍然可以一口坚定,“世界上有圣诞老人”,“看,这就是圣诞老人给我的礼物”。
感谢我的圣诞老人。

不知道为什么今晚会想写这一些的文字,只是真的很感谢我的爸爸妈妈。因为你们,帮助我在这个城市生活的远比很多人轻松,也是今年经历了太多的事情,我才明白,你们给我的帮助,远比我想象的还要更多。也许我永远无法当面说出“我爱你们”这些话,可是我会努力不让你们失望。

《JavaScript 正则表达式迷你书》读后感

发表于 2017-11-19 | 分类于 javascript | 阅读次数
| 字数统计 2,417 | 阅读时长 9

国庆时间看到作者出版了一本正则表达式的书,因为之前一直对模板好奇,于是就去作者的网盘里面下载了这一本书的pdf版观看。最近看完之后觉得作者写的很棒,弥补了自己很多正则的基础 知识。附上链接《JavaScript 正则表达式迷你书》问世了!。

文章主要记录了一下自己的学习心得

1. 复杂的正则表达式可以由几个简单的正则表达式组合而成

记得之前每次写密码验证的时候,总希望一个正则表达式搞定全部的情况,看完书后觉得原来没有必要。

以密码验证为例出题:
密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字符。大家可以想一下怎么实现。

书中一开始得出了一个非常复杂的正则表达式,但是其实后期维护修改未必简单,而且换一个同事来维护,刚开始理解也很辛苦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--复杂版正则表达式-->
let regex = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;
<!--简单易懂版正则表达式-->
let regex1 = /^[0-9A-Za-z]{6,12}$/; //6-12位的数字、小写字符和大写字母组成
let regex2 = /^[0-9]{6,12}$/; //不包含数字(就是只包含大小写字母)
let regex3 = /^[A-Z]{6,12}$/; //不包含大写字母(就是只包含数字和小写)
let regex4 = /^[a-z]{6,12}$/; //不包含小写字母(就是只包含数字和大写)
function checkPassword (string) {
if (!regex1.test(string)) return false;
if (regex2.test(string)) return false;
if (regex3.test(string)) return false;
if (regex4.test(string)) return false;
return true;
}

可以看到,第一种对于我这种刚开始实战不多的,颇有一点炫技的表现(也有可能是我太菜)。第二种一看,会舒服很多,高可读性和高可维护性。

我个人认为在团队合作中,第二种对于后期伙伴的维护应该是更佳的。


2. ?的各个含义

在看书的时候,因为之前正则的基础很薄弱,看见书中频频出现的?用在不同地方实现不一样的效果,我是一脸懵逼,经常要上百度看一下?用在这里表示什么意思。这里小总结一下

2.1 本身符号“?”

表达自身一个“?”字符,但是因为?在正则表达式中的作用太多了,所以当它需要表达自身的时候,需要进行一次转义

1
\?

2.2 表示匹配次数,

这是常见的第一种用法,允许重复匹配的次数,0次或者1次。

例子

1
2
3
4
5
6
7
8
9
let regex1 = /\d*/;
let str = "12345";
str.match(regex); //["12345", index: 0, input: "12345"]
//======使用了?号======//
let regex = /\d?/; //最大允许匹配一次数字
let str = "12345";
str.match(regex); //["1", index: 0, input: "1234"]

2.3 表示懒惰匹配

这是常见的第二种用法,因为正则表达式默认是贪婪匹配的,所以很多时候我们会在某组匹配字符后加一个问号表示非贪婪匹配

例子

1
2
3
4
5
6
7
8
9
let regex = /\d{1,3}/
let str = "12345";
str.match(regex); //["123", index: 0, input: "12345"]
//======添加了?号======//
let regex = /\d{1,3}?/
let str = "12345";
str.match(regex); //["1", index: 0, input: "1234"]

2.4 配合字符实现位置匹配

书中讲到了 这么一句话

正则表达式是匹配模式,要么匹配字符,要么匹配位置。请记住这句话。

关于”位置”这个概念的理解推荐看书中的第二章

而当你匹配位置的时候,两个匹配位置的正则表达式就非常关键了。

(?=p),其中 p 是一个子模式,即 p 前面的位置,或者说,该位置后面的字符要匹配 p。

1
2
let result = "hello".replace(/(?=l)/g, '#');
console.log(result); // "he#l#lo"

而 (?!p) 就是 (?=p) 相反的意思,比如:

1
2
let result = "hello".replace(/(?!l)/g, '#');
console.log(result); // "#h#ell#o#"

这两个用法在数字格式化的时候有非常大的用处。给大家出个题目吧,如何实现数字的千位分隔符表示。比如讲1234567转化为12,345,678。

大家思考一下

….

….

….

答案

1
2
let regex = /(?!^)(?=(\d{3})+$)/g;
"12345678".replace(regex1,","); // "12,345,678"

具体实现看不懂还是推荐去看原书,作者写的很好,我相信对大家帮助肯定也很大。

2.5 非捕获模式

还有最后一种不怎么常见(可能是没怎么见过)的用法(?:),表示非捕获模式。我是这么理解的(不知道自己理解的对不对),就是当你遇到匹配的字符时,它并没有马上捕获匹配的内容,并且记录下拉,而是继续匹配下去作为为整体匹配服务。讲的不好,大家还是看例子实在吧(手动捂脸)。

例子

1
2
3
4
5
let regex = /(?:a)(b)(c)/; "abcabc".match(regex)
//结果 ["abc", "b", "c"]
// m[0] 是/(?:a)(b)(c)/匹配到的整个字符串,这里包括了a
// m[1] 是捕获组1,即(b)匹配的子字符串substring or sub sequence
// m[2] 是捕获组2,即(c)匹配到的

大家可以注意到第一个括号里面的a并没有被提取出来,但是整体匹配的字符时有a的。这就是我理解的非捕获模式,为整体存在的匹配。


3. 回溯的学习

性能和效率始终是绕不开的一环,文中提到回溯造成原因我感觉主要是由2点造成的,

  • 一是由于匹配默认是贪婪的
  • 二是由于匹配有时候是懒惰的。(使用分支情况下)
3.1 贪婪匹配造成的回溯

先说第一种情况,贪婪匹配造成的回溯,举个书中的例子

1
2
3
let str = '"abc"de';
let regex = /".*"/;
str.match(regex); // ['"abc"', index: 0, input: '"abc"de']

当用此正则表达式去匹配字符串的时候,发现最后无法完成整体匹配的时候,会不断回吐一个字符再次去尝试整体正确的匹配。大家可以结合下图理解。
image

书中最后讲到回溯是非常影响效率的,但是自己在写例子测试的时候,发现其实时间基本上没有任何差别,不知道是不是因为自己测试的正则比较简单,还是浏览器现在对于正则的优化做的比较好,总之没有达到书中说的到非常影响效率的程度。

效率对比例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function test(){
let str = '"abc"dddddddddddddddddddddddddddddde';
let regex = /".*"/;
};
console.time()
for(var i = 0;i< 1000000000;i++){test()}
console.timeEnd()
//default: 2321.663818359375ms
//========修改为减少贪婪回溯的写法========//
function test(){
let str = '"abc"dddddddddddddddddddddddddddddde';
let regex = /"[^"]]*"/;
};
console.time()
for(var i = 0;i< 1000000000;i++){test()}
console.timeEnd()
//default: 2327.2890625ms

对于这种回溯的解决方法来说:

  • 方法1 :写尽量正确的匹配。像上面例子中的修改版就是这种解决方法,
  • 方法2 :尽可能少的匹配。比如加个惰性量词“?”。(其实就是尽量减少贪婪匹配)
3.2懒惰匹配造成的回溯

然而并不是所有回溯的情况都是由贪婪造成的。比如当我们在使用分支匹配的时候。

例子

1
2
3
let str = 'candy';
let regex =/can|candy/;
str.match(regex); //["can", index: 0, input: "candy"]

当我们用/can|candy/去匹配字符串 “candy”,得到的结果是 “can”,因为分支会
一个一个尝试,如果前面的满足了,后面就不会再试验了。但是如果我们的目标字符串是“candy”的时候,那怎么办呢。

例子

1
2
3
let str = 'candy';
let regex = /^(?:can|candy)$/;
str.match(regex); //["candy", index: 0, input: "candy"]

大家可以先看图理解一下懒惰造成的回溯
image


4. 跟正则有关几个正则方法

字符串对象和正则对象提供了很多跟正则有关的基础方法,很多方法都都有很好的使用场景。

4.1 RegExp#test

比如我在表单验证的场景里,用户每次输入值我需要进行判断用户是否输入正确,我可是使用regex.test()方法来确定是否给用户提示

只允许输入数字

1
2
3
4
5
6
<input onkeyup="test(this.value)" />
function test(value){
let regex = /[^\d]/g;
if(regex.test(value))console.log("请输入数字")
}

4.2 String#replace

这个replace方法用处实在是太大了,已经到了可以单开一篇的地步了,大家可以前往这里去看MDN上replace的文档,这里就不详细介绍了。这里写个简单的例子

最简单的模板编译

1
2
3
4
5
6
7
8
9
10
let str = '我是{{name}},年龄{{age}},性别{{sex}}';
let obj = {
name:'姓名',
age:18,
sex: '男'
}
let strEnd = str.replace(/\{\{(.+?)\}\}/g,function (match, m1) {
return obj(m1)
})
// "我是姓名,年龄18,性别undefined"

4.3 String#search

这个方法感觉和indexOf效率有一些相似,都是寻找符合匹配的下标。不过indexOf方法是为字符串使用的,而search是为正则表达式实现的

1
2
3
let str = 'abc123456';
let regex = /\d/;
console.log(str.search(regex)); // 3

4.3 String#split

字符串的split方法同样支持正则表达式进行切割

1
2
3
var regex = /,/;
var string = "html,css,javascript";
console.log( string.split(regex) );

4.4 String#match

这个方法更多是为了提取匹配内容而存在的。当你的正则表达式里面有小括号()的存在时,match方法可以帮你提取出字符串中符合括号正则的表达式。

1
2
3
4
var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
console.log( string.match(regex) );
// ["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]

算法图解读后感

发表于 2017-10-29 | 分类于 javascript | 阅读次数
| 字数统计 354 | 阅读时长 1

算法算法,一个这两年被人念叨最多的词汇,但很多时候身为一个前端人员完全感受不到其所在,所以一直对此也很是懵逼。这段时间也是无聊,于是看了一本算法图解的书,还是很能感受到其中的一些魅力所在的额

先开坑

一

二分查找算法,是本书的第一个算法,用于有序数据的快速查找。

二

链表相对于数组的优势

  • 当插入数据较多而读取数据较少时,
1 数组 链表
插入 O(n),需要从第一个开始查找到最后一个才能插入 O(1)随便扔一个地方
读取 O(1)直接知道数据位置在哪里 O(n)需要知道前面的才能知道后面的
删除 O(n)遍历所有的一个个开始删除 O(1)直接修改每个项所代表的索引

三 快速查找排序算法

快速查找排序算法:最常见的排序算法,举例描述:先找出一个数组中最小的,推出到新数组中,然后来n次遍历,得出由小到大的新数组。时间复杂度为O(n2);

四递归调用

主要讲解了基本的栈和递归调用,最后讲到的高级递归主题尾递归需要看一下

五

123
linzx

linzx

30 日志
6 分类
9 标签
© 2018 linzx
由 Hexo 强力驱动
主题 - NexT.Pisces