Coder Social home page Coder Social logo

blog's People

Contributors

laughing-pic-zhu avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar

blog's Issues

js对象监听实现

前言

随着前端交互复杂度的提升,各类框架如angular,react,vue等也层出不穷,这些框架一个比较重要的技术点就是数据绑定。数据的监听有较多的实现方案,本文将粗略的描述一番,并对其中一个兼容性较好的深入分析。
#实现方案简介
目前对象的监听可行的方案:

  • 脏检查: 需要遍历scope对象树里的$watch数组,使用不当容易造成性能问题
  • ES5 object.defineproperty: 除ie8部分支持 其他基本都完全支持
  • ES7 object.observe : 已经移除([缘由][1])出ES7草案
  • gecko object.watch :目前只有基于gecko的浏览器如火狐支持,官方建议仅供调试用
  • ES6 Proxy: 目前支持较差,babel也暂不支持转化

ES5现代浏览器基本都支持了,OK,本文将介绍目前支持度最好的object.defineproperty 的Setters 和 Getters方式

object.defineproperty介绍

简洁的介绍

它属于es5规范,有两种定义属性:

  • 一种是 数据属性 包含Writable,Enumerable,Configurable
  • 一种是 访问器属性 包含get 和set

数据属性的例子

obj.key='static';
//等效于
Object.defineProperty(obj, "key", {
  enumerable: true,
  configurable: true,
  writable: true,
  value: "static"
});

访问器属性例子

var obj = {
    temperature:'test'
};
var temperature='';
Object.defineProperty(obj, 'temperature', {
    get: function() {
        return temperature+'-----after';
    },
    set: function(value) {
        temperature = value;
    }
})
obj.temperature='Test';
//Test-----after
console.log(obj.temperature);

详细的介绍

火狐开发者

实现监听的思路

  1. 将需要监听对象/数组 obj和回调函数callback传入构造函数,this.callback = callback 存储回调函数
  2. 遍历对象/数组obj,通过Object.defineProperty将属性全部定义一遍。在set函数里面添加callback函数,设置val值。get函数返回val。
  3. 判断对应的obj[key]是否为对象,是则进入第二步,否则继续遍历
  4. 遍历结束之后判断该对象是否为数组,是则对操作数组函数如push,pop,shift,unshift等进行封装,操作数组前调用callback函数

数组的封装

比较复杂的是数组的封装,结构如下:
新建一个对象newProto,继承Array的原型,并在newProto上面封装push,pop等数组操作方法,再将传入的array对象的原型设置为newProto。

对应图

1211051750-57d91409736d8_articlex

##路径的定位
在获取数据变化的同时,定位该变化数据在原始根对象的位置,以数组表示如:
如[ 'a', 'dd', 'ddd' ] 表示对象obj.a.dd.ddd的属性改变
实现:每个遍历对象属性都通过path.slice(0)的方式复制入参数组path,生成新数组tpath,给tpath数组push对应的对象属性key,最后在执行set的回调函数时候将tpath当参数传入

带注释代码

watch.js

  /**
   *
   * @param obj 需要监听的对象或数组
   * @param callback 当对应属性变化的时候触发的回调函数
   * @constructor
   */
  function Watch(obj, callback) {
      this.callback = callback;
      //监听_obj对象 判断是否为对象,如果是数组,则对数组对应的原型进行封装
      //path代表相应属性在原始对象的位置,以数组表示. 如[ 'a', 'dd', 'ddd' ] 表示对象obj.a.dd.ddd的属性改变
      this.observe = function (_obj, path) {
          var type=Object.prototype.toString.call(_obj);
          if (type== '[object Object]'||type== '[object Array]') {
              this.observeObj(_obj, path);
              if (type == '[object Array]') {
                  this.cloneArray(_obj, path);
              }
          }
      };
  
      //遍历对象obj,设置set,get属性,set属性能触发callback函数,并将val的值改为newVal
      //遍历结束后再次调用observe函数 判断val是否为对象,如果是则在对val进行遍历设置set,get
      this.observeObj = function (obj, path) {
          var t = this;
          Object.keys(obj).forEach(function (prop) {
              var val = obj[prop];
              var tpath = path.slice(0);
              tpath.push(prop);
              Object.defineProperty(obj, prop, {
                  get: function () {
                      return val;
                  },
                  set: function (newVal) {
                      t.callback(tpath, newVal, val);
                      val = newVal;
                  }
              });
              t.observe(val, tpath);
          });
      };
  
      //通过对特定数组的原型中间放一个newProto原型,该原型继承于Array的原型,但是对push,pop等数组操作属性进行封装
      this.cloneArray = function (a_array, path) {
          var ORP = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
          var arrayProto = Array.prototype;
          var newProto = Object.create(arrayProto);
          var t = this;
          ORP.forEach(function (prop) {
              Object.defineProperty(newProto, prop, {
                  value: function (newVal) {
                      path.push(prop);
                      t.callback(path, newVal);
                      arrayProto[prop].apply(a_array, arguments);
                  },
                  enumerable: false,
                  configurable: true,
                  writable: true
              });
          });
          a_array.__proto__ = newProto;
      };
  
      //开始监听obj对象,初始path为[]
      this.observe(obj, []);
  }

index.html

    <body>
    <ul>
        <li>
            <a href="javascript:void(0)" onClick="dataOne()">
                将obj b属性改变
            </a>
        </li>
        <li>
            <a href="javascript:void(0)" onClick="dataTwo()">
                将obj a属性的dd属性的ddd属性改变
            </a>
        </li>
        <li>
            <a href="javascript:void(0)" onClick="dataThree()">
                将obj a属性的g属性数组第一个值的a属性改变
            </a>
        </li>
        <li>
            <a href="javascript:void(0)" onClick="dataFour()">
                将obj a属性的g属性数组push新的值
            </a>
        </li>
    </ul>
    
    <div id="path">
    </div>
    <div id="old-val">
    </div>
    <div id="new-val">
    </div>
    </body>
    <script src="../src/watch.js"></script>
    <script>
        var obj = {
            a: {e: 4, f: 5, g: [{a: 1, b: 2}, [3, 4]], dd: {ddd: 1}},
            b: 2,
            c: 3
        };
    
        new Watch(obj, call);
        function call(path, newVal, oldVal) {
            document.getElementById('path').innerHTML='路径:'+path;
            document.getElementById('old-val').innerHTML='新的值:'+newVal;
            document.getElementById('new-val').innerHTML='老的值:'+oldVal;
        }
    
        function dataOne() {
            obj.b = Math.floor(Math.random()*10);
        }
    
        function dataTwo() {
            obj.a.dd.ddd = Math.floor(Math.random()*10);
        }
    
        function dataThree() {
            obj.a.g[0].a=Math.floor(Math.random()*10);
        }
    
        function dataFour() {
            obj.a.g.push(Math.floor(Math.random()*10));
        }
    </script>

效果图

123299756-57d964f6af327_articlex

#代码地址
完整代码地址

流程图

具体流程的复杂度基于监听对象的深度,所以下图只对父对象做流程分析
2294675073-57d91b4224ee9_articlex

归纳

  • 通过定义对象内部属性的setter和getter方法,对将要变化的属性进行拦截代理,在变化前执行预设的回调函数来达到对象监听的目的。
  • 数组则在对象监听之外额外在数组对象上的原型链上加一层原型对象,来拦截掉push,pop等方法,然后在执行预设的回调函数

最后

本文有什么不完善的地方,或者流程图有待改进的地方,敬请斧正。

babel源码分析之一:AST生成

前言

babel是javaScript编译器,主要用于将ECMAScript2015+版本的代码转化为具有兼容性的较低版本,从而让代码适用于各种环境。
它的早期代码从acorn项目中fork出来,后续提供了acorn不具备的一整套的代码解析,转换,生成的功能。

现在的babel有着抽象的工程实现很难直接啃源码。笔者打算通过一系列早期功能点的实现来慢慢揭开其内部机制。babel代码转换第一步是将源码转化成AST(Abstract Syntax Tree)。
本文将借助babel/acorn初期的源码,详细讲解其生成AST的工程细节。

阅读本文前,希望您对AST的概念如Statement,Expression有一些了解。ESTree 规范文档传送门

功能分析

示例

先举个代码转为ast的例子:

/*
  测试whileStatement
*/
while(b !== 0){
  if (a > b){
   a = a - b;
  }else{
   b = b - a;
  }
}
console.log(a)

转化后的ast结构

Program

实现思路

上图的整个树的生成都是由一次次词法,语法解析中递归出来的。

