Skip to content

Instantly share code, notes, and snippets.

@teabyii
Last active August 23, 2017 13:40
Show Gist options
  • Save teabyii/7f6bddf5934915081c5d to your computer and use it in GitHub Desktop.
Save teabyii/7f6bddf5934915081c5d to your computer and use it in GitHub Desktop.
riotjs 源码阅读记录

Riot 源码阅读笔记

官网:https://muut.com/riotjs/

第一次看到 Riot,感觉就是惊艳。我对 React 不大了解,只是简单知道一些,所以在我看来,React 实现的一些东西,Riot 也可以实现,而且代码精简,麻雀虽小,五脏俱全,可以用很简单的语法创建类似 web component 的自定义标签,渲染的性能好像还不赖。

注:不支持自定义标签的浏览器需要手动创建标签,如 demo/index.html

整体实现思路

我们不聊 compiler

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 = ...
})

Custom Tag

Riot 暴露出 tag 接口用于自定义标签, 这个方法只是简单地把自定义的标签相关数据储存起来。

当调用 mount 方法时,通过选择器筛选,对选择出来的每一个元素执行 mountTo 方法。

mountTo 方法会实例化对应的自定义标签,去调用内部的 Tag 构造方法,并且将实例存储在内部。

重点就在这个 Tag 方法干的活了,大致流程是这样的:

  • 实例继承事件接口(赋予 on, off, one, trigger 方法)
  • 声明更新自定义标签属性的方法并调用(处理 <custom-tag class="a"> 中 this.opts.class === 'a' // true
  • 如果是子元素,建立和父元素的关联
  • 创建一个空的 DOM 元素,来作为一个容器,暂时存放内部元素
  • 创建对象的 updateunmount 方法
  • 遍历所创建的 DOM 元素,扫描节点内容和属性,如果值为模板中的表达式,则创建对象,存储对应的 DOM 元素和表达式,以后用于更新数据
  • 执行传入的 fn
  • 执行自身的 update 方法,刷新数据
  • 执行内部的 mount 方法,把容器中的元素移到 root 元素上,即页面上的自定义标签

这样就完成了自定义标签,解析模板表达式还是用 new Function () 方式创建后保存起来,需要时传入 data 执行。

Update DOM

更新数据时,需要手动地使用 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)

    用来做一个变量的引用和防错

  • 模板解析后会存入缓存中,避免重复解析

Update 的实现

这个的处理在 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 的功能,可以很方便地组织起页面的代码,在其基础上可以积累出一批通用的组件。

@xudafeng
Copy link

总结的很好,学习了

@qquunn
Copy link

qquunn commented Aug 23, 2017

可以转载吗

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment