<select class="form-control" id='select' r-model='select'>
{#list list as item}
<option value={item}>{item}</option>
{/list}
</select>
这种节点会生成这样的 ast
list 结构主要有 body 属性,body 属性就是被 list 包起来的节点,有 sequence 属性,就是需要遍历的对象,就是上面的 list ,这个 list 当然也是一个 expression 对象,自己的 get set 方法,filter 属性等等,有 track属性,就是我们一般用的 by ,还有一个 variable 就是 as 后面的那个 item
对于 r-demol 和 on-click 这种当做一般的 attr 来对待,跟 class id 这些一样。就像这样
指令的实现其实就是一个 .directive 函数,接受两个参数,一个是匹配到该指令的正则,第二个是 link 函数。在注册指令的时候,所有注册的指令都会被添加到这个组件的 _directives 这个数组里面,在 ast 到 dom 的过程中,对每一个属性去匹配指令,如果能匹配到就执行 link 函数。
title: 你不知道的 regularjs date: 2017-06-12 lilang ppt: https://163.lu/lEBQ00
regularjs 源码整体感知
目录结构
从头开始走一遍 regularjs
首先是最重要的 regular.js 文件,我们挑一些看一看。
继续给 Regular 加一些方法
继承的实现
extent 的过程到底发生了什么
比如
我们知道 Regular 是一个构造函数,那么 BaseComponent, ListComponent, Demo 应该都是类似 Regular 的一个构造函数才对。
看一下 extend 方法
这个方法最后 return 的是一个 fn, fn 就是我们要找的构造函数。这个地方特别的绕,我们简单的过一下。比如上面的例子。
最后 BaseComponent.prototype 是 Regular 的一个实例。
同理 ListComponent.prototype 是 BaseComponent 的一个实例。
同理 Demo.prototype 是 ListComponent 的一个实例。
这样就构成了继承的体系。
最后 var demo = new Demo() 的时候可以看出
到 new Demo() 的时候就执行了 Demo 这个构造函数
就是执行 fn ,fn 里面是
supr.apply(this, arguments);
supr 指向的是父级的构造函数,也就是 ListComponent 的构造函数,这个构造函数也是一个 fnsupr.apply(this, arguments);
,那么这个会一层一层往上调,直到调到 Regular 的构造函数,就是最开始定义的 Regular 方法,也就是说所有的 Regular 组件最后都会用自己的作用域去执行 Regular 方法。我们来看一下这个方法。这里最重要的是 compile 过程
整个 compile 过程分成 lexer(词法分析) => parser(语法分析) => walker(生成 dom) 模板常用的语法有 插值({ expression | filter }),功能语句({#if}, {#list}),指令(r-model),事件(on-click)
比如下面这一段模板
这段模板 lexer 之后会生成一个数组,这个过程是挨个字符进行解析,数组一般如下
生成这个之后接下来就是 parser 的过程,这个过程就是将上面生成的数组转换成 ast 比如上面这个数组。parser 过程会从头开始对数组的每一项进行处理。 比如检测到
TAG_OPEN
那么会去匹配后面紧接着的 type 是 name 的项,将这些项视为这个节点的 attr ,然后去匹配自闭合标签或者>
,如果这个标签没有闭合,会以递归的形式继续向下处理继续进行 parser 过程,并将 parser 过程生成的结果赋值给当前标签的 children 属性,如果在往下处理的过程中再次遇到TAG_OPEN
,那么继续递归,直到遇到TAG_CLOSE
,就结束最近的一次递归,类似这样。然后每一次匹配到一个完整的标签的时候会去生成这个标签对应的 element ,因为每次递归的时候会把这个标签包住的内容赋值给 children 那么最后生成的内容就是以一颗树的形式呈现,就是我们要的 ast 。生成 ast 的整体过程大概就是这样。ast 大概就是这个样子下面介绍重点,就是处理模板表达式是怎么处理的。
模板常用的语法有 插值({ expression | filter }),功能语句({#if}, {#list}),指令(r-model),事件(on-click)
如果检测到插值语句了,比如上面的{ email | demo },会解析成一个 expression 对象,这个 expression 对象是一个特别重要的概念
图中的 body 会最终解析成 get 函数,setbody 会解析成 set 函数,filters 是用到的过滤器。
检测到比如
<label>Password(value: {#if password === 'demo'} {password} {#else} {email} {/if})</label>
这种节点会生成这样的 ast这种节点会生成这样的 ast
这个 label 有三个 child ,第一个是文本节点
Password(value:
,第三部分也是文本节点)
,第二部分是 if 的主体,有三个对象,alternate 这个对象存放的是 else 成立后的内容,consequent 存放的是 if 成立后的内容,test 存放的是判断条件。如果检测到
这种节点会生成这样的 ast
list 结构主要有 body 属性,body 属性就是被 list 包起来的节点,有 sequence 属性,就是需要遍历的对象,就是上面的 list ,这个 list 当然也是一个 expression 对象,自己的 get set 方法,filter 属性等等,有 track属性,就是我们一般用的 by ,还有一个 variable 就是 as 后面的那个 item
对于 r-demol 和 on-click 这种当做一般的 attr 来对待,跟 class id 这些一样。就像这样
下面介绍一下 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 推入 _warchers 数组。在脏值检测的过程中会从根组件开始进行 digest 过程,这个过程会遍历 _watchers 这个数组然后对每一个 watcher 进行 diff,简单diff 时会调用 get 方法获取到这个 watcher 最新的值并跟 last 进行比较,如果两者不一致会调用 fn 方法,然后将最新的值赋给 last。那么比如上面的插值 {email} 在 ast => dom 时会执行这么一个 $watch 方法
那么 email 这个 expression 对象会生成一个 watcher 并推入 _watchers 在第一次脏值检测时,执行 get 方法,返回 this.data.email ,和 last 进行比较,不一致,就将新值赋给该节点的 text。那么就完成了 model => view 的过程并且在后面再进行脏值检测的时候会根据这个值是否更新来决定是否继续更改这个节点的 text 。
同理,
{#if password === 'demo'} {password} {#else} {email} {/if}
的时候也会执行一个 $watch和上面一样,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 对象,比如有这么一个计算属性
会直接生成一个 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 是怎么实现的?
比如
简单点说 .use 这个方法干了这么一件事
将当前作用域,和 Regular 当做 plugin 的参数传递了进去,然后在 plugin 里面会执行 this.implement({})
这里的这个 implement 就是简单的把 implement 函数的参数和 this 的 prototype 进行了一下 merge 。如果有 data, computed, events 等对象,就把对象 merge 一下,如果是其他的,就是直接 merge
一个彩蛋
在看源码的时候发现的一些写法。
if(template = o.template)
当时看了好半天,才发现这是一个赋值语句,类似的还有return (fn.prototype = new Foo())
这种句法相当于先赋值,再进行判断。。