实现Vue的双向绑定

coding

一、概述

之前有讲到过vue实现整体的整体流程,讲到过数据的响应式,是通过Object.defineProperity来实现的,当时只是举了一个小小的例子,那么再真正的vue框架里是如何实现数据的双向绑定呢?是如何将vm.data中的属性通过“v-model”和“{{}}”绑定到页面上的呢?下面我们先抛弃vue中DOM渲染的机制,自己来动手实现一双向绑定的demo。

二、实现步骤

1、html部分

根据Vue的语法,定义html需要绑定的DOM,如下代码

2、js部分

由于直接操作DOM是非常损耗性能的,所以这里我们使用DocumentFragment(以下简称为文档片段),由于createDocumentFragment是在内存中创建的一个虚拟节点对象,所以往文档片段里添加DOM节点是不太消耗性能的;此处我们将app下面的节点都劫持到文档片段中,在文档片段中对DOM进行一些操作,然后将文档片段总体重新插入app容器里面去,而且此处插入到app中的节点都是属于文档片段的子孙节点。代码如下:

 1// 劫持DOM节点到DocumentFragment中

2function nodeToFragment(node) {

3var flag = document.createDocumentFragment();

4while(node.firstChild) {

5 flag.appendChild(node.firstChild) // 劫持节点到文档片段中,在此之前对节点进行一些操作; 劫持到一个,对应的DOM容器里会删除掉一个节点

6 }

7return flag

8};

9var dom = nodeToFragment(document.getElementById('app'))

10 document.getElementById('app').apendChild(dom) // 将文档片段重新放入app中

对于双向绑定的实现,首先我们来创建vue的实例

 1// 创建Vue对象

2function Vue(data) {

3var id = data.el;

4var ele = document.getElementById(id);

5this.data = data.data;

6 obersve(this.data, this) // 将vm.data指向vm

7var dom = nodeToFragment(ele, this); // 通过上面的函数劫持DOM节点

8 ele.appendChild(dom); // 将文档片段重新放入容器

9};

10// 实例化Vue对象

11var vm = new Vue({

12 el: 'app',

13 data: {

14 text: 'hello world'

15 }

16 })

通过以上代码我们可以看到,实例化Vue对象的时候先是将vm.data指向到了vm,而后是对html节点进行的数据绑定,此处分两步,我们先来看对vm的数据源绑定:

 1function definevm(vm, key, value) {

2 Object.defineProperty(vm, key, {

3 get: function() {

4return value

5 },

6 set: function(newval) {

7 value = newval

8 console.log(value)

9 }

10 })

11};

12// 指定data到vm

13function obersve(data, vm) {

14for(var key in data) {

15 definevm(vm, key, data[key]);

16 }

17}

18

19 vm.text = 'MrGao';

20 console.log(vm.text); // MrGao

此处将vm.data的属性指定到vm上,并且实现了对vm的监听,一旦vm的属性发生变化,便会触发其set方法;接下来我们来看下对DOM节点的数据绑定:

 1// 绑定数据

2function compile(node, vm) {

3// console.log(node.nodeName)

4var reg = /\{\{(.*)\}\}/; // 匹配{{}}里的内容

5if (node.nodeType === 1) { // 普通DOM节点nodeType为1

6var attr = node.attributes 遍历节点属性

7for(var i = 0; i < attr.length; i++) {

8if (attr[i].nodeName === 'v-model') {

9var name = attr[i].nodeValue; // 获取绑定的值

10 node.addEventListener('keyup', function(e) {

11// console.log(e.target.value)

12 vm[name] = e.target.value //监听input值的变化,重新给vm.text赋值

13 })

14 node.value = vm[name];

15 node.removeAttribute('v-model');

16 };

17 };

18 };

19if (node.nodeType === 3) {

20if (reg.test(node.nodeValue)) {

21var name = RegExp.$1;

22 name = name.trim();

23 node.nodeValue = vm[name]; // 将vm.text的值赋给文本节点

24 }

25 }

26}

27// 劫持DOM节点到DocumentFragment中

28function nodeToFragment(node, vm) {

29var flag = document.createDocumentFragment();

30while(node.firstChild) {

31 compile(node.firstChild, vm); // 进行数据绑定

32 flag.appendChild(node.firstChild); // 劫持节点到文档片段中

33 }

34return flag;

35 };

这样一来,我们就可以通过compile方法将vm.text绑定到input节点和下面的文本节点上,并且监听input节点的keyup事件,当input的value发生改变是,将input的值赋给vm.text,如此vm.text的值也改变了,同时会触发对vm的ste函数;但是vm.text的值是改变了,我们应该如何让文本节点的值同样跟随者vm.text的值改变呢?此时我们就可以使用订阅模式(观察者模式)来实现这一功能;那什么是订阅模式呢?

订阅模式就是好比有一家报社,他每天都要对新的世界大事进行发布,然后报社通知送报员去把发布的新的报纸推送给订阅者,订阅这在拿到报纸后可以获取到新的消息;反映到代码里可以这样理解;当vm.text改变时,触发set方法,然后发布变化的消息,在数据绑定的那里定义订阅者,在定义一个连接两者的“送报员”,每当发布者发布新的消息,订阅者都可以拿到新的消息,代码如下:

 1// 定义发布订阅

2function Dep() {

3this.subs = []

4}

5 Dep.prototype = {

6 addSub: function(sub) {

7this.subs.push(sub);

8 },

9 notify: function() {

10this.subs.forEach(function(sub) {

11 sub.update();

12 })

13 }

14};

15// 定义观察者

16function Watcher (vm, node, name) {

17 Dep.target = this; // 发布者和订阅者的桥梁(送报员)

18this.name = name;

19this.node = node;

20this.vm = vm;

21this.update();

22 Dep.target = null;

23};

