最近看了珠峰的架构课——实现一个MVVM。

首先,我们来了解一下什么是MVVM。

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。

先贴一下代码,然后再做分析。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>myMVVM</title>
  8. </head>
  9. <body>
  10. <div id="root">
  11. <input type="text" v-model='person.name' >
  12. <h1>{{message}}</h1>
  13. <ul>
  14. <li>1</li>
  15. <li>2</li>
  16. </ul>
  17. {{person.age}}
  18. <br />
  19. <button v-on:click="change">点我</button>
  20. <br />
  21. {{getNewName}}
  22. <div v-html="message"></div>
  23. </div>
  24. <!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
  25. <script src="Vue.js"></script>
  26. <script>
  27. let vm = new Vue({
  28. el: '#root',
  29. data: {
  30. message: 'she is a bad girl',
  31. person: {
  32. age: 20,
  33. name: 'zhangsan'
  34. }
  35. },
  36. computed: {
  37. getNewName() {
  38. return this.person.name + 'hahaha'
  39. }
  40. },
  41. methods: {
  42. change() {
  43. console.log("11111")
  44. }
  45. }
  46.  
  47. })
  48. </script>
  49. </body>
  50. </html>

MVVM的简易实现(还有很多功能没有涉及到)

  1. /**
  2. * 订阅发布 调度中心
  3. */
  4. class Dep {
  5. constructor() {
  6. this.subs = [] // 存放所有的watcher
  7. }
  8. // 添加watcher, 订阅
  9. addSub(watcher) {
  10. this.subs.push(watcher)
  11. }
  12. // 发布
  13. notify() {
  14. this.subs.forEach(watcher => watcher.update())
  15. }
  16. }
  17.  
  18. /**
  19. * 观察者模式 观察者,被观察者
  20. */
  21. class Watcher {
  22. constructor(vm, expr, cb) {
  23. this.vm = vm
  24. this.expr = expr
  25. this.cb = cb
  26.  
  27. // 默认先存放旧值
  28. this.oldValue = this.get(vm, expr)
  29. }
  30. // 获取旧的值
  31. get() {
  32. Dep.target = this
  33. let value = CompileUtil.getVal(this.vm, this.expr)
  34. Dep.target = null
  35. return value
  36. }
  37. // 数据更新
  38. update() {
  39. let newVal = CompileUtil.getVal(this.vm, this.expr)
  40. if(newVal != this.oldValue) {
  41. this.cb(newVal)
  42. }
  43. }
  44. }
  45.  
  46. /**
  47. * 实现数据劫持
  48. */
  49. class Observer {
  50. constructor(data) {
  51. this.observe(data)
  52. }
  53. observe(data) {
  54. if(data && typeof data == 'object') {
  55. for(let key in data) {
  56. this.defineReactive(data, key, data[key])
  57. }
  58. }
  59. }
  60.  
  61. defineReactive(obj, key, value) {
  62. this.observe(value)
  63. let dep = new Dep() // 给每一个属性都加上一个具有发布订阅的功能
  64. Object.defineProperty(obj, key, {
  65. get() {
  66. Dep.target && dep.addSub(Dep.target)
  67. return value
  68. },
  69. set: (newValue) => {
  70. if(newValue != value) {
  71. this.observe(newValue)
  72. value = newValue
  73. dep.notify()
  74. }
  75. }
  76. })
  77. }
  78. }
  79.  
  80. /**
  81. * 编译模板
  82. */
  83. class Compiler {
  84. constructor(el, vm) {
  85. this.vm = vm
  86. // 判断el是否是个元素,如果不是就获取它
  87. this.el = this.isElementNode(el) ? el : document.querySelector(el)
  88. // console.log(this.el)
  89. // 把当前节点中的元素放到内存中
  90. let fragment = this.node2fragment(this.el)
  91.  
  92. // 把节点中的内容进行替换
  93.  
  94. // 编译模板 用数据来编译
  95. this.compile(fragment)
  96.  
  97. // 把内容再塞回页面中
  98. this.el.appendChild(fragment)
  99. }
  100.  
  101. // 是否是指令 v-开头的
  102. isDirective(attrName) {
  103. // startsWith('v-') 或 split('-')
  104. return attrName.indexOf('v-') !== -1
  105. }
  106. // 编译元素
  107. compileElement(node) {
  108. let attributes = node.attributes
  109. // [...attributes] 或 Array.from 等价 Array.prototype.slice.call
  110. Array.from(attributes).forEach(attr => {
  111. let { name, value: expr } = attr
  112. if(this.isDirective(name)) {
  113. //
  114. let [, directive] = name.split('-')
  115. // console.log(name,node, expr, directive)
  116. if(directive.indexOf(':' !== -1)) {
  117. let [directiveName, eventName] = directive.split(':')
  118. CompileUtil[directiveName](node, expr, this.vm, eventName)
  119. } else {
  120. CompileUtil[directive](node, expr, this.vm)
  121. }
  122.  
  123. }
  124. })
  125. }
  126. // 编译文本 找{{}}
  127. compileText(node) {
  128. let content = node.textContent
  129. if(/\{\{(.+?)\}\}/.test(content)) {
  130. // console.log(content) // 找到所有文本
  131. CompileUtil['text'](node, content, this.vm)
  132. }
  133. }
  134.  
  135. // 编译内存中的dom节点 核心的编译方法
  136. compile(node) {
  137. let childNodes = node.childNodes
  138. Array.from(childNodes).forEach(child => {
  139. if(this.isElementNode(child)) {
  140. this.compileElement(child)
  141. // 如果是元素的话,需要除去自己,再去遍历子节点
  142. this.compile(child)
  143. } else {
  144. this.compileText(child)
  145. }
  146. })
  147. }
  148. // 把节点移动到内存中 appendChild方法
  149. node2fragment(node) {
  150. let fragment = document.createDocumentFragment()
  151. let firstChild
  152. while(firstChild = node.firstChild) {
  153. fragment.appendChild(firstChild)
  154. }
  155.  
  156. return fragment
  157. }
  158. // 判断是否为元素节点
  159. isElementNode(node) {
  160. return node.nodeType === 1
  161. }
  162. }
  163.  
  164. /**
  165. * 编译工具函数
  166. */
  167. CompileUtil = {
  168. // 根据表达式取对应的数据
  169. getVal(vm, expr) {
  170. return expr.split('.').reduce((data, current) => {
  171. return data[current]
  172. }, vm.$data)
  173. },
  174. getContentVal(vm, expr) {
  175. return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
  176. return this.getVal(vm, args[1])
  177. })
  178. },
  179. setVal(vm, expr, value) {
  180. expr.split('.').reduce((data, current, index, arr) => {
  181. if(index === arr.length - 1) {
  182. return data[current] = value
  183. }
  184. return data[current]
  185. }, vm.$data)
  186. },
  187. // 解析v-model指令
  188. model(node, expr, vm) {
  189. // node.value
  190. let fn = this.updater['modelUpdater']
  191. new Watcher(vm, expr, (newVal) => { // 给输入框加一个观察者,稍后数据更新,触发此方法,用新值给输入框赋值
  192. fn(node, newVal)
  193. })
  194. node.addEventListener('input', e => {
  195. let value = e.target.value
  196. this.setVal(vm, expr, value)
  197. })
  198. let value = this.getVal(vm, expr)
  199. fn(node, value)
  200. },
  201. on(node, expr, vm, eventName) {
  202. node.addEventListener(eventName, e => {
  203. vm[expr].call(vm, e)
  204. })
  205. },
  206. text(node, expr, vm) {
  207. let fn = this.updater['textUpdater']
  208. let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
  209. // 给表达式每个变量都加上观察者
  210. new Watcher(vm, args[1], () => {
  211. fn(node, this.getContentVal(vm, expr)) // 返回一个全的字符串
  212. })
  213. return this.getVal(vm, args[1])
  214. })
  215. fn(node, content)
  216. },
  217. html(node, expr, vm) {
  218. // node.innerHTML
  219. let fn = this.updater['htmlUpdater']
  220. new Watcher(vm, expr, (newVal) => { // 给输入框加一个观察者,稍后数据更新,触发此方法,用新值给输入框赋值
  221. fn(node, newVal)
  222. })
  223. let value = this.getVal(vm, expr)
  224. fn(node, value)
  225.  
  226. },
  227. updater: {
  228. modelUpdater(node, value) {
  229. node.value = value
  230. },
  231. textUpdater(node, value) {
  232. node.textContent = value
  233. },
  234. htmlUpdater(node, value) {
  235. node.innerHTML = value
  236. }
  237. }
  238. }
  239.  
  240. /**
  241. * Vue构造函数
  242. */
  243. class Vue {
  244. constructor(options) {
  245. this.$el = options.el
  246. this.$data = options.data
  247.  
  248. let computed = options.computed
  249. let methods = options.methods
  250.  
  251. if(this.$el) {
  252. // 做数据劫持
  253. new Observer(this.$data)
  254. // console.log(this.$data)
  255.  
  256. for(let key in computed) { // 依赖关系
  257. Object.defineProperty(this.$data, key, {
  258. get: () => {
  259. return computed[key].call(this)
  260. }
  261. })
  262. }
  263.  
  264. for(let key in methods) {
  265. Object.defineProperty(this, key, {
  266. get() {
  267. return methods[key]
  268. }
  269. })
  270. }
  271.  
  272. // vm上的取值操作都代理上vm.$data上
  273. this.proxyVm(this.$data)
  274.  
  275. // 编译模板
  276. new Compiler(this.$el, this)
  277. }
  278. }
  279. // 代理
  280. proxyVm(data) {
  281. for(let key in data) {
  282. Object.defineProperty(this, key, {
  283. get() {
  284. return data[key]
  285. },
  286. set(newVal) {
  287. data[key] = newVal
  288. }
  289. })
  290. }
  291. }
  292. }

