当前位置:Gxlcms > JavaScript > vue.js 实现v-model与{{}}指令方法

vue.js 实现v-model与{{}}指令方法

时间:2021-07-01 10:21:17 帮助过:5人阅读

上次我们已经分析了vue.js是通过Object.defineProperty以及发布订阅模式来进行数据劫持和监听,并且实现了一个简单的demo。今天,我们就基于上一节的代码,来实现一个MVVM类,将其与html结合在一起,并且实现v-model以及{{}}语法。

tips:本节新增代码(去除注释)在一百行左右。使用的Observer和Watcher都是延用上一节的代码,没有修改。

接下来,让我们一步步来,实现一个MVVM类。

构造函数

首先,一个MVVM的构造函数如下(和vue.js的构造函数一样):

  1. class MVVM {
  2. constructor({ data, el }) {
  3. this.data = data;
  4. this.el = el;
  5. this.init();
  6. this.initDom();
  7. }
  8. }

和vue.js一样,有它的data属性以及el元素。

初始化操作

vue.js可以通过this.xxx的方法来直接访问this.data.xxx的属性,这一点是怎么做到的呢?其实答案很简单,它是通过Object.defineProperty来做手脚,当你访问this.xxx的时候,它返回的其实是this.data.xxx。当你修改this.xxx值的时候,其实修改的是this.data.xxx的值。具体可以看如下代码:

  1. class MVVM {
  2. constructor({ data, el }) {
  3. this.data = data;
  4. this.el = el;
  5. this.init();
  6. this.initDom();
  7. }
  8. // 初始化
  9. init() {
  10. // 对this.data进行数据劫持
  11. new Observer(this.data);
  12. // 传入的el可以是selector,也可以是元素,因此我们要在这里做一层处理,保证this.$el的值是一个元素节点
  13. this.$el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el);
  14. // 将this.data的属性都绑定到this上,这样用户就可以直接通过this.xxx来访问this.data.xxx的值
  15. for (let key in this.data) {
  16. this.defineReactive(key);
  17. }
  18. }
  19. defineReactive(key) {
  20. Object.defineProperty(this, key, {
  21. get() {
  22. return this.data[key];
  23. },
  24. set(newVal) {
  25. this.data[key] = newVal;
  26. } //前端全栈学习交流圈:866109386
  27. })//面向1-3年前端开发人员
  28. }//帮助突破技术瓶颈,提升思维能力。
  29. // 是否是属性节点
  30. isElementNode(node) {
  31. return node.nodeType === 1;
  32. }
  33. }

在完成初始化操作后,我们需要对this.$el的节点进行编译。目前我们要实现的语法有v-model和{{}}语法,v-model这个属性只可能会出现在元素节点的attributes里,而{{}}语法则是出现在文本节点里。

fragment

在对节点进行编译之前,我们先考虑一个现实问题:如果我们在编译过程中直接操作DOM节点的话,每一次修改DOM都会导致DOM的回流或重绘,而这一部分性能损耗是很没有必要的。因此,我们可以利用fragment,将节点转化为fragment,然后在fragment里编译完成后,再将其放回到页面上。

  1. class MVVM {
  2. constructor({ data, el }) {
  3. this.data = data;
  4. this.el = el;//前端全栈交流学习圈:866109386
  5. this.init();//针对1-3年前端开发人员
  6. this.initDom();//帮助突破技术瓶颈,提升思维能力。
  7. }
  8. initDom() {
  9. const fragment = this.node2Fragment();
  10. this.compile(fragment);
  11. // 将fragment返回到页面中
  12. document.body.appendChild(fragment);
  13. }
  14. // 将节点转为fragment,通过fragment来操作DOM,可以获得更高的效率
  15. // 因为如果直接操作DOM节点的话,每次修改DOM都会导致DOM的回流或重绘,而将其放在fragment里,修改fragment不会导致DOM回流和重绘
  16. // 当在fragment一次性修改完后,在直接放回到DOM节点中
  17. node2Fragment() {
  18. const fragment = document.createDocumentFragment();
  19. let firstChild;
  20. while(firstChild = this.$el.firstChild) {
  21. fragment.appendChild(firstChild);
  22. }
  23. return fragment;
  24. }
  25. }

实现v-model

在将node节点转为fragment后,我们来对其中的v-model语法进行编译。

由于v-model语句只可能会出现在元素节点的attributes里,因此,我们先判断该节点是否为元素节点,若为元素节点,则判断其是否是directive(目前只有v-model),若都满足的话,则调用CompileUtils.compileModelAttr来编译该节点。

