kaola-fed / blog

kaola blog
722 stars 56 forks source link

你不知道的 Regularjs #65

Open lllang opened 7 years ago

lllang commented 7 years ago

title: 你不知道的 regularjs date: 2017-06-12 lilang ppt: https://163.lu/lEBQ00

regularjs 源码整体感知

目录结构

regular
    |== directive                              指令相关
        |-- animation.js                   r-animation 动画指令相关
        |-- base.js                        r-class,r-style,r-hide,r-html,ref 模板相关的指令
        |-- event.js                       on-xxx 事件的指令,delegate 委托指令
        |-- form.js                        r-model 指令,初始化一些表单控件(input, textarea, checkbox, radio, select)
    |== helper                                 一些基础函数库
        |-- animate.js                     动画函数库
        |-- combine.js                    处理 ast 相关的函数
        |-- diff.js                       用于处理脏检查过程中两次的值是否一致的函数
        |-- entities.js                   实体表
        |-- event.js                      $on, $off, $emit, $once
        |-- filter.js                     定义一些过滤器 json, average, last, total
        |-- parse.js                      处理 template => ast 的过程用到的一些函数
        |-- shim.js                       shim for es5
        |-- watcher.js                    $watch, $unwatch, $digest, $get, $set, $update
        |-- extend.js                     继承体系
    |== module
        |-- timeout.js                    regular 内部版的 setTimeout
    |== parser                                模板相关
        |-- lexer.js                      词法分析器
        |-- node.js                       一些内置模板语法
        |-- parser.js                     将模板处理成 ast 的过程
    |-- config.js                             默认的配置
    |-- const.js                              默认的配置
    |-- dom.js                                dom 相关的一些函数
    |-- env.js                                 一些运行时的变量
    |-- group.js                               $inject 的过程
    |-- index.js                               入口函数
    |-- regular.js                             初始化一个 regular 组件需要做的事情
    |-- util.js                                util 函数
    |-- walkers.js                             从 ast 到 dom 的过程

从头开始走一遍 regularjs

