月度归档:2015年11月

Javascript变量作用域

众所周知,在ES6之前JS中的变量作用域是函数级的,不具有块级作用域,而这句话也仅限于对Javascript语言本身而言的,聪明的开发人员可以通过闭包可以创建出伪块级作用域用来模拟这种其他语言一般都有的块级作用域.就像这样:

function foo(){
 for(var a=0;a<10;a++){}
 console.log(a);//10
 for(a=0;a<10;a++){}
}
foo();
function foo(){
 (function(){
  for(var a=0;a<10;a++){}
 })();
 console.log(a);//undefined
}
foo();

然而es2015给你提供了更好的方案:

function foo(){
 "use strict";
 for(let a=0;a<10;a++){}
 console.log(a); //undefined
}
foo();

但写本文的主要目的与上面的关系不是太大,本文主要想探究函数级的作用域问题

function foo(){
 var a=0;
 function b(){
  console.log('b');
 }
 function bar(){
  console.log(a);//a
  b();//b
 }
 bar();
}
foo();

执行这一段程序之后可以发现函数里的所有变量和函数是可以被该函数以及该函数内部的子函数所访问的,稍微改一下代码

function foo(){
 var a=0;
 function b(){
  console.log('b');
 }
 function bar(){
  console.log(a);//a
  b();//b
 }
 bar.call(d);
}
function d(){
}
foo();

发现输出的结果是相同的,上面这个例子是为了证明解释器在查找变量的时候不是通过this定位查找的,因为在第一个例子中this为window(strict mode 为undefined),第二个例子中为d函数,这个现象的具体原因可以通过观察实际的作用域链去解释.
这段程序在运行到bar()时会将foo()指针压栈,开始执行bar,但解释bar发现有在foo中的同名变量c,这样就会将bar中使用的变量形成一个闭包,在作用域中同时会出现自己的c和闭包中的c.
很多人包括我在这个地方就会有一个疑问:闭包包的是引用变量还是整个函数?
不管闭包包了引用变量还是整个函数,对与开发者而言并没有什么两样,因为你就算只剩下引用部分剩余都被回收了我也不会受什么影响,而这个问题的影响范围只是GC是不是会部分回收,我想大部分开发者并不会去关心这个问题.然而这件事请其实是非常严重的,因为现有的GC机制是只要引用存在,整个函数就不能被GC回收,所以实际情况下闭包是包的整个函数,但你并不能访问到不存在引用的成员,这也非常符合常理.
通过以上的分析,我们就可以解释下面这个稍微复杂一点的例子的作用域范围了

function Foo(){
 var a=1;
 this.a=2;
 function geta(){
  "use strict";
  console.log(a); //1
  console.log(this.a); //Cannot read property 'a' of undefined
 }
 this.geta=function(){
  console.log(a);//1
  console.log(this.a);//2
 }
 geta();
}
foo=new Foo();
foo.geta();

总体来说,可以简单概括成一句话:this是this,作用域是作用域.不能将this和作用域混淆,这样以来哪些可以访问到,哪些不能被访问,很容易就能区分出来,也能很快理解为啥好多其他人写的程序里面会出现apply或call,这肯定是因为里面用到了this,而和作用域就扯不上任何关系了.

Riot.js源码分析

