簡介

一個簡單的帶有雙向繫結的 MVVM 實現.

例子

使用

新建一個 ViewModel 物件, 引數分別為 DOM 元素以及繫結的資料即可.

指令

本 MVVM 的指令使用 data 資料, 即 data-html = "text" 表示這個 DOM 元素的 innerHTMl 為 model 中的 text 屬性.

對某些指令還可以新增引數, 比如 data-on="reverse:click", 表示 DOM 元素新增 click 事件, 處理函式為 model 中的 reverse 屬性.

  • value: 可以在 input 中使用, 只對 checkbox 進行特殊處理
  • text, html: 分別修改 innerText 和 innerHTML
  • show: 控制指定元素顯示與否
  • each: 迴圈 DOM 元素, 每個元素繫結新的 ViewModel, 通過 $index 可以獲取當前索引, $root 表示根 ViewModel 的屬性
  • on: 繫結事件,
  • *: 繫結特定屬性

參考

本實現主要參考 rivets.js 的 es6 分支, 其中 Observer 類是參考 adapter.js 實現.

Binding 就是 bindings.js 對應的簡化, 相當於其他 MVVM 中指令, ViewModel 對應 view.js.

PS: 由於雙向繫結只是簡單的實現, 因此指令中的值只能是 Model 的屬性