一个通用完整的statement递归函数逻辑:

  • 去除注释,空格,换行。
  • 词法分析: 将源码中的字符转化为单词。即解析token,如while(b !== 0){将被识别为的[while,(,b,!==,0,),{]这7个单词。
  • 语法分析:通过词法解析出来的单词token来获取statement节点的类型并解析,然后对其中可能含有的expression进行相应的语法解析。解析出其开始的start,结束的end,值value以及值的类型label。
  • 索引移到下一位,开启新一轮的递归。以此循环直到将文件字符串读取完毕。
  ...
  while (tokType !== _eof) {
        const statement = parseStatement();
        if (first) {
            first = false;
            if (isUseStrict(statement)) {
                setStrict(true);
            }
        }
        node.body.push(statement);
    }
 ...   

部分Statement, Experssion内部也有自己的递归逻辑:

Statement内部递归

VariableDeclaration

以逗号分隔的递归如var a,b,c

FunctionDeclaration

参数内部以逗号分隔的行参递归,大括号内部的以分号分割的statement递归,如function a(b,c,d){e;f;g;}

BlockStatement

大括号内部的以分号分割的statement递归,直到到遇到大括号结束符,如{e;f;g}

IfStatement

以else关键字的递归,如if(a){}else if(b){}else{}

SwitchStatement

以 case以及default关键字的递归,如switch(a){case a:xxx;caseb :xxx; default: xxx;}

ForStatement,ForInStatement,WhileStatement,DoWhileStatement,TryStatement,LabeledStatement

都是大括号内部的以分号分割的statement递归,大括号结束符,如

for(;;){e;f;g;} 
for (var a in b){e;f;g} 
while(a){e;f;g} 
do{e;f;g;}while(a) 
try{e;f;g}catch(a){e;f;g}finally{e;f;g;}

Experssion内部递归

ObjectExpression

以逗号分隔分割的递归,直到遇到大括号结束符。如{a,b,c,}

ArrayExpression

以逗号分隔分割的递归,直到遇到中括号结束符。如[a,b,c,]

FunctionExpression

和FunctionDeclaration一样,参数内部以逗号分隔的行参递归,大括号内部的以分号分割的statement递归,如var a=function (b,c,d){e;f;g;}

功能实现

单个通用的递归函数的实现的功能如下

去除注释,空格,换行

注释分两种:

/* */,或者 / ** */的多行注释以及//的单行注释。

空格分为' ' \t \n \r \f

function skipSpace() {
    while (tokenPos < inputLen) {
        const ch = input.charAt(tokenPos);
        if (ch === '/') {
            if (input.charAt(tokenPos + 1) === '/') {
                tokenPos += 2;
                while (tokenPos < inputLen && !newline.test(input.charAt(tokenPos))) {
                    tokenPos++;
                }
            } else if (input.charAt(tokenPos + 1) === '*') {
                const i = input.indexOf('*/', tokenPos + 2);
                if (i < 0) {
                    raise(tokenPos - 2, 'Unterminated comment');
                }
                tokenPos = i + 2;
            } else {
                ++tokenPos;
            }
        } else if (ch === '\n' || ch === '\t' || ch === " " || ch === "\r" || ch === "\f") {
            ++tokenPos;
        } else {
            break;
        }
    }
}

解析token

具体token有非常多,但是按类型分的话,可以分为以下6种:

  • string字符串类型。以' " 开头,且以' "结尾。
  • regexp正则类型。以/开头,不在[]内且上一个字符不是转译符\的情况下以/结尾。
  • word单词类型。关键字如break case catch continue debugger。保留关键字如implements interface let package private以及普通的变量名称。
  • number数字类型。类型有:二进制,八进制,十进制和十六进制。其中0x或者0X开头的是十六进制。
  • punctuation标点符号类型。如[ { ( , ; ? 等符号。
  • operator运算符类型。如+ - * % | & = 等符号。对于不同符号在一起解析的时候,会有不同的解析优先级。
    • 优先级最高为10: - * % /
    • 优先级为9:+ -
    • 优先级为8: >> >>> << <<<
    • 优先级为7: > >= < <=
    • 优先级为6: === == !== !===
    • 优先级为5: &
    • 优先级为4: ^
    • 优先级为3: |
    • 优先级为2: &&
    • 优先级为1: ||
function readToken() {
    lastStart = tokStart;
    lastEnd = tokEnd;
    tokStart = tokenPos;
    const ch = input.charAt(tokenPos);
    if (tokenPos >= inputLen) {
        return finishToken(_eof);
    }
    if (ch === '\'' || ch === '"') {
        readString(ch);
    } else if (indentifierReg.test(ch)) {
        readWord();
    } else if (digest.test(ch)) {
        readNumber();
    } else if (puncChars.test(ch)) {
        tokenPos++;
        finishToken(puncTypes[ch]);
    } else if (operatorChar.test(ch)) {
        readOperator(ch)
    }
}

解析节点Statements

除了BlockStatement,其他statement是以;或者换行符结束。
每种statement都由自己不同的解析方式以及名称。一个statement可能含有0个或者多个expression。

如while类型的statement解析函数如下

function parseWhile() {
    const node = startNode();
    next();
    node.test = parseParenExpression();
    node.body = parseBlock();
    return finishNode(node, 'WhileStatement');
}

解析后的简单的json类型为

{
      "type": "WhileStatement",
      "test": {
        "type": "AssignmentExpression",
        "operator": "=",
        "left": {
          "type": "Identifier",
          "name": "a"
        },
        "right": {
          "type": "Identifier",
          "name": "b"
        }
      },
      "body": {
        "type": "BlockStatement",
        "body": []
      }
    }

解析expression

这个模块个人认为是最核心的模块,对不同表达式进行解析。

最基本的表达式如:Identifier,Literal,FunctionExpression,ObjectExpression,ArrayExpression,NewExpression。

建立在基本表达式之上的如:a.b的MemberExpression,a()的CallExpression。
++a,a--之类的UpdateExpression。
!a,!!a之类的UnaryExpression。
a||b,a&&b的LogicalExpression,a-b之类的BinaryExpression。
a=b之类的AssignmentExpression。
a?b:c之类的ConditionalExpression。

上面这些复杂类型的解析执行顺序如下:
parseExpression

举个复杂的例子:

var a=!b++?c+1:d.e(f,g);

解析之后的json格式如下

{
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "type": "ConditionalExpression",
            "test": {
              "type": "UnaryExpression",
              "operator": "!",
              "prefix": true,
              "argument": {
                "type": "UpdateExpression",
                "operator": "++",
                "prefix": false,
                "argument": {
                  "type": "Identifier",
                  "name": "b"
                }
              }
            },
            "consequent": {
              "type": "BinaryExpression",
              "left": {
                "type": "Identifier",
                "name": "c"
              },
              "operator": "+",
              "right": {
                "type": "Literal",
                "value": 1,
                "raw": "1"
              }
            },
            "alternate": {
              "type": "CallExpression",
              "callee": {
                "type": "MemberExpression",
                "object": {
                  "type": "Identifier",
                  "name": "d"
                },
                "property": {
                  "type": "Identifier",
                  "name": "e"
                },
                "computed": false
              },
              "arguments": [
                {
                  "type": "Identifier",
                  "name": "f"
                },
                {
                  "type": "Identifier",
                  "name": "g"
                }
              ]
            }
          }
        }
      ],
      "kind": "var"
    }

代码实现

本人的简易版babel实现simple-babel

后话

实现了AST之后,后续也可以拓展很多有趣的功能如代码转换,代码风格检测,代码自动格式化,代码压缩。目前我还不是太明白,以后可以尝试实现一下。

(完)

简易版前端mvc设计

web前端mvc库实现

前言

随着前端应用日趋复杂,如今如angular,vue的mvvm框架,基于virtual dom的react等前端库基本成为了各个公司的首选。而以当初最流行的头号大哥backbone为代表的mvc库基本退出了历史舞台。

在现如今人人都说mvvm/react多好,backbone多差的时代。笔者看别人文章,看的时候总是感觉好像有点道理,看完之后如耳边风一般左耳朵进,右耳朵出。

so,痛定思痛之后,笔者定了个小目标,实现了份简易版的backbone库。以设计,实现的角度来对比其它类型库的差异。

ok,废话不多说,上正菜。

思路整理

MVC即将前端应用抽象为Model,View,Control三大模块。View为用户视图,通过浏览器事件接受用户输入。Model为数据模型,他可以随时和后端同步数据。Control则是具体实现View派发的事件,计算并改变Model的数据。

UI可以被抽象为模版+数据,随着用户不断的触发浏览器提供的各种事件,交互不断的进行,Control接受了View指令改变着Model的数据,而View则随着Model的改变做出响应,最终展现在用户面前。

流程图:

mvc

模块划分

本篇文章的思路来自于backbone,并屏弃了耦合的后端操作。早期MVC并没有对Control做严格的划分,也许是数据的改变计算并不那么复杂,所以Control功能在View的事件内完成了,也就是说View模块里面耦合了Control的功能。

但近几年flux的action,store的出现,View调用action,具体数据变化计算则在store内部实现,也算是把Control功能从View内部抽象出来了吧。

Event模块

为对象提供对事件的处理和回调,内部实现了观察者(订阅者)模式,如view订阅了model的变化,model变化之后则通知view。

基本方法。

  • on函数通过event名,在object上绑定callback函数,将回调函数存储在数组里。

  • off函数移除在object上绑定的callback函数

    • 通过event名移除指定callback。如object.off("change", onChange)
    • 通过event名移除所有callback。如object.off("change")
    • 移除所有指定callback。如object.off(null, onChange);
    • 移除所有指定context的callback。如object.off(null, null, context);
    • 清空object所有的callback。如object.off()
  • trigger函数通过event名,找到object对应的数组,并触发所有数组内回调函数。

注意事项

其所有方法应该支持类似on(name,callback),on('name1 name2 name3',callback), on({name1:callback1,name2:callback2})

这时候则可以抽象内部公用方法。通过递归的方式,on({name1:callback1,name2:callback2})类型的和on('name1 name2 name3',callback)类型,最终转化为最基本的on(name,callback)类型。核心代码如下:

this.eventsApi = function (iteratee, name, callback, context) {
    let event;
    if (name && typeof name === 'object') {
      Object.keys(name).forEach(key=> {
        event = this.eventsApi(key, name[key], context);
      })
    } else if (SEPARATE.test(name)) {
      var keys = name.split(SEPARATE);
      keys.forEach(key=> {
        event = iteratee.call(this,key, name[key], context);
      });
    } else {
      event = iteratee.call(this,name, callback, context);
    }
    return event;
};

View模块

  • 无状态,实例化的时候可以对应多个model实例,并以观察者的身份观察这些model的变化,通过这些model数据,加上指定的模版渲染dom,展示UI。
  • 销毁的时候注销掉所有model的观察,取消与相关model之间的关联。
  • 实例化的时候通过事件委托注册浏览器事件

实现

  • _ensureElement,确保View有一层dom包裹,如果this.el这个dom不存在,则通过id,className,tagName创建一个dom并赋值于this.el。

  • listenTo,将model与view实例关联起来,并收集关联model,存储于listenTo数组内,内部实现则是调用model的on函数

  • stopListening,view销毁前调用,通过listenTo数组找到关联model,并取消view与这些model之间的观察者关系。

  • $,将dom的查找定位在 this.$el下

  • delegateEvents,事件委托,以{'click #toggle-all': 'choose'}为例,为在this.el子节点的id等于toggle-all的dom注册click事件choose函数。核心代码如下:

     delegateEvents: function (events) {
         var $el = this.$el;
         Object.keys(events).forEach(item=> {
           var arr = item.split(' ');
           if (arr.length === 2) {
             var event = arr[0];
             var dom = arr[1];
             $el.on(event + '.delegateEvents' + this.$id, dom, this[events[item]].bind(this));
           }
         })
     },
    
  • undelegateEvents,注销掉通过delegateEvents注册的dom事件

Model模块

Model在backbone里被抽象为object类型的Model和array类型的Collection

  • 承载着应用的状态,可以随时和后端保持同步。
  • 内部实现了对数据变化的监听,一旦发生变化则通知观察者View发生变化。

Model

监听数据的变化,对model的修改,删除之后调用对应的trigger函数,通知订阅了model变化的view。

  • set函数,改变model数据,并触发change事件

     set: function (obj) {
         this._changing = true;
         this.changed = obj;
         this._previousAttributes = Object.assign({}, this.attributes);
         this.attributes = Object.assign({}, this.attributes, obj);
         const keys = [];
         Object.keys(obj).forEach(key=> {
           keys.push(key);
           this.trigger('change:' + key, this);
         }, this);
     
         if (keys.length > 0) {
           this.trigger('change', this);
         }
         this._changing = false;
      },
    
  • destroy函数触发destroy事件

     destroy: function () {
        this.stopListening();
        this.trigger('destroy', this);
     },
    

Collection

提供数组类型models的push,unshift,pop,shift,remove,reset等功能。push,unshift实际调用add函数,pop,shift实际调用remove函数。

  • add函数支持任意索引插入指定数组,触发add事件。核心的代码如下:

     export const splice = (array, insert, at)=> {
       at = Math.max(0, Math.min(array.length, at));
       let len = insert.length;
       let tail = [];
       for (let i = at; i < array.length; i++) {
         tail.push(array[i]);
       }
       for (let i = 0; i < tail.length; i++) {
         array[i + at + len] = tail[i];
       }
       for (let i = 0; i < len; i++) {
         array[i + at] = insert[i];
       }
       return array;
     };
    
  • remove函数支持删除指定model,触发update事件。

  • _addReference,调用add方法新增model时,通过观察者模式增加该model与collection之间的关联,model的变化通知collection。核心代码如下:

     _addReference: function (model) {
     model.on('all',this._onModelEvent,this);
     }
    
  • _removeReference,调用remove,reset移除model时,取消该model与collection关联。核心代码如下:

      _removeReference: function(model) {
        if (this === model.collection) delete model.collection;
        model.off('all', this._onModelEvent, this);
      }
    

extend

生产环境下需要在保留原生View,Model类的功能情况下做一些业务拓展,这时候需要用到类的继承。

虽然es6支持extend继承,但这边我还是手写了一份。思路则是返回一个构造函数,该函数的原型为新的实例对象props,而props的原型对象则是父函数的原型(有点拗口,自己看代码理解)。
核心代码如下:

export const extend = function (props) {
  var parent = this;
  var child = function () {
    parent.apply(this, arguments);
  };
  child.prototype = Object.assign(Object.create(parent.prototype), props, { constructor: child });
  return child;
};

todomvc效果图

todomvc

源码

web前端mvc实现

小节

整篇文章基本是围绕着如下2点

  • view-model,collection-model的观察者模式的实现展开,期间view,model的销毁则取消与之有关联对象的关系,如view销毁时,注销掉与之关联的model的回调函数。
  • 监听数据变化,并通知观察者作出响应,如model变化后触发trigger('change')

好了,文章草草写到这了,多谢各位看官,以上也是纯个人观点,有问题欢迎各位web前端mvc设计指教。

webpack源码分析之六:hot module replacement

前言

在web开发中,webpack的hot module replacement(HMR)功能是可以向运行状态的应用程序定向的注入并更新已经改变的modules。它的出现可以避免像LiveReload那样,任意文件的改变而刷新整个页面。

这个特性可以极大的提升开发拥有运行状态,以及资源文件普遍过多的前端应用型网站的效率。完整介绍可以看官网文档

本文是先从使用者的角度去介绍这个功能,然后从设计者的角度去分析并拆分需要实现的功能和实现的一些细节。

功能介绍

对于使用者来说,体验到这个功能需要以下的配置。

webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');

module.exports = {
    entry: {
        app: './src/index.js'
    },
    devServer: {
        contentBase: './dist',
        hot: true,
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new HtmlWebpackPlugin()
    ],
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
};

代码: index.js 依赖print.js,使用module.hot.accept接受print.js的更新:

import './print';

if (module.hot) {
    module.hot.accept('./print', function () {
        console.log('i am updated');
    })
}



改变print.js代码:

console.log('print2')
console.log('i am change');

此时服务端向浏览器发送socket信息,浏览器收到消息后,开始下载以hash为名字的下载的json,jsonp文件,如下图:

image

浏览器会下载对应的hot-update.js,并注入运行时的应用中:

webpackHotUpdate(0,{

/***/ 30:
/***/ (function(module, exports) {

console.log('print2')
console.log('i am change');




/***/ })

})

0 代表着所属的chunkid,30代表着所属的moduleid。

替换完之后,执行module.hot.accept的回调函数,如下图:

image

简单来讲,开启了hmr功能之后,处于accepted状态的module的改动将会以jsonp的形式定向的注入到应用程序中。

一张图来表示HMR的整体流程:
image

功能分析

提出问题

当翻开bundle.js的时候,你会发现Runtime代码多了许多以下的代码:

/******/ 	function hotDownloadUpdateChunk(chunkId) {
/******/			...
/******/ 	}
/******/	function hotDownloadManifest(requestTimeout) {
/******/			...
/******/ 	}
/******
/******/ 	function hotSetStatus(newStatus) {
/******/			...
/******/ 	}
/******/ 	

打包的时候,明明只引用了4个文件,但是整个打包文件却有30个modules之多:

image

/* 30 */
/***/ (function(module, exports) {

console.log('print3')
console.log('i am change');




/***/ })

到现在你可能会有以下几个疑问:

  1. hmr模式下Runtime是怎么加上HMR Runtime代码的。
  2. 业务代码并没有打包socketjs,hot代码的,浏览器为什么会有这些代码的。
  3. 浏览器是如何判断并下载如:501eaf61104488775d2e.hot-update.json,。501eaf61104488775d2e.hot-update.js文件的,并且如何将js内容替换应用程序的内容。
  4. 编译器如何监听资源文件的变化,并将改动的文件输出到Server里面供客户端下载,如501eaf61104488775d2e.hot-update.json,0.501eaf61104488775d2e.hot-update.js。
  5. 服务器如何监听编译器的生命周期,并在其编译开始,结束的时候,向浏览器发送socket信息。
  6. 浏览器替换新的module之后,如何及时清理缓存。

分析问题

  1. Runtime代码是根据MainTemplate内部实现的,有多种场景如normal,jsonp,hot模式,则可以考虑将字符串拼接改成事件。
  2. 编译开始时候,如果是hot模式,在编译器层面将socketjs,hot代码一并打包进来。
  3. 监听文件变化,webpack 封装了watchpack模块去监听如window,linux,mac系统的文件变化
  4. 编译结束后生成hash,文件变化后对比最近一次的hash。有变动则生成新的变动文件。
  5. server层监听编译器的编译开始,结束的事件,如compile,watch,done事件,触发事件后,像浏览器发送对应的websocket消息。
  6. 浏览器接受到了websocket消息后,根据hash信息,得到[hash].hot-update.json文件,从中解析到chunkId,在根据chunkId,hash信息去下载[chunkId].[hash]-update.js。
  7. 浏览器替换新的module之前,installedModules对象中删除缓存的module,在替换后,执行__webpack_require__(id),将其并入到installedModules对象中。

功能实现

以上问题,可以从三个不同的角度去解决。server,webpack,brower。

webpack-dev-server

  • 对入口entry做包装处理,如将
entry:{app:'./src/index.js'}

,转换为

entry:{app:['/Users/zhujian/Documents/workspace/webpack/webpack-demos-master/node_modules/[email protected]@webpack-dev-server/client/index.js?http://localhost:8082'],'webpack/hot/dev-server','./src/index.js'}

构建业务代码时,附带上socketjs,hot代码。

  • 初始化服务端sockjs,并注册connection事件,向客户端发送hot信息,开启hmr功能。

Server.js

   if (this.hot) this.sockWrite([conn], 'hot');

浏览器

hot: function hot() {
    _hot = true;
    log.info('[WDS] Hot Module Replacement enabled.');
  }
  • 监听编译器的生命周期模块。

    • socket
      • 监听compiler的compile事件,通过webSocket向客户端发送invalid 信息
      • 监听compiler的done事件,通过webSocket向客户端发送still-ok,hash以及hash内容,并将所有请求资源文件设置为可用状态
  compiler.plugin('compile', invalidPlugin);
  compiler.plugin('invalid', invalidPlugin);
  compiler.plugin('done', (stats) => {
    this._sendStats(this.sockets, stats.toJson(clientStats));
    this._stats = stats;
  });
  • 资源文件锁定
    • 监听compiler的invalid,watch-run,run事件。将所有请求资源文件设置为pending状态,直到构建结束。
    • 监听compiler的done事件,将所有请求资源文件重新设置为可用状态
	context.compiler.plugin("done", share.compilerDone);
	context.compiler.plugin("invalid", share.compilerInvalid);
	context.compiler.plugin("watch-run", share.compilerInvalid);
	context.compiler.plugin("run", share.compilerInvalid);

webpack

Template

MainTemplate增加module-obj,module-require事件

module-obj事件负责生成以下代码

/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {},
/******/ 			hot: hotCreateModule(moduleId),
/******/ 			parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
/******/ 			children: []
/******/ 		};
/******/

module-require事件负责生成以下代码

/******/ 		modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));

Compiler

新增Watching类支持watch模式,并结合watchpack监听文件变化。

class Watching {
	....
}

Module

新增updateHash实现

updateHash(hash) {
   	this.updateHashWithSource(hash);
   	this.updateHashWithMeta(hash);
   	super.updateHash(hash);
   }

Chunk

新增updateHash实现

	updateHash(hash) {
		hash.update(`${this.id} `);
		hash.update(this.ids ? this.ids.join(",") : "");
		hash.update(`${this.name || ""} `);
		this._modules.forEach(m => m.updateHash(hash));
	}

Compilation

增加createHash方法,默认调用md5计算compilation hash。调用依赖树module,chunk的updateHash方法。

createHash() {
	....
}

Parser

  • 增加对ifStatement的statement类的解析支持

如:

if(module.hot){}

编译后

if(true){}

MultiEntryPlugin

  • 增加MultiEntryDependency,MultiModule,MultiModuleFactory。将数组的entry对象,打包为以下的资源文件。
entry:{app:['/Users/zhujian/Documents/workspace/webpack/webpack-demos-master/node_modules/[email protected]@webpack-dev-server/client/index.js?http://localhost:8082'],'webpack/hot/dev-server','./src/index.js'}

打包后

/* 5 */
/***/ (function(module, exports, __webpack_require__) {

// webpack-dev-server/client/index.js
__webpack_require__(6); 
//webpack/hot/dev-server
__webpack_require__(26); 
// .src/index.js
module.exports = __webpack_require__(28);

/***/ })

HotModuleReplacementPlugin

  • 监听module-require,require-extensions,hash,bootstrap,current-hash,module-obj等事件生成HMR Runtime 代码
  • 监听record事件,存储最近一次的compilation hash。
compilation.plugin("record", function(compilation, records) {
				if(records.hash === this.hash) return;
				records.hash = compilation.hash;
				records.moduleHashs = {};
				this.modules.forEach(module => {
					const identifier = module.identifier();
					const hash = require("crypto").createHash("md5");
					module.updateHash(hash);
					records.moduleHashs[identifier] = hash.digest("hex");
				});
				records.chunkHashs = {};
				this.chunks.forEach(chunk => {
					records.chunkHashs[chunk.id] = chunk.hash;
				});
				records.chunkModuleIds = {};
				this.chunks.forEach(chunk => {
					records.chunkModuleIds[chunk.id] = chunk.mapModules(m => m.id);
				});
			});
  • 监听additional-chunk-assets 事件,对比record的最近一次hash,判断变化之后。生成以[hash].hot-update.json,[chunkId].[hash].hot-update.js为名称的assets对象。
compilation.plugin("additional-chunk-assets", function() {
				....
				this.assets[filename] = source;
			});

Brower

  • 初始化runtime,将所有附加的模块代码统一增加parents,children等属性。并提供check,以及apply方法去管理hmr的生命周期。
    • check,发送http请求请求并更新manifest,请求成功之后,会将待更新的chunk hash与当前chunk hash做比较。多个chunk,则会等待相应的chunk 完成下载之后,将状态转回ready状态,表示更新已准备并可用。
    • apply,当应用状态为ready时,将所有待更新模块置为无效(清除客户端缓存),更新中调用新模块(更新缓存),更新完成之后,应用程序切回idle状态。
  • 初始化websocket,与server端建立长链接,并注册事件。如ok,invalid,hot,hash等事件。
  • 初始化hot 代码,注册事件对比新老hash,不相等则调用check方法开启模块更新功能。
