经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » JavaScript » 查看文章
浅谈vue原理(四)
来源:cnblogs  作者:java小新人  时间:2021/1/4 9:40:08  对本文有异议

  经过了前面这么久,我们已经弄好了数据劫持,数据代理,还通过了发布订阅模式实现了data中数据变化,页面上也会跟着变化;

  现在还差点东西,就是当页面上的数据变化的时候,data中的数据也能跟着变化,进而使得页面的数据都刷新成最新数据,这就是view->model这条线;

1.html准备

  首先我们需要准备一个input标签

  1. <body>
  2. <div id="app">
  3. <h1>呵呵:{{message}}</h1>
  4. <input type="text" v-model="message">
  5. </div>
  6.  
  7. <script src="./mvvm.js"></script>
  8. <script>
  9.  
  10. // 自定义的myVue实例
  11. let myVue = new MyVue({
  12. el: '#app',
  13. data: {
  14. message: "hello world"
  15. }
  16. })
  17. </script>
  18. </body>

 

2.数据双向绑定的实现

  之前说过,html标签中每一个{{}}占位符都是对应着一个Watcher,而且{{}}占位符处于文本节点中,我们在初始化的时候使用node.nodeType === 3 && reg.test(nodeText)条件,使用正则就能匹配文本节点中的{{}}占位符;

  现在在初始化的过程,添加新逻辑:首先需要找到input标签,我们可以使用node.nodeType === 1先找到元素节点,再找有v-model属性的节点,然后取出其中的属性值,这个属性就对应着data中的数据;

  初始化完成之后,有两种可能:

  (1)当我们手动的在input框中改变值,data中对应的属性值也应该发生变化,这个就添加监听事件去处理

  1. node.addEventListener("input", e => {
  2. // 获取input输入框中的值
  3. let inputValue = e.target.value;
  4. //input中修改的值同步到到data中去,这里又会触发该属性的set方法,set方法中又会触发发布订阅模式,将所有的Watcher都调用一遍
  5. vm[exp] = inputValue;
  6. })

 

(2)当手动的修改data中的数据,set方法中会触发所有的Watcher的回调函数,这里的input标签中的值也应该变化;

  而之前我们只是对{{xxx}}占位符的文本节点创建了Watcher,这种占位符节点的值肯定会刷新,但是input是元素节点,不会刷新!所以需要将这种带有v-model的元素节点也创建Watcher:

 

  这段逻辑完整的代码下图所示,初始化的过程中首先找到v-model所在标签的属性值,将data中对应的属性值覆盖input框中内容!而且还需要创建一个Watcher(Watcher注册逻辑之前说过)以及给input添加事件监听;

  事件监听用于初始化完成之后,用户手动修改input框中的值,触发data中的数据发生变化,进一步触发set方法的notify方法,调用所有的Watcher的回调函数,刷新页面数据;

 

