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

  上篇说了一下vue中的数据劫持和数据代理,就是将data中的数据都添加set/get方法,这使得扩展性更好了,后续的会在这个set/get方法添加我们需要的逻辑;

  现在我们说说怎么才能够使得data中的数据和html标签中的内容绑定呢?

1.编译模板

  首先我们要思考一下,如果是你,你会怎么让data和html标签中的{{user.name}}这种东西进行匹配啊?

 

  千万别想花里胡哨的东西,最直接的办法就是遍历所有的html标签,根据正则匹配到有两个大括号的就行啊,然后就取出大括号中的数据user.name,然后切割一下,就成了[user,name],再之后遍历这个数组,拿到data[user][name]就行了,而且由于前面已经做好数据代理,我们可以直接这样取值myVue[user][name],取到值之后,就把html标签中{{xxx}}进行覆盖,然后继续找下一个有{{}}占位符的;

  这就是大概的逻辑,基于这个就可以将html中的标签中的{{user.name}}变成实际的数据了;

  但是还需要解决一个问题,怎么拿到所有的html标签呢?而且在遍历之后把数据渲染到页面上效率提高一点呢?

  所以我们需要知道一个容器:document.createDocumentFragment,这是一个虚拟节点的容器树,我们可以将当多个dom元素丢到DocumentFragment中,再统一将DocumentFragment添加到页面,会减少页面渲染dom的次数,效率会明显提升。有兴趣的可以自行了解一下这个

  ok,理论说完了,大概就是这么几步

  (1)首先根据实例化myVue实例时候传进去的el属性 "#app",就可以找到dom节点

  1. let myVue = new MyVue({
  2. el: '#app',
  3. data: {
  4. message: { a: { b: 1 } }
  5. }
  6. })

  (2)根据document.createDocumentFragment创建虚拟节点的容器树,遍历所有的dom节点都丢到容器树中

  (3)再遍历虚拟容器树中每个节点,使用正则匹配到有两个大括号的节点,将节点中的表达式取出来,例如user.name

    (4)  根据表达式取出data中的值,由于进行了数据劫持,data中的值可以直接用myVue[user][name]获取,取到了值之后,就将对应的容器树中的对应节点中{{user.name}}覆盖掉

  (5)将虚拟容器树渲染到页面中,我们就能看出效果了;

下面用代码简单实现一下模板编译的代码:

  1. function compile (el, vm) {
  2. return new Compile(el, vm);
  3. }
  4. function Compile (el, vm) {
  5. //找到el代表的那个dom节点,并挂载到myVue实例中
  6. vm.$el = document.querySelector(el);
  7. //创建虚拟节点容器树
  8. let fragment = document.createDocumentFragment();
  9. //将el下所有的dom节点都放到容器树中,注意appendChild方法,这里是将将dom节点移动到容器树中啊,不是死循环!
  10. while (child = vm.$el.firstChild) {
  11. // console.log('count:' + vm.$el.childElementCount);
  12. fragment.appendChild(child)
  13. };
  14. //遍历虚拟节点中的所有节点,将真实数据填充覆盖这种占位符{{}}
  15. replace(fragment, vm);
  16. //将虚拟节点树中内容渲染到页面中
  17. vm.$el.appendChild(fragment);
  18. }
  19. function replace (n, vm) {
  20. //遍历容器树中所有的节点,解析出{{}}里面的内容,然后将数据覆盖到节点中去
  21. Array.from(n.childNodes).forEach(node => {
  22. //console.log('nodeType:' + node.nodeType);
  23. let nodeText = node.textContent;
  24. let reg = /\{\{(.*)\}\}/;
  25. // 节点类型常用的有元素节点,属性节点和文本节点,值分别是1,2,3
  26. //一定要弄清楚这三种节点,比如<p id="123">hello</p>,这个p标签整个的就是元素节点,nodeType==1
  27. //id="123"可以看作是属性节点,nodeType==2
  28. //hello 表示文本节点,nodeType==3
  29. //因为占位符{{}}只在文本节点中,所以需要判断是否等于3
  30. if (node.nodeType === 3 && reg.test(nodeText)) {
  31. // RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串,以此类推,RegExp.$2。。。
  32. let arr = RegExp.$1.split(".");
  33. let val = vm;
  34. // 这个for循环就是取出这样的值:myVue[name][user]
  35. arr.forEach(i => {
  36. val = val[i];
  37. })
  38. // 把值覆盖到虚拟节点的占位符{{xxx}}这里
  39. node.textContent = nodeText.replace(reg, val);
  40. }
  41. // 最开始第一个遍历的节点是<div id="app">这一行后面有个你看不到的换行,所以nodeType等于3,但是没有占位符{{}},所以会进行递归调用内部
  42. //的每一个节点,直到找到是文本节点而且有占位符{{}}
  43. if (node.childNodes) {
  44. replace(node, vm);
  45. }
  46. })
  47. }

  

