上一篇日志简单的介绍了一下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框架代码量少,易理解,二次开发的成本比较低.所以比较适合定制化程度高的项目与小型项目快速开发.