上一篇日志简单的介绍了一下Riot.js这个东西,这三周以来我一直再看React的源码,依然对它的batchUpdate一头雾水,所以决定先分析一下Riot.js.
Riot.js同样使用了VirtualDOM,但与React不同的是,它的DOM刷新并不像React那样是BatchUpdate做一下DOM与VirtualDOM diff,而是直接修改nodeText,这样的一个好处是在于算法不复杂,只需要遍历所有节点,相应进行插入就可以.
Riot同样具有自定义节点的功能,一般情况下我们不使用Riot.tag2进行自定义节点的定义,而是通过Riot提供的语法进行开发,最后使用浏览器/服务器端编译自动生成,但最后执行还是避免不了Riot.tag2,因此首先从该方法开始分析.
riot.tag2 = function(name, html, css, attrs, fn, bpair) {
//如果具有CSS,则将CSS插入到<head></head>中
if (css && injectStyle) injectStyle(css)
//注册自定义标签
__tagImpl[name] = { name: name, tmpl: html, attrs: attrs, fn: fn }
return name
}
Riot的注册标签流程非常简单,仅做了两步动作:插入CSS,存储到__tagImpl对象当中
注册完自定义标签之后就可以使用riot.mount将自定义标签载入进实际的DOM节点.
riot.mount = function(selector, tagName, opts) {
var els,
allTags,
tags = []
//添加Riot标签
function addRiotTags(arr) {
var list = ''
//遍历每一个对象,添加Riot标记
each(arr, function (e) {
list += ', *[' + RIOT_TAG + '="' + e.trim() + '"]'
})
//返回标记List
return list
}
//返回所有标签
function selectAllTags() {
//返回自定义标签列表
var keys = Object.keys(__tagImpl)
//返回新的自定义标签列表
return keys + addRiotTags(keys)
}
function pushTags(root) {
var last
//如果root标签存在
if (root.tagName) {
//如果root标签还未初始化
if (tagName && (!(last = getAttr(root, RIOT_TAG)) || last != tagName))
//设置RIOT_TAG
setAttr(root, RIOT_TAG, tagName)
//插入到root中
var tag = mountTo(root, tagName || root.getAttribute(RIOT_TAG) || root.tagName.toLowerCase(), opts)
//如果插入成功,则push到tags列表中
if (tag) tags.push(tag)
} else if (root.length)
//每一个对象都进行mountTo操作
each(root, pushTags)
}
// ----- mount code ----
//如果初始化属性存在(对象)
if (typeof tagName === T_OBJECT) {
opts = tagName
tagName = 0
}
//标签检测:字符串
if (typeof selector === T_STRING) {
//全部节点
if (selector === '*')
// 选取所有自定义节点
selector = allTags = selectAllTags()
else
// 选取当前传入节点
selector += addRiotTags(selector.split(','))
// 选择器选择DOM节点
els = selector ? $$(selector) : []
}
else
// 传入的是DOM对象,直接选取
els = selector
// 如果第二个参数为节点
if (tagName === '*') {
// 获取所有节点
tagName = allTags || selectAllTags()
// 如果是单个节点
if (els.tagName)
els = $$(tagName, els)
else {
var nodeList = []
选取所有_el节点下的tagName节点
each(els, function (_el) {
nodeList.push($$(tagName, _el))
})
els = nodeList
}
// get rid of the tagName
tagName = 0
}
//单个节点
if (els.tagName)
pushTags(els)
else
//批量节点
each(els, pushTags)
//返回节点对象
return tags
}
从代码中可以看出,mount方法主要完成分析DOM节点并依次生成自定义标签的过程.而具体的生成逻辑则在mountTo方法中.
function mountTo(root, tagName, opts) {
//从注册的标签库对象中获取
var tag = __tagImpl[tagName],
// 存储root中的innerHTML,实为yield
innerHTML = root._innerHTML = root._innerHTML || root.innerHTML
// 清空innerHTML
root.innerHTML = ''
//自定义标签和容器都存在,生成tag对象
if (tag && root) tag = new Tag(tag, { root: root, opts: opts }, innerHTML)
//tag对象生成成功
if (tag && tag.mount) {
//插入到文档中
tag.mount()
// 添加该tag对象到__virtualDom列表中
if (!contains(__virtualDom, tag)) __virtualDom.push(tag)
}
//返回tag对象
return tag
}
mountTo做了两项工作:存储&清空当前节点,注册新tag到virtualDOM中,然后将其余工作交给了Tag类:
function Tag(impl, conf, innerHTML) {
//使tag具有事件功能
var self = riot.observable(this),
//继承传入的opts
opts = inherit(conf.opts) || {},
//创建DOM容器
dom = mkdom(impl.tmpl),
//父节点
parent = conf.parent,
//是循环节点
isLoop = conf.isLoop,
//
hasImpl = conf.hasImpl,
//获取外置数据
item = cleanUpData(conf.item),
//表达式列表
expressions = [],
//子节点列表
childTags = [],
//DOM节点
root = conf.root,
//扩展方法
fn = impl.fn,
//标签名称
tagName = root.tagName.toLowerCase(),
//属性
attr = {},
propsInSyncWithParent = []
//若已经存在,则卸载
if (fn && root._tag) root._tag.unmount(true)
// 标记为尚未载入
this.isMounted = false
//是否为循环节点
root.isLoop = isLoop
// 保持tag对象的引用,这样可以多次进行该标签的装载
root._tag = this
// 为该tag对象分配唯一id,这样可以提升virtualDOM的渲染效果
//至于如何提升,在随后的分析中将看到
defineProperty(this, '_riot_id', ++__uid)
//在tag对象上注册这些引用
extend(this, { parent: parent, root: root, opts: opts, tags: {} }, item)
// 遍历每一个属性
each(root.attributes, function(el) {
var val = el.value
// 将具有表达式的属性放入attr对象中
if (tmpl.hasExpr(val)) attr[el.name] = val
})
//dom中具有内容并且不存在特殊节点
if (dom.innerHTML && !/^(select|optgroup|table|tbody|tr|col(?:group)?)$/.test(tagName))
// 将yeild替换为innerHTML中的内容
dom.innerHTML = replaceYield(dom.innerHTML, innerHTML)
// options
function updateOpts() {
var ctx = hasImpl && isLoop ? self : parent || self
// update opts from current DOM attributes
each(root.attributes, function(el) {
opts[toCamel(el.name)] = tmpl(el.value, ctx)
})
// recover those with expressions
each(Object.keys(attr), function(name) {
opts[toCamel(name)] = tmpl(attr[name], ctx)
})
}
function normalizeData(data) {
for (var key in item) {
if (typeof self[key] !== T_UNDEF && isWritable(self, key))
self[key] = data[key]
}
}
function inheritFromParent () {
if (!self.parent || !isLoop) return
each(Object.keys(self.parent), function(k) {
// some properties must be always in sync with the parent tag
var mustSync = !contains(RESERVED_WORDS_BLACKLIST, k) && contains(propsInSyncWithParent, k)
if (typeof self[k] === T_UNDEF || mustSync) {
// track the property to keep in sync
// so we can keep it updated
if (!mustSync) propsInSyncWithParent.push(k)
self[k] = self.parent[k]
}
})
}
defineProperty(this, 'update', function(data) {
//防止节点的核心方法被覆盖
data = cleanUpData(data)
//处理父节点的内置属性
inheritFromParent()
//处理非可写属性与undefined属性
if (data && typeof item === T_OBJECT) {
normalizeData(data)
item = data
}
//浅拷贝
extend(self, data)
//属性名转换
updateOpts()
//触发update事件(开始更新)
self.trigger('update', data)
//更新表达式
update(expressions, self)
//触发updated事件(更新完毕)
self.trigger('updated')
return this
})
defineProperty(this, 'mixin', function() {
each(arguments, function(mix) {
mix = typeof mix === T_STRING ? riot.mixin(mix) : mix
each(Object.keys(mix), function(key) {
// bind methods to self
if (key != 'init')
self[key] = isFunction(mix[key]) ? mix[key].bind(self) : mix[key]
})
// init method will be called automatically
if (mix.init) mix.init.bind(self)()
})
return this
})
defineProperty(this, 'mount', function() {
//设置名转换
updateOpts()
// 添加自定义函数
if (fn) fn.call(self, opts)
// 递归处理表达式
parseExpressions(dom, self, expressions)
// 加载自定义子节点
toggle(true)
// 获取自定义属性,提取表达式
if (impl.attrs || hasImpl) {
walkAttributes(impl.attrs, function (k, v) { setAttr(root, k, v) })
parseExpressions(self.root, self, expressions)
}
//自定义节点不是子节点也非循环节点,执行更新
if (!self.parent || isLoop) self.update(item)
//仅限内部使用的mount事件之后的事件,开发者需要使用mounted事件
self.trigger('before-mount')
if (isLoop && !hasImpl) {
// 从loop节点中取出实际节点
self.root = root = dom.firstChild
} else {
//插入到root节点中
while (dom.firstChild) root.appendChild(dom.firstChild)
if (root.stub) self.root = root = parent.root
}
// 插入到root节点中
if (isLoop)
parseNamedElements(self.root, self.parent, null, true)
// 触发顶层自定义节点的mount事件,设置定义节点的已加载标记
if (!self.parent || self.parent.isMounted) {
self.isMounted = true
self.trigger('mount')
}
// 触发当前节点的mount事件,同时设置父节点和当前节点的已加载标记
else self.parent.one('mount', function() {
if (!isInStub(self.root)) {
self.parent.isMounted = self.isMounted = true
self.trigger('mount')
}
})
})
defineProperty(this, 'unmount', function(keepRootTag) {
//待卸载节点
var el = root,
//父节点
p = el.parentNode,
ptag
//触发卸载前事件
self.trigger('before-unmount')
// 从virtualDom中删除
__virtualDom.splice(__virtualDom.indexOf(self), 1)
//具有相关其他节点
if (this._virts) {
each(this._virts, function(v) {
v.parentNode.removeChild(v)
})
}
//存在父节点
if (p) {
if (parent) {
ptag = getImmediateCustomParentTag(parent)
// 从父节点中删除所有该标签节点
if (isArray(ptag.tags[tagName]))
each(ptag.tags[tagName], function(tag, i) {
if (tag._riot_id == self._riot_id)
ptag.tags[tagName].splice(i, 1)
})
else
ptag.tags[tagName] = undefined
}
else
//删除当前节点内首节点
while (el.firstChild) el.removeChild(el.firstChild)
//删除根节点中的el
if (!keepRootTag)
p.removeChild(el)
else
// 仅删除riot控制,不删除节点
remAttr(p, 'riot-tag')
}
self.trigger('unmount')
toggle()
self.off('*')
self.isMounted = false
// somehow ie8 does not like `delete root._tag`
root._tag = null
})
function toggle(isMount) {
// mount/unmount children
each(childTags, function(child) { child[isMount ? 'mount' : 'unmount']() })
// listen/unlisten parent (events flow one way from parent to children)
if (parent) {
var evt = isMount ? 'on' : 'off'
// the loop tags will be always in sync with the parent automatically
if (isLoop)
parent[evt]('unmount', self.unmount)
else
parent[evt]('update', self.update)[evt]('unmount', self.unmount)
}
}
// 插入节点,
parseNamedElements(dom, this, childTags)
}
上面的部分主要工作是为tag对象添加相关方法属性,为生成DOM处理做准备,准备完成之后会调用parseNamedElements函数,parseNamedElements函数主要是调用递归函数walk
function parseNamedElements(root, tag, childTags, forceParsingNamed) {
//递归匿名函数
walk(root, function(dom) {
//为element节点
if (dom.nodeType == 1) {
//循环DOM
dom.isLoop = dom.isLoop || (dom.parentNode && dom.parentNode.isLoop || getAttr(dom, 'each')) ? 1 : 0
// 存在自定义节点
if (childTags) {
//获取自定义tag
var child = getTag(dom)
//存在自定义tag并且不是循环DOM
if (child && !dom.isLoop)
//初始化tag并push到childTags列表
childTags.push(initChildTag(child, {root: dom, parent: tag}, dom.innerHTML, tag))
}
//
if (!dom.isLoop || forceParsingNamed)
setNamed(dom, tag, [])
}
})
}
//递归函数
function walk(dom, fn) {
// 如果DOM节点存在
if (dom) {
//如果执行fn返回false,结束当前递归操作,返回上一层
if (fn(dom) === false) return
else {
//选取第一个子节点
dom = dom.firstChild
while (dom) {
//递归执行
walk(dom, fn)
//移到下一个节点
dom = dom.nextSibling
}
}
}
}
递归处理主要处理以下两部分:
自定义子节点
具有name属性的节点
处理完之后,tag对象将返回,继续回到调用处向下执行tag.mount,实际代码在new Tag()时被添加进,主要提取所有表达式,将提取处的表达式传给update函数进行表达式计算,update函数如下:
function update(expressions, tag) {
//遍历每一个表达式
each(expressions, function(expr, i) {
//表达式节点
var dom = expr.dom,
//表达式属性值(如果是属性节点)
attrName = expr.attr,
//计算表达式
value = tmpl(expr.expr, tag),
//表达式节点的父节点
parent = expr.dom.parentNode
//fix 布尔表达式
if (expr.bool)
value = value ? attrName : false
//fix 非布尔表达式
else if (value == null)
value = ''
// textarea节点的特殊处理
if (parent && parent.tagName == 'TEXTAREA') value = ('' + value).replace(/riot-/g, '')
// 无变化
if (expr.value === value) return
expr.value = value
// 表达式所在节点是文本节点
if (!attrName) {
//替换当前值
dom.nodeValue = '' + value // #815 related
return
}
// 删除原来的属性
remAttr(dom, attrName)
// 如果是函数
if (isFunction(value)) {
//绑定事件
setEventHandler(attrName, value, dom, tag)
// 具有逻辑的expression
} else if (attrName == 'if') {
var stub = expr.stub,
add = function() { insertTo(stub.parentNode, stub, dom) },
remove = function() { insertTo(dom.parentNode, dom, stub) }
// 添加到DOM节点
if (value) {
if (stub) {
add()
dom.inStub = false
// avoid to trigger the mount event if the tags is not visible yet
// maybe we can optimize this avoiding to mount the tag at all
if (!isInStub(dom)) {
walk(dom, function(el) {
if (el._tag && !el._tag.isMounted) el._tag.isMounted = !!el._tag.trigger('mount')
})
}
}
// 从DOM节点删除
} else {
stub = expr.stub = stub || document.createTextNode('')
// if the parentNode is defined we can easily replace the tag
if (dom.parentNode)
remove()
// otherwise we need to wait the updated event
else (tag.parent || tag).one('updated', remove)
dom.inStub = true
}
// show / hide 属性特殊处理
} else if (/^(show|hide)$/.test(attrName)) {
if (attrName == 'hide') value = !value
dom.style.display = value ? '' : 'none'
// value属性特殊处理
} else if (attrName == 'value') {
dom.value = value
// <img src="{ expr }" alt="" /> 特殊处理
} else if (startsWith(attrName, RIOT_PREFIX) && attrName != RIOT_TAG) {
if (value)
setAttr(dom, attrName.slice(RIOT_PREFIX.length), value)
//普通属性
} else {
if (expr.bool) {
dom[attrName] = value
if (!value) return
}
//Object无法赋值到属性当中
if (typeof value !== T_OBJECT) setAttr(dom, attrName, value)
}
})
}
执行完update之后,DOM中的数据已经更新完毕,接着会执行mount的后续操作
分析中可以发现,riot对于text节点和属性节点的控制能力相对好,而对于动态删除DOM节点,则会出现一些问题,而React的batchUpdate算法可以diff节点级的变化,而快速做出反应.riot对DOM节点的控制不是增加删除级别的控制,而是css级别的控制,这有区别于React.
riot的数据更新不是自动化的,需要通过手动去维护DOM与数据模型之间的数据,避免出现不同步的问题,而React的update是自动化的,但从以前使用类React框架的经验看来,完全的自动update还是不可靠,组件之间的通信还是免不了手动更新,因此这个东西有的时候感觉比较鸡肋,如果需要的话riot也可以整合进相关功能.
总体来说,riot.js框架代码量少,易理解,二次开发的成本比较低.所以比较适合定制化程度高的项目与小型项目快速开发.