首先是最重要的 regular.js 文件,我们挑一些看一看。

  1. 首先定义了 Regular 这个变量,值是一个方法,这个方法是一个构造函数,当实例化 regular 组件的时候会调用。这个函数大致定义了 regular 组件的生命周期。比如config,init,compile等等
    var Regular = function(definition, options){}
  2. 然后给 Regular 这个对象增加了一些属性
    _.extend(Regular, {
    _directives: { __regexp__:[] },                                          // 初始化一个 _directives 对象,这个对象包含有所有的指令
    _plugins: {},                                                            // 初始化一个 _plugins 对象,包含已经注册在全局的 plugin
    _protoInheritCache: [ 'directive', 'use'] ,
    __after__: function(supr, o) {},                                         // 在继承的时候调用该方法
    directive: function(name, cfg){},                                        // 定义指令的实现方式
    plugin: function(name, fn){},                                            // 在下面的use方法里面会调用这个,主要是用来获取已经在全局定义过了的plugin
    use: function(fn){},                                                     // 定义 use 方法
    config: function(name, value){},                                         // 定义模板的规则
    expression: parse.expression,                                            // 定义 expression 方法,用于生成 expression 对象
    Parser: Parser,                                                          // 定义 Parser,用于语法分析
    Lexer: Lexer,                                                            // 定义 Lexer,用于词法分析
    _addProtoInheritCache: function(name, transform){},                      // 在后面调用这个方法,将 component 和 filter 函数的定义绑到 Regular 上
    _inheritConfig: function(self, supr){}                                   //在上面的__after__方法里面会调用这个,这个方法会在继承时将父级的filter,component,directives,use全部在当前组件上都绑一遍
    });
  3. 继续给 Regular 加一些方法

    extend(Regular);                                                        // 给 Regular 加上继承相关的方法 (extend和implement)
    Regular._addProtoInheritCache("component")                              // 给 Regular 加上 component 方法以及 _components 对象
    Regular._addProtoInheritCache("filter", function(cfg){})                // 给 Regular 加上 filter 方法以及 _filters 对象
    events.mixTo(Regular);                                                  // 给 Regular 加上 event 相关的方法 ($on, $off, $emit, $once)
    Watcher.mixTo(Regular);                                                 // 给 Regular 加上脏值检测相关的方法 ($watch, $unwatch, $digest, $get, $set, $update)
    Regular.implement({...});                                               // 在 Regular 的原型链上加上一系列的基础方法 (init, config, destory, $inject...)
    Regular.prototype.inject = function(){}                                 // 在 Regular 的原型链上加上 inject 方法,等于 $inject 方法
    Regular.filter(filter);                                                 // 给 Regular 加上最基础的一系列过滤器

    继承的实现

    extent 的过程到底发生了什么

    比如

    let BaseComponent = Regular.extend({});
    let ListComponent = BaseComponent.extend({});
    let Demo = ListComponent.extend({});
    let demo = new Demo();

    我们知道 Regular 是一个构造函数,那么 BaseComponent, ListComponent, Demo 应该都是类似 Regular 的一个构造函数才对。
    看一下 extend 方法

    module.exports = function extend(o){
    o = o || {};
    var supr = this, proto,
    supro = supr && supr.prototype || {};
    
    if(typeof o === 'function'){     // 一般 extend 都是传递一个对象过去,只有第一次调用的时候是 extend(Regular) 这里是给 Regular 增加 extend 和 implement 方法
    proto = o.prototype;
    o.implement = implement;
    o.extend = extend;
    return o;
    } 
    
    function fn() {                  // 创建一个构造函数,里面有一个 supr,指向 this 
    supr.apply(this, arguments);
    }
    
    proto = _.createProto(fn, supro);    
    
    _.createProto = function(fn, o){
    function Foo() { this.constructor = fn;}
    Foo.prototype = o;
    return (fn.prototype = new Foo());
    }
    
    // 这里的 proto 是 this 的一个实例,同时也是 fn 的原型,在这里把 fn 和 this 给联系了起来,就是 fn 的 prototype 是 this 的一个实例
    
    function implement(o){
    // we need merge the merged property
    var len = mlen;
    for(;len--;){
      var prop = merged[len];
      if(proto[prop] && o.hasOwnProperty(prop) && proto.hasOwnProperty(prop)){
        _.extend(proto[prop], o[prop], true) 
        delete o[prop];
      }
    }
    
    process(proto, o, supro); 
    return this;
    }
    
    fn.implement = implement             // 定义 implement 方法
    fn.implement(o)                      // 调用 implement 方法,如果父级和当前对象都有 events,data,computed 属性的话,将他们 merge 一下,如果有相同的话用当前对象的覆盖父级的,然后将这些属性写到 proto (fn 的原型)上去
    
    if(supr.__after__) supr.__after__.call(fn, supr, o);     // 执行父级的 __after__ 方法,这个方法首先看看组件里面有没有 name 属性,有的话将在全局注册一遍,然后有 template 的话处理一下 template ,有 computed 的话处理 computed (将单个方法转换成对象里面的 get),然后给当前 fn 添加私有属性(_animations, _filters, _components, _directives, _events)
    
    fn.extend = extend;
    return fn;
    }

    这个方法最后 return 的是一个 fn, fn 就是我们要找的构造函数。这个地方特别的绕,我们简单的过一下。比如上面的例子。
    最后 BaseComponent.prototype 是 Regular 的一个实例。
    同理 ListComponent.prototype 是 BaseComponent 的一个实例。
    同理 Demo.prototype 是 ListComponent 的一个实例。
    这样就构成了继承的体系。
    最后 var demo = new Demo() 的时候可以看出
    image
    到 new Demo() 的时候就执行了 Demo 这个构造函数
    就是执行 fn ,fn 里面是 supr.apply(this, arguments); supr 指向的是父级的构造函数,也就是 ListComponent 的构造函数,这个构造函数也是一个 fn supr.apply(this, arguments);,那么这个会一层一层往上调,直到调到 Regular 的构造函数,就是最开始定义的 Regular 方法,也就是说所有的 Regular 组件最后都会用自己的作用域去执行 Regular 方法。我们来看一下这个方法。

    var Regular = function(definition, options){
    var prevRunning = env.isRunning;
    env.isRunning = true;
    var node, template;
    
    definition = definition || {};
    var usePrototyeString = typeof this.template === 'string' && !definition.template;
    options = options || {};
    
    definition.data = definition.data || {};
    definition.computed = definition.computed || {};
    if( this.data ) _.extend( definition.data, this.data );                                // merge 参数里面的 data 和 构造函数里面的 data
    if( this.computed ) _.extend( definition.computed, this.computed );                    // 同理 merge computed
    
    var listeners = this._eventListeners || [];
    var normListener;
    // hanle initialized event binding                                                     // 如果有参数里面有 events 的话,将 events 赋值给 listeners
    if( definition.events){
    normListener = _.normListener(definition.events);
    if(normListener.length){
      listeners = listeners.concat(normListener)
    }
    delete definition.events;
    }
    
    _.extend(this, definition, true);                                                      // merge 参与到构造函数里面去,一样的覆盖掉
    
    if(this.$parent){                                                                      // 给父组件增加子组件
     this.$parent._append(this);
    }
    this._children = [];                                                                   // 初始化子组件列表
    this.$refs = {};                                                                       // 初始化 $refs
    
    template = this.template;                                           // 处理 template,如果 template 的值符合选择器的话,把节点找到并赋值给 temlapte,在这个阶段 template 会从 string => ast
    
    // template is a string (len < 16). we will find it container first
    if((typeof template === 'string' && template.length < 16) && (node = dom.find(template))) {
    template = node.innerHTML;
    }
    // if template is a xml
    if(template && template.nodeType) template = template.innerHTML;
    if(typeof template === 'string') {
    template = new Parser(template).parse();
    if(usePrototyeString) {
    // avoid multiply compile
      this.constructor.prototype.template = template;
    }else{
      delete this.template;
    }
    }
    
    this.computed = handleComputed(this.computed);                      // 处理 computed
    this.$root = this.$root || this;
    // if have events
    
    if(listeners && listeners.length){                                  // 绑定事件
    listeners.forEach(function( item ){
      this.$on(item.type, item.listener)
    }.bind(this))
    }
    this.$emit("$config");                                              // emit config
    this.config && this.config(this.data);                              // 执行config
    this.$emit("$afterConfig");                                         // emit afterConfig
    
    var body = this._body;
    this._body = null;
    
    if(body && body.ast && body.ast.length){                            // 处理 $body
    this.$body = _.getCompileFn(body.ast, body.ctx , {
      outer: this,
      namespace: options.namespace,
      extra: options.extra,
      record: true
    })
    }
    // handle computed
    if(template){                                                       // compile 过程
    this.group = this.$compile(template, {namespace: options.namespace});
    combine.node(this);
    }
    
    if(!this.$parent) this.$update();                                   // compile 之后进行一次脏值检测
    this.$ready = true;                                                 // 设置 $ready 为 true
    this.$emit("$init");                                                // emit init
    if( this.init ) this.init(this.data);                               // 执行 init 
    this.$emit("$afterInit");                                           // emit afterInit
    
    // @TODO: remove, maybe , there is no need to update after init; 
    // if(this.$root === this) this.$update();
    env.isRunning = prevRunning;
    
    // children is not required;
    
    if (this.devtools) {
    this.devtools.emit("init", this)
    }
    }

    这里最重要的是 compile 过程
    整个 compile 过程分成 lexer(词法分析) => parser(语法分析) => walker(生成 dom) 模板常用的语法有 插值({ expression | filter }),功能语句({#if}, {#list}),指令(r-model),事件(on-click)
    比如下面这一段模板

    <form role="form" name="app">
    <div class="form-group">
      <label >Email address(value: {email | demo})</label>
      <input 
        type="email" class="form-control" placeholder="Enter email" value="87399126@163.com" 
        r-model={email}>
    </div>
    </form>

    这段模板 lexer 之后会生成一个数组,这个过程是挨个字符进行解析,数组一般如下

image

生成这个之后接下来就是 parser 的过程,这个过程就是将上面生成的数组转换成 ast 比如上面这个数组。parser 过程会从头开始对数组的每一项进行处理。 比如检测到 TAG_OPEN 那么会去匹配后面紧接着的 type 是 name 的项,将这些项视为这个节点的 attr ,然后去匹配自闭合标签或者 > ,如果这个标签没有闭合,会以递归的形式继续向下处理继续进行 parser 过程,并将 parser 过程生成的结果赋值给当前标签的 children 属性,如果在往下处理的过程中再次遇到 TAG_OPEN,那么继续递归,直到遇到 TAG_CLOSE,就结束最近的一次递归,类似这样。然后每一次匹配到一个完整的标签的时候会去生成这个标签对应的 element ,因为每次递归的时候会把这个标签包住的内容赋值给 children 那么最后生成的内容就是以一颗树的形式呈现,就是我们要的 ast 。生成 ast 的整体过程大概就是这样。ast 大概就是这个样子
image image
下面介绍重点,就是处理模板表达式是怎么处理的。
模板常用的语法有 插值({ expression | filter }),功能语句({#if}, {#list}),指令(r-model),事件(on-click)
如果检测到插值语句了,比如上面的{ email | demo },会解析成一个 expression 对象,这个 expression 对象是一个特别重要的概念
image 图中的 body 会最终解析成 get 函数,setbody 会解析成 set 函数,filters 是用到的过滤器。
检测到比如<label>Password(value: {#if password === 'demo'} {password} {#else} {email} {/if})</label>这种节点会生成这样的 ast image
这种节点会生成这样的 ast
这个 label 有三个 child ,第一个是文本节点Password(value:,第三部分也是文本节点),第二部分是 if 的主体,有三个对象,alternate 这个对象存放的是 else 成立后的内容,consequent 存放的是 if 成立后的内容,test 存放的是判断条件。
如果检测到

<select class="form-control" id='select' r-model='select'>
{#list list as item}
  <option value={item}>{item}</option>
{/list}
</select>

这种节点会生成这样的 ast
image
list 结构主要有 body 属性,body 属性就是被 list 包起来的节点,有 sequence 属性,就是需要遍历的对象,就是上面的 list ,这个 list 当然也是一个 expression 对象,自己的 get set 方法,filter 属性等等,有 track属性,就是我们一般用的 by ,还有一个 variable 就是 as 后面的那个 item
对于 r-demol 和 on-click 这种当做一般的 attr 来对待,跟 class id 这些一样。就像这样
imageimage

下面介绍一下 ast 到我们看到的 dom 的过程。

主要处理函数是 walk,也是用递归的形式调用的。比如上面生成的 ast ,对于某一个节点,首先判断节点的类型,一般有这么几种
text: 文本,遇到这种节点会直接生成文本节点

element: 标签,遇到这种首先判断这个节点的标签是否是已被注册的组件,或者是 r-component,如果不是,就递归处理当前节点的 children,处理完 children 之后会生成一个 group ,这个 group 包含每一个 child 生成的 dom 节点,然后处理自身,处理方式就是生成一个当前标签对应的节点,然后将 group 赋给这个 节点的 childNode,然后处理 attr,最后 return 的 ast 对象会加上 gourp,node,destory 方法,group 就是子节点的数组,node 是当前节点,destory 方法就是销毁当前节点的方法,这个方法会将 gourp 也销毁掉。

if: 会先生成一个 dom 注释,regular-if,然后会 $watch test 这个 expression 对象,回调是,如果 test 是真,则 compile consequent 里面的内容,并生成节点插入,如果是假,则 compile alternate 的内容并生成节点插入

list: 和 if 也比较类似,会去 $watch test 的内容,只是回调不太一样,如果内容为真,会根据 sequence 去重复生成 body 里面的节点

expression: 碰到 expression 对象时,会先创建一个文本节点 node,并 $watch 这个 expression 对象,watch 的回调是将这个 expression 对象的值赋给这个 node 的 value

template: 这个是 {#include} 的时候,会 $watch 后面的表达式,回调是将表达式的值重新进行一遍 compile 的过程并插入到当前节点

component: 上面的 element 如果判断是已注册的组件时,会处理 attr 然后生成对应组件的实例并插入到当前节点

attr: 处理节点的方法,会先判断节点是否是已注册过的指令,如果是的话会调用指令的 link 方法,

至此 ast => dom 的过程已经结束了,但是还没有显示出来,而且里面关于模板的部分也没有值。只是空的 dom 节点。

然后执行至关重要的脏值检测的过程。脏值检测只能由 $update 主动触发。脏值检测的过程有一个重要的东西叫做 watcher ,这个东西是在 $watch 的时候产生的。
上面在处理if, list, 插值语法等等地方都执行了 $watch 函数,这个函数接受三个参数,我们一般只用到了两个。第一个需要 watch 的对象,一般是 expression 对象,第二个是改变是否的回调,第三个是一些 option。在执行 $watch 的时候最后会生成一个 watcher ,基本结构是这样的

watcher = {
  id: uid,                                       // 每一个 watcher 都有一个唯一的 id 是自增的
  get: get,                                      // watch 的 get 函数,取这个 watch 的值就是靠这个函数
  fn: fn,                                        // watch 的值发生改变的时候会调用的函数,一般是 $watch 的第二个参数
  once: once,                                    // 是否只 watch 一次
  force: options.force,                          
  // don't use ld to resolve array diff
  diff: options.diff,                            // diff 的方式
  test: test,
  deep: options.deep,
  last: options.sync? get(this): options.last    // 上一次的值
}

然后将 watcher 推入 _warchers 数组。在脏值检测的过程中会从根组件开始进行 digest 过程,这个过程会遍历 _watchers 这个数组然后对每一个 watcher 进行 diff,简单diff 时会调用 get 方法获取到这个 watcher 最新的值并跟 last 进行比较,如果两者不一致会调用 fn 方法,然后将最新的值赋给 last。那么比如上面的插值 {email} 在 ast => dom 时会执行这么一个 $watch 方法

this.$watch(ast, function(newval){
    dom.text(node,  newval == null? "": String(newval) );
}, OPTIONS.STABLE_INIT )

那么 email 这个 expression 对象会生成一个 watcher 并推入 _watchers 在第一次脏值检测时,执行 get 方法,返回 this.data.email ,和 last 进行比较,不一致,就将新值赋给该节点的 text。那么就完成了 model => view 的过程并且在后面再进行脏值检测的时候会根据这个值是否更新来决定是否继续更改这个节点的 text 。
同理,{#if password === 'demo'} {password} {#else} {email} {/if} 的时候也会执行一个 $watch

var update = function(nvalue){
  if(!!nvalue){
    if(alternate) combine.destroy(alternate)
    if(ast.consequent) consequent = self.$compile(ast.consequent, {record: true, element: options.element , extra:extra});
  }else{
    if(consequent) combine.destroy(consequent)
    if(ast.alternate) alternate = self.$compile(ast.alternate, {record: true, element: options.element, extra: extra});
  }
}
this.$watch(ast.test, update, OPTIONS.FORCE);

和上面一样,password 会生成 watcher ,只是 get 函数不一样,返回 this.data.password === 'demo' ,如果返回是,则将 else 后面的部分 destory 掉并将 if 后面的部分 compile 出来,如果返回否,则相反。 同理,其他的模板语法都是以类似的方式跟 dom 结合在一起。 regular 的整个过程就是这样。

一些其他的地方:

脏值检测是什么时候进行的?

前面说了 脏值检测都是主动 $update 进入的。比如我们定义的事件on-click={ email = 'email' },这里我们并没有手动执行 $update ,这个时候 view 还是会更新,是因为在事件发生的时候,regular 自动帮我们在执行事件函数之前和之后各 执行了一遍 $update。所以有时候在一些异步中更新了 model 的时候得手动去 $update 一下 view 才会更新过来。

指令是怎么实现的?

指令的实现其实就是一个 .directive 函数,接受两个参数,一个是匹配到该指令的正则,第二个是 link 函数。在注册指令的时候,所有注册的指令都会被添加到这个组件的 _directives 这个数组里面,在 ast 到 dom 的过程中,对每一个属性去匹配指令,如果能匹配到就执行 link 函数。

计算属性是怎么实现的?

计算属性在执行 regular 函数的时候会直接处理成 expression 对象,比如有这么一个计算属性

selectAll: {
  // only every item is selected, we return true
  get: function(data){
    if(!data.list) return false;
    return data.list.filter(

        function(item){ return !!item.selected}

      ).length===data.list.length;
  },
  set: function(value, data){
    if(!data.list) return
    // set every item.selected with passed value
    data.list.forEach(function(item){
      item.selected = value;
    })
  }
}

会直接生成一个 expression 对象,get ,set 函数就是我们自己定义的 get, set 函数。我们在模板里面调用的时候,比如 <input r-model = {selectAll} /> 在生成 dom 的时候去取 selectAll 的值,会去调用这个 expression 对象的 get 函数,在给 selectAll 赋值时会执行 expression 对象的 set 函数。但是在 data 里面并没有定义这个对象,所以 计算属性定义的对象在 regular 函数里面需要用 this.$get 去取值。

过滤器是怎么实现的?

过滤器也是给 expression 对象用的,比如有模板<input r-model = {email | demo} /> email 这个 expression 对象调用了 demo 过滤器。那么在取值时除了调用在 expression 中的 get 函数,还需要调用定义在 demo 过滤器里面 get 函数,同理,在给 email 赋值的时候除了调用本身的 set 函数还要调用 过滤器的 set 函数

r-model 指令到底做了什么?

在生成 dom 的过程中,如果解析到了 r-model 指令,对于不同的标签,执行的函数会有一点不一样,但是大致思路的相同的。
比如 <input r-model = {selectAll} /><select r-model = {selectAll} />
首先会 $watch(selectAll) ,回调是将 selectAll 的值赋给该 dom 节点的 value(调用 expression 的 get 函数),这一步只是实现了 demol => view 的过程,然后 r-demol 做了另外一件事,给该 dom 节点绑定 onchange 事件,回调是将 dom 节点的 value 赋值给 selectAll (调用 expression 的 set 函数),这样就实现了 view => model 的过程。 就实现了双向绑定。

implement 是怎么实现的?

比如

let Plugin = function(Comp) {
    Comp.implement({
        aaa,
        bbb,
        ccc
    })
}

let BaseComponent = Regular.extend({}).use(Plugin);

简单点说 .use 这个方法干了这么一件事

function use(fn) {
    fn(this, Regular);
}

将当前作用域,和 Regular 当做 plugin 的参数传递了进去,然后在 plugin 里面会执行 this.implement({})
这里的这个 implement 就是简单的把 implement 函数的参数和 this 的 prototype 进行了一下 merge 。如果有 data, computed, events 等对象,就把对象 merge 一下,如果是其他的,就是直接 merge

一个彩蛋

在看源码的时候发现的一些写法。if(template = o.template) 当时看了好半天,才发现这是一个赋值语句,类似的还有return (fn.prototype = new Foo())这种句法相当于先赋值,再进行判断。。

int64ago commented 7 years ago

Hexo 的模板貌似跟 Regular 的插值语法有冲突…… 所以就没法归档了……

showonne commented 7 years ago

{{}}冲突了 https://hexo.io/docs/troubleshooting.html#Escape-Contents

int64ago commented 7 years ago

@showonne 其实是 {#,他的文章里没有 {{}}

image

showonne commented 7 years ago

soga...之前自己踩坑踩的{{}},不过解决方案应该是相同的。