未完待续......

手写一个MVVM的更多相关文章

  1. 剖析手写Vue,你也可以手写一个MVVM框架

    剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ...

  2. 『练手』手写一个独立Json算法 JsonHelper

    背景: > 一直使用 Newtonsoft.Json.dll 也算挺稳定的. > 但这个框架也挺闹心的: > 1.影响编译失败:https://www.cnblogs.com/zih ...

  3. 教你如何使用Java手写一个基于链表的队列

    在上一篇博客[教你如何使用Java手写一个基于数组的队列]中已经介绍了队列,以及Java语言中对队列的实现,对队列不是很了解的可以我上一篇文章.那么,现在就直接进入主题吧. 这篇博客主要讲解的是如何使 ...

  4. 【spring】-- 手写一个最简单的IOC框架

    1.什么是springIOC IOC就是把每一个bean(实体类)与bean(实体了)之间的关系交给第三方容器进行管理. 如果我们手写一个最最简单的IOC,最终效果是怎样呢? xml配置: <b ...

  5. 放弃antd table,基于React手写一个虚拟滚动的表格

    缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...

  6. 只会用就out了,手写一个符合规范的Promise

    Promise是什么 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果.从语法上说,Promise 是一个对象,从它可以获取异步操作的消息.Prom ...

  7. 搞定redis面试--Redis的过期策略?手写一个LRU?

    1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...

  8. 利用SpringBoot+Logback手写一个简单的链路追踪

    目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ...

  9. 手写一个简单的ElasticSearch SQL转换器(一)

    一.前言 之前有个需求,是使ElasticSearch支持使用SQL进行简单查询,较新版本的ES已经支持该特性(不过貌似还是实验性质的?) ,而且git上也有elasticsearch-sql 插件, ...

