使用proxy实现一个双向绑定

小咪咪 2023-05-30 08:52 6阅读 0赞

如何实现

  • 在学习vue的时候,vue是通过劫持数据的变化,监听到数据变化时改变前端视图。
  • 那么要实现双向绑定,必然需要一个监听数据的方法。如文章标题所示,这里使用的 proxy实现数据的监听。
  • 当监听到数据变化时,需要一个watcher响应并调用更新数据的compile方法去更新前端视图。
  • 在vue中 v-model 作为绑定的入口。当我们监听到前端input输入信息并绑定了数据项的时候,需要先告知watcher,由watcher改变监听器的数据。
  • 大概的双向绑定的原理为:

    format_png

1.实现一个observer(数据监听器)

利用 proxy 实现一个数据监听器很简单,因为 proxy 是监听整个对象的变化的,所以可以这样写:

  1. class VM {
  2. constructor(options, elementId) {
  3. this.data = options.data || {}; // 监听的数据对象
  4. this.el = document.querySelector(elementId);
  5. this.init(); // 初始化
  6. }
  7. // 初始化
  8. init() {
  9. this.observer();
  10. }
  11. // 监听数据变化方法
  12. observer() {
  13. const handler = {
  14. get: (target, propkey) => {
  15. console.log(`监听到${propkey}被取啦,值为:${target[propkey]}`);
  16. return target[propkey];
  17. },
  18. set: (target, propkey, value) => {
  19. if(target[propkey] !== value){
  20. console.log(`监听到${propkey}变化啦,值变为:${value}`);
  21. }
  22. return true;
  23. }
  24. };
  25. this.data = new Proxy(this.data, handler);
  26. }
  27. }
  28. // 测试一下
  29. const vm = new VM({
  30. data: {
  31. name: 'defaultName',
  32. test: 'defaultTest',
  33. },
  34. }, '#app');
  35. vm.data.name = 'changeName'; // 监听到name变化啦,值变为:changeName
  36. vm.data.test = 'changeTest'; // 监听到test变化啦,值变为:changeTest
  37. vm.data.name; // 监听到name被取啦,值为:changeName
  38. vm.data.test; // 监听到test被取啦,值为:changeTest
  39. 复制代码

这样,数据监听器已经基本实现了,但是现在这样只能监听到数据的变化,不能改变前端的视图信息。现在需要实现一个更改前端信息的方法,在VM类中添加方法 changeElementData

  1. // 改变前端数据
  2. changeElementData(value) {
  3. this.el.innerHTML = value;
  4. }
  5. 复制代码

在监听到数据变化时调用 changeElementData 改变前端数据, handlerset 方法中调用方法

  1. set(target, propkey, value) {
  2. this.changeElementData(value);
  3. return true;
  4. }
  5. 复制代码

在init中设置一个定时器更改数据

  1. init() {
  2. this.observer();
  3. setTimeout(() => {
  4. console.log('change data !!');
  5. this.data.name = 'hello world';
  6. }, 1000)
  7. }
  8. 复制代码

已经可以看到监听到的信息改变到前端了,但是!

aHR0cHM6Ly9pbWcwLnR1aWNvb2wuY29tL2JBWkpaM1ouZ2lm

这样写死的绑定数据显然是没有意义,现在实现的逻辑大概如下面的图

format_png 1

2.实现数据动态更新到前端

上面实现了一个简单的数据绑定展示,但是只能绑定一个指定的节点去改变此节点的数据绑定。这样显然是不能满足的,我们知道vue中是以 { {key}} 这样的形式去绑定展示的数据的,而且vue中是监听指定的节点的所有子节点的。因此对象中需要在 VIEWOBSERVER 之间添加一个监听层 WATCHER 。当监听到数据发生变化时,通过 WATCHER 去改变 VIEW ,如图:

format_png 2

根据这个流程,下一步我们需要做的是:

  1. {
  2. {text}}

在VM类的构造器中添加三个参数

  1. constructor() {
  2. this.fragment = null; // 文档片段
  3. this.matchModuleReg = new RegExp('\{\{\s*.*?\s*\}\}', 'gi'); // 匹配所有{
  4. {}}模版
  5. this.nodeArr = []; // 所有带有模板的前端结点
  6. }
  7. 复制代码

新建一个方法遍历 el 中的所有节点,并存放到 fragment

  1. /**
  2. * 创建一个文档片段
  3. */
  4. createDocumentFragment() {
  5. let fragment = document.createDocumentFragment();
  6. let child = this.el.firstChild;
  7. // 循环添加到文档片段中
  8. while (child) {
  9. this.fragment.appendChild(child);
  10. child = this.el.firstChild;
  11. }
  12. this.fragment = fragment;
  13. }
  14. 复制代码

