在码农中最持久的一个口号就是:
低耦合、高内聚
先来看低耦合,在用Java写代码的时候大家已经习惯用共享内存的方式来实现多线程间的通信(就不举栗子了),有点问题的时候排查起来比较头疼,而另外一种思路是:
用通信的方式来共享内存
比如在古老的Erlang中是这样的:
receive
Message1 [when Guard1] ->
Actions1 ;
Message2 [when Guard2] ->
Actions2 ;
...
end
而比较新的Golang中是这样的:
func Producer (queue chan<- int){
for i:= 0; i < 10; i++ {
queue <- i
}
}
func Consumer( queue <-chan int){
for i :=0; i < 10; i++{
v := <- queue
fmt.Println("receive:", v)
}
}
func main(){
queue := make(chan int, 1)
go Producer(queue)
go Consumer(queue)
time.Sleep(1e9) //让Producer与Consumer完成
}
再回过头来看HTML其实也是通过消息来驱动的,页面上的每个操作都会转换成事件传递给JavaScript进行处理,在JavaScript中的事件处理方式如下(单线程):

在可视化编程中这种方式还是挺给力的,在使用一个组件时,需要根据它的事件来让用户去扩展(写代码):
@input(name = "abc")
@on(click)
......
@on(change)
......
用户在编写代码的时候只需要关注两个对象:
组件可以监听另一个组件的事件,这样两个组件就可以实现交互(和HTML的区别是动作与展示结合在一起):
@input(name = "abc")
.....
@table(...)
@target(abc)
@on(click)
.....
@on(target = "abc" type="click")
....
在event中包含的字段有:
| 字段 | 作用 |
|---|---|
| target | 产生事件的组件名称 |
| type | 事件类型 |
| data | 数据 |
实现事件的订阅/发布需要三个操作:
在组件从页面上消失的时候是需要取消绑定的,不然可能会有问题,配置完成页面上组件之间的关系如下:

在接收到事件的时候可以做任何你想做的事情,比如更新一下页面的展示。因为组件已经封装的比较彻底了,暴露给用户的只有数据,那么只能通过更新数据来更新展示:
@table
@on(click)
setState({list:[1,2,3,4]});
如果对应的模板为:
@render
for(var i in list){
<li>${i}</li>
}
那么得到的HTML的代码如下:
<li>1</li> <li>2</li> <li>3</li> <li>4</li>
在更新展示时如果直接用innerHTML或者outerHTML实现起来比较简单,但问题是体验比较差:
用户的输入(或者列表的选择)都会消失,感觉有点像区域被刷新了一下。
另外在全部更新的情况下要比逐个更新元素要快一些,但是很多情况下我们只需要更新页面上的一小部分,那么就可以考虑用JS原生的DOM操作来搞:
| 操作 | 作用 |
|---|---|
| createElement | 创建HTML元素 |
| appendChild | 将一个节点插入到指定的父节点的最末尾处 |
| removeChild | 从某个父节点中移除指定的子节点,并返回那个子节点 |
| replaceChild | 用指定的节点替换当前节点的一个子节点,并返回被替换掉的节点 |
| insertBefore | 在当前节点的某个子节点之前再插入一个子节点 |
虽然没有insertAfter方法,但是实现起来非常简单。DOM结构中另一个变化的大头就是属性:
| 操作 | 作用 |
|---|---|
| getAttribute | 获取属性 |
| setAttribute | 设置属性 |
| removeAttribute | 移除属性 |
最后想修改标签中的字符时可以用innerHTML或者textContent直接搞定,最后一个问题就是如何更新DOM结构:

追求性能可以根据增、删、改的成本用动态规划算出一个最优的修改方式,但是长期来看处理的数据都相当有限,只需要用贪心地策略来保证用户体验即可。