{"id":13835467,"url":"https://github.com/mrhaoxiaojun/MVVM","last_synced_at":"2025-07-10T07:32:15.325Z","repository":{"id":98782654,"uuid":"140789066","full_name":"mrhaoxiaojun/MVVM","owner":"mrhaoxiaojun","description":"Vue 2.0 -- 手把手教你MVVM，史最全的注释、包含你关心的defineProperty结合发布者-订阅者模式实现数据的数据劫持监听，配合compile模版编译+watcher做为桥梁","archived":false,"fork":false,"pushed_at":"2018-07-30T09:10:56.000Z","size":187,"stargazers_count":25,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-08-05T14:16:17.524Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mrhaoxiaojun.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-07-13T02:52:47.000Z","updated_at":"2022-10-20T05:30:58.000Z","dependencies_parsed_at":"2023-03-16T11:15:48.324Z","dependency_job_id":null,"html_url":"https://github.com/mrhaoxiaojun/MVVM","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrhaoxiaojun%2FMVVM","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrhaoxiaojun%2FMVVM/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrhaoxiaojun%2FMVVM/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrhaoxiaojun%2FMVVM/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrhaoxiaojun","download_url":"https://codeload.github.com/mrhaoxiaojun/MVVM/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225627382,"owners_count":17498975,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-04T14:01:02.689Z","updated_at":"2024-11-20T20:31:27.845Z","avatar_url":"https://github.com/mrhaoxiaojun.png","language":"JavaScript","funding_links":[],"categories":["目录"],"sub_categories":[],"readme":"---\ntitle:  手把手带你走进MVVM\ndate: 2018-01-9 11:04:01 \ncategories: others \n\n---\n\n\n\n[参考地址](https://github.com/DMQ/mvvm) \n\n[本文地址](https://github.com/mrhaoxiaojun/MVVM.git)\n\n## 剖析Vue实现原理 - 如何实现双向绑定mvvm\n\n![mvvm-jian](手把手带你走进MVVM/mvvm-jian.png)\n\n\u003c!--more--\u003e\n\n\u003e 本文能帮你做什么？\n\u003e 1、了解vue的双向数据绑定原理以及核心代码模块\n\u003e 2、缓解好奇心的同时了解如何实现双向绑定\n\n**特此申明**：*小编我怀着一颗诚挚的内心，通过github上牛人针对vue的mvvm的实现方案进行了解，已经对vue的一些应用经验及原理认识，后又进行es6改写，意在知其然知其所以然，在这里也是单纯实现经典的mvvm，代码比较粗糙简陋加之一些东西删减，本着分享的精神，拒绝单纯的拿来主义，万望各路码神海涵不足之处，在这里特别感谢各种无私奉献的分享者*\n\n### 几种实现双向绑定的做法\n\n目前几种主流的mvc(vm)框架都实现了单向数据绑定，而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素（input、textare等）添加了change(input)事件，来动态修改model和 view，并没有多高深。所以无需太过介怀是实现的单向或双向绑定。\n\n实现数据绑定的做法有大致如下几种：\n\n\u003e 发布者-订阅者模式（backbone.js）\n\n\u003e 脏值检查（angular.js） \n\n\u003e 数据劫持（vue.js）\n\n**发布者-订阅者模式:** 一般通过sub, pub的方式实现数据和视图的绑定监听，更新数据方式通常做法是 `vm.set('property', value)`，不太熟悉去问一下度娘\n\n这种方式现在毕竟太low了，我们更希望通过 `vm.property = value`这种方式更新数据，同时自动更新视图，于是有了下面两种方式\n\n**脏值检查:** angular.js 是通过脏值检测的方式比对数据是否有变更，来决定是否更新视图，最简单的方式就是通过 `setInterval()` 定时轮询检测数据变动，当然Google不会这么low，angular只有在指定的事件触发时进入脏值检测，大致如下：\n\n- DOM事件，譬如用户输入文本，点击按钮等。( ng-click )\n- XHR响应事件 ( $http )\n- 浏览器Location变更事件 ( $location )\n- Timer事件( $timeout , $interval )\n- 执行 $digest() 或 $apply()\n\n**数据劫持:** vue.js 则是采用数据劫持结合发布者-订阅者模式的方式，通过`Object.defineProperty()`来劫持各个属性的`setter`，`getter`，在数据变动时发布消息给订阅者，触发相应的监听回调。\n\n### 思路整理\n\n已经了解到vue是通过数据劫持的方式来做数据绑定的，其中最核心的方法便是通过`Object.defineProperty()`来实现对属性的劫持，达到监听数据变动的目的，无疑这个方法是本文中最重要、最基础的内容之一，如果不熟悉defineProperty，猛戳[这里](http://www.baidu.com) 整理了一下，要实现mvvm的双向绑定，就必须要实现以下几点： 1、实现一个数据监听器Observer，能够对数据对象的所有属性进行监听，如有变动可拿到最新值并通知订阅者 2、实现一个指令解析器Compile，对每个元素节点的指令进行扫描和解析，根据指令模板替换数据，以及绑定相应的更新函数 3、实现一个Watcher，作为连接Observer和Compile的桥梁，能够订阅并收到每个属性变动的通知，执行指令绑定的相应回调函数，从而更新视图 4、mvvm入口函数，整合以上三者\n\n![mvvm](手把手带你走进MVVM/mvvm-new.png)\n\n不多赘述，一言不合就上图\n\n大家可去下载去具体文件里面看，我写了详尽的注释，每个模块的功能，分工，每个方法任务，等等\n\n上图为小编我根据自己的理解后重新绘制，本打算绘制再细一些，感觉会让人理解更复杂而后就有了上图，代码中如果问题，欢迎指正，一起学习，你们的start是小编的动力\n\n下面为具体代码实现，为了大家方便我还是粘贴在readme里面，每个文件不多说了，前面做了文案及脑图思路梳理，文件里我也了详尽的注释\n\n### MVVM.html\n\n```\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n  \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"\u003e\n  \u003ctitle\u003emvvm\u003c/title\u003e\n  \u003cscript src=\"./observer.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"./watcher.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"./compile.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"./mvvm.js\"\u003e\u003c/script\u003e\n  \u003c!-- \u003cscript src=\"http://cdn.bootcss.com/vue/1.0.25/vue.js\"\u003e\u003c/script\u003e --\u003e\n\u003c/head\u003e\n\u003cbody id=\"app\"\u003e\n  \u003cinput type=\"text\" v-model=\"msg\"\u003e\n  \u003cdiv v-text=\"a.b\"\u003e\u003c/div\u003e\n  \u003cdiv\u003e\n    {{msg}}\n  \u003c/div\u003e\n\u003c/body\u003e\n\u003cscript\u003e\n  let vm = new MVVM({\n    el:\"#app\",\n    data:{\n      msg:\"helloword\",\n      a:{\n        b:\"a\"\n      }\n    }\n  })\n\u003c/script\u003e\n\u003c/html\u003e\n```\n\n### MVVM.js\n\n```\n/**\n *  create haoxiaojun on 2018.7\n * -----------------------------------------------------\n * 1、实现数据代理\n * 2、模版解析\n * 3、劫持监所有的属性\n * -----------------------------------------------------\n */\nclass MVVM {\n  /**\n   *Creates an instance of MVVM.\n   * @param {*} options 当前实例传递过来的参数\n   * @memberof MVVM\n   */\n  constructor(options){\n    this.$opt = options|| {}\n    this.$data = options.data;\n    // 实现数据代理\n    Object.keys(this.$data).forEach((key)=\u003e{\n      this._proxyData(key)\n    })\n    // 劫持监所有的属性\n    observe(this.$data,this)\n    // 模版编译\n    new Compile(options.el || document.body,this)\n  }\n  _proxyData(key){\n    Object.defineProperty(this,key,{\n      configurable:false,\n      enumerable:true,\n      get(){\n        return this.$data[key]\n      },\n      set(newVal){\n        this.$data[key] = newVal\n      }\n    })\n  }\n}\n```\n\n### Observer.js\n\n```\n/**\n *  create haoxiaojun on 2018.7\n * -----------------------------------------------------\n * 1、实现一个数据监听器Observer\n * 2、通知和添加订阅者\n * -----------------------------------------------------\n */\nclass Observer {\n  /**\n   *Creates an instance of Observer.\n   * @param {*} data 需要劫持监听的数据\n   * @memberof Observer\n   */\n  constructor(data){\n    this.$data = data || {}\n    this.init()\n  }\n  init(){\n    Object.keys(this.$data).forEach(key=\u003e{\n      this.defineReative(key,this.$data[key])\n    })\n  }\n  defineReative(key,val){\n    // 创建发布者-订阅者\n    let dep = new Dep()\n    // 再去观察子对象\n    observe(val)\n    Object.defineProperty(this.$data,key,{\n      configurable:false,\n      enumerable:true,\n      get(){\n        // 添加订阅者\n        Dep.target \u0026\u0026 dep.addSub(Dep.target)\n        return val\n      },\n      set(newVal){\n        if( newVal == val ) return false;\n        val = newVal\n        // 新的值是object的话，进行监听\n        observe(newVal)\n        // 通知订阅者\n        dep.notfiy()\n      }\n    })\n  }\n}\n/**\n * 是否进行劫持监听\n *\n * @param {*} value 监听对象\n * @param {*} vm 当前实例\n * @returns 返回 监听实例\n */\nfunction observe(value, vm) {\n  if (!value || typeof value !== 'object') {\n      return;\n  }\n  return new Observer(value);\n};\nclass Dep{\n  constructor(){\n    this.subs = []\n  }\n  /**\n   *维护订阅者数组\n   *\n   * @param {*} sub 订阅实例\n   * @memberof Dep\n   */\n  addSub(sub){\n    this.subs.push(sub)\n  }\n  notfiy(){\n    this.subs.forEach(sub=\u003e{\n      // 通知数据更新\n      sub.update()\n    })\n  }\n}\n```\n\n## Compile.js\n\n```\n/**\n *  create haoxiaojun on 2018.7\n * -----------------------------------------------------\n * 1、取真实dom节点\n * 2、我们fragment 创建文档碎片，将真是dmo，移动指缓存\n * 3、编译虚拟dom，解析模版语法\n * 4、回填至真是dom，实现模版语法解析，更新试图\n * -----------------------------------------------------\n */\nclass Compile{\n  /**\n   * \n   *Creates an instance of Compile.\n   * @param {*} el dmo选择器\n   * @param {*} vm 当前实例\n   * @memberof Compile\n   */\n  constructor(el,vm){\n    this.$vm = vm;\n    this.$el = this.isElementNode(el) ? el : document.querySelector(el)\n    if(this.$el){\n      this.$fragment = this.node2Fragment(this.$el)\n      this.init()\n      this.$el.appendChild(this.$fragment)\n    }\n  }\n  init(){\n    this.compileElement(this.$fragment)\n  }\n  /**\n   *\n   * 编译element\n   * @param {*} el dmo节点\n   * @memberof Compile\n   */\n  compileElement(el){\n    // 1、取所有子节点\n    let childNodes = el.childNodes\n    // 2、循环子节点\n    Array.from(childNodes).forEach((node)=\u003e{\n      // 判断是文本节点还是dom节点\n      if(this.isElementNode(node)){\n        this.compileDom(node)\n      }else if (this.isTextNode(node)){\n        this.compileText(node)\n      }\n      // 判断当前节点是否有子节点，如果有，递归查找\n      if(node.childNodes \u0026\u0026 node.childNodes.length){\n        this.compileElement(node)\n      }\n    })\n  }\n  /**\n   *\n   * 编译元素节点\n   * @param {*} node 需要编译的当前节点\n   * @memberof Compile\n   */\n  compileDom(node){\n    // 取当前节点的属性集合\n    let attrs = node.attributes\n    // 循环属性数组\n    Array.from(attrs).forEach(attr =\u003e {\n      let attrName = attr.name\n      // 判断当前属性是否是指令\n      if(this.isDirective(attrName)){\n        let [,dir] = attrName.split(\"-\")\n        let expr = attr.value\n        //判断当前属性是普通指令还是事件指令\n        if(this.isEventDirective(dir)){\n          compileUtil.eventHandler(node,expr,dir,this.$vm)\n        }else{\n          compileUtil[dir] \u0026\u0026 compileUtil[dir](node,expr,this.$vm)\n        }\n      }\n    });\n  }\n  /**\n   * \n   * 编译文本节点\n   * @param {*} node 需要编译的当前节点\n   * @memberof Compile\n   */\n  compileText(node){\n    var text = node.textContent;\n    var reg = /\\{\\{(.*)\\}\\}/;\n    if(reg.test(text)){\n      compileUtil.text(node,RegExp.$1,this.$vm)\n    }\n  }\n  /**\n   * 判断是否是元素节点\n   *\n   * @param {*} el 节点\n   * @returns 是否\n   * @memberof Compile\n   */\n  isElementNode(el){\n    return el.nodeType == 1\n  }\n  /**\n   * 过滤是否是指令\n   *\n   * @param {*} name 属性名\n   * @returns 是否\n   * @memberof Compile\n   */\n  isDirective(name){\n    return name.indexOf(\"v-\") == 0\n  }\n  /**\n   * 判断是否是事件指令\n   *\n   * @param {*} dir 指令,on:click\n   * @returns 是否\n   * @memberof Compile\n   */\n  isEventDirective(dir){\n    return dir.indexOf(\"on\") == 0\n  }\n  /**\n   * 判断是否是文本节点\n   *\n   * @param {*} el 节点\n   * @returns 是否\n   * @memberof Compile\n   */\n  isTextNode(el){\n    return el.nodeType == 3\n  }\n  /**\n   * 将真实dom拷贝到内存中\n   *\n   * @param {*} el 真实dom\n   * @returns 文档碎片\n   * @memberof Compile\n   */\n  node2Fragment(el){\n    let fragment = document.createDocumentFragment();\n    let children\n    while(children = el.firstChild){\n      fragment.appendChild(el.firstChild)\n    }\n    return fragment\n  }\n}\n\n// 指令处理工具\nlet compileUtil = {\n  /**\n   * 处理文本节点\n   *\n   * @param {*} node 当前节点\n   * @param {*} expr 表达式\n   * @param {*} vm 当前实例\n   */\n  text(node,expr,vm){\n    this.buid(node,expr,vm,\"text\")\n  },\n  /**\n   * 处理表单元素节点\n   *\n   * @param {*} node 当前节点\n   * @param {*} expr 表达式\n   * @param {*} vm \b当前实例\n   */\n  model(node,expr,vm){\n    this.buid(node,expr,vm,\"model\")\n    var me = this,\n    val = this.getVMVal(vm, expr);\n    node.addEventListener('input', function(e) {\n        var newValue = e.target.value;\n        if (val === newValue) {\n            return;\n        }\n\n        me.setVMVal(vm, expr, newValue);\n        val = newValue;\n    });\n  },\n  /**\n   * 事件处理\n   *\n   * @param {*} node 当前节点\n   * @param {*} expr 表达式\n   * @param {*} dir 指令\n   * @param {*} vm 当前实例\n   */\n  eventHandler(node,expr,dir,vm){\n    let [,eventType] = dir.split(\":\");\n    let fn = vm.$opt.methods \u0026\u0026 vm.$opt.methods[expr]\n    if(eventType \u0026\u0026 fn){\n      node.addEventListener(eventType,fn.bind(vm),false)\n    }\n  },\n  /**\n   * 绑定事件统一处理方法抽离，添加watcher\n   *\n   * @param {*} node 当前节点\n   * @param {*} expr 表达式\n   * @param {*} vm 当前实例\n   * @param {*} dir 指令\n   */\n  buid(node,expr,vm,dir){\n    let updateFn = update[dir+'Update']\n    updateFn \u0026\u0026 updateFn(node,this.getVMVal(vm,expr))\n\n    new Watcher(vm, expr, function(value, oldValue) {\n      updateFn \u0026\u0026 updateFn(node, value, oldValue);\n  });\n  },\n  /**\n   * 获取表达式代表的值\n   *\n   * @param {*} vm 当前实例\n   * @param {*} expr 表达式\n   * @returns\n   */\n  getVMVal(vm,expr){\n    // return vm[expr] 要考虑，a.b.c的情况\n    let exp = expr.split(\".\");\n    let val = vm\n    exp.forEach((k)=\u003e{\n      val = val[k]\n    })\n    return val\n  },\n  /**\n   * 设置更新数据里对应的表达式的值\n   *\n   * @param {*} vm\n   * @param {*} expr\n   * @param {*} newValue\n   */\n  setVMVal(vm,expr,newValue){\n    let exp = expr.split('.');\n    let val = vm\n    exp.forEach((key,i)=\u003e{\n      if(i\u003cexp.length-1){\n        val = val[key]\n      }else{\n        val[key] = newValue\n      }\n    })\n  }\n}\n// 数据更新\nlet update = {\n  textUpdate(node,value){\n    node.textContent = typeof value == 'undefined' ? '' : value\n  },\n  modelUpdate(node,value){\n    node.value = typeof value == 'undefined' ? '' : value\n  }\n}\n```\n\n### Watcher.js\n\n```\n/**\n *  create haoxiaojun on 2018.7\n * -----------------------------------------------------\n * 1、实现一个Watcher，作为连接Observer和Compile的桥梁\n * 2、通知和添加订阅者\n * -----------------------------------------------------\n */\nclass Watcher {\n  /**\n   *Creates an instance of Watcher.\n   * @param {*} vm 当前实例\n   * @param {*} expOrFn 表达式\n   * @param {*} cb 更新回调用\n   * @memberof Watcher\n   */\n  constructor(vm,expOrFn,cb){\n    this.$vm = vm\n    this.$expOrFn = expOrFn\n    this.$cb = cb\n    this.value = this.get()\n  }\n  get(){\n    // 添加订阅者\n    Dep.target = this;\n    // let dep = new Dep()\n    // 去modal中取值，这个时候必然会触发defineProperty的getter，真正的push订阅者\n    let value = this.getVMVal(this.$vm,this.$expOrFn)\n    // 用完了，重置回去\n    Dep.target = null\n    return value\n  }\n  /**\n   * 取modal里的值\n   *\n   * @param {*} vm 当前实例\n   * @param {*} expr 表达式\n   * @returns 返回指\n   * @memberof Watcher\n   */\n  getVMVal(vm,expr){\n    // return vm[expr] 要考虑，a.b.c的情况\n    let exp = expr.split(\".\");\n    let val = vm\n    exp.forEach((k)=\u003e{\n      val = val[k]\n    })\n    return val\n  }\n  // 对外暴露的跟新方法，比较新老值，得到订阅通知进行更新\n  update(){\n    let oldVal = this.value;\n    let newVal = this.getVMVal(this.$vm,this.$expOrFn)\n    if (newVal !== oldVal) {\n        this.value = newVal;\n        this.$cb(newVal, oldVal);\n    }\n  }\n}\n```\n\n\n\n最后感谢您的阅读","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrhaoxiaojun%2FMVVM","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrhaoxiaojun%2FMVVM","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrhaoxiaojun%2FMVVM/lists"}