匹配 { {}} 的数据并替换模版

  1. /**
  2. * 匹配模板
  3. * @param { string } key 触发更新的key
  4. * @param { documentElement } fragment 结点
  5. */
  6. matchElementModule(key, fragment) {
  7. const childNodes = fragment || this.fragment.childNodes;
  8. [].slice.call(childNodes).forEach((node) => {
  9. if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
  10. node.defaultContent = node.textContent; // 将初始化的前端内容保存到节点的defaultContent中
  11. this.changeData(node);
  12. this.nodeArr.push(node); // 保存带有模板的结点
  13. }
  14. // 递归遍历子节点
  15. if(node.childNodes && node.childNodes.length) {
  16. this.matchElementModule(key, node.childNodes);
  17. }
  18. })
  19. }
  20. /**
  21. * 改变视图数据
  22. * @param { documentElement } node
  23. */
  24. changeData(node) {
  25. const matchArr = node.defaultContent.match(this.matchModuleReg); // 获取所有需要匹配的模板
  26. let tmpStr = node.defaultContent;
  27. for(const key of matchArr) {
  28. tmpStr = tmpStr.replace(key, this.data[key.replace(/\{\{|\}\}|\s*/g, '')] || '');
  29. }
  30. node.textContent = tmpStr;
  31. }
  32. 复制代码

实现watcher,数据变化是触发此watcher更新前端

  1. watcher(key) {
  2. for(const node of this.nodeArr) {
  3. this.changeData(node);
  4. }
  5. }
  6. 复制代码

initproxyset 方法中执行新增的方法

  1. init() {
  2. this.observer();
  3. this.createDocumentFragment(this.el); // 将绑定的节点都放入文档片段中
  4. for (const key of Object.keys(this.data)) {
  5. this.matchElementModule(key);
  6. }
  7. this.el.appendChild(this.fragment); // 将初始化的数据输出到前端
  8. }
  9. set: () => {
  10. if(target[propkey] !== value) {
  11. target[propkey] = value;
  12. this.watcher(propkey);
  13. }
  14. return true;
  15. }
  16. 复制代码

测试一下:

format_png 3

aHR0cHM6Ly9pbWcyLnR1aWNvb2wuY29tL3JJWlo3cm0uZ2lm

3.实现数据双向绑定

现在我们的程序已经可以通过改变data动态地改变前端的展示了,接下来需要实现的是一个类似VUE v-model 绑定input的方法,通过input输入动态地将输入的信息输出到对应的前端模板上。大概的流程图如下:

format_png 4

一个简单的实现流程大概如下:

  1. 获取所有带有v-model的input结点
  2. 监听输入的信息并设置到对应的data中

在constructor中添加

  1. constructor() {
  2. this.modelObj = {};
  3. }
  4. 复制代码

在VM类中新增方法

  1. // 绑定 y-model
  2. bindModelData(key, node) {
  3. if (this.data[key]) {
  4. node.addEventListener('input', (e) => {
  5. this.data[key] = e.target.value;
  6. }, false);
  7. }
  8. }
  9. // 设置 y-model 值
  10. setModelData(key, node) {
  11. node.value = this.data[key];
  12. }
  13. // 检查y-model属性
  14. checkAttribute(node) {
  15. return node.getAttribute('y-model');
  16. }
  17. 复制代码

watcher 中执行 setModelData 方法, matchElementModule 中执行 bindModelData 方法。

修改后的 matchElementModulewatcher 方法如下

  1. matchElementModule(key, fragment) {
  2. const childNodes = fragment || this.fragment.childNodes;
  3. [].slice.call(childNodes).forEach((node) => {
  4. // 监听所有带有y-model的结点
  5. if (node.getAttribute && this.checkAttribute(node)) {
  6. const tmpAttribute = this.checkAttribute(node);
  7. if(!this.modelObj[tmpAttribute]) {
  8. this.modelObj[tmpAttribute] = [];
  9. };
  10. this.modelObj[tmpAttribute].push(node);
  11. this.setModelData(tmpAttribute, node);
  12. this.bindModelData(tmpAttribute, node);
  13. }
  14. // 保存所有带有{
  15. {}}模版的结点
  16. if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
  17. node.defaultContent = node.textContent; // 将初始化的前端内容保存到节点的defaultContent中
  18. this.changeData(node);
  19. this.nodeArr.push(node); // 保存带有模板的结点
  20. }
  21. // 递归遍历子节点
  22. if(node.childNodes && node.childNodes.length) {
  23. this.matchElementModule(key, node.childNodes);
  24. }
  25. })
  26. }
  27. watcher(key) {
  28. if (this.modelObj[key]) {
  29. this.modelObj[key].forEach(node => {
  30. this.setModelData(key, node);
  31. })
  32. }
  33. for(const node of this.nodeArr) {
  34. this.changeData(node);
  35. }
  36. }
  37. 复制代码

来看一下是否已经成功绑定了,写一下测试代码:

format_png 5

aHR0cHM6Ly9pbWcxLnR1aWNvb2wuY29tL0ZGYkV6YU0uZ2lm