全部的js代码:

  1. function MyVue (options = {}) {
  2. //第一步:首先就是将实例化的对象给拿到,得到data对象
  3. this.$options = options;
  4. this._data = this.$options.data;
  5. //第二步:数据劫持,将data对象中每一个属性都设置get/set方法
  6. observe(this._data);
  7. //第三步:数据代理,这里就是将_data的对象属性放到myVue实例中一份,实际的数据还是_data中的
  8. for (let key in this._data) {
  9. //这里的this代表当前myVue实例对象
  10. Object.defineProperty(this, key, {
  11. enumerable: true,
  12. get () {
  13. return this._data[key];
  14. },
  15. set (newVal) {
  16. this._data[key] = newVal;
  17. }
  18. })
  19. }
  20. //第四步:compile模板,需要将el属性和当前myVue实例
  21. compile(options.el, this)
  22. }
  23. function compile (el, vm) {
  24. return new Compile(el, vm);
  25. }
  26. function Compile (el, vm) {
  27. //将el代表的那个dom节点挂载到myVue实例中
  28. vm.$el = document.querySelector(el);
  29. //创建虚拟节点容器树
  30. let fragment = document.createDocumentFragment();
  31. //将el下所有的dom节点都放到容器树中,注意appendChild方法,这里是将将dom节点移动到容器树中啊,不是死循环!
  32. while (child = vm.$el.firstChild) {
  33. // console.log('count:' + vm.$el.childElementCount);
  34. fragment.appendChild(child)
  35. };
  36. //遍历虚拟节点中的所有节点,将真实数据填充覆盖这种占位符{{}}
  37. replace(fragment, vm);
  38. //将虚拟节点树中内容渲染到页面中
  39. vm.$el.appendChild(fragment);
  40. }
  41. function replace (n, vm) {
  42. //遍历容器树中所有的节点,解析出{{}}里面的内容,然后将数据覆盖到节点中去
  43. Array.from(n.childNodes).forEach(node => {
  44. console.log('nodeType:' + node.nodeType);
  45. let nodeText = node.textContent;
  46. let reg = /\{\{(.*)\}\}/;
  47. // 节点类型常用的有元素节点,属性节点和文本节点,值分别是1,2,3
  48. //一定要弄清楚这三种节点,比如<p id="123">hello</p>,这个p标签整个的就是元素节点,nodeType==1
  49. //id="123"可以看作是属性节点,nodeType==2
  50. //hello 表示文本节点,nodeType==3
  51. //因为占位符{{}}只在文本节点中,所以需要判断是否等于3
  52. if (node.nodeType === 3 && reg.test(nodeText)) {
  53. // RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串,以此类推,RegExp.$2。。。
  54. let arr = RegExp.$1.split(".");
  55. let val = vm;
  56. // 这个for循环就是取出这样的值:myVue[name][user]
  57. arr.forEach(i => {
  58. val = val[i];
  59. })
  60. // 创建Watcher,最主要的是传入的这个回调函数,会覆盖node节点中的占位符{{xxx}}
  61. new Watcher(vm, RegExp.$1, function (newVal) {
  62. node.textContent = nodeText.replace(reg, newVal);
  63. })
  64. // 把值覆盖到虚拟节点的占位符{{}}这里
  65. node.textContent = nodeText.replace(reg, val);
  66. }
  67. //这里遍历到元素的节点,例如:<p id="xx">aaa</p></p>,<input type="text v-model=" username">
  68. //然后需要取到其中的vue相关的指令,例如v-model="xxx",一般是以v-开头的
  69. if (node.nodeType === 1) {
  70. // console.log(node);
  71. let nodeAttributes = node.attributes;
  72. Array.from(nodeAttributes).forEach(attr => {
  73. let name = attr.name;
  74. let exp = attr.value;
  75. // 找到v-model指令,data中的数据填充到input框中
  76. if (name.indexOf("v-model") == 0) {
  77. node.value = vm[exp];
  78. }
  79. // data中的数据变化,input中的数据也要跟着变化
  80. new Watcher(vm, exp, function (newVal) {
  81. node.value = newVal;
  82. })
  83. node.addEventListener("input", e => {
  84. // 获取input输入框中的值
  85. let inputValue = e.target.value;
  86. //input中修改的值同步到到data中去,这里又会触发该属性的set方法,set方法中又会触发发布订阅模式,将所有的Watcher都调用一遍
  87. vm[exp] = inputValue;
  88. })
  89. })
  90. }
  91. // 第一个遍历的节点是<div id="app">这一行后面的换行,nodeType等于3,但是没有占位符{{}},所以会进入到这里进行递归调用内部
  92. //的每一个节点,直到找到文本节点而且占位符{{}}
  93. if (node.childNodes) {
  94. replace(node, vm);
  95. }
  96. })
  97. }
  98. //数据劫持操作
  99. function observe (data) {
  100. // 如果data不是对象,就结束,不然递归调用会栈溢出的
  101. if (typeof data !== 'object') return;
  102. return new Observe(data);
  103. }
  104. function Observe (data) {
  105. let dep = new Dep();
  106. // 遍历data所有属性
  107. for (let key in data) {
  108. let val = data[key];
  109. //初始化的时候, data中就有复杂对象的时候,例如data: { message:{a:{b:1}}} ,就需要递归的遍历这个对象中每一个属性都添加get和set方法
  110. observe(val);
  111. Object.defineProperty(data, key, {
  112. enumerable: true,
  113. get () {
  114. // 订阅
  115. Dep.target && dep.addSub(Dep.target);
  116. return val;
  117. },
  118. set (newVal) {
  119. if (val === newVal) return;
  120. val = newVal;
  121. //当后续可能因为业务逻辑使得_data.message = {name: "小王"},设置对象类型的属性值,就需要递归的给对象中{name: "小王"}的每个属性也添加get和set方法
  122. //否则name是没有get/set方法的
  123. observe(val);
  124. dep.notify();
  125. }
  126. })
  127. }
  128. }
  129. // ===============================发布订阅===============================
  130. // 可以看做是公众号端
  131. function Dep () {
  132. // 存放每个用户的容器
  133. this.subs = [];
  134. }
  135. //对外提供的api之一,供用户订阅
  136. Dep.prototype.addSub = function (sub) {
  137. this.subs.push(sub);
  138. }
  139. // 对外提供的api之二,遍历每个用户,给每个用法发信息
  140. Dep.prototype.notify = function () {
  141. this.subs.forEach(sub => {
  142. sub.update();
  143. });
  144. }
  145. // 用户端
  146. function Watcher (vm, exp, fn) {
  147. // 这个可以看作是用户的标志;注意,这个fn一般是一个回调函数
  148. this.vm = vm;
  149. this.exp = exp;
  150. this.fn = fn;
  151. Dep.target = this;
  152. let val = vm;
  153. let arr = exp.split(".");
  154. arr.forEach(item => {
  155. val = val[item];
  156. })
  157. Dep.target = null;
  158. }
  159. // 用户端提供的对外api,让公众号端使用
  160. Watcher.prototype.update = function () {
  161. let val = this.vm;
  162. let arr = this.exp.split(".");
  163. arr.forEach(item => {
  164. val = val[item];
  165. })
  166. this.fn(val);
  167. }
View Code

原文链接:http://www.cnblogs.com/wyq1995/p/14212948.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号