上一篇日志简单的介绍了一下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插入到&lt;head&gt;&lt;/head&gt;中
if (css &amp;&amp; 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 &amp;&amp; (!(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 &amp;&amp; root) tag = new Tag(tag, { root: root, opts: opts }, innerHTML)
//tag对象生成成功
if (tag &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; !/^(select|optgroup|table|tbody|tr|col(?:group)?)$/.test(tagName))
// 将yeild替换为innerHTML中的内容
dom.innerHTML = replaceYield(dom.innerHTML, innerHTML)

// options
function updateOpts() {
var ctx = hasImpl &amp;&amp; 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 &amp;&amp; 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) &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; !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 &amp;&amp; dom.parentNode.isLoop || getAttr(dom, 'each')) ? 1 : 0

// 存在自定义节点
if (childTags) {
//获取自定义tag
var child = getTag(dom)
//存在自定义tag并且不是循环DOM
if (child &amp;&amp; !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 &amp;&amp; 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 &amp;&amp; !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) &amp;&amp; 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框架代码量少,易理解,二次开发的成本比较低.所以比较适合定制化程度高的项目与小型项目快速开发.

Riot.js初探

正如Riot.js宣传标语:

类似 React 的微型 UI 库

与React相同,Riot 同样提供的是V层解决方案,与React不同的是,Riot非常轻量,也非常容易与其他类库结合来提升整个项目的开发效率.

似乎现在都意识到DOM过于复杂,直接去维护DOM与数据之间的关系是一件相当浪费资源的事情,因此Virtual DOM自然而然就成为了一个大部分开发者比较关注的焦点.Riot同样使用了Virtual DOM来解决update的问题.

Riot.js为国内开发者提供了一个非常好的本地化帮助文档,我感觉都不需要再下面多啰嗦几句了.

官方中文:http://riotjs.com/zh/

Riot为快速开发小型Web应用提供了极佳的解决方案,总结起来有以下几个方面:

1.由于小型Web应用逻辑相对不复杂,所需的功能并不多,而同样有维护需求.Riot.js提供的自定义标签可以使代码具有组件特性,整个应用维护与阅读都非常友好.

2.特别是对于移动端应用开发,Riot.js提供全平台支持,加上体积小巧,因此在流量是金的移动端上更有实际意义.

3.小型Web应用不会配备太多人员进行开发,因此如何进行快速开发是非常重要的问题,Riot.js语法简单,API数量少,开发人员可以边看文档边进行开发,并迅速熟悉Riot.js.

 

前端架构:Flux架构初探

最近查找React Transaction的相关资料的时候,无意中看到了Flux这个东西,简单看了一下,突然想到了我以前稍微想过的一个前端架构.

以前是这个想的:

(Controller) ->   (View)

所有关于操作视图的逻辑都单独抽出来,Controller负责处理所有非视图逻辑,这样做的一个好处就是可以将视图部分和逻辑部分分隔开,修改逻辑或者视图并不需要在一堆代码中寻找,只需要找到相应视图变化点,切入修改非常方便.

data=controller.requestData();//获取数据

data=controller.transData(data);

controller.bind(‘click’,view.getButton1(),function(){ controller.doBtn1Click();});//监听

view.renderAll(data);//更新全部视图

view.renderHead(data);//更新头部视图

但这样的一个缺陷就在于大部分关于DTO的逻辑会掺在Controller当中,整个业务逻辑十分不透明,

然后改成了这样:

(Data Model)  ->  (Controller) -> (View)

将数据模型单独抽离出来,Controller部分将更加地清晰

controller.init(); //初始化controller

controller.bind(‘click’,view.getButton1(),function(){ controller.doBtn1Click();});//监听

renderData=dataModel.getData(); //从数据模型获取数据

view.renderAll(renderData);// 完成视图

读了Flux之后,按照这种思想来了一套我所想的Flux

 

其实在前端领域,没有什么真正的所谓的MVC,Flux则从这种MVC的思想框当中跳出来,提出了有一个更适合前端架构的一个架构模型,我也用过类似React的一个前端框架,正如React本身所介绍的那样,React仅仅是一个View层解决方案,如果单使用React代码还是一团糟,其实这种理解是建立在一个范围上的,如果单看View层,React其实已经做到了视图分离,控制器自行去指派各种事件,视图与数据模型的同步也都做得不错,看起来已经像是一个比较小的MVC,但从前端整体看来,这种分离还是不够透彻,各个部分的逻辑还是只能写在render之前,双向数据绑定经常会出现数据不同步的情况(React本身也不支持双向数据绑定),所以最终React只能成为一个View层解决方案.

Flux则提供了一种新的思路,使得数据流向真正的成为了一个单向的过程,这样的一个最好的优点肯定是稳定性要好很多,数据不同步几乎不可能发生(没控制好也可能发生),下面稍微讲一下我理解的Flux架构(有些地方不太一样),所以一下称为这种前端架构.

这种前端架构主要是由Dispatcher,Store,View,Action四部分组成,实际上一切的开始都是Action触发的,所以顺序是Action->Dispatcher->Store->View,但Action的始终还是View,所以我把它放到了最后.

Dispatcher是事件分派器,起初数据仓库里面并没有数据,由事件分派器通过分析事件,将所需的数据列表或者事件发给Store,事件的来源是Action.在Flux中,数据请求是在Action中发送的,但我将这一步移到了Store中,目的是让Store的数据仓库除了具备数据仓库,又具备数据获取的功能.如果可能,前端的数据仓库与后端数据仓库也可以保持1:1的同步状态.

Store是数据仓库,数据仓库包含大部分的DTO逻辑,数据的相关操作是由事件分派器所传出的指令操作的.数据的推送是通过事件,但此时的事件分派器是被弱化的,与Dispatcher不同,是一种属于View的事件机制.

View是视图,所有视图的数据来源都是Store,按照规定,View在任何时刻都不应该主动访问Store去获取数据,而是通过触发Action再通过Dispatcher分配最终间接获取数据,Store也不应该提供对外接口供View访问,这样就可以保证数据的一致性.

为了让整个前端架构的View层更加详细,下面再引入React,扩充View.

State是Store数据与render数据的暂存区,由于React可以保障renderData与State之间的同步关系,所以我们只需要做到State与Store之间的同步关系就可以,这种解决方案就是通过上述的Flux去维持.而中间的React VitrualDOM能使View更加高效灵活.