成功!!

最终的代码如下:

  1. class VM {
  2. constructor(options, elementId) {
  3. this.data = options.data || {}; // 监听的数据对象
  4. this.el = document.querySelector(elementId);
  5. this.fragment = null; // 文档片段
  6. this.matchModuleReg = new RegExp('\{\{\s*.*?\s*\}\}', 'gi'); // 匹配所有{
  7. {}}模版
  8. this.nodeArr = []; // 所有带有模板的前端结点
  9. this.modelObj = {}; // 绑定y-model的对象
  10. this.init(); // 初始化
  11. }
  12. // 初始化
  13. init() {
  14. this.observer();
  15. this.createDocumentFragment();
  16. for (const key of Object.keys(this.data)) {
  17. this.matchElementModule(key);
  18. }
  19. this.el.appendChild(this.fragment);
  20. }
  21. // 监听数据变化方法
  22. observer() {
  23. const handler = {
  24. get: (target, propkey) => {
  25. return target[propkey];
  26. },
  27. set: (target, propkey, value) => {
  28. if(target[propkey] !== value) {
  29. target[propkey] = value;
  30. this.watcher(propkey);
  31. }
  32. return true;
  33. }
  34. };
  35. this.data = new Proxy(this.data, handler);
  36. }
  37. /**
  38. * 创建一个文档片段
  39. */
  40. createDocumentFragment() {
  41. let documentFragment = document.createDocumentFragment();
  42. let child = this.el.firstChild;
  43. // 循环向文档片段添加节点
  44. while (child) {
  45. documentFragment.appendChild(child);
  46. child = this.el.firstChild;
  47. }
  48. this.fragment = documentFragment;
  49. }
  50. /**
  51. * 匹配模板
  52. * @param { string } key 触发更新的key
  53. * @param { documentElement } fragment 结点
  54. */
  55. matchElementModule(key, fragment) {
  56. const childNodes = fragment || this.fragment.childNodes;
  57. [].slice.call(childNodes).forEach((node) => {
  58. // 监听所有带有y-model的结点
  59. if (node.getAttribute && this.checkAttribute(node)) {
  60. const tmpAttribute = this.checkAttribute(node);
  61. if(!this.modelObj[tmpAttribute]) {
  62. this.modelObj[tmpAttribute] = [];
  63. };
  64. this.modelObj[tmpAttribute].push(node);
  65. this.setModelData(tmpAttribute, node);
  66. this.bindModelData(tmpAttribute, node);
  67. }
  68. // 保存所有带有{
  69. {}}模版的结点
  70. if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
  71. node.defaultContent = node.textContent; // 将初始化的前端内容保存到节点的defaultContent中
  72. this.changeData(node);
  73. this.nodeArr.push(node); // 保存带有模板的结点
  74. }
  75. // 递归遍历子节点
  76. if(node.childNodes && node.childNodes.length) {
  77. this.matchElementModule(key, node.childNodes);
  78. }
  79. })
  80. }
  81. /**
  82. * 改变视图数据
  83. * @param { documentElement } node
  84. */
  85. changeData(node) {
  86. const matchArr = node.defaultContent.match(this.matchModuleReg); // 获取所有需要匹配的模板
  87. let tmpStr = node.defaultContent;
  88. for(const key of matchArr) {
  89. tmpStr = tmpStr.replace(key, this.data[key.replace(/\{\{|\}\}|\s*/g, '')] || '');
  90. }
  91. node.textContent = tmpStr;
  92. }
  93. watcher(key) {
  94. if (this.modelObj[key]) {
  95. this.modelObj[key].forEach(node => {
  96. this.setModelData(key, node);
  97. })
  98. }
  99. for(const node of this.nodeArr) {
  100. this.changeData(node);
  101. }
  102. }
  103. // 绑定 y-model
  104. bindModelData(key, node) {
  105. if (this.data[key]) {
  106. node.addEventListener('input', (e) => {
  107. this.data[key] = e.target.value;
  108. }, false);
  109. }
  110. }
  111. // 设置 y-model 值
  112. setModelData(key, node) {
  113. node.value = this.data[key];
  114. }
  115. // 检查y-model属性
  116. checkAttribute(node) {
  117. return node.getAttribute('y-model');
  118. }
  119. }
  120. 复制代码

最后

本节我们使用 Proxy ,从监听器开始,到观察者一步步实现了一个模仿VUE的双向绑定,代码中也许会有很多写的不严谨的地方,如发现错误麻烦大佬们指出~~

发表评论

表情:
评论列表 (有 0 条评论,6人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Vue双向

    实现mvvm的双向绑定,是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发

    相关 JS 实现双向数据

    > 小编推荐:[Fundebug][]提供JS错误监控、微信小程序错误监控、微信小游戏错误监控,Node.j错误监控和Java错误监控。真的是一个很好用的错误监控费服务,众多大