module.hot.check(true).then(function(updatedModules) {
	....
})

代码实现

本人的简易版webpack实现simple-webpack

(完)

简易mvvm库的设计实现

前言

mvvm模式即model-view-viewmodel模式简称,单项/双向数据绑定的实现,让前端开发者们从繁杂的dom事件中解脱出来,很方便的处理数据和ui之间的联动。
本文将从vue的双向数据绑定入手,剖析mvvm库设计的核心代码与思路。

需求整理与分析

整理

  • 数据一旦改变则更新数据对应的ui
  • ui改变则触发事件改变ui对应的数据

分析

  • 通过dom节点的指令获取刷新函数,用来刷新指定的ui。
  • 实现一个桥接的方法,让刷新函数和需要的数据关联起来
  • 监听数据变化,数据改变后通过桥接方法调用刷新函数
  • ui改变触发对应的dom事件在改变特定的数据

实现思路

  • 实现observer,重新定义data,为data上每个属性增加setter,getter以监听数据的变化
  • 实现compile,扫描模版template,提取每个dom节点中的指令信息
  • 实现directive,通过指令信息是实例化对应的directive实例,不同类型的directive拥有不同的刷新函数update
  • 实现watcher,让observer的属性监听函数与directive的update函数做一一对应,以实现数据变化后更新视图

模块划分

MVVM目前划分为observer,compile,directive,watcher四个模块

数据监听模块observer

通过es5规范中的object.defineProperty方式实现对数据的监听
实现思路:
递归遍历data,将data下面所有属性都加上set,get方法,以实现对所有属性的拦截.
注意:对象可能含有数组属性,数组的内置有push,pop,splice等方法改变内部数据.
此时做法是改变数组的原型链,在原型链中增加一层自定义的push,pop,splice方法做拦截,这些方法里面加上我们自己的回调函数,然后在调用原生的push,pop,splice等方法.
具体可以看我上一篇文章[js对象监听实现][1]
observer.js代码

export function Observer(obj) {
    this.$observe = function(_obj) {
        var type = Object.prototype.toString.call(_obj);
        if (type == '[object Object]') {
            this.$observeObj(_obj);
        } else if (type == '[object Array]') {
            this.$cloneArray(_obj);
        }
    };

    this.$observeObj = function(obj) {
        var t = this;
        Object.keys(obj).forEach(function(prop) {
            var val = obj[prop];
            defineProperty(obj, prop, val);
            if (prop != '__observe__') {
                t.$observe(val);
            }
        });
    };

    this.$cloneArray = function(a_array) {
        var ORP = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
        var arrayProto = Array.prototype;
        var newProto = Object.create(arrayProto);
        ORP.forEach(function(prop) {
            Object.defineProperty(newProto, prop, {
                value: function(newVal) {
                    var dep = a_array.__observe__;
                    var re=arrayProto[prop].apply(a_array, arguments);
                    dep.notify();
                    return re;
                },
                enumerable: false,
                configurable: true,
                writable: true
            });
        });
        a_array.__proto__ = newProto;
    };

    this.$observe(obj, []);
}

var addObserve = function(val) {
    if (!val || typeof val != 'object') {
        return;
    }
    var dep = new Dep();
    if (isArray(val)) {
        val.__observe__ = dep;
        return dep;
    }

}

export function defineProperty(obj, prop, val) {
    if (prop == '__observe__') {
        return;
    }
    val = val || obj[prop];
    var dep = new Dep();

    obj.__observe__ = dep;
    var childDep = addObserve(val);

    Object.defineProperty(obj, prop, {
        get: function() {
            var target = Dep.target;
            if (target) {
                dep.addSub(target);
                if (childDep) {
                    childDep.addSub(target);
                }
            }
            return val;
        },
        set: function(newVal) {
            if(newVal!=val){
                val = newVal;
                dep.notify();
            }
        }
    });
}

编译模块compiler

实现思路:
1.将模版template上的dom遍历一遍,将其存入文档碎片frag
2.遍历frag,通过attributes获取节点的属性信息,在通过正则表达式过滤属性信息,进而拿到元素节点和文档节点的指令信息

 var complieTemplate = function (nodes, model) {
      if ((nodes.nodeType == 1 || nodes.nodeType == 11) && !isScript(nodes)) {
        paserNode(model, nodes);
        if (nodes.hasChildNodes()) {
          nodes.childNodes.forEach(node=> {
            complieTemplate(node, model);
          })
        }
      }
    };
    
    var paserNode = function (model, node) {
  var attributes = node.attributes || [];
  var direct_array = [];
  var scope = {
    parentNode: node.parentNode,
    nextNode: node.nextElementSibling,
    el: node,
    model: model,
    direct_array: direct_array
  };

  attributes = toArray(attributes);
  var textContent = node.textContent;
  var attrs = [];
  var vfor;

  attributes.forEach(attr => {
    var name = attr.name;
    if (isDirective(name)) {
      if (name == 'v-for') {
        vfor = attr;
      } else {
        attrs.push(attr);
      }
      removeAttribute(node, name);
    }
  });

  //bug  nodeType=3
  var textValue = stringParse(textContent);
  if (textValue) {
    attrs.push({
      name: 'v-text',
      value: textValue
    });
    node.textContent = '';
  }

  if (vfor) {
    scope.attrs = attrs;
    attrs = [vfor];
  }

  attrs.forEach(function (attr) {
    var name = attr.name;
    var val = attr.value;
    var directiveType = 'v' + /v-(\w+)/.exec(name)[1];
    var Directive = directives[directiveType];
    if (Directive) {
      direct_array.push(new Directive(val, scope));
    }
  });
};

var isDirective = function (attr) {
  return /v-(\w+)/.test(attr)
};

var isScript = function isScript(el) {
  return el.tagName === 'SCRIPT' && (
      !el.hasAttribute('type') ||
      el.getAttribute('type') === 'text/javascript'
    )
}

指令模块directive

  • 指令信息如:v-text,v-for,v-model等。
  • 每种指令信息需要的初始化动作以及指令的刷新函数update都可能不一样,所以我们把它抽象出来单独做一个模块。当然也有公用的如公共属性,统一的watcher实例化,unbind.
  • update函数则具体定义所属指令如何渲染ui

如简单的vtext指令的update函数如下:

vt.update = function (textContent) {
    this.el.textContent = textContent;
};

结构图

1014009740-585118dc043f4_articlex

数据订阅模块watcher

watcher的功能是让directive和observer模块关联起来。
初始化的时候做两件事:

  • 将directive模块的update函数当参数传入,并将其存入自身update属性中
  • 调用getValue,从而获取对象data的特定属性值,进而触发一次之前在observer定义的属性函数的getter方法。

由于在defineProperty函数中定义的dep变量在setter和getter函数里有引用,使dep变量处于闭包状态没有释放,此时在getter方法中通过判断Depend.target的存在,来获取订阅者watcher,通过发布者dep储存起来。
数据的每个属性都有一个唯一的的dep变量,记录着所有订阅者watcher的信息,一旦属性有变化,调用setter函数的时候触发dep.notify(),通知所有已订阅的watcher,进而执行所有与该属性关联的刷新函数,最后更新指定的ui。

watcher 初始化部分代码:
Depend.target = this;
this.value = this.getValue();
Depend.target = null;

observer.js 属性定义代码:
export function defineProperty(obj, prop, val) {
if (prop == 'observe') {
return;
}
val = val || obj[prop];
var dep = new Dep();

    obj.__observe__ = dep;
    var childDep = addObserve(val);

    Object.defineProperty(obj, prop, {
        get: function() {
            var target = Dep.target;
            if (target) {
                dep.addSub(target);
                if (childDep) {
                    childDep.addSub(target);
                }
            }
            return val;
        },
        set: function(newVal) {
            if(newVal!=val){
                val = newVal;
                dep.notify();
            }
        }
    });
}

流程图

简单的流程图如下:
1936111855-5855652d24460_articlex

效果图

869258730-5857caf0e1698_articlex

简易mvvm库的设计实现

总结

本文基本对mvvm库的需求整理,拆分,以及对拆分模块的逐一实现来达到整体双向绑定功能的实现,当然目前市场上的mvvm库功能绝不止于此,本文只是略举个人认为的核心代码。
如果思路和实现上的问题,也请各位斧正,谢谢阅读!

简易的前端路由实现

前言

在前端单页面应用里面,路由是比较重要的部分,现有的路由系统从简易的director.js到backbone,react等内置路由,功能一步步增强。那么这些系统原理是什么呢,本文将分析并实现一份简易的路由,以阐述其工作原理。

简易路由实现

以hash做示范,其运行机制如下:

  • 储存hash与对应的回调函数,以key,value的形式存入对象cache中
  • 设置监听函数onhashchange监听url的hash变化
  • 一旦hash变化,则遍历cache对象,将属性key做正则处理,生成对应的正则,再将其拿去和hash做正则匹配,匹配到后执行相应的value/回调函数
  • 回调函数中执行渲染ui的代码,进而更新页面

router.js