编译含有v-model的节点主要有两步:

  1. 为元素节点注册input事件,在input事件触发的时候,更新vm(this.data)上对应的属性值。
  2. 对v-model依赖的属性注册一个Watcher函数,当依赖的属性发生变化,则更新元素节点的value。
  1. class MVVM {
  2. constructor({ data, el }) {
  3. this.data = data;
  4. this.el = el;
  5. this.init();
  6. this.initDom();
  7. }
  8. initDom() {
  9. const fragment = this.node2Fragment();
  10. this.compile(fragment);
  11. // 将fragment返回到页面中
  12. document.body.appendChild(fragment);
  13. }
  14. compile(node) {
  15. if (this.isElementNode(node)) {
  16. // 若是元素节点,则遍历它的属性,编译其中的指令
  17. const attrs = node.attributes;
  18. Array.prototype.forEach.call(attrs, (attr) => {
  19. if (this.isDirective(attr)) {
  20. CompileUtils.compileModelAttr(this.data, node, attr)
  21. }
  22. })
  23. }
  24. // 若节点有子节点的话,则对子节点进行编译
  25. if (node.childNodes && node.childNodes.length > 0) {
  26. Array.prototype.forEach.call(node.childNodes, (child) => {
  27. this.compile(child);
  28. })
  29. }
  30. }
  31. // 是否是属性节点
  32. isElementNode(node) {
  33. return node.nodeType === 1;
  34. }
  35. // 检测属性是否是指令(vue的指令是v-开头)
  36. isDirective(attr) {
  37. return attr.nodeName.indexOf('v-') >= 0;
  38. }
  39. }
  40. const CompileUtils = {
  41. // 编译v-model属性,为元素节点注册input事件,在input事件触发的时候,更新vm对应的值。
  42. // 同时也注册一个Watcher函数,当所依赖的值发生变化的时候,更新节点的值
  43. compileModelAttr(vm, node, attr) {
  44. const { value: keys, nodeName } = attr;
  45. node.value = this.getModelValue(vm, keys);
  46. // 将v-model属性值从元素节点上去掉
  47. node.removeAttribute(nodeName);
  48. node.addEventListener('input', (e) => {
  49. this.setModelValue(vm, keys, e.target.value);
  50. });
  51. new Watcher(vm, keys, (oldVal, newVal) => {
  52. node.value = newVal;
  53. });
  54. },
  55. /* 解析keys,比如,用户可以传入
  56. * <input v-model="obj.name" />
  57. * 这个时候,我们在取值的时候,需要将"obj.name"解析为data[obj][name]的形式来获取目标值
  58. */
  59. parse(vm, keys) {
  60. keys = keys.split('.');
  61. let value = vm;
  62. keys.forEach(_key => {
  63. value = value[_key];
  64. });
  65. return value;
  66. },
  67. // 根据vm和keys,返回v-model对应属性的值
  68. getModelValue(vm, keys) {
  69. return this.parse(vm, keys);
  70. },
  71. // 修改v-model对应属性的值
  72. setModelValue(vm, keys, val) {
  73. keys = keys.split('.');
  74. let value = vm;
  75. for(let i = 0; i < keys.length - 1; i++) {
  76. value = value[keys[i]];
  77. }
  78. value[keys[keys.length - 1]] = val;
  79. },
  80. }

实现{{}}语法

{{}}语法只可能会出现在文本节点中,因此,我们只需要对文本节点做处理。如果文本节点中出现{{key}}这种语句的话,我们则对该节点进行编译。在这里,我们可以通过下面这个正则表达式来对文本节点进行处理,判断其是否含有{{}}语法。

  1. const textReg = /\{\{\s*\w+\s*\}\}/gi; // 检测{{name}}语法
  2. console.log(textReg.test('sss'));
  3. console.log(textReg.test('aaa{{ name }}'));
  4. console.log(textReg.test('aaa{{ name }} {{ text }}'));