下面的程式碼採用 es6 實現, 如果想要本地執行的話, 請 clone git@github.com:445141126/mvvm.git, 然後執行 npm install 安裝依賴, 最後 npm run dev 開啟開發伺服器, 瀏覽器中開啟 http://127.0.0.1:8080/

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>MVVM</title>
  6. </head>
  7. <body>
  8. <div id="vm">
  9. 設定style: <input type="text" data-value="text" data-*="text: style">
  10. <br/>
  11. 顯示: <input type="checkbox" data-value="show">
  12. <br/>
  13. style: <span data-show="show" data-html="text"></span>
  14. <br/>
  15. <button data-on="reverse: click">reverse</button>
  16. </div>
  17. <script src="bundle.js"></script>
  18. </body>
  19. </html>
  1. import _ from 'lodash'
  2. function defined(obj) {
  3. return !_.isUndefined(obj) && !_.isNull(obj)
  4. }
  5. class Observer {
  6. constructor(obj, key, cb) {
  7. this.obj = obj
  8. this.key = key
  9. this.cb = cb
  10. this.obj.$$callbacks = this.obj.$$callbacks || {}
  11. this.obj.$$callbacks[this.key] = this.obj.$$callbacks[this.key] || []
  12. this.observe()
  13. }
  14. observe() {
  15. const observer = this
  16. const obj = observer.obj
  17. const key = observer.key
  18. const callbacks = obj.$$callbacks[key]
  19. let value = obj[key]
  20. const desc = Object.getOwnPropertyDescriptor(obj, key)
  21. if(!(desc && (desc.get || desc.set))) {
  22. Object.defineProperty(obj, key, {
  23. get() {
  24. return value
  25. },
  26. set(newValue) {
  27. if(value !== newValue) {
  28. value = newValue
  29. callbacks.forEach((cb) => {
  30. cb()
  31. })
  32. }
  33. }
  34. })
  35. }
  36. if(callbacks.indexOf(observer.cb) === -1) {
  37. callbacks.push(observer.cb)
  38. }
  39. }
  40. unobserve() {
  41. if(defined(this.obj.$$callbacks[this.key])) {
  42. const index = this.obj.$$callbacks[this.key].indexOf(this.cb)
  43. this.obj.$$callbacks[this.key].splice(index, 1)
  44. }
  45. }
  46. get value() {
  47. return this.obj[this.key]
  48. }
  49. set value(newValue) {
  50. this.obj[this.key] = newValue
  51. }
  52. }
  53. class Binding {
  54. constructor(vm, el, key, binder, type) {
  55. this.vm = vm
  56. this.el = el
  57. this.key = key
  58. this.binder = binder
  59. this.type = type
  60. if(_.isFunction(binder)) {
  61. this.binder.sync = binder
  62. }
  63. this.bind = this.bind.bind(this)
  64. this.sync = this.sync.bind(this)
  65. this.update = this.update.bind(this)
  66. this.parsekey()
  67. this.observer = new Observer(this.vm.model, this.key, this.sync)
  68. }
  69. parsekey() {
  70. this.args = this.key.split(':').map((k) => k.trim())
  71. this.key = this.args.shift()
  72. }
  73. bind() {
  74. if(defined(this.binder.bind)) {
  75. this.binder.bind.call(this, this.el)
  76. }
  77. this.sync()
  78. }
  79. unbind() {
  80. if(defined(this.observer)) {
  81. this.observer.unobserve()
  82. }
  83. if(defined(this.binder.unbind)) {
  84. this.binder.unbind(this.this.el)
  85. }
  86. }
  87. sync() {
  88. if(defined(this.observer) && _.isFunction(this.binder.sync)) {
  89. this.binder.sync.call(this, this.el, this.observer.value)
  90. }
  91. }
  92. update() {
  93. if(defined(this.observer) && _.isFunction(this.binder.value)) {
  94. this.observer.value = this.binder.value.call(this, this.el)
  95. }
  96. }
  97. }
  98. class ViewModel {
  99. constructor(el, model) {
  100. this.el = el
  101. this.model = model
  102. this.bindings = []
  103. this.compile(this.el)
  104. this.bind()
  105. }
  106. compile(el) {
  107. let block = false
  108. if(el.nodeType !== 1) {
  109. return
  110. }
  111. const dataset = el.dataset
  112. for(let data in dataset) {
  113. let binder = ViewModel.binders[data]
  114. let key = dataset[data]
  115. if(binder === undefined) {
  116. binder = ViewModel.binders['*']
  117. }
  118. if(defined(binder)) {
  119. this.bindings.push(new Binding(this, el, key, binder))
  120. }
  121. }
  122. if(!block) {
  123. el.childNodes.forEach((childEl) => {
  124. this.compile(childEl)
  125. })
  126. }
  127. }
  128. bind() {
  129. this.bindings.sort((a, b) => {
  130. let aPriority = defined(a.binder) ? (a.binder.priority || 0) : 0
  131. let bPriority = defined(b.binder) ? (b.binder.priority || 0) : 0
  132. return bPriority - aPriority
  133. })
  134. this.bindings.forEach(binding => {
  135. binding.bind()
  136. })
  137. }
  138. unbind() {
  139. this.bindins.forEach(binding => {
  140. binding.unbind()
  141. })
  142. }
  143. }
  144. ViewModel.binders = {
  145. value: {
  146. bind(el) {
  147. el.addEventListener('change', this.update)
  148. },
  149. sync(el, value) {
  150. if(el.type === 'checkbox') {
  151. el.checked = !!value
  152. } else {
  153. el.value = value
  154. }
  155. },
  156. value(el) {
  157. if(el.type === 'checkbox') {
  158. return el.checked
  159. } else {
  160. return el.value
  161. }
  162. }
  163. },
  164. html: {
  165. sync(el, value) {
  166. el.innerHTML = value
  167. }
  168. },
  169. show: {
  170. priority: 2000,
  171. sync(el, value) {
  172. el.style.display = value ? '' : 'none'
  173. }
  174. },
  175. each: {
  176. block: true
  177. },
  178. on: {
  179. bind(el) {
  180. el.addEventListener(this.args[0], () => { this.observer.value() })
  181. }
  182. },
  183. '*': {
  184. sync(el, value) {
  185. if(defined(value)) {
  186. el.setAttribute(this.args[0], value)
  187. } else {
  188. el.removeAttribute(this.args[0])
  189. }
  190. }
  191. }
  192. }
  193. const obj = {
  194. text: 'Hello',
  195. show: false,
  196. reverse() {
  197. obj.text = obj.text.split('').reverse().join('')
  198. }
  199. }
  200. const ob = new Observer(obj, 'a', () => {
  201. console.log(obj.a)
  202. })
  203. obj.a = 'You should see this in console'
  204. ob.unobserve()
  205. obj.a = 'You should not see this in console'
  206. const vm = new ViewModel(document.getElementById('vm'), obj)