{"id":18897566,"url":"https://github.com/zexiplus/my-vue","last_synced_at":"2026-03-01T02:30:21.360Z","repository":{"id":91168521,"uuid":"157471363","full_name":"zexiplus/my-vue","owner":"zexiplus","description":"以学习为目的，实现一个自己的mvvm思想类vue框架","archived":false,"fork":false,"pushed_at":"2018-11-19T02:28:27.000Z","size":185,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-12-31T08:30:08.343Z","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/zexiplus.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,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-11-14T01:27:31.000Z","updated_at":"2020-01-03T09:01:00.000Z","dependencies_parsed_at":null,"dependency_job_id":"32fd81fc-e8dc-40ef-aee5-70f6ee4c670d","html_url":"https://github.com/zexiplus/my-vue","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/zexiplus%2Fmy-vue","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zexiplus%2Fmy-vue/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zexiplus%2Fmy-vue/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zexiplus%2Fmy-vue/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zexiplus","download_url":"https://codeload.github.com/zexiplus/my-vue/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239879022,"owners_count":19712174,"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-11-08T08:38:51.675Z","updated_at":"2026-03-01T02:30:21.285Z","avatar_url":"https://github.com/zexiplus.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MyVue\n以学习为目的，实现一个自己的mvvm思想类vue框架\n\n\n\n## Table of Contents\n\n[TOC]\n\n\n\n## 思路分析\n\n![mvvm](./img/mvvm.png)\n\n- 实现一个Compile，对指令进行解析，初始化视图，并且订阅数据的变更，绑定好更新函数\n- 实现一个Observer，对数据进行劫持，通知数据的变化\n- 实现一个Watcher，将其作为以上两者的一个中介点，在接收数据变更的同时，让Dep添加当前Watcher，并及时通知视图进行update\n- 实现MVVM，整合以上三者，作为一个入口函数\n\n\n\n\n\n\n\n## STEP1\n\n步骤1， 此章节主要实现Complier编译器和MVVM构造函数的主要逻辑\n\n- **MVVM**\n- Compiler\n\n\n\n### MVVM\n\n- **保存el， data等 options参数**\n\n  ```js\n  new MVVM({\n      el: '#app',\n      data() {...}\n  })\n  ```\n\n- **数据监控与劫持**\n\n  ```js\n  new Observer(this.$data)\n  ```\n\n- **编译dom节点**\n\n  ```js\n  new Complier(this.$el, this)\n  ```\n\n- **数据代理，**` this.data.message === this.message`\n\n  ```js\n  proxy(data) {\n      Object.keys(data).forEach(key =\u003e {\n          Object.defineProperty(this, key, {\n              enumerable: true,\n              configurable: true,\n              get() {\n                  return data[key]\n              },\n              set(newValue) {\n                  data[key] = newValue\n              }\n          })\n      })\n  }\n  ```\n\n\n\n- **MVVM Class**\n\n  ```js\n  class MVVM {\n      constructor(options) {\n          // 初始化参数， 把el， data等进行赋值与绑定\n          this.$el = options.el\n          // data如果是函数就取返回值， 如果不是则直接赋值\n          this.$data = typeof options.data === 'function' ? options.data() : options.data\n          // 数据代理, 把data对象属性代理到vm实例上\n          this.proxy(this.$data)\n          // debugger\n          // 把$el真实的dom节点编译成vdom, 并解析相关指令\n          if (this.$el) {\n              // 数据劫持, \n              new Observer(this.$data)\n              new Complier(this.$el, this)\n          }\n      }\n      // 数据代理, 访问/设置 this.a 相当于访问设置 this.data.a\n      proxy(data) {\n          Object.keys(data).forEach(key =\u003e {\n              Object.defineProperty(this, key, {\n                  enumerable: true,\n                  configurable: true,\n                  get() {\n                      return data[key]\n                  },\n                  set(newValue) {\n                      data[key] = newValue\n                  }\n              })\n          })\n      }\n  }\n  ```\n\n\n### Complier\n\n- 通过选择器找到根节点，保存el到自身实例, 保存mvvm单例对象\n\n  ```js\n  this.el = document.querySelector(el)\n  this.vm = vm\n  ```\n\n- 创建fragment文档片段并把原始el dom元素的所有子节点增加到 fragment并返回\n\n  ```js\n  let fragment = document.createDocumentFragment()\n  let firstChild\n  while (firstChild = el.firstChild) {\n      fragment.appendChild(firstChild)\n  \n  }\n  return fragment\n  ```\n\n\n\n\n\n\n\n## STEP2\n\n步骤2， 此章节在步骤1的基础上完善了compiler的逻辑功能\n\n- MVVM\n- Compiler\n\n\n\n### Compiler\n\nCompiler 编译器， 把传入的fragment\n\n- **constructor**\n\n  - 绑定el， 和 vm实例， 便于之后使用\n\n    ```js\n    this.el = document.querySelector(el)\n    this.vm = vm\n    ```\n\n  - 把 el 的所有子节点推入 fregment  并从 dom 移除\n\n    ```js\n    let fragment = this.toFragment(this.el)\n    ```\n\n  - 编译 fragment \n\n    ```js\n    this.compile(fragment)\n    ```\n\n  - 把编译后的 fragment 重新放入 dom\n\n    ```js\n    this.el.appendChild(fragment)\n    ```\n\n- **toFragment**\n\n  创建fragment文档片段并把原始el dom元素的所有子节点增加到 fragment并返回\n\n  ```js\n  let fragment = document.createDocumentFragment()\n  let firstChild\n  while (firstChild = el.firstChild) {\n      fragment.appendChild(firstChild)\n  }\n  return fragment\n  ```\n\n- **compile**\n\n  **编译器函数**， 传入节点， 循环遍历子节点， 如果子节点是非文本元素就**递归调用自身**和对应编译非文本节点的逻辑函数， 如果是文本节点就调用编译文本节点的函数\n\n  ```js\n  compile(parentNode) {\n      let childNodes = parentNode.childNodes\n      console.log('childNodes is', childNodes)\n      childNodes.forEach((node, index) =\u003e {\n          if (this.isElement(node)) {\n              this.compile(node)\n              this.compileNode(node)\n          } else if (this.isText(node)) {\n              this.compileText(node)\n          }\n      })\n  }\n  ```\n\n- **compileNode**\n\n  编译元素节点，把元素的属性放入数组中过滤出带有v-前缀的属性名组成的数组， 调用相应的函数处理对应指令的逻辑功能。\n\n  **主要API**\n\n  - **node.getAttributeNames()** 返回所有node属性名的数组例如 ['id', 'class', 'v-model']\n  - **node.getAttribute(attrname)**  获取节点对应的属性值 \n\n  ```js\n   compileNode(node) {\n          let attrs = node.getAttributeNames()\n          // 把已v-指令存到一个数组中\n          attrs = attrs.filter(this.isDirective)\n          attrs.forEach((item) =\u003e {\n              let value = this.splitData(node.getAttribute(item), this.vm.$data)\n              if (compileUtil[item]) {\n                  compileUtil[item](value, node)\n              } else {\n                  console.warn(`can't find directive ${item}`)\n              }\n          })\n      }\n  ```\n\n- **v-model指令的实现**\n\n  在绑定有v-model的节点上注册input事件回调， 把event.target.value的值传递给vm.$data...\n\n  ```js\n  'v-model': function (value, node, vm, expr) {\n      node \u0026\u0026 (node.value = value)\n      node.addEventListener('input', (e) =\u003e {\n          this.setVal(vm.$data, expr, e.target.value)\n      })\n  }\n  ```\n\n- **compileText**\n\n  **编译文本节点函数**, 取出文本节点带有{{message}}的属性做替换， 并把替换后的值插入node\n\n  替换文本节点内容api :  **node.textContent = value**\n\n  ```js\n  compileText(node) {\n      // 测试文本节点含有 {{val}} 的 regexp\n      let reg = /\\{\\{([^}]+)\\}\\}/g\n      // 拿到文本节点的文本值\n      let text = node.textContent\n      if (reg.test(text)) {\n          // 去掉{{}} 保留 value\n          let attrName = text.replace(reg, (...args) =\u003e {\n              return args[1]\n          })\n          let textValue = this.splitData(attrName, this.vm.$data)\n          compileUtil.updateText(textValue, node, this.vm)\n      }\n  }\n  ```\n\n- **splitData**\n\n  **迭代取值器**， 把形如 'group.member.age'这样的字符串找到 $data上的对应的值并返回,\n\n  **主要api**： **array.reduce**\n\n  ```js\n  splitData(attr, data) {\n      // 传入 attr 形如 'group.member.name', 找到$data上对应的属性值并返回\n      let arr = attr \u0026\u0026 attr.split('.')\n      let ret = arr.reduce((prev, next) =\u003e {\n          return prev[next]\n      }, data)\n      return ret\n  }\n  ```\n\n\n\n\n\n\n\n## STEP3\n\n步骤3， 此章节在步骤1, 2的基础上， 新增加 Observer类实现数据劫持\n\n- MVVM \n- Compiler\n- Observer \n\n\n\n\n\n### Observer\n\nObserver 观察者， 对数据进行劫持， 对data的每一个属性设置 getter和setter， 用来数据发生变动时通知观察者触发视图更新\n\n\n\n- **observer**\n\n  数据观察器， 遍历对象属性， 设置属性为响应式\n\n  ```js\n  observer(data) {\n      // 递归的终止条件： 当观察数据不存在或不再是对象是停止\n      if (!data || typeof data !== 'object') {\n          return\n      }\n      Object.keys(data).forEach(key =\u003e {\n          // 递归调用自身， 深层遍历对象属性\n          this.observer(data[key])\n          // 调用响应式函数， 设置响应式属性\n          this.setReactive(data, key)\n      })\n  }\n  ```\n\n- **setReactive**\n\n  设置响应式的函数， 对数据进行劫持， 设置getter和setter，当get时订阅消息， 当set时发布通知\n\n  ```js\n  setReactive(data, key) {\n      let _this = this\n      Object.defineProperty(data, key, {\n          enumerable: true,\n          configurable: true,\n          get(value) {\n              // 进行订阅\n              return value\n          },\n          set(newValue) {\n              if (newValue !== data[key]) {\n                  // 设置新值， 新值没有进行过响应式处理， 所以要重新observer\n                  data[key] = newValue\n                  _this.observer(data[key])\n                  // 发布通知\n              }\n          }\n      })\n  }\n  ```\n\n\n\n\n\n## STEP4\n\n步骤4， 此章节实现Watcher类, 将其作为comiler和observer的一个中介点，在接收数据变更的同时，让Dep添加当前Watcher，并及时通知视图进行update\n\n- MVVM \n- Compiler\n- Observer \n- **Watcher**\n\n\n\n### Watcher\n\n- 保存初始值, 设置Dep的target对象\n- 保存接收通知时的回调函数.\n-  对比新旧值, 更新视图变化\n\n```js\nclass Watcher {\n\tconstructor(vm, expr, cb) {\n        this.vm = vm\n        this.expr = expr\n        this.cb = cb\n        \n        // 保存原始值\n        this.value = this.getValAndSetTarget()\n    }\n    getValAndSetTarget() {\n        // 设置Dep.target \n    \tDep.target = this\n        let value = this.parseExpr()\n        Dep.target = null\n        return value\n    }\n    parseExpr(expr) {\n    \tlet attr = expr.split('.')\n        return attr.reduce((prev, next) =\u003e {\n            return prev[next]\n        }\n    }\n    update() {\n    \tlet newVal = this.parseExpr(this.expr)\n        let oldVal = this.value\n        if (newVal !== oldVal) {\n            this.cb \u0026\u0026 this.cb()\n        }\n    }\n}\n```\n\n\n\n### Compiler\n\n每个表达式都有一个对应的watcher. \n\n例如 {{message.a}}   就会有一个 new Watcher(vm, 'message.a', updateText)\n\n```js\nclass Compiler {\n\tconstructor() {...}\n    text(node, vm, expr) {\n        let updateFn = this.updater['textUpdater'];\n        \n        let value = this.getTextVal(vm,expr);\n        expr.replace(/\\{\\{([^}]+)\\}\\}/g,(...arguments)=\u003e{\n            /********************* add code **************************/\n            new Watcher(vm,arguments[1],(newValue)=\u003e{\n                updateFn \u0026\u0026 updateFn(node,this.getTextVal(vm,expr));\n            })\n            /********************************************************/\n            return arguments[1];\n        });\n        updateFn \u0026\u0026 updateFn(node,value);\n    }\n}\n```\n\n\n\n\n\n### Dep\n\nDep用于添加watcher对象和通知数据变化\n\n```javascript\nclass Dep {\n\tconstructor() {\n        this.subs = []\n    }\n    // 添加watcher对象\n    addSubs(watcher) {\n        this.subs.push(watcher)\n    }\n    // 通知数据变化\n    notify() {\n        this.subs.forEach(watcher =\u003e watcher.update()\n    }\n}\n```\n\n\n\n\n\n### Observer\n\nObserver 观察者， 对数据进行劫持， 对data的每一个属性设置 getter和setter， 用来数据发生变动时通知观察者触发视图更新\n\n- **setReactive**\n\n  设置响应式的函数， 对数据进行劫持， 设置getter和setter，**当get时订阅消息， 当set时发布通知**\n\n  ```js\n  setReactive(data, key) {\n      let _this = this\n      let dep = new Dep()\n      Object.defineProperty(data, key, {\n          enumerable: true,\n          configurable: true,\n          get(value) {\n              // 进行订阅, Dep.target是一个watcher对象\n              /**************** add code ********************/\n              Dep.target \u0026\u0026 dep.addSubs(Dep.target)\n              /**************** ******** ********************/\n              return value\n          },\n          set(newValue) {\n              if (newValue !== data[key]) {\n                  // 设置新值， 新值没有进行过响应式处理， 所以要重新observer\n                  data[key] = newValue\n                  _this.observer(data[key])\n                  // 发布通知\n                  /****************** add code **********************/\n                  dep.notify()\n                  /**************************************************/\n              }\n          }\n      })\n  }\n  ```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzexiplus%2Fmy-vue","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzexiplus%2Fmy-vue","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzexiplus%2Fmy-vue/lists"}