随机推荐

  1. nginx日志文件的配置

    文章来源 运维公会: nginx日志文件的配置 1.日志介绍 nginx有两种日志,一种是访问日志,一种是错误日志. 访问日志中记录的是客户端对服务器的所有请求. 错误日志中记录的是在访问过程中,因为 ...

  2. 一个 Git 分支协作模式的进化故事

    从不用版本管理到使用 Git 分支管理的故事,也就是从这个时候开始的... 某公司只有一个程序员,一开始并没有版本管理的概念.项目开发只有一个人在参与,所以也没用版本管理工具. 后来,老板又招了两个程 ...

  3. 基于CentOS构建企业镜像站

    参考:How to Setup Local HTTP Yum Repository on CentOS 7 实验环境 CentOS7 1804 步骤一:安装Nginx Web Server 最小化安装 ...

  4. C++——虚函数表解析

     转自:https://blog.csdn.net/haoel/article/details/1948051 前言 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型指针指 ...

  5. SUSE Ceph Cephfs - Storage6

    (1)Policy 配置文件,添加MDS角色定义 # vim /srv/pillar/ceph/proposals/policy.cfg # MDS role-mds/cluster/mds*.sls ...

  6. SQL Server 2005的几个新功能

    SQL Server 2005相对于SQL Server 2000改进很大,有些还是非常实用的. 举几个例子来简单说明 这些例子我引用了Northwind库. 1. TOP 表达式  SQL Serv ...

  7. Mybatis-Plus 插件学习

    官方指南 1.逻辑删除 在相应字段上添加注解 @TableLogic private Integer deleted; 说明: 使用mp自带方法删除和查找都会附带逻辑删除功能 (自己写的xml不会) ...

  8. ARP详解

    1.学习ARP前要了解的内容 建立TCP连接与ARP的关系 应用接受用户提交的数据,触发TCP建立连接,TCP的第一个SYN报文通过connect函数到达IP层,IP层通过查询路由表: 如果目的IP和 ...

  9. kombu在redis中的键值名

    参考flower源码 取队列名,发送到求数量的函数中 queue_names = ControlHandler.get_active_queue_names() queues = yield brok ...

  10. 随便写一个c++类

    为了让代码更贴合实际项目需要,我们分别用xxx.h文件,xxx.cpp文件来包含类的定义,类的声明和类的调用部分,实验平台vs2010 mycoach.h文件 #pragma once #includ ...