function Router() {
        this.cache = {};
        //将url/callback 以key/value形式储存在cache内
        this.on = function (key, value) {
            var cache = this.cache;
            cache[key] = value;
        };
        //匹配hash对应的回调函数,并触发
        this.trigger = function (hash) {
            var cache = this.cache;
            for (var r in cache) {
                var reg = this.initRegexps(r);
                if (reg.test(hash)) {
                    var callback = cache[r] || function () {
                        };
                    var params = this.getParams(reg, hash);
                    callback.apply(this, params);
                }
            }

        };
        //初始化 添加监听浏览器hashchange 以及dom loaded函数
        this.init = function () {
            window.addEventListener('hashchange', function () {
                var hash = location.hash.slice(1);
                router.trigger(hash);
            });
            window.addEventListener('load', function () {
                var hash = location.hash.slice(1) || 'default';
                router.trigger(hash);
            })
        };
        /**
         *将cache内的key 做正则处理,并返回
         * 第一个正则 匹配诸如/,.+-?$#{}[]] 关键字  并在关键字前面加转译字符\
         * 第二个正则 匹配() 标示()内部内容可有可无
         * 第三个正则 匹配: 在/后面可以由接受任意字符,直到遇到下一个/
         * 第四个正则 匹配* 在*后面可以由接受任意字符
         */
        this.initRegexps = function (route) {
            route = route.replace(/[/,.+\-?$#{}\[\]]/g, '\\$&')
                .replace(/\((.*?)\)/g, '(?:$1)?')
                .replace(/(\/\w?:\w+)+/g, '\/([^/]+)')
                .replace(/\*\w*/g, '([^?]*?)');

            return new RegExp('^' + route + '$');
        };

        //将匹配的正则返回,为回调函数提供参数
        this.getParams = function (reg, hash) {
            return reg.exec(hash).slice(1);
        }
    }

index.html

    <style>
        .test {
            width: 200px;
            height: 200px;
            color:white;
        }
    </style>
    
    <div>
        <a href="#/aaaa/bcd">hash=aaaa/bcd 匹配/aaaa/:id</a>
    </div>
    <div>
        <a href="#/bbbb">hash=bbbb 匹配/bbbb(/:name)</a>
    </div>
    <div>
        <a href="#/bbbb/ddd">hash=bbbb/ddd 匹配/bbbb(/:name)</a>
    </div>
    <div>
        <a href="#/cccc/s/d">hash=cccc/s/d 匹配cccc/*</a>
    </div>
    <div class="test">
    </div>
    
    <script>
            var router = new Router();
            var test = $('.test');
            router.on('/', function () {
                test.css('background-color', 'green').css('color','white').html('我是绿色');
            })
            router.on('/aaaa/:id', function (id) {
                console.log(id);
                test.css('background-color', 'red').css('color','white').html('我是红色');
            })
            router.on('/bbbb(/:name)', function (name) {
                console.log(name);
                test.css('background-color', 'yellow').css('color','red').html('我是黄色');
            })
            router.on('/cccc/*', function (x) {
                console.log(x);
                test.css('background-color', 'black').css('color','white').html('我是黑色');
            })
            router.init();
    </script>

代码地址

简易的前端路由实现
##效果图
1402976833-57c2897e6f742_articlex

流程图

2330372332-57c291e9d639d_articlex

归纳

虽然本文实现比较简单,但很多框架的内部路由也是基于这种机制,只不过有基于对自身的特性做了一些优化。

最后

本文有什么不完善的地方,或者流程图有待改进的地方,敬请斧正。

webpack源码分析之四:plugin

前言

插件plugin,webpack重要的组成部分。它以事件流的方式让用户可以直接接触到webpack的整个编译过程。plugin在编译的关键地方触发对应的事件,极大的增强了webpack的扩展性。它的出现让webpack从一个面向过程的打包工具,变成了一套完整的打包生态系统。

功能分析

Tapable

既然说到了事件流,那么就得介绍Tapable了,Tapable是webpack里面的一个小型库,它允许你自定义一个事件,并在触发后访问到触发者的上下文。当然他也支持异步触发,多个事件同步,异步触发。本次实现用的是较早的v0.1.9版,具体文档可查看tapable v0.19文档

在webpack内使用,如SingleEntryPlugin中

compiler.plugin("make",function(compilation,callback){
   compilation.addEntry(this.context, new SingleEntryDependency({request: this.entry}), this.name, callback);
})

在compiler内部触发。

 this.applyPluginsParallel('make',compilation, err => {
 	/* do something */
 })

解析入口文件时,通过EntryOptionPlugin解析entry类型并实例化SingleEntryPlugin, SingleEntryPlugin在调用compilation的addEntry函数开启编译。这种观察者模式的设计,解耦了compiler, compilation,并使它们提供的功能更加纯粹,进而增加扩展性。

流程划分

纵观整个打包过程,可以流程划分为四块。

  1. 初始化
  2. 构建
  3. 封装
  4. 文件写入

模块划分

接入plugin后,webpack对parse,resolve,build,writeSource等功能的大规模重构。
目前拆分模块为

  • Parser模块,负责编译module。
  • Resolver模块,负责对文件路径进行解析。
  • ModuleFactory模块,负责完成module的实例化。
  • Module模块,负责解析出modules依赖,chunk依赖。构建出打包后自身module的源码。
  • Template模块,负责提供bundle,chunk模块文件写入的模版。
  • Compilation模块,负责文件编译细节,构建并封装出assets对象供Compiler模块进行文件写入。
  • Compiler模块,负责实例化compilation,bundle文件的写入。监听modules的变化,并重新编译。

核心类关系图

webpack-plugin-module

功能实现

Parser模块

通过exprima将源码解析为AST树,并拆分statements,以及expression直至Identifier基础模块。

  1. 解析到CallExpression时触发call事件。
  2. 解析到MemberExpression,Identifier时触发expression事件。
  3. 提供evaluateExpression函数,订阅Literal,ArrayExpression,CallExpression,ConditionalExpression等颗粒化的事件供evaluateExpression调用。
 case 'CallExpression':
    		//do something
        	this.applyPluginsBailResult('call ' + calleeName, expression);
    		//do something
            break;
 case 'MemberExpression':
            //do something
            this.applyPluginsBailResult('expression ' + memberName, expression);
            //do something
            break;
 case 'Identifier':
            //do something
            this.applyPluginsBailResult('expression ' + idenName, expression);
           	//do something
            break;           
 this.plugin('evaluate Literal', (expr) => {})
 this.plugin('evaluate ArrayExpression', (expr) => {})
 this.plugin('evaluate CallExpression', (expr) => {})
 ...

如需要解析require("a"),require.ensure(["b"],function(){})的时候,注册plugin去订阅"call require",以及"call require.ensure",再在回调函数调用evaluateExpression解析expression。

Resolver模块

封装在enhanced-resolve库,提供异步解析文件路径,以及可配置的filestream能力。在webpack用于缓存文件流以及以下三种类型模块的路径解析。

  • 普通的module模块
  • 带context的module模块
  • loader模块

用法如

ResolverFactory.createResolver(Object.assign({
            fileSystem: compiler.inputFileSystem,
            resolveToContext: true
        }, options.resolve));

具体配置可去查看github文档

ModuleFactory模块

子类有NormalModuleFactory,ContextModuleFactory。常用的NormalModuleFactory功能如下

  1. 实例化module之前,调用Resolver模块解析出module和preloaders的绝对路径。
  2. 通过正则匹配module文件名,匹配出rules内的loaders,并和preloaders合并。
  3. 实例化module

这里主要是使用async库的parallel函数并行的解析loaders和module的路径,并整合运行结果。

async.parallel([
                (callback) => {
                    this.requestResolverArray( context, loader, resolver, callback)
                },
                (callback) => {
                    resolver.normal.resolve({}, context, req, function (err, result) {
                        callback(null, result)
                    });
                },
            ], (err, result) => {
             	   let loaders = result[0];
                const resource = result[1];
                //do something
            })

async模块是一整套异步编程的解决方案。async官方文档

Module模块

  1. 运行loaders数组内的函数,支持同步,异步loaders,得到编译前源码。
  2. 源码交由Parser进行解析,分析出modules依赖和blocks切割文件依赖
  3. 提供替换函数,将源码替换,如require('./a')替换为__webpack_require__(1)

一个编译好的module对象包含modules依赖ModuleDependency和blocks依赖RequireEnsureDependenciesBlock,loaders,源码_source,其数据结构如下:

{
  chunks: [],
  id: null,
  parser: 
   Tapable {
     _plugins: 
      { 'evaluate Literal': [Array],
        'evaluate ArrayExpression': [Array],
        'evaluate CallExpression': [Array],
        'call require': [Array],
        'call require:commonjs:item': [Array],
        'call require.ensure': [Array] },
     options: {},
     scope: { declarations: [] },
     state: { current: [Circular], module: [Circular] },
     _currentPluginApply: undefined },
  fileDependencies: 
   [ '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js' ],
  dependencies: 
   [ ModuleDependency {
       request: './module!d',
       range: [Array],
       class: [Function: ModuleDependency],
       type: 'cms require' },
     ModuleDependency {
       request: './assets/test',
       range: [Array],
       class: [Function: ModuleDependency],
       type: 'cms require' } ],
  blocks: 
   [ RequireEnsureDependenciesBlock {
       blocks: [],
       dependencies: [Array],
       requires: [Array],
       chunkName: '',
       beforeRange: [Array],
       afterRange: [Array] } ],
  loaders: [],
  request: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js',
  fileName: 'a.js',
  requires: [ [ 0, 7 ], [ 23, 30 ] ],
  context: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example',
  built: true,
  _source: 
   RawSource {
     _result: 
      { source: 'require(\'./module!d\');\nrequire(\'./assets/test\');\nrequire.ensure([\'./e\',\'./b\'], function () {\n    console.log(1)\n    console.log(1)\n    console.log(1)\n    console.log(1)\n    require(\'./m\');\n    require(\'./e\');\n});\n' },
     _source: 'require(\'./module!d\');\nrequire(\'./assets/test\');\nrequire.ensure([\'./e\',\'./b\'], function () {\n    console.log(1)\n    console.log(1)\n    console.log(1)\n    console.log(1)\n    require(\'./m\');\n    require(\'./e\');\n});\n' 
 			} 
     }

Compilation模块

  1. 通过entry和context,获取到入口module对象,并创建入口chunk。
  2. 通过module的modules依赖和blocks切割文件构建出含有chunk和modules包含关系的chunk对象。
  3. 给modules和chunks的排序并生成id,触发一系列optimize相关的事件(如CommonsChunkPlugin就是使用optimize-chunks事件进行开发),最终构建出有文件名和源码映射关系的assets对象

一个典型的含有切割文件的多入口entry的assets对象数据结构如下:

assets: 
   { '0.bundle.js': 
      Chunk {
        name: '',
        parents: [Array],
        modules: [Array],
        id: 0,
        source: [Object] },
     'main.bundle.js': 
      Chunk {
        name: 'main',
        parents: [],
        modules: [Array],
        id: 1,
        entry: true,
        chunks: [Array],
        blocks: true,
        source: [Object] },
     'multiple.bundle.js': 
      Chunk {
        name: 'multiple',
        parents: [],
        modules: [Array],
        id: 2,
        entry: true,
        chunks: [Array],
        source: [Object] } 
  }

Compiler模块

  1. 解析CLI, webpack配置获取options对象,初始化resolver,parser对象。
  2. 实例化compilation对象,触发make 并行事件调用compilation对象的addEntry开启编译。
  3. 获取到assets对象,通过触发before-emit事件开启文件写入。通过JsonMainTemplate模版完成主入口bundle文件的写入,JsonpChunkTemplate模版完成chunk切割文件的写入。 使用async.forEach管理异步多文件写入的结果。
  4. 监听modules的变化,并重新编译。

考虑到多入口entry的可能,make调用的是并行异步事件

this.applyPluginsParallel('make', compilation, err => {
	//do something
	compilation.seal(err=>{})
	//do something
}

代码实现

本人的简易版webpack实现simple-webpack

总结

相信大家都有设计过业务/开源代码,很多情况是越往后写,越难维护。一次次的定制化的需求,将原有的设计改的支离破碎。这个时候可以试试借鉴webpak的**,充分思考并抽象出稳定的基础模块,划分生命周期,将模块之间的业务逻辑,特殊需求交由插件去解决。

完。

es6 promise源码实现

promise源码分析

初级入门以及如何使用请看 阮一峰promise对象讲解

先上一坨代码,后面我们要基于这坨代码来实现自定义promise

原始方法

setTimeout(function(){
    var a=100;
    console.log(a);
    setTimeout(function () {
        var b=200;
        console.log(b)
        setTimeout(function () {
            var c=300;
            console.log(c)
        }, 1000);
    }, 1000);
},1000);   

promise实现

 new Promise(function (resolve, reject) {
    setTimeout(function () {
        var a=100;
        resolve(a);
    }, 1000);
}).then(function (res) {
    console.log(res);
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            var b=200;
            resolve(b);
        }, 1000);
    })
}).then(function (res) {
    console.log(res);
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            var c=300
            resolve(c);
        }, 1000);
    })
}).then(function (res) {
        console.log(res);
    }
    )