若含有{{}}语法,我们则可以对其处理,由于一个文本节点可能出现多个{{}}语法,因此编译含有{{}}语法的文本节点主要有以下两步:

  1. 找出该文本节点中所有依赖的属性,并且保留原始文本信息,根据原始文本信息还有属性值,生成最终的文本信息。比如说,原始文本信息是"test {{test}} {{name}}",那么该文本信息依赖的属性有this.data.test和this.data.name,那么我们可以根据原本信息和属性值,生成最终的文本。
  2. 为该文本节点所有依赖的属性注册Watcher函数,当依赖的属性发生变化的时候,则更新文本节点的内容。
  1. class MVVM {
  2. constructor({ data, el }) {
  3. this.data = data;
  4. this.el = el;
  5. this.init();
  6. this.initDom();
  7. }
  8. initDom() {
  9. const fragment = this.node2Fragment();
  10. this.compile(fragment);
  11. // 将fragment返回到页面中
  12. document.body.appendChild(fragment);
  13. }
  14. compile(node) {
  15. const textReg = /\{\{\s*\w+\s*\}\}/gi; // 检测{{name}}语法
  16. if (this.isTextNode(node)) {
  17. // 若是文本节点,则判断是否有{{}}语法,如果有的话,则编译{{}}语法
  18. let textContent = node.textContent;
  19. if (textReg.test(textContent)) {
  20. // 对于 "test{{test}} {{name}}"这种文本,可能在一个文本节点会出现多个匹配符,因此得对他们统一进行处理
  21. // 使用 textReg来对文本节点进行匹配,可以得到["{{test}}", "{{name}}"]两个匹配值
  22. const matchs = textContent.match(textReg);
  23. CompileUtils.compileTextNode(this.data, node, matchs);
  24. }
  25. }
  26. // 若节点有子节点的话,则对子节点进行编译
  27. if (node.childNodes && node.childNodes.length > 0) {
  28. Array.prototype.forEach.call(node.childNodes, (child) => {
  29. this.compile(child);
  30. })
  31. }
  32. }
  33. // 是否是文本节点
  34. isTextNode(node) {
  35. return node.nodeType === 3;
  36. }
  37. }
  38. const CompileUtils = {
  39. reg: /\{\{\s*(\w+)\s*\}\}/, // 匹配 {{ key }}中的key
  40. // 编译文本节点,并注册Watcher函数,当文本节点依赖的属性发生变化的时候,更新文本节点
  41. compileTextNode(vm, node, matchs) {
  42. // 原始文本信息
  43. const rawTextContent = node.textContent;
  44. matchs.forEach((match) => {
  45. const keys = match.match(this.reg)[1];
  46. console.log(rawTextContent);
  47. new Watcher(vm, keys, () => this.updateTextNode(vm, node, matchs, rawTextContent));
  48. });
  49. this.updateTextNode(vm, node, matchs, rawTextContent);
  50. },
  51. // 更新文本节点信息
  52. updateTextNode(vm, node, matchs, rawTextContent) {
  53. let newTextContent = rawTextContent;
  54. matchs.forEach((match) => {
  55. const keys = match.match(this.reg)[1];
  56. const val = this.getModelValue(vm, keys);
  57. newTextContent = newTextContent.replace(match, val);
  58. })
  59. node.textContent = newTextContent;
  60. }
  61. }

结语

这样,一个具有v-model和{{}}功能的MVVM类就已经完成了

这里也有一个简单的样例(忽略样式)。

接下来的话,可能会继续实现computed属性,v-bind方法,以及支持在{{}}里面放表达式。如果觉得这个文章对你有帮助的话,麻烦点个赞,嘻嘻。