2.js实现发布订阅模式

  我们说说设计模式,什么叫做发布订阅模式呢?你想想你微信订阅的公众号就知道了,假如一个微信的公众号Dep,然后有3个用户Watcher去订阅它,然后公众号Dep只要一发布什么信息,那么每一个Watcher都会收到一份;

  然后我们要思考,首先是Dep,这里面肯定有一个容器,可以装订阅过当前公众号的所有用户,而且还需要对外提供两个api,一个是订阅当前公众号的方法,方便用户使用;另外一个是发布信息的方法,是给公众号所属的那个人使用,只要他调用这个方法,就会遍历这个容器中的所有用户,向每个用户发送信息;

  再就是用户端Watcher,这端需要啥呢?肯定也需要一个对外的api,方便公众号那一端调用这个api把信息发送到用户手机这边来吧!然后每个用户都是不一样的,我们需要有个属性作为标识;

  1. // 可以看做是公众号端
  2. function Dep () {
  3. // 存放每个用户的容器
  4. this.subs = [];
  5. }
  6. //对外提供的api之一,供用户订阅
  7. Dep.prototype.addSub = function (sub) {
  8. this.subs.push(sub);
  9. }
  10. // 对外提供的api之二,遍历每个用户,给每个用法发信息
  11. Dep.prototype.notify = function () {
  12. this.subs.forEach(sub => {
  13. sub.update();
  14. });
  15. }
  16. // 用户端
  17. function Watcher (fn) {
  18. // 这个可以看作是用户的标志;注意,这个fn一般是一个回调函数
  19. this.fn = fn;
  20. }
  21. // 用户端提供的对外api,让公众号端使用
  22. Watcher.prototype.update = function () {
  23. this.fn();
  24. }
  25. // ================================测试================================
  26. // 新建三个用户
  27. let watcher1 = new Watcher(() => {
  28. console.log('发送给用户1');
  29. })
  30. let watcher2 = new Watcher(() => {
  31. console.log('发送给用户2');
  32. })
  33. let watcher3 = new Watcher(() => {
  34. console.log('发送给用户3');
  35. })
  36. // 用户注册
  37. let dep = new Dep();
  38. dep.addSub(watcher1);
  39. dep.addSub(watcher2);
  40. dep.addSub(watcher3);
  41. // 像每个用户发送信息
  42. dep.notify();

  使用node运行一下结果如下

 

  理解了这个模式之后,后面我们需要将这个设计模式应用到我们自己的vue中,使得当我们呢修改data中的数据之后,页面上也会立刻刷新;

  其实也很好理解,data只有一份嘛,就可以想象成上面说的公众号端,然后html中肯定有多个占位符{{}}的吧,那就是用户端呗!只要data中的数据一修改,这肯定就会触发数据劫持的那个set方法吧,然后我们只需要再set方法中搞点事,调用一下Dep的notify方法通知一下所有的Watcher,在Watcher中就会触发那个fn回调函数去把页面上的数据覆盖局ok了;

  这个发布订阅的实现代码下一篇再写;

 

3.现阶段代码

html文件;

  1. <body>
  2. <div id="app">
  3. <h1>呵呵:{{user.name}}</h1>
  4. </div>
  5.  
  6. <script src="./mvvm.js"></script>
  7. <script>
  8.  
  9. // 自定义的myVue实例
  10. let myVue = new MyVue({
  11. el: '#app',
  12. data: {
  13. user: { name: "小王" }
  14. }
  15. })
  16. </script>
  17. </body>

 

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. // 把值覆盖到虚拟节点的占位符{{}}这里
  61. node.textContent = nodeText.replace(reg, val);
  62. }
  63. // 第一个遍历的节点是<div id="app">这一行后面的换行,nodeType等于3,但是没有占位符{{}},所以会进入到这里进行递归调用内部
  64. //的每一个节点,直到找到文本节点而且占位符{{}}
  65. if (node.childNodes) {
  66. replace(node, vm);
  67. }
  68. })
  69. }
  70. //数据劫持操作
  71. function observe (data) {
  72. // 如果data不是对象,就结束,不然递归调用会栈溢出的
  73. if (typeof data !== 'object') return;
  74. return new Observe(data);
  75. }
  76. function Observe (data) {
  77. // 遍历data所有属性
  78. for (let key in data) {
  79. let val = data[key];
  80. //初始化的时候, data中就有复杂对象的时候,例如data: { message:{a:{b:1}}} ,就需要递归的遍历这个对象中每一个属性都添加get和set方法
  81. observe(val);
  82. Object.defineProperty(data, key, {
  83. enumerable: true,
  84. get () {
  85. return val;
  86. },
  87. set (newVal) {
  88. if (val === newVal) return;
  89. val = newVal;
  90. //当后续可能因为业务逻辑使得_data.message = {name: "小王"},设置对象类型的属性值,就需要递归的给对象中{name: "小王"}的每个属性也添加get和set方法
  91. //否则name是没有get/set方法的
  92. observe(val);
  93. }
  94. })
  95. }
  96. }
View Code

 

当前代码的效果是页面中可以显示数据了,下一篇我们说说根据发布订阅原理,使得我们改变data中的数据,页面上的数据也会跟着变化

 

原文链接:http://www.cnblogs.com/wyq1995/p/14209319.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号