上一篇中我们实现了一个通用的组件模型。但这个模型在前端这种用户交互密集的应用场景下,会遇到更多的挑战。

数据驱动

你接到了这样一个需求,要在banner上加几个指示的圆点(indicator),当前页面是第几个页面,第几个圆点就会高亮。你想着这个需求很简单啊,刷刷刷就写好了。

HTML

<div class="banner">
	<li>
		<img src="{{ url1 }}"/>
		<img src="{{ url2 }}"/>
	</li>
	<div class="button-left"></div>
	<div class="button-right"></div>
	<div class="indicator">
		<li class="item"></li>
		<li class="item"></li>
		<li class="item"></li>
	</div>
</div>

JS

import * as _ from "utility"

var banner = new Banner({
	el: document.querySelector("template"),
	data: {
		index:0,
		url1: "/somewhere/1.jpg",
		url2: "/somewhere/2.jpg"
	},
	events: {
		".left-button.click": this.onSwitch.bind(this, 1),
		".right-button.click": this.onSwitch.bind(this, -1)
	},
	methods: {
		onSwitch: function(event, direction) {
			// 切换banner逻辑	
			data.index += direction
			var indicators = document.querySelector(".indicator");
			indicators.map( (item, index) => {
				_.removeClass(item, "hightLight");
				if (index === data.index) {
					_.addClass(item, "highLight");
				}
			})	
		}
	}
})

这的确可以实现我们想要的逻辑,但是看起来还是有点累赘。

如果我们能把一部分逻辑写在模板里,然后在组件中,只需要修改数据,模板就会进行对应的渲染,是不是很酷呢?

这就是所谓的数据驱动。组件帮我们封装了底层的DOM操作细节。我们只需要声明组件的状态就可以了。用户的交互造成组件的状态改变,然后状态的改变又造成了DOM层面的重新渲染。组件就像是一个黑盒子,接收我们输入的数据,输出视图。计算机做的事情其实就是处理数据。因此,我们做这样的抽象,是非常符合本能的一种抽象。可以让我们集中精力来关注业务逻辑。同时也降低了前端组件的复杂度

那就让我们来试试吧。

HTML

<div class="banner">
	<li>
		<img src="{{ url }}" for="url in urls"/>
	</li>
	<div class="button-left"></div>
	<div class="button-right"></div>
	<div class="indicator" >
		<li class="item" 
		class="{{ $index === index ? 'highLight' : 'normal' }}"
		  for="url in urls"></li>
	</div>
</div>

JS

import * as _ from "utility"

var banner = new Banner({
	el: document.querySelector("template"),
	data: {
		index:0,
		url: ["/somewhere/1.jpg", "/somewhere/2.jpg"]
	},
	events: {
		".left-button.click": this.onSwitch.bind(this, 1),
		".right-button.click": this.onSwitch.bind(this, -1)
	},
	methods: {
		onSwitch: function(event, direction) {
			// 切换banner逻辑	
			data.index += direction //更新index
			this.render() //重新渲染
		}
	}
})

这里有一个新的for="url in urls",我们把这个叫做指令。指令可以理解为为模板增加分支、循环等逻辑的标记。模板引擎在编译这个指令时就会按数据进行对应的渲染。这里用到的是循环指令,作用就是按给定的数据循环遍历,每遍历到一个就讲数据填充到模板中进行渲染。$index是这个指令中一个特殊的变量,代表当前遍历到的元素的下标。

这样就清楚多啦。我们修改data.index,然后调用this.render()这个生命周期方法。不用操纵任何DOM节点。视图就更新了。是的,我们在某种程度上实现了数据驱动!

Vue and beyond

大家可以对比一下Vue的初始化:

new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  },
  methods: {
    reverseMessage: function () {
      this.message = this.message.split('').reverse().join('')
    }
  }
})

和我们的组件的初始化:

new Banner({
	el: document.querySelector("template"),
	data: {
		message: 'Hello Vue.js!'
	},
	events: {
		".some-button.click": this.oClick,
	},
	methods: {
		onClick: function() {
			// 业务逻辑
		}
	}
})

是不是很像呢,除了events这个选项。因为Vue把这些逻辑都放到指令中了,比如:

<button v-on:click="reverseMessage">Reverse Message</button>

所以Vue的本质,就是一个用来构建用户界面的组件库。和我们一步步的设计API,调整模板,构建出来的这个组件,没有本质的区别。

但Vue为什么好用呢?首先Vue有完善的指令系统,比如v-for``v-forv-if等。其次是Vue实现了数据的双向绑定。双向绑定就是用户在界面上输入的数据,可以被同步到组件的状态中。刚才,我们在自己的组件中实现了将组件状态同步到界面,而双向绑定就以为着这个同步可以是反向的。

双向绑定的实现是一个单独的话题。Angular使用的脏检查是一个流派。Vue使用Object.definePropertyAPI来实现。React倒是没有数据绑定的概念,不过React的Virtual DOM Diff从某种角度来说,其实也是脏检查。

我们所实现的其实是一种粗犷的同步方式。并没有实现所谓的数据“绑定”。数据绑定就以为着为每个模板中被绑定的数据创建一个Watcher,这个Watcher决定数据变化时做什么操作。比如对于DOM中的表达式,Watcher就会对这个表达式重新求值,然后更新这个表达式所在的DOM节点的对应属性。

按之前我们的做法,如果一个数据变动,就会造成整个组件的重新渲染。这样明显是低效的。数据绑定可以做到特定DOM节点的更新,这是目前前端组件的主流。

从MVC到MVVM

前端的MVC或者MVVM,View一般就指模板(模板可以看成是抽象的View,DOM则是这个抽象View的implementation detail)。我们的第一个组件构造器构造出来的对象其实是MVC中的Controller。里面的data属性应该被单独拿出作为一个Model对象,这个对象可以通过观察者模式和Controller进行通信。

MVVM中的Model就是一个plain object,比如我们打造的组件中的data。Vue这样的MVVM框架,Vue实例其实是MVVM中的VM,即ViewModel。Model,如上文说的,是一个挂载在data属性上的普通的对象。我们修改这个对象,就会驱动View的更新。MVVM的独特处之一就在于此。MVVM的独特处之二就是,Model和View直接数据的双向绑定。VM和MVC中Controller的不同之处在于通信方式的不同。MVVM中各个部分的联动比较复杂,我们叫reactive system。MVC中的事件广播模式,主要是在组件之间的通信这个层面比较明显。