最后,贴上所有的代码:

  1. class Observer {
  2. constructor(data) {
  3. // 如果不是对象,则返回
  4. if (!data || typeof data !== 'object') {
  5. return;
  6. }
  7. this.data = data;
  8. this.walk();
  9. }
  10. // 对传入的数据进行数据劫持
  11. walk() {
  12. for (let key in this.data) {
  13. this.defineReactive(this.data, key, this.data[key]);
  14. }
  15. }
  16. // 创建当前属性的一个发布实例,使用Object.defineProperty来对当前属性进行数据劫持。
  17. defineReactive(obj, key, val) {
  18. // 创建当前属性的发布者
  19. const dep = new Dep();
  20. /*
  21. * 递归对子属性的值进行数据劫持,比如说对以下数据
  22. * let data = {
  23. * name: 'cjg',
  24. * obj: {
  25. * name: 'zht',
  26. * age: 22,
  27. * obj: {
  28. * name: 'cjg',
  29. * age: 22,
  30. * }
  31. * },
  32. * };
  33. * 我们先对data最外层的name和obj进行数据劫持,之后再对obj对象的子属性obj.name,obj.age, obj.obj进行数据劫持,层层递归下去,直到所有的数据都完成了数据劫持工作。
  34. */
  35. new Observer(val);
  36. Object.defineProperty(obj, key, {
  37. get() {
  38. // 若当前有对该属性的依赖项,则将其加入到发布者的订阅者队列里
  39. if (Dep.target) {
  40. dep.addSub(Dep.target);
  41. }
  42. return val;
  43. },
  44. set(newVal) {
  45. if (val === newVal) {
  46. return;
  47. }
  48. val = newVal;
  49. new Observer(newVal);
  50. dep.notify();
  51. }
  52. })
  53. }
  54. }
  55. // 发布者,将依赖该属性的watcher都加入subs数组,当该属性改变的时候,则调用所有依赖该属性的watcher的更新函数,触发更新。
  56. class Dep {
  57. constructor() {
  58. this.subs = [];
  59. }
  60. addSub(sub) {
  61. if (this.subs.indexOf(sub) < 0) {
  62. this.subs.push(sub);
  63. }
  64. }
  65. notify() {
  66. this.subs.forEach((sub) => {
  67. sub.update();
  68. })
  69. }
  70. }
  71. Dep.target = null;
  72. // 观察者
  73. class Watcher {
  74. /**
  75. *Creates an instance of Watcher.
  76. * @param {*} vm
  77. * @param {*} keys
  78. * @param {*} updateCb
  79. * @memberof Watcher
  80. */
  81. constructor(vm, keys, updateCb) {
  82. this.vm = vm;
  83. this.keys = keys;
  84. this.updateCb = updateCb;
  85. this.value = null;
  86. this.get();
  87. }
  88. // 根据vm和keys获取到最新的观察值
  89. get() {
  90. // 将Dep的依赖项设置为当前的watcher,并且根据传入的keys遍历获取到最新值。
  91. // 在这个过程中,由于会调用observer对象属性的getter方法,因此在遍历过程中这些对象属性的发布者就将watcher添加到订阅者队列里。
  92. // 因此,当这一过程中的某一对象属性发生变化的时候,则会触发watcher的update方法
  93. Dep.target = this;
  94. this.value = CompileUtils.parse(this.vm, this.keys);
  95. Dep.target = null;
  96. return this.value;
  97. }
  98. update() {
  99. const oldValue = this.value;
  100. const newValue = this.get();
  101. if (oldValue !== newValue) {
  102. this.updateCb(oldValue, newValue);
  103. }
  104. }
  105. }
  106. class MVVM {
  107. constructor({ data, el }) {
  108. this.data = data;
  109. this.el = el;
  110. this.init();
  111. this.initDom();
  112. }
  113. // 初始化
  114. init() {
  115. // 对this.data进行数据劫持
  116. new Observer(this.data);
  117. // 传入的el可以是selector,也可以是元素,因此我们要在这里做一层处理,保证this.$el的值是一个元素节点
  118. this.$el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el);
  119. // 将this.data的属性都绑定到this上,这样用户就可以直接通过this.xxx来访问this.data.xxx的值
  120. for (let key in this.data) {
  121. this.defineReactive(key);
  122. }
  123. }
  124. initDom() {
  125. const fragment = this.node2Fragment();
  126. this.compile(fragment);
  127. document.body.appendChild(fragment);
  128. }
  129. // 将节点转为fragment,通过fragment来操作DOM,可以获得更高的效率
  130. // 因为如果直接操作DOM节点的话,每次修改DOM都会导致DOM的回流或重绘,而将其放在fragment里,修改fragment不会导致DOM回流和重绘
  131. // 当在fragment一次性修改完后,在直接放回到DOM节点中
  132. node2Fragment() {
  133. const fragment = document.createDocumentFragment();
  134. let firstChild;
  135. while(firstChild = this.$el.firstChild) {
  136. fragment.appendChild(firstChild);
  137. }
  138. return fragment;
  139. }
  140. defineReactive(key) {
  141. Object.defineProperty(this, key, {
  142. get() {
  143. return this.data[key];
  144. },
  145. set(newVal) {
  146. this.data[key] = newVal;
  147. }
  148. })
  149. }
  150. compile(node) {
  151. const textReg = /\{\{\s*\w+\s*\}\}/gi; // 检测{{name}}语法
  152. if (this.isElementNode(node)) {
  153. // 若是元素节点,则遍历它的属性,编译其中的指令
  154. const attrs = node.attributes;
  155. Array.prototype.forEach.call(attrs, (attr) => {
  156. if (this.isDirective(attr)) {
  157. CompileUtils.compileModelAttr(this.data, node, attr)
  158. }
  159. })
  160. } else if (this.isTextNode(node)) {
  161. // 若是文本节点,则判断是否有{{}}语法,如果有的话,则编译{{}}语法
  162. let textContent = node.textContent;
  163. if (textReg.test(textContent)) {
  164. // 对于 "test{{test}} {{name}}"这种文本,可能在一个文本节点会出现多个匹配符,因此得对他们统一进行处理
  165. // 使用 textReg来对文本节点进行匹配,可以得到["{{test}}", "{{name}}"]两个匹配值
  166. const matchs = textContent.match(textReg);
  167. CompileUtils.compileTextNode(this.data, node, matchs);
  168. }
  169. }
  170. // 若节点有子节点的话,则对子节点进行编译。
  171. if (node.childNodes && node.childNodes.length > 0) {
  172. Array.prototype.forEach.call(node.childNodes, (child) => {
  173. this.compile(child);
  174. })
  175. }
  176. }
  177. // 是否是属性节点
  178. isElementNode(node) {
  179. return node.nodeType === 1;
  180. }
  181. // 是否是文本节点
  182. isTextNode(node) {
  183. return node.nodeType === 3;
  184. }
  185. isAttrs(node) {
  186. return node.nodeType === 2;
  187. }
  188. // 检测属性是否是指令(vue的指令是v-开头)
  189. isDirective(attr) {
  190. return attr.nodeName.indexOf('v-') >= 0;
  191. }
  192. }
  193. const CompileUtils = {
  194. reg: /\{\{\s*(\w+)\s*\}\}/, // 匹配 {{ key }}中的key
  195. // 编译文本节点,并注册Watcher函数,当文本节点依赖的属性发生变化的时候,更新文本节点
  196. compileTextNode(vm, node, matchs) {
  197. // 原始文本信息
  198. const rawTextContent = node.textContent;
  199. matchs.forEach((match) => {
  200. const keys = match.match(this.reg)[1];
  201. console.log(rawTextContent);
  202. new Watcher(vm, keys, () => this.updateTextNode(vm, node, matchs, rawTextContent));
  203. });
  204. this.updateTextNode(vm, node, matchs, rawTextContent);
  205. },
  206. // 更新文本节点信息
  207. updateTextNode(vm, node, matchs, rawTextContent) {
  208. let newTextContent = rawTextContent;
  209. matchs.forEach((match) => {
  210. const keys = match.match(this.reg)[1];
  211. const val = this.getModelValue(vm, keys);
  212. newTextContent = newTextContent.replace(match, val);
  213. })
  214. node.textContent = newTextContent;
  215. },
  216. // 编译v-model属性,为元素节点注册input事件,在input事件触发的时候,更新vm对应的值。
  217. // 同时也注册一个Watcher函数,当所依赖的值发生变化的时候,更新节点的值
  218. compileModelAttr(vm, node, attr) {
  219. const { value: keys, nodeName } = attr;
  220. node.value = this.getModelValue(vm, keys);
  221. // 将v-model属性值从元素节点上去掉
  222. node.removeAttribute(nodeName);
  223. new Watcher(vm, keys, (oldVal, newVal) => {
  224. node.value = newVal;
  225. });
  226. node.addEventListener('input', (e) => {
  227. this.setModelValue(vm, keys, e.target.value);
  228. });
  229. },
  230. /* 解析keys,比如,用户可以传入
  231. * let data = {
  232. * name: 'cjg',
  233. * obj: {
  234. * name: 'zht',
  235. * },
  236. * };
  237. * new Watcher(data, 'obj.name', (oldValue, newValue) => {
  238. * console.log(oldValue, newValue);
  239. * })
  240. * 这个时候,我们需要将keys解析为data[obj][name]的形式来获取目标值
  241. */
  242. parse(vm, keys) {
  243. keys = keys.split('.');
  244. let value = vm;
  245. keys.forEach(_key => {
  246. value = value[_key];
  247. });
  248. return value;
  249. },
  250. // 根据vm和keys,返回v-model对应属性的值
  251. getModelValue(vm, keys) {
  252. return this.parse(vm, keys);
  253. },
  254. // 修改v-model对应属性的值
  255. setModelValue(vm, keys, val) {
  256. keys = keys.split('.');
  257. let value = vm;
  258. for(let i = 0; i < keys.length - 1; i++) {
  259. value = value[keys[i]];
  260. }
  261. value[keys[keys.length - 1]] = val;
  262. },
  263. }

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

人气教程排行