24 Watcher.prototype = {

25 update: function() {

26this.get();

27// console.log(this.node.nodeName)

28if (this.node.nodeName === 'INPUT') {

29this.node.value = this.value;

30 } else {

31this.node.nodeValue = this.value;

32 }

33 },

34 get: function() {

35this.value = this.vm[this.name];

36 }

37 }

此时,发布者和订阅者要分别在数据更新时和数据绑定时进行绑定

 1// 绑定发布者

2function definevm(vm, key, value) {

3var dep = new Dep // 实例化发布者

4 Object.defineProperty(vm, key, {

5 get: function() {

6if (Dep.target) {

7 dep.addSub(Dep.target) // 为每个属性绑定watcher

8 }

9return value

10 },

11 set: function(newval) {

12 value = newval

13 console.log(value)

14 dep.notify(); // 数据改变执行发布

15 }

16 })

17};

18

19// 绑定订阅者到节点上面

20function compile(node, vm) {

21// console.log(node.nodeName)

22var reg = /\{\{(.*)\}\}/;

23if (node.nodeType === 1) {

24var attr = node.attributes

25for(var i = 0; i < attr.length; i++) {

26if (attr[i].nodeName === 'v-model') {

27var name = attr[i].nodeValue;

28 node.addEventListener('keyup', function(e) {

29// console.log(e.target.value)

30 vm[name] = e.target.value

31 })

32// node.value = vm[name];

33new Watcher(vm, node, name); // 初始化绑定input节点

34 node.removeAttribute('v-model');

35 };

36 };

37 };

38if (node.nodeType === 3) {

39if (reg.test(node.nodeValue)) {

40var name = RegExp.$1;

41 name = name.trim();

42// node.nodeValue = vm[name];

43new Watcher(vm, node, name); // 文本节点绑定订阅者

44 }

45 }

46 }

到这里vue的双绑定就实现了,此文仅为实现最简单的双向绑定,一些其它复杂的条件都没有考虑在内,为理想状态下,如有纰漏还望指正,下面附上完整代码

  1 <!DOCTYPE html>

2 <html lang="en">

3 <head>

4 <meta charset="UTF-8">

5 <title>Vue</title>

6 </head>

7 <body>

8 <div id="app">

9 <input type="text" id="a" v-model="text">

10 {{text}}

11 </div>

12 </body>

13 <script>

14// 定义发布订阅

15function Dep() {

16this.subs = []

17 }

18 Dep.prototype = {

19 addSub: function(sub) {

20this.subs.push(sub);

21 },

22 notify: function() {

23this.subs.forEach(function(sub) {

24 sub.update();

25 })

26 }

27 };

28// 定义观察者

29function Watcher (vm, node, name) {

30 Dep.target = this;

31this.name = name;

32this.node = node;

33this.vm = vm;

34this.update();

35 Dep.target = null;

36 };

37 Watcher.prototype = {

38 update: function() {

39this.get();

40// console.log(this.node.nodeName)

41if (this.node.nodeName === 'INPUT') {

42this.node.value = this.value;

43 } else {

44this.node.nodeValue = this.value;

45 }

46 },

47 get: function() {

48this.value = this.vm[this.name];

49 }

50 }

51// 绑定数据

52function compile(node, vm) {

53// console.log(node.nodeName)

54var reg = /\{\{(.*)\}\}/;

55if (node.nodeType === 1) {

56var attr = node.attributes

57for(var i = 0; i < attr.length; i++) {

58if (attr[i].nodeName === 'v-model') {

59var name = attr[i].nodeValue;

60 node.addEventListener('keyup', function(e) {

61// console.log(e.target.value)

62 vm[name] = e.target.value

63 })

64// node.value = vm[name];

65new Watcher(vm, node, name);

66 node.removeAttribute('v-model');

67 };

68 };

69 };

70if (node.nodeType === 3) {

71if (reg.test(node.nodeValue)) {

72var name = RegExp.$1;

73 name = name.trim();

74// node.nodeValue = vm[name];

75new Watcher(vm, node, name);

76 }

77 }

78 }

79// 劫持DOM节点到DocumentFragment中

80function nodeToFragment(node, vm) {

81var flag = document.createDocumentFragment();

82while(node.firstChild) {

83// console.log(node.firstChild)

84 compile(node.firstChild, vm)

85 flag.appendChild(node.firstChild) // 劫持节点到文档片段中

86 }

87return flag

88 };

89function definevm(vm, key, value) {

90var dep = new Dep

91 Object.defineProperty(vm, key, {

92 get: function() {

93if (Dep.target) {

94 dep.addSub(Dep.target)

95 }

96return value

97 },

98 set: function(newval) {

99 value = newval

100 console.log(value)

101 dep.notify();

102 }

103 })

104 };

105// 指定data到vm

106function obersve(data, vm) {

107for(var key in data) {

108 definevm(vm, key, data[key]);

109 }

110 }

111// 创建Vue类

112function Vue (options) {

113this.data = options.data;

114var id = options.el;

115var ele = document.getElementById(id);

116

117// 将data的数据指向vm

118 obersve(this.data, this);

119// 存DOM到文档片段

120var dom = nodeToFragment(ele, this);

121// 编译完成将DOM返回挂在容器中

122 ele.appendChild(dom);

123 };

124// 创建Vue实例

125var vm = new Vue({

126 el: 'app',

127 data: {

128 text: 'hello world'

129 }

130 })

131 </script>

132 </html>

参考文章:https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension

以上是 实现Vue的双向绑定 的全部内容, 来源链接: utcz.com/z/508751.html

回到顶部