让我们来加点互动
前面学生信息的身高的单位都是默认m
,如果新增一个需求,要求学生的身高的单位可以在m
和cm
之间切换呢?
首先需要一个变量来保存度量单位,因此这里必须用一个新的Model:
const tk = { 'first-name': 'Jessica', 'last-name': 'Bre', 'height': 180, 'weight': 70, } const measurement = 'cm'
为了让tk
更方便的被其他模块重用,这里选择增加一个measurement
数据源,而不是直接修改tk
。
在视图部分要增加一个radio单选表单,用来切换身高单位。
const createList = function(kvPairs){ const createListItem = function (label, content) { const li = document.createElement('li') const labelSpan = document.createElement('span') labelSpan.textContent = label const contentSpan = document.createElement('span') contentSpan.textContent = content li.appendChild(labelSpan) li.appendChild(contentSpan) return li } const root = document.createElement('ul') kvPairs.forEach(function (x) { root.appendChild(createListItem(x.key, x.value)) }) return root } const createToggle = function (options) { const createRadio = function (name, opt){ const radio = document.createElement('input') radio.name = name radio.value = opt.value radio.type = 'radio' radio.textContent = opt.value radio.addEventListener('click', opt.onclick) radio.checked = opt.checked return radio } const root = document.createElement('form') options.opts.forEach(function (x) { root.appendChild(createRadio(options.name, x)) root.appendChild(document.createTextNode(x.value)) }) return root } const createToggleableList = function(vm){ const listView = createList(vm.kvPairs) const toggle = createToggle(vm.options) const root = document.createElement('div') root.appendChild(toggle) root.appendChild(listView) return root }
接下来是ViewModel
部分,createToggleableList
函数需要与之前的createList
函数不同的参数。因此,对View-Model结构重构是有必要的:
const createVm = function (model) { const calcHeight = function (measurement, cms) { if (measurement === 'm'){ return cms / 100 + 'm' }else{ return cms + 'cm' } } const options = { name: 'measurement', opts: [ { value: 'cm', checked: model.measurement === 'cm', onclick: () => model.measurement = 'cm' }, { value: 'm', checked: model.measurement === 'm', onclick: () => model.measurement = 'm' } ] } const kvPairs = [ { key: 'Name: ', value: model.student['first-name'] + ' ' + model.student['last-name'] }, { key: 'Height: ', value: calcHeight(model.measurement, model.student['height']) }, { key: 'Weight: ', value: model.student['weight'] + 'kg' }, { key: 'BMI: ', value: model.student['weight'] / (model.student['height'] * model.student['height'] / 10000) }] return {kvPairs, options} }
这里为createToggle
添加了ops
,并且将ops
封装成了一个对象。根据度量单位,使用不同的方式去计算身高。当任何一个radio
被点击,数据的度量单位将会改变。
看上去很完美,但是当你点击radio标签的时候,视图不会有任何改变。因为这里还没有为视图做更新算法。有关MVVM
如何处理视图更新,那是一个比较大的课题,需要另辟一个博文来讲,由于本文写的是一个精简的MVVM
框架,这里就不再赘述,并用最简单的方式实现视图更新:
const smvvm = function (root, {model, view, vm}) { let m = {...model} let m_old = {} setInterval( function (){ if(!_.isEqual(m, m_old)){ const rendered = view(vm(m)) root.innerHTML = '' root.appendChild(rendered) m_old = {...m} } },1000) } smvvm(document.body, { model: {student:tk, measurement}, view: createToggleableList, vm: createVm })
上述代码引用了一个外部库lodash
的isEqual
方法来比较数据模型是否有更新。此段代码应用了轮询,每秒都会检测数据是否发生变化,有变化了再更新视图。这是最笨的方法,并且在DOM结构比较复杂时,性能也会受到很大的影响。还是同样的话,本文的主题是一个精简的MVVM框架,因此略去了很多细节性的东西,只把主要的东西提炼出来,以达到更好的理解MVVM模式的目的。
MVVM框架的诞生
以上便是一个简短精简的MVVM风格的学生信息的示例。至此,一个精简的MVVM框架其实已经出来了:
/** * @param {Node} root * @param {Object} model * @param {Function} view * @param {Function} vm */ const smvvm = function (root, {model, view, vm}) { let m = {...model} let m_old = {} setInterval( function (){ if(!_.isEqual(m, m_old)){ const rendered = view(vm(m)) root.innerHTML = '' root.appendChild(rendered) m_old = {...m} } },1000) }
什么?你确定不是在开玩笑?一个只有十行的框架?
请记住: 框架是对如何组织代码和整个项目如何通用运作的抽象。
这并不意味着你应该有一堆代码或混乱的类,尽管企业可用的API列表经常都很可怕的长。但是如果你研读一个框架仓库的核心文件夹,你可能发现它会出乎意料的小(相比于整个项目来说)。其核心代码包含主要工作进程,而其他部分只是帮助开发人员以更加舒适的方式构建应用程序的附件。有兴趣的同学可以去看看cycle.js,这个框架只有124行(包含注释和空格)。
总结
此时用一张图来作为总结再好不过了!