如何让你的promise能有此魔力

  • 让a,b,c的值能在then里面的回调接收到
  • 在连续调用异步,如何确保异步函数的执行顺序
    ####如何让异步的value在thenable函数中拿到
  • 将resolve/reject函数和onfulfiled/onrejected放入同一个对象(promise对象)里面,resolve/reject的时候将value设置this.value=xxx。onfulfiled/onrejected执行的时候呢,onfulfiled(this.value)即可
    ####如何处理链式的promise且保证顺序
  • 每个promise后面链一个对象该对象包含onfulfiled,onfulfiled,子promise三个属性.
    当父promise 状态改变完毕,执行完相应的onfulfiled/onfulfiled的时候呢,拿到子promise,在等待这个子promise状态改变,在执行相应的onfulfiled/onfulfiled。依次循环直到当前promise没有子promise

最终代码(内涵注释)

            /*
            我们要满足状态只能三种状态:PENDING,FULFILLED,REJECTED三种状态,且状态只能由PENDING=>FULFILLED,或者PENDING=>REJECTED
            */
            var PENDING = 0;
            var FULFILLED = 1;
            var REJECTED = 2;
            /*
            value状态为执行成功事件的入参,deferreds保存着状态改变之后的需要处理的函数以及promise子节点,构造函数里面应该包含这三个属性的初始化
             */
            function Promise(callback) {
                this.status = PENDING;
                this.value = null;
                this.defferd = [];
                setTimeout(callback.bind(this, this.resolve.bind(this), this.reject.bind(this)), 0);
            }
            
            Promise.prototype = {
                constructor: Promise,
                //触发改变promise状态到FULFILLED
                resolve: function (result) {
                    this.status = FULFILLED;
                    this.value = result;
                    this.done();
                },
                //触发改变promise状态到REJECTED
                reject: function (error) {
                    this.status = REJECTED;
                    this.value = error;
                },
                //处理defferd
                handle: function (fn) {
                    if (!fn) {
                        return;
                    }
                    var value = this.value;
                    var t = this.status;
                    var p;
                    if (t == PENDING) {
                         this.defferd.push(fn);
                    } else {
                        if (t == FULFILLED && typeof fn.onfulfiled == 'function') {
                            p = fn.onfulfiled(value);
                        }
                        if (t == REJECTED && typeof fn.onrejected == 'function') {
                            p = fn.onrejected(value);
                        }
                    var promise = fn.promise;
                    if (promise) {
                        if (p && p.constructor == Promise) {
                            p.defferd = promise.defferd;
                        } else {
                            p = this;
                            p.defferd = promise.defferd;
                            this.done();
                        }
                    }
                    }
                },
                //触发promise defferd里面需要执行的函数
                done: function () {
                    var status = this.status;
                    if (status == PENDING) {
                        return;
                    }
                    var defferd = this.defferd;
                    for (var i = 0; i < defferd.length; i++) {
                        this.handle(defferd[i]);
                    }
                },
                /*储存then函数里面的事件
                返回promise对象
                defferd函数当前promise对象里面
                */
                then: function (success, fail) {
                   var o = {
                        onfulfiled: success,
                        onrejected: fail
                    };
                    var status = this.status;
                    o.promise = new this.constructor(function () {
            
                    });
                    if (status == PENDING) {
                        this.defferd.push(o);
                    } else if (status == FULFILLED || status == REJECTED) {
                        this.handle(o);
                    }
                    return o.promise;
                }
            };

在附上一张手绘的流程图

2873136445-5799d21591606_articlex

参考资料

源码地址

webpack源码分析之一:文件打包

前言

自动化打包工具webpack,相信很多人和我一样尝试着研究下它,但是繁杂的功能以及高度抽象的代码实在是很难理解,所以笔者只能通过github的webpack的第一次提交进行分析,实现,并将实现的一些心得分享一下。

功能分析

对于node端来讲,有commonjs来规范模块的标识,定义,引用。而浏览器端由于缺乏原生对象支持就需要通过自我实现来模拟commonjs规范。
webpack是通过一个IIFE立即调用函数表达式去实现这个规范的。简要的去注释,去除内部运行的代码,其格式如下:

(function(module){})([function(){},function(){}]);

简单点说就是各个模块代码以数组的形式传递给运行函数,在进行存储。详细分析可以参考简要分析webpack打包后代码

所以实现以上的功能需求点如下:

  • 文件路径分析与定位resolve
  • 文件编译&解析,分析出依赖文件parse
  • 生成需要打包的文件树depTree
  • 将依赖文件写入输出文件内writeChunk

文件分析与定位

本功能和node的require类似,故有参考node require源码

文件分析,将文件为两种类型

  • 以 "./","../","/" 标识符开头的路径文件模块
    • 该类文件会通过path.join 转化为真实的路径而定位。
  • 自定义的文件模块
    • 这类相对比较麻烦,他在当前目录下面的node_modules,查找文件,未找到则一路向上查找,最终查找到或者抛出异常。如:
[ '/Users/zhujian/Documents/workspace/webpack/simple-webpack/node_modules',
  '/Users/zhujian/Documents/workspace/webpack/node_modules',
  '/Users/zhujian/Documents/workspace/node_modules',
  '/Users/zhujian/Documents/node_modules',
  '/Users/zhujian/node_modules',
  '/Users/node_modules',
  '/node_modules' ]

文件定位

  • 对于部分文件并没有带扩展名,此时有默认的扩展名依次以.js,.jsx为后缀依次补充。当然我们可以用传入extensions,修改默认的扩展名。
{extensions:['js','jsx','jpg']}
  • 当发现该路径为文件夹时则,则依次查找如下文件
    • package.json(main字段)
    • index+(扩展名)

文件解析

  • 文件可以定位之后,则是解析定位下来的文件了,本文用的是exprima,文档如parser规范文档
    • esprima解析文件,返回一个语法树。
    • 对语法树进行遍历,对遇到type 为CallExpression,且其callee为name为require的节点,将该节点的value,以及下标包装成对象储存起来。

比如

const b = require('./b');

解析后

....
"init": {
        "type": "CallExpression",
        "callee": {
            "type": "Identifier",
            "name": "require",
            "range": [
                10,
                17
            ]
        },
        "arguments": [
            {
                "type": "Literal",
                "value": "./b",
                "raw": "'./b'",
                "range": [
                    18,
                    23
                ]
            }
        ],
        "range": [
            10,
            24
        ]
    },
 ....   

我们要做的就是提取value "./b",以及该字符串在文件所处的位置range。

文件树生成

主要是从入口文件开始,将所有依赖的js,以及其内容,分配的id组成一个可操作的扁平化的对象和存储着name与id对应的map对象。

实现手法上也是递归resolve函数,获取到各个文件的依赖,文件,id的信息,最后得到depTree对象

举个例子:

{ modules: 
   { '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js',
        id: 0,
        requires: [Array],
        rangeRequires: [Array],
        source: 'const b = require(\'./b\');\nconst c = require(\'c\');\nconst {e, f, g} = require(\'./m\');\n\n 
      },
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js',
        id: 1,
        requires: [],
        rangeRequires: [],
        source: 'const b = \'b\';\n\nmodule.exports = b;\n' 
      },
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js',
        id: 2,
        requires: [],
        rangeRequires: [],
        source: 'const c = \'c\';\n\nmodule.exports = c;\n' 
      },
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js',
        id: 3,
        requires: [],
        rangeRequires: [],
        source: '// const core = require(\'./core\');\nconst a = 1;\n\n
      },
  nextModuleId: 4,
  mapNameToId: 
   { '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js': 0,
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js': 1,
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js': 2,
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js': 3 
 	} 
 }

文件写入

写入主函数,替换入口的执行函数。这块会用到之前的path和id关系的map对象,通过路口文件的绝对路径,找出入口文件的mainId,并进行替换。

写入参数数组。遍历文件树,将文件节点的source内容替换掉

大致如下:

require('module') 替换为__webpack_require__(0)
  • 这个地方要考虑的点是
    • 如果用replace替换的话,会影响source带部分关键字的内容,不可取。
    • 用索引替换的字符串的话,一旦第一个替换成功,整个字符串长度发生变化,原先的索引下标就失效了。

官方实现

 	const result = [source];
    replaces.forEach(replace => {
        const {from, value, end} = replace;
        const source = result.shift();
        result.unshift(source.substr(0, from), value, source.substr(end))
    });

代码实现

本人的简易版webpack实现simple-webpack

(完)

参考资料

  1. webpack早期源码
  2. require源码解读
  3. node require源码
  4. parser规范文档
  5. 简要分析webpack打包后代码

webpack源码分析之二:code-splitting

前言

code-splitting是webpack最引人瞩目的特性之一,此特性将代码分离到不同的bundle文件中。详细介绍官网code-split,这次实现则在笔者上次文件打包之上做开发。

功能分析

官网上有三种方式实现

  1. 入口起点:使用 entry 选项手动分离代码。
  2. 防止重复:使用 CommonsChunkPlugin 去重和分离 chunk。
  3. 动态导入:通过模块的内联函数调用来分离代码。

1本质则是多个入口的chunk,2则在以common.js为入口文件,将多入口的chunk切分为按切割文件,通过jsonp加载。在这里笔者则介绍最为复杂的3的实现,

对于webpack 的切割文件的引入本质就是jsonp,动态引入一个约定好格式的js,并运行。

__webpack_require__.e = function requireEnsure(chunkId) {
....
	var head = document.getElementsByTagName('head')[0];
	var script = document.createElement('script');
	script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
	head.appendChild(script);
....
}

切割文件去除注释如下:

webpackJsonp([1],[function(){},function(){}])

而在入口文件的webpackJsonpCallback函数内,则是将切割的文件包含的modules依次放入存储在modules内

function webpackJsonpCallback(chunkIds, moreModules){
....
for(moduleId in moreModules) {
 			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
 				modules[moduleId] = moreModules[moduleId];
 			}
 		}
} 	
....	

所以实现以上功能需求如下:

所以实现以上功能需求如下:

  1. parse模块:定位切割点并组装异步加载文件所需要的依赖。
  2. chunks模块:各个chunk包含module模块的集合,通过文件树里面模块的依赖关系生成。
  3. writeChunks模块:根据chunks,通过文件流写入文件。

例子

require('d');

function a() {
    require.ensure(['./a'], function () {
        require('c');
    });
}

require.ensure(['./b'], function () {
    require('./m');
});

require('./e');

实现

parse模块

实现思路:

  1. 通过递归,以及文件树的特征定位到require.ensure
  2. 将arguments第一个参数的数组,第二个参数的函数内递归搜索require,存入数组asyncs内,并递归下去

数据结构如下:

{
    filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/main.js',
    id: 0,
    requires: [{
            name: 'd',
            range: [8, 11],
            id: 1
        }],
    rangeRequires: [[0, 7]],
    asyncs: [{
            requires: [{
                    name: './a',
                    id: 2
                }, {
                    name: 'c',
                    range: [88, 91],
                    id: 3
                }],
            asyncs: [],
            rangeRequires: [80, 87],
            ensureRequires: [34, 58]
        },
        {
            requires: [{
                    name: './b',
                    id: 4
                }, {
                    name: './m',
                    range: [156, 161],
                    id: 5
                }],
            asyncs: [],
            rangeRequires: [148, 155],
            ensureRequires: [106, 130]
        }],
}

chunks模块

由于各个依赖文件的源码都包含在modlues内,所以chunks包含的是具体各个切割文件所包需module的moduleId。

实现思路:

  1. 通过入口mainPath 找到modules的入口mainModule
  2. 将mainModule 的requires遍历,将值归入本chunk的modules内,将asyncs遍历,依次新建chunk,并关联父chunk,以上两个依次递归遍历。
  3. 最终生成完了之后,将各个非根节点的chunk遍历,将依赖的modules遍历对比父节点的chunk,如有重复标记'in-parent'

数据结构如下

{ '0': 
   { id: 0,
     modules: { '0': 'include', '1': 'include', '2': 'include' } },
  '1': 
   { id: 1,
     modules: 
      { '1': 'in-parent',
        '3': 'include',
        '4': 'include',
        '5': 'include',
        '6': 'include' },
     parentId: 0 },
  '2': 
   { id: 2,
     modules: { '5': 'include', '6': 'include' },
     parentId: 0 
 	}
}


writeChunks模块

实现思路:

  1. 如果chunks的个数超过1,入口chunk则加载包含webpackJsonp,webpack_require.e等支持jsonp函数的模版,未超过则加载简单的仅包含__webpack_require__的模版
  2. 加载非入口chunk则头部加载webpackJsonp,以及标示chunkid
  3. webpackJsonp的入参有两种,一种数组,一种以moduleId为key的对象。为数组时候则需要将以[,,modlue]等方式保证顺序
    如:
    2018-5-26 8 26

代码实现

本人的简易版webpack实现simple-webpack

(完)

webpack源码分析之三:loader

前言

在webpack特性里面,它可以支持将非javaScript文件打包,但前面写到webpack的模块化打包只能应用于含有特定规范的JavaScript文件。本次介绍的loader则是用来解决这类问题的。本文章loader的实现基于code-splitting

功能分析

举个例子:

webpack.config.js中的配置loader

	module: {
            rules: [
                {
                    test: /\.js$/,
                    loader: "test-loader!test-loader2"
                }
            ]
        }

业务代码中的内联loader

require('d!c');

分析:

我们需要将这些loader解析成可运行的函数,并在parse模块解析语法树之前运行掉这些loader函数

所以我们需要:

  1. resolve模块:分析出module对应的loader字符串,并解析出各个loader的绝对路径
  2. buildDeps模块:通过文件路径获取需要运行的loader函数,将其压入队列,之后再依次按序递归出loader函数运行,如果是异步loader,则要通过回调函数来递归下一个loader函数。

实现

resolve 模块

实现思路:

  1. 将配置内的loaders,shell命令的loaders,require/import的内联loader从前至后组成一个数组。配置内的loaders需要正则匹配test属性,来获取配置内的loader字符串。所有loader字符串内部又可以截取'!',获取完整的loader。
  2. 分析并解析该数组内的字符串,获取各个loader的绝对路径。并返回解析好的字符串。这块的实现和文件打包类似。

最终require内的字符串如下

/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/d.js!
/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/test-loader/index.js!
/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/test-loader2/index.js!
/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js

buildDeps模块

实现思路:

  1. 解析文件路径,并获取需要运行的loader函数,存入数组
  2. 数组在通过pop函数一个个递归,考虑到存在异步loader函数的情况,需要为运行函数提供async,以及callback的上下文。具体的上下文可参考Loader API

loader递归逻辑如下:

	nextLoader.apply(null, content);
	function nextLoader() {
        const args = Array.prototype.slice.apply(arguments);
        if (loaderFunctions.length > 0) {
            const loaderFunction = loaderFunctions.pop();
            let async = false;
            const context = {
                fileName,
                options,
                debug: options.debug,
                async: function () {
                    async = true;
                    return nextLoader;
                },
                callback: function () {
                    async = true;
                    nextLoader.apply(null, arguments)
                }
            };
            const resVal = loaderFunction.apply(context, args);
            if (!async) {
                nextLoader(resVal);
            }
        } else {
            callback(null, args[0])
        }
    }

测试

将以上3个loader,test-loader,test-loader2,异步loader d.js,打包c.js

test-loader

module.exports = function(content) {
    return content+"\nexports.answer = 42;\n"
}

test-loader2

module.exports = function(content) {
    return content+"\nexports.test2 = test2;\n"
}

异步loader d.js

module.exports = function (content) {
    const d = 'd';
    this.async();
    const b = content + "\nexports.d = 2000;\n";
    setTimeout(this.callback.bind(this, b), 2000);
}

c.js

const c = 'c';

module.exports = c;

最终打包出来的c.js的代码如下

....
/* 1 */
/***/(function(module, exports,__webpack_require__) {
const c = 'c';

module.exports = c;

exports.test2 = test2;

exports.answer = 42;

exports.d = 2000;

/***/}
....

代码实现

本人的简易版webpack实现simple-webpack

(完)

使用typescript开发前端组件

背景

goc前端分工越来越细,一个前端应用划分为通用的工具类,通用的组件库,业务组件库,以及业务代码。在下游写业务代码,写组件库的时候是不清楚上游的依赖函数的入参,出参。口头问问或者去翻一翻源码,需要沟通成本。typescript集成的静态类型可以解决这种情况。

typescript是什么

TypeScript=Type+Script 是微软开发一款开源的编程语言。
官方的定义TypeScript is a typed superset of JavaScript that compiles to plain JavaScript官方文档.翻译下就是TypeScript是一个编译到纯JS的有类型定义的JS超集。

特性

类型批注,类型推导,编译时类型检查

const x: number = '123';
//error

function foo(flagA: boolean, flagB: string):string {
    return ''
}

var y=1;
const z:string=y;
//error

接口

普通使用

类型命名的一种表现形式。其中?:代表可选属性。

interface NameObject {
 name: string;
 nickName?:string; 
}

function printLabel(label: NameObject) {
  console.log(label.name);
}

继承

方便接口之间的复用,比如模糊搜索的接口参数可以继承antd select的接口

interface Names extends NameObject {
    num: number;
}

实现

类似C#和java,他也可以强制一个类去实现类型规范

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

泛型

和java类似,更好的重用组件。

泛型类型

确保返回值的类型与传入参数的类型是相同。

function identity<T>(arg: T): T {
    return arg;
}

泛型类

确保类所有的属性使用相同的类型

class Queue<T> {
  private data: T[] = [];
  push = (item: T) => this.data.push(item);
  pop = (): T | undefined => this.data.shift();
}

// 简单的使用
const queue = new Queue<string>();
queue.push('1');

枚举

enum Color {
    Red=3,
    Green,
    Blue
}

typescript优势

基于以上特性可以知道使用时的优势

  • 编译阶段就暴露问题,提升代码的健壮性。
  • 代码可读性比较好,提升代码的易用性。

如何使用typescript

已有类库typescript申明

需要对已有的上游依赖库进行typescript申明。以goc的goc-request库为例:需要在文件夹下新建xxx.d.ts的申明文件,对对外提供的函数进行ts申明,并在根目录的package.json文件下补充对应的文件地址。如"types": "types/index.d.ts"。

tsconfg.json的配置

通用配置可以直接查询官方文档

编译

ts自带了tsc编译器,可以将其编译成直接执行的js代码。使用webpack+babel体系的话,需要@babel/preset-typescript插件。 使用gulp的话gulp-typescript转化。我们使用的是antd-tools工具,它内部封装了gulp并使用了gulp-typescript去编译。

踩过的坑

类型强制转换问题

一个变量可能有多种类型的时候,需要强制转换类型。

type fetchValue = string | string[] | number | number[];

function show(value: fetchValue) {
    (value as number[]).forEach(v => {
        console.log(v)
    })
}

show([1,2])

在webpack体系下面用fork-ts-checker-webpack-plugin去开启webpack的校验,才能在编译出错时候阻断编译。

//webpack.config.js
  const FilterWarningsPlugin = require('webpack-filter-warnings-plugin');

  plugins: [
    new ForkTsCheckerWebpackPlugin({
      eslint: true
    }),
  ],

(完)

webpack源码分析之五:sourcemap

前言

经过webpack打包压缩之后的javascript, css文件和原有构建之前的文件相差比较大,对于开发者而言比较难在客户端上调试定位问题。为了解决这类问题,webpack引入了source maps。

source maps是一个存储了打包前所有代码的行,列信息的mapping文件,其核心的mapping属性以;,以及VLQ编码的字母记录着转换前的代码的位置。

本文重点不在sourcemap的算法实现。而是重点介绍的是基于github source-map之上,在webpack中的工程实现。
SourceNode是该库生成mapping文件的核心。所以先介绍SourceNode。

功能分析

SourceNode

它提供了一种抽象方法将含有行,列关系的多个代码片以插入或者连接的形式组合成含有这些代码的行,列信息的对象。

举个例子:

一个A.js 文件内容如下:

var a = 1;

console.log(a);

调用SourceNode方法

const SourceNode = require("source-map").SourceNode;

var node = new SourceNode(null, null, null, [
    new SourceNode(1, 0, 'webpack:///./example/a.js', "var a = 1;\n"),
    '\n',
    new SourceNode(3, 0, 'webpack:///./example/a.js', 'console.log(a);"')
]);

node.setSourceContent('index.js', 'var a = 1;\n\nconsole.log(a);')
const t = node.toStringWithSourceMap({file: "index.js"})
const map = t.map.toJSON();
map.sourceRoot = '/';
console.log(JSON.stringify(map))

生成的mapping数据结构如下:

{
"version":3,
"sources":["webpack:///./example/a.js"],
"names":[],
"mappings":"AAAA;;AAEA",
"file":"index.js",
"sourcesContent":["var a = 1;\n\nconsole.log(a);"],
"sourceRoot":"/"
}

在chrome下的调试模式效果如下:

image

了解了SourceNode之后,往下接可以讲在webpack内的实现了。

功能实现

首先得抽象一个模块originalSource,它负责记录源代码的行,列信息,并通过这些信息结合源代码生成出mapping对象。

originalSource实现

行信息

将源码以\n形式分割组合成数组,在通过数组索引来确定行信息。

以上图A.js为例,表示3行信息的代码如下:

 new SourceNode(1, 0, 'webpack:///./example/a.js', "var a = 1;\n") //第1行
 \n //第2行
 new SourceNode(3, 0, 'webpack:///./example/a.js', 'console.log(a);"')//第3行

列信息

将行信息的代码块以(;,{,})进行分割组成数组后在通过每次分割的字符串长度来确定列信息。

源码

var a = 1;var b=2;{a=b}

生成soucemap代码

 new SourceNode(1, 0, 'webpack:///./example/a.js', "var a = 1;") //第0列
 new SourceNode(1, 10, 'webpack:///./example/a.js', "var b=2;{") //第10列
 new SourceNode(1, 19, 'webpack:///./example/a.js', "a=b}") //第19列

光标下标可以在第10列上的效果如下:
image

故基于行,列信息生产mapping对象的核心代码如下:

node(options) {
        const value = this._value;
        const name = this._name;
        const lines = value.split('\n');
        const len = lines.length;
        const columns = options.columns;
        const node = new SourceNode(null, null, null, lines.map((line, idx) => {
            let i = 0;
            const content = idx !== len - 1 ? line + '\n' : line;
            if (/^\s*$/.test(content)) return content;
            if (columns === false) {
                return new SourceNode(idx + 1, 0, name, content);
            }
            return new SourceNode(null, null, null, _splitCode(content).map(item => {
                const result= new SourceNode(idx + 1, i, name, item);
                i = i + item.length;
                return result
            }))
        }));

        node.setSourceContent(name, value);
        return node;
    }

上图A.js的在originalSource下的数据结构如下

SourceNode {
  children: [ 
	  SourceNode {
       children: [
		  {
		    "children": [
		      "var a = 1;\n"
		    ],
		    "sourceContents": {},
		    "line": 1,
		    "column": 0,
		    "source": "/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js",
		    "name": null,
		    "$$$isSourceNode$$$": true
		  }
		],
       sourceContents: {},
       line: null,
       column: null,
       source: null,
       name: null,
       '$$$isSourceNode$$$': true 
      },
     '\n',
     SourceNode {
       children: [
		  {
		    "children": [
		      "console.log(a);\n"
		    ],
		    "sourceContents": {},
		    "line": 3,
		    "column": 0,
		    "source": "/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js",
		    "name": null,
		    "$$$isSourceNode$$$": true
		  }
		],
       sourceContents: {},
       line: null,
       column: null,
       source: null,
       name: null,
       '$$$isSourceNode$$$': true } ],
  sourceContents: 
   { '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js': 'var a = 1;\n\nconsole.log(a);\n' },
  line: null,
  column: null,
  source: null,
  name: null,
  '$$$isSourceNode$$$': true }

webpack其他的source实例如cachedSource,prefixSource,concatSource,replaceSource除去一些本生特性之外,底层都是调用originalSource实现。

另外webpack在不展示columns的情况下,优先使用source-map-list,这块实现是参考mozilla的github source-map实现,并结合自身情况,去优化生成sourcemap的性能。如大规模的字符串split使用递归indexOf和substr去替代。

proto.sourceAndMap = function(options) {
		options = options || {};
		if(options.columns === false) {
			//console.log(this.listMap(options).debugInfo());
			return this.listMap(options).toStringWithSourceMap({
				file: "x"
			});
		}
		const temp=this.node(options)
		var res = this.node(options).toStringWithSourceMap({
			file: "x"
		});
		return {
			source: res.code,
			map: res.map.toJSON()
		};
	};

对这块有兴趣的可以到webpack-sources去深入探索

plugins实现

webpack通过SourceMapDevToolPlugin,EvalSourceMapDevToolPlugin,EvalDevToolModulePlugin这三个插件以及devtool对外输出sourcemap。devtool本质也是indexOf截取不同的关键字而实例化不同的plugin类

if(options.devtool && (options.devtool.indexOf("sourcemap") >= 0 || options.devtool.indexOf("source-map") >= 0)) {
			...
			comment = legacy && modern ? "\n/*\n//@ source" + "MappingURL=[url]\n//# source" + "MappingURL=[url]\n*/" :
				legacy ? "\n/*\n//@ source" + "MappingURL=[url]\n*/" :
				modern ? "\n//# source" + "MappingURL=[url]" :
				null;
			let Plugin = evalWrapped ? EvalSourceMapDevToolPlugin : SourceMapDevToolPlugin;
			compiler.apply(new Plugin({
				filename: inline ? null : options.output.sourceMapFilename,
				moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
				fallbackModuleFilenameTemplate: options.output.devtoolFallbackModuleFilenameTemplate,
				append: hidden ? false : comment,
				module: moduleMaps ? true : cheap ? false : true,
				columns: cheap ? false : true,
				lineToLine: options.output.devtoolLineToLine,
				noSources: noSources,
			}));
			} else if(options.devtool && options.devtool.indexOf("eval") >= 0) {
			...
			compiler.apply(new EvalDevToolModulePlugin(comment, options.output.devtoolModuleFilenameTemplate));
		}

对于这几个插件,实现思路如下

  1. 含有sourcemap信息的插件需要取source以及对应sourcemap(EvalDevToolModulePlugin不含sourcemap信息除外),eval模式每个module源码用eval包裹源码。

  2. 文件名生成。如需要将真实路径/Users/zhujian/Documents/workspace/webpack/webpack-demos-master/src/index.js转变为goc:///./src/index.js
    image

  3. 文件底部信息WEBPACK FOOTER生成。展示信息有原始文件名,moduleId,chunkId。

SourceMapDevToolPlugin

整个webpack的打包流程完毕compilation的after-optimize-chunk-asset阶段的source作为原始源码。拼凑bundle文件底部map信息,修改map文件文件名。以devtool为source-map为例生成文件如下

bundle文件

(function(module, exports) {
var a = 1;
console.log(a);
 })
 ]);
