第一次看到 Riot,感觉就是惊艳。我对 React 不大了解,只是简单知道一些,所以在我看来,React 实现的一些东西,Riot 也可以实现,而且代码精简,麻雀虽小,五脏俱全,可以用很简单的语法创建类似 web component
的自定义标签,渲染的性能好像还不赖。
注:不支持自定义标签的浏览器需要手动创建标签,如 demo/index.html
Riot 提供的 compiler 没有 React 的解析 JSX 那么复杂,只是相对简单地把 template 和 js 区分开来,并且加入多个 transpiler 的支持。
先撇开解析 html + js 写法的 compiler (这个另开坑聊),我们来看单纯的 riot.js,留意到 Riot 源码库中的 demo/todo.js,不难发现:
<custom-tag>
...
this.prop = ...
</custom-tag>
经 compiler 之手后是生成这样的代码的:
riot.tag('custom-tag', '...', function () {
this.prop = ...
})
Riot 暴露出 tag
接口用于自定义标签, 这个方法只是简单地把自定义的标签相关数据储存起来。
当调用 mount
方法时,通过选择器筛选,对选择出来的每一个元素执行 mountTo
方法。
mountTo
方法会实例化对应的自定义标签,去调用内部的 Tag
构造方法,并且将实例存储在内部。
重点就在这个 Tag
方法干的活了,大致流程是这样的:
- 实例继承事件接口(赋予
on
,off
,one
,trigger
方法) - 声明更新自定义标签属性的方法并调用(处理
<custom-tag class="a"> 中 this.opts.class === 'a' // true
) - 如果是子元素,建立和父元素的关联
- 创建一个空的 DOM 元素,来作为一个容器,暂时存放内部元素
- 创建对象的
update
和unmount
方法 - 遍历所创建的 DOM 元素,扫描节点内容和属性,如果值为模板中的表达式,则创建对象,存储对应的 DOM 元素和表达式,以后用于更新数据
- 执行传入的
fn
- 执行自身的
update
方法,刷新数据 - 执行内部的
mount
方法,把容器中的元素移到root
元素上,即页面上的自定义标签
这样就完成了自定义标签,解析模板表达式还是用 new Function ()
方式创建后保存起来,需要时传入 data
执行。
更新数据时,需要手动地使用 this.update()
来刷新视图,例如官方给的例子: tag-update
这里的 update
是这样干活的:
- 遍历和该元素相关的表达式
- 对比表达式的值,如果无变更,则不做处理
- 根据表达式关联的 DOM 和其类型(元素属性,文本等),更新 DOM 元素
使用表达式和 DOM 关联,来实现局部刷新,记录表达式的值,不更改则不更新,减少 DOM 更改,提升性能。
同时,对外提供一个 riot.update
的方法来刷新所有的自定义元素
这里只挑出几个我觉得的比较重要的点出来聊一下。
这个处理在 lib/parse.js,主要是 parseLayout
方法
这个方法中的处理,每解析出一个字符串模板,都会使用 addExpr
方法,将其关联的 DOM ,模板存在元素的类型(文本内容或者某个属性等),以及模板存储在 expressions
变量。
function addExpr(dom, value, data) {
if (tmpl(value) || data) {
var expr = { dom: dom, expr: value }
expressions.push(extend(expr, data || {}))
}
}
从 root
开始遍历其所有子孙元素:
- 如果是带
each
属性的,遍历输出多个元素 - 文本节点则将其内容作为模板进行解析
- 自定义标签的,实例化,并且将其 root 属性设置为当前的 DOM 元素
- 遍历当前的元素的属性,解析其中的模板,照样把提取出来的表达式和元素关联存储起来
这里记录的 expressions
变量会作为私有变量存储起来,用于 Update
函数来进行变更修改和局部刷新处理。
注:这里我把模板中的 {}
用来包裹表达式的称为界定符
这里谈到的只是处理字符串的模板,即出现在元素中的属性或者文本节点中的内容中的字符串模板
具体的代码都在:lib/tmpl.js
提供一个 tmpl
方法,用于解析模板的,其中使用了大量的 replace
,我们依照一个主要流程了解下:
-
拿到一个模板后,先把转义的界定符处理了,转成
non-character
,处理后再转回来,保证转义的界定符不做处理.replace(re(/\\{/), '\uFFF0') .replace(re(/\\}/), '\uFFF1')
-
使用
/({[\s\S]*?})/
来把模板分割成字符串和表达式,当然,其中做了自定义界定符的处理,即把正则中的{}
换掉就可以了 -
分割后如果仅仅只有表达式,即结果为:
!p[0] && !p[2] && !p[3] // 只有捕获到的在 {} 中的表达式
直接调用
expr
方法来创建用于函数的代码字符串 -
如果分割结果有多个,包括字符串和表达式,则做一个
map
操作来分类处理:'[' + p.map(function(s, i) { // 判断是表达式还是字符串,能被二整除的为字符串 return i % 2 ? expr(s, 1) // 字符串的话做换行和引号的转义 : '"' + s // preserve new lines .replace(/\n/g, '\\n') // escape quotes .replace(/"/g, '\\"') + '"' ).join(',') + '].join("")' // 全部合并以一个数组的形式,如 // 1{a}2 // "['1', expr(a), '2'].join("")" // 这里的 expr(a) 当做处理后的结果。
-
这里来聊聊
expr
方法了,首先,模板的需求要支持三种格式的表达式:{ value }
to "hello"Hi { name } { surname }
to "Hi Boom Lee"{ show: !done, highlight: active }
to "show highlight"
刚才的
map
已经处理了前两种,现在剩下最后一种,正则匹配一下,如果是类似对象字面量的字符串,则获取:
后的 value 部分,作为变量判断,看是否输出 key 部分:return /^\s*[\w- "']+ *:/.test(s) ? '[' + s.replace(/\W*([\w- ]+)\W*:([^,]+)/g, function(_, k, v) { // 拼接成这样的结构: [!done?"show":"", highlight?"active":""].join(" ") // 这里的 wrap 方法是给变量包裹一个 try catch 结构来防止报错 return v.replace(/\w[^,|& ]*/g, function(v) { return wrap(v, n) }) + '?"' + k.trim() + '":"",' }) + '].join(" ")'
-
轮到
wrap
方法了,每一个变量都经它处理后变成大概如下的函数:(function(v){try{v=d.v}finally{return v}}).call(d)
用来做一个变量的引用和防错
-
模板解析后会存入缓存中,避免重复解析
这个的处理在 lib/update.js。
setEventHandler
方法用于处理 DOM 元素的事件绑定,而且这里默认,自定义的事件,触发的时候都调用 tag.update()
来刷新 DOM
这里回到 expressions
,这个私有变量是关键,有了它,Update
方法就很容易实现了。
遍历 expressions
,获取关联的 DOM 元素,以及通过 tmpl(expr.expr, tag)
来获取模板当前值,和过去记录的值进行对比,不同则需要进行更新处理。
更新处理依照模板关联的类型来处理:
- 文本内容的,直接:
dom.nodeValue = value
- 值为空,而且关联的 DOM 属性是
checked/selected
等这种没有属性值的,移除对应的属性 - 值为函数的,则进行事件绑定
- 属性名为
if
,则做条件判断处理 - 做了
show/hide
的语法糖处理:remAttr(dom, attr_name) if (attr_name == 'hide') value = !value dom.style.display = value ? '' : 'none'
- 普通属性的,直接设置其值
更新的方式相对清晰,通过记录的值对比也防止了相同值时操作 DOM,模板和 DOM 元素关联的记录也容易地做到局部更新。
考虑一下使用 Riot 可能会出现的一些问题:
-
元素外部,特别是针对自定义元素的通信
感觉使用元素的事件来进行处理,给自定义元素绑定事件,外部通过事件来进行通信以及调用内部方法会是比较合适的办法。
如果是直接使用实例的方法,如何确切找到你要的那个实例是个问题,可以考虑挂 id 进行关联的方式。
-
动态添加自定义元素如何处理
动态添加的元素,只能添加后使用
mount
方法再处理,这个得再考虑一下是否有合理的方案。
总的来说,作为一个小小的类库,Riot 已经让人感觉很精彩,提供了类似 web components
的功能,可以很方便地组织起页面的代码,在其基础上可以积累出一批通用的组件。
总结的很好,学习了