//# sourceMappingURL=main.output.js.map

map文件

{
"version":3,
"sources":["goc:///webpack/bootstrap 0ee5c00ca3b31f99a2e0?","goc:///./src/index.js?"],
"names":[],
"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;AC7DA;;AAEA",
"file":"main.output.js",
"sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/dist/\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 0ee5c00ca3b31f99a2e0","var a = 1;\n\nconsole.log(a);\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = 0\n// module chunks = 0"],
"sourceRoot":""
}

EvalSourceMapDevToolPlugin

每个module构建完成,moduleTemplate的module阶段的source作为原始源码,以module id做为文件名,将得到的sourcemap以base64形式存储在sourceMappingURL内,并紧跟在module源码后面。bundle文件如下

eval("var a = 1;\n\nconsole.log(a);" +
    "\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImdvYzovLy8uL3NyYy9pbmRleC5qcz8iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7O0FBRUEiLCJmaWxlIjoiMC5qcyIsInNvdXJjZXNDb250ZW50IjpbInZhciBhID0gMTtcblxuY29uc29sZS5sb2coYSk7XG5cblxuXG4vLy8vLy8vLy8vLy8vLy8vLy9cbi8vIFdFQlBBQ0sgRk9PVEVSXG4vLyAuL3NyYy9pbmRleC5qc1xuLy8gbW9kdWxlIGlkID0gMFxuLy8gbW9kdWxlIGNodW5rcyA9IDAiXSwic291cmNlUm9vdCI6IiJ9" +
    "\n//# sourceURL=webpack-internal:///0\n");

EvalDevtoolModulePlugin

不包含sourcemap信息,每个module构建完成,moduleTemplate的module。用eval包裹,并拼凑底部信息,sourceURL信息。
bundle文件如下:

eval("var a = 1;\n\nconsole.log(a);" +
    "\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = 0\n// module chunks = 0\n" +
    "\n//# sourceURL=goc:///./src/index.js?"
);

代码实现

本人的简易版webpack实现simple-webpack

(完)

react-router 2.7.0源码深度分析

前言

在前端单页面应用里面,路由是比较重要的部分,笔者的上一篇博文[简单的路由][1]介绍了简单的路由内部机制,本文则将分析react-router的内部机制。

介绍

react-router为react提供路由管理,为基于jsx格式的app系统提供了方便的切换页面功能。
它在前端提供给了2种方式,通过hashchange或者浏览器原生的history api进行地址更新,上一篇介绍了hash的方式,本文则以history api的形式切入分析。

代码剖析

路由配置

react-router本生为react组建,内部组建如Router,Route,IndexRoute, Redirect,Link等。
以下是摘自react-router example的路由配置
<Router history={withExampleBasename(browserHistory, __dirname)}>








点此获取完整代码

对应结构图

在初始化过程中他会以children形式读入Router生命周期内,在被转化为数组,此时它内部的结构如下
![图片描述][3]
2387537345-57c769d3cdb90_articlex

路由初始化

初始化browserHistory对象

react-router依赖history^2.0模块生成的history对象,然后在Router生命周期componentWillMount中加入对应的封装如basename,query
useQueries.js 对history对象内的方法进行封装

function listen(listener) {
  return history.listen(function (location) {
    listener(addQuery(location))
  })
}

// Override all write methods with query-aware versions.
function push(location) {
  history.push(appendQuery(location, location.query))
}

useBasename.js 对history对象内的方法进行封装

   function listen(listener) {
      return history.listen(function (location) {
        listener(addBasename(location))
      })
    }

    // Override all write methods with basename-aware versions.
    function push(location) {
      history.push(prependBasename(location))
    }

Router.js 对history对象增加setRouteLeaveHook钩子函数以及isActive函数

最终生成router对象 以this.router = router存在Router组建内部,this.history 已过时([issues][4]),不建议使用

###初始化监听事件

this._unlisten = transitionManager.listen(function (error, state) {
  if (error) {
    _this.handleError(error);
  } else {
    _this.setState(state, _this.props.onUpdate);
  }
});


function listen(listener) {
changeListeners.push(listener);
if (location) {
  listener(location);
} else {
  var _location = getCurrentLocation();
  allKeys = [_location.key];
  updateLocation(_location);
}

此时整体初始化完毕

改变路由

  <Link to="about" activeStyle={ACTIVE}>/</Link>

以一次Link点击为例

  • 触发Link组建的handleClick方法
  • 调用router对象 push方法
  • 拼装location对象
  • 改变url栏的地址
  • 调用updateLocation 触发changeListeners内的所有监听事件
  • 回调函数内调用match方法,根据location对象正则匹配router对象,匹配出对应的组建 执行runLeaveHooks钩子
  • 调用Router组建的setState(nextstate)
  • Router-context组建的render方法调用createElement
  • 调用react.createElement
  • 完成渲染

简洁的流转图

放上一个本人总结的一个简单流转过程图
897134101-57c7733cab6b9_articlex

详细流程图

对简洁的流程图熟悉之后,则可深入了解内部机制的细节如下图
3145767227-57cf86dd0b0c9_articlex

归纳

虽然源码繁琐复杂,但是内部的核心仍是围绕着下面3块动作做一系列的封装.

  • 注册监听事件:封装history对象的,生成router对象存储在Router内,并通过其注册监听事件,绑定相应的回调函数
  • 触发监听事件:通过Link/browserHistory.push/浏览器回退快进/dom ready 等四种方式触发回调函数
  • 回调函数: Router内的setState(next)最终触发react.createElement进而更新UI

3995787512-57c9668ac412d_articlex

最后

本文有什么不完善的地方,或者流程图有待改进的地方,敬请斧正。

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.