• Vue双向数据绑定原理(面试必问)


      给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

    地址:前端面试题库

     

    vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。

    具体步骤

    • 1、需要observer的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter 这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
    • 2、compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
    • 3、Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
      (1)在自身实例化时往属性订阅器(dep)里面添加自己
      (2)自身必须有一个update()方法
      (3)待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
    • 4、MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

    什么是数据双向绑定

    vue是一个mvvm框架,即数据双向绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化。这也算是vue的精髓之处了。值得注意的是, 我们所说的数据双向绑定,一定是对于UI控件来说的,非UI控件不会涉及到数据双向绑定。 单向数据绑定是使用状态管理工具(如redux)的前提。如果我们使用vuex,那么数据流也是单项的,这时就会和双向数据绑定有冲突,我们可以这么解决

    为什么要实现数据的双向绑定

    在vue中,如果使用vuex,实际上数据还是单向的,之所以说是数据双向绑定,这是用的UI控件来说,对于我们处理表单,vue的双向数据绑定用起来就特别舒服了。

    即两者并不互斥, 在全局性数据流使用单项,方便跟踪; 局部性数据流使用双向,简单易操作。

    1. 什么是Object.defineProperty?

    1.1 语法:

    Object.defineProperty(obj, prop, descriptor)
    

    参数说明:

    1. obj:必需。目标对象
    2. prop:必需。需定义或修改的属性的名字
    3. descriptor:必需。目标属性所拥有的特性

    返回值:

    传入函数的对象。即第一个参数obj;

    针对属性,我们可以给这个属性设置一些特性,比如是否只读不可以写;是否可以被for…in或Object.keys()遍历。

    给对象的属性添加特性描述,目前提供两种形式:数据描述和存取器描述。

    当修改或定义对象的某个属性的时候,给这个属性添加一些特性:

    一、访问器属性

    Object.defineProperty()函数可以定义对象的属性相关描述符, 其中的set和get函数对于完成数据双向绑定起到了至关重要的作用,下面,我们看看这个函数的基本使用方式。

    1. var obj = {
    2. foo: 'foo'
    3. }
    4. Object.defineProperty(obj, 'foo', {
    5. get: function () {
    6. console.log('将要读取obj.foo属性');
    7. },
    8. set: function (newVal) {
    9. console.log('当前值为', newVal);
    10. }
    11. });
    12. obj.foo; // 将要读取obj.foo属性
    13. obj.foo = 'name'; // 当前值为 name

    可以看到,get即为我们访问属性时调用,set为我们设置属性值时调用。

    二、简单的数据双向绑定实现方法

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>forvuetitle>
    6. head>
    7. <body>
    8. <input type="text" id="textInput">
    9. 输入:<span id="textSpan">span>
    10. <script>
    11. var obj = {},
    12. textInput = document.querySelector('#textInput'),
    13. textSpan = document.querySelector('#textSpan');
    14. Object.defineProperty(obj, 'foo', {
    15. set: function (newValue) {
    16. textInput.value = newValue;
    17. textSpan.innerHTML = newValue;
    18. }
    19. });
    20. textInput.addEventListener('keyup', function (e) {
    21. obj.foo = e.target.value;
    22. });
    23. script>
    24. body>
    25. html>

    最终效果图

    可以看到,实现一个简单的数据双向绑定还是不难的: 使用Object.defineProperty()来定义属性的set函数,属性被赋值的时候,修改Input的value值以及span中的innerHTML;然后监听input的keyup事件,修改对象的属性值,即可实现这样的一个简单的数据双向绑定。

    双向绑定指令为v-model:

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>Vue入门之htmlrawtitle>
    6. <script src="https://unpkg.com/vue/dist/vue.js">script>
    7. head>
    8. <body>
    9. <div id="app">
    10. <input type="text" name="txt" v-model="msg">
    11. <p>您输入的信息是:{{ msg }}p>
    12. div>
    13. <script>
    14. var app = new Vue({
    15. el: '#app',
    16. data: {
    17. msg: '双向数据绑定的例子'
    18. }
    19. });
    20. script>
    21. body>
    22. html>

    最终的结果就是:你改变 input 文本框的内容的时候,p 标签中的内容会跟着进行改变。

    三、 实现任务的思路

    上面我们只是实现了一个最简单的数据双向绑定,而我们真正希望实现的时下面这种方式:

    1. <div id="app">
    2. <input type="text" v-model="text">
    3. {{ text }}
    4. div>
    5. <script>
    6. var vm = new Vue({
    7. el: '#app',
    8. data: {
    9. text: 'hello world'
    10. }
    11. });
    12. script>

    即和vue一样的方式来实现数据的双向绑定。那么,我们可以把整个实现过程分为下面几步:

    • 输入框以及文本节点与 data 中的数据绑定
    • 输入框内容变化时,data 中的数据同步变化。即 view => model 的变化
    • data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化

    四、DocumentFragment

    如果希望实现任务一,我们还需要使用到 DocumentFragment 文档片段,可以把它看做一个容器,如下所示:

    1. <div id="app">
    2. div>
    3. <script>
    4. var flag = document.createDocumentFragment(),
    5. span = document.createElement('span'),
    6. textNode = document.createTextNode('hello world');
    7. span.appendChild(textNode);
    8. flag.appendChild(span);
    9. document.querySelector('#app').appendChild(flag)
    10. script>

    这样,我们就可以得到下面的DOM树:

    使用文档片段的好处在于:在文档片段上进行操作DOM,而不会影响到真实的DOM,操作完成之后,我们就可以添加到真实DOM上,这样的效率比直接在正式DOM上修改要高很多 。

    vue进行编译时,就是将挂载目标的所有子节点劫持到DocumentFragment中,经过一番处理之后,再将DocumentFragment整体返回插入挂载目标

    如下所示 

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>forvuetitle>
    6. head>
    7. <body>
    8. <div id="app">
    9. <input type="text" id="a">
    10. <span id="b">span>
    11. div>
    12. <script>
    13. var dom = nodeToFragment(document.getElementById('app'));
    14. console.log(dom);
    15. function nodeToFragment(node) {
    16. var flag = document.createDocumentFragment();
    17. var child;
    18. while (child = node.firstChild) {
    19. flag.appendChild(child);
    20. }
    21. return flag;
    22. }
    23. document.getElementById('app').appendChild(dom);
    24. script>
    25. body>
    26. html>

    即首先获取到div,然后通过documentFragment劫持,接着再把这个文档片段添加到div上去。

    五、初始化数据绑定

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>forvuetitle>
    6. head>
    7. <body>
    8. <div id="app">
    9. <input type="text" v-model="text">
    10. {{ text }}
    11. div>
    12. <script>
    13. function compile(node, vm) {
    14. var reg = /{{(.*)}}/;
    15. // 节点类型为元素
    16. if (node.nodeType === 1) {
    17. var attr = node.attributes;
    18. // 解析属性
    19. for (var i = 0; i < attr.length; i++) {
    20. if (attr[i].nodeName == 'v-model') {
    21. var name = attr[i].nodeValue; // 获取v-model绑定的属性名
    22. node.value = vm.data[name]; // 将data的值赋值给该node
    23. node.removeAttribute('v-model');
    24. }
    25. }
    26. }
    27. // 节点类型为text
    28. if (node.nodeType === 3) {
    29. if (reg.test(node.nodeValue)) {
    30. var name = RegExp.$1; // 获取匹配到的字符串
    31. name = name.trim();
    32. node.nodeValue = vm.data[name]; // 将data的值赋值给该node
    33. }
    34. }
    35. }
    36. function nodeToFragment(node, vm) {
    37. var flag = document.createDocumentFragment();
    38. var child;
    39. while (child = node.firstChild) {
    40. compile(child, vm);
    41. flag.appendChild(child); // 将子节点劫持到文档片段中
    42. }
    43. return flag;
    44. }
    45. function Vue(options) {
    46. this.data = options.data;
    47. var id = options.el;
    48. var dom = nodeToFragment(document.getElementById(id), this);
    49. // 编译完成后,将dom返回到app中。
    50. document.getElementById(id).appendChild(dom);
    51. }
    52. var vm = new Vue({
    53. el: 'app',
    54. data: {
    55. text: 'hello world'
    56. }
    57. });
    58. script>
    59. body>
    60. html>

    以上的代码实现而立任务一,我们可以看到,hello world 已经呈现在了输入框和文本节点中了。

    六、响应式的数据绑定

    我们再来看看任务二的实现思路: 当我们在输入框输入数据的时候,首先触发的时input事件(或者keyup、change事件),在相应的事件处理程序中,我们获取输入框的value并赋值给vm实例的text属性。 我们会利用defineProperty将data中的text设置为vm的访问器属性,因此给vm.text赋值,就会触发set方法。 在set方法中主要做两件事情,第一是更新属性的值,第二留在任务三种说。

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>forvuetitle>
    6. head>
    7. <body>
    8. <div id="app">
    9. <input type="text" v-model="text">
    10. {{ text }}
    11. div>
    12. <script>
    13. function compile(node, vm) {
    14. var reg = /{{(.*)}}/;
    15. // 节点类型为元素
    16. if (node.nodeType === 1) {
    17. var attr = node.attributes;
    18. // 解析属性
    19. for (var i = 0; i < attr.length; i++) {
    20. if (attr[i].nodeName == 'v-model') {
    21. var name = attr[i].nodeValue; // 获取v-model绑定的属性名
    22. node.addEventListener('input', function (e) {
    23. // 给相应的data属性赋值,进而触发属性的set方法
    24. vm[name] = e.target.value;
    25. })
    26. node.value = vm[name]; // 将data的值赋值给该node
    27. node.removeAttribute('v-model');
    28. }
    29. }
    30. }
    31. // 节点类型为text
    32. if (node.nodeType === 3) {
    33. if (reg.test(node.nodeValue)) {
    34. var name = RegExp.$1; // 获取匹配到的字符串
    35. name = name.trim();
    36. node.nodeValue = vm[name]; // 将data的值赋值给该node
    37. }
    38. }
    39. }
    40. function nodeToFragment(node, vm) {
    41. var flag = document.createDocumentFragment();
    42. var child;
    43. while (child = node.firstChild) {
    44. compile(child, vm);
    45. flag.appendChild(child); // 将子节点劫持到文档片段中
    46. }
    47. return flag;
    48. }
    49. function Vue(options) {
    50. this.data = options.data;
    51. var data = this.data;
    52. observe(data, this);
    53. var id = options.el;
    54. var dom = nodeToFragment(document.getElementById(id), this);
    55. // 编译完成后,将dom返回到app中。
    56. document.getElementById(id).appendChild(dom);
    57. }
    58. var vm = new Vue({
    59. el: 'app',
    60. data: {
    61. text: 'hello world'
    62. }
    63. });
    64. function defineReactive(obj, key, val) {
    65. // 响应式的数据绑定
    66. Object.defineProperty(obj, key, {
    67. get: function () {
    68. return val;
    69. },
    70. set: function (newVal) {
    71. if (newVal === val) {
    72. return;
    73. } else {
    74. val = newVal;
    75. console.log(val); // 方便看效果
    76. }
    77. }
    78. });
    79. }
    80. function observe (obj, vm) {
    81. Object.keys(obj).forEach(function (key) {
    82. defineReactive(vm, key, obj[key]);
    83. });
    84. }
    85. script>
    86. body>
    87. html>

    以上,任务二也就完成了,text属性值会和输入框的内容同步变化。

    七、 订阅/发布模式(subscribe & publish)

    text属性变化了,set方法触发了,但是文本节点的内容没有变化。 如何才能让同样绑定到text的文本节点也同步变化呢? 这里又有一个知识点: 订阅发布模式。

    订阅发布模式又称为观察者模式,定义了一种一对多的关系让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有的观察者对象。

    发布者发出通知 =>主题对象收到通知推送给订阅者 => 订阅者执行相应的操作。

    1. // 一个发布者 publisher,功能就是负责发布消息 - publish
    2. var pub = {
    3. publish: function () {
    4. dep.notify();
    5. }
    6. }
    7. // 多个订阅者 subscribers, 在发布者发布消息之后执行函数
    8. var sub1 = {
    9. update: function () {
    10. console.log(1);
    11. }
    12. }
    13. var sub2 = {
    14. update: function () {
    15. console.log(2);
    16. }
    17. }
    18. var sub3 = {
    19. update: function () {
    20. console.log(3);
    21. }
    22. }
    23. // 一个主题对象
    24. function Dep() {
    25. this.subs = [sub1, sub2, sub3];
    26. }
    27. Dep.prototype.notify = function () {
    28. this.subs.forEach(function (sub) {
    29. sub.update();
    30. });
    31. }
    32. // 发布者发布消息, 主题对象执行notify方法,进而触发订阅者执行Update方法
    33. var dep = new Dep();
    34. pub.publish();

    不难看出,这里的思路还是很简单的: 发布者负责发布消息、 订阅者负责接收接收消息,而最重要的是主题对象,他需要记录所有的订阅这特消息的人,然后负责吧发布的消息通知给哪些订阅了消息的人。

    所以,当set方法触发后做的第二件事情就是作为发布者发出通知: “我是属性text,我变了”。 文本节点作为订阅者,在接收到消息之后执行相应的更新动作。

    八、 双向绑定的实现

    回顾一下,每当new一个Vue,主要做了两件事情:第一是监听数据:observe(data),第二是编译HTML:nodeToFragment(id)

    在监听数据的过程中,会为data中的每一个属性生成一个主题对象dep。

    在编译HTML的过程中,会为每一个与数据绑定相关的节点生成一个订阅者 watcher,watcher会将自己添加到相应属性的dep中。

    我们已经实现了: 修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的set方法。

    接下来我们要实现的是: 发出通知 dep.notify() => 触发订阅者update方法 => 更新视图。

    这里的关键逻辑是: 如何将watcher添加到关联属性的dep中。

    1. function compile(node, vm) {
    2. var reg = /{{(.*)}}/;
    3. // 节点类型为元素
    4. if (node.nodeType === 1) {
    5. var attr = node.attributes;
    6. // 解析属性
    7. for (var i = 0; i < attr.length; i++) {
    8. if (attr[i].nodeName == 'v-model') {
    9. var name = attr[i].nodeValue; // 获取v-model绑定的属性名
    10. node.addEventListener('input', function (e) {
    11. // 给相应的data属性赋值,进而触发属性的set方法
    12. vm[name] = e.target.value;
    13. })
    14. node.value = vm[name]; // 将data的值赋值给该node
    15. node.removeAttribute('v-model');
    16. }
    17. }
    18. }
    19. // 节点类型为text
    20. if (node.nodeType === 3) {
    21. if (reg.test(node.nodeValue)) {
    22. var name = RegExp.$1; // 获取匹配到的字符串
    23. name = name.trim();
    24. // node.nodeValue = vm[name]; // 将data的值赋值给该node
    25. new Watcher(vm, node, name);
    26. }
    27. }
    28. }

    在编译HTML的过程中,为每个和data关联的节点生成一个Watcher。那么Watcher函数中发生了什么呢?

    1. function Watcher(vm, node, name) {
    2. Dep.target = this;
    3. this.name = name;
    4. this.node = node;
    5. this.vm = vm;
    6. this.update();
    7. Dep.target = null;
    8. }
    9. Watcher.prototype = {
    10. update: function () {
    11. this.get();
    12. this.node.nodeValue = this.value;
    13. },
    14. // 获取data中的属性值
    15. get: function () {
    16. this.value = this.vm[this.name]; // 触发相应属性的get
    17. }
    18. }

    首先,将自己赋值给了一个全局变量 Dep.target;

    其次,执行了update方法,进而执行了 get 方法,get方法读取了vm的访问器属性, 从而触发了访问器属性的get方法,get方法将该watcher添加到对应访问器属性的dep中;

    再次,获取顺序性的值, 然后更新视图。

    最后将Dep.target设置为空。 因为他是全局变量,也是watcher和dep关联的唯一桥梁,任何时候,都必须保证Dep.target只有一个值。

    最终如下

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>forvuetitle>
    6. head>
    7. <body>
    8. <div id="app">
    9. <input type="text" v-model="text"> <br>
    10. {{ text }} <br>
    11. {{ text }}
    12. div>
    13. <script>
    14. function observe(obj, vm) {
    15. Object.keys(obj).forEach(function (key) {
    16. defineReactive(vm, key, obj[key]);
    17. });
    18. }
    19. function defineReactive(obj, key, val) {
    20. var dep = new Dep();
    21. // 响应式的数据绑定
    22. Object.defineProperty(obj, key, {
    23. get: function () {
    24. // 添加订阅者watcher到主题对象Dep
    25. if (Dep.target) {
    26. dep.addSub(Dep.target);
    27. }
    28. return val;
    29. },
    30. set: function (newVal) {
    31. if (newVal === val) {
    32. return;
    33. } else {
    34. val = newVal;
    35. // 作为发布者发出通知
    36. dep.notify()
    37. }
    38. }
    39. });
    40. }
    41. function nodeToFragment(node, vm) {
    42. var flag = document.createDocumentFragment();
    43. var child;
    44. while (child = node.firstChild) {
    45. compile(child, vm);
    46. flag.appendChild(child); // 将子节点劫持到文档片段中
    47. }
    48. return flag;
    49. }
    50. function compile(node, vm) {
    51. var reg = /{{(.*)}}/;
    52. // 节点类型为元素
    53. if (node.nodeType === 1) {
    54. var attr = node.attributes;
    55. // 解析属性
    56. for (var i = 0; i < attr.length; i++) {
    57. if (attr[i].nodeName == 'v-model') {
    58. var name = attr[i].nodeValue; // 获取v-model绑定的属性名
    59. node.addEventListener('input', function (e) {
    60. // 给相应的data属性赋值,进而触发属性的set方法
    61. vm[name] = e.target.value;
    62. })
    63. node.value = vm[name]; // 将data的值赋值给该node
    64. node.removeAttribute('v-model');
    65. }
    66. }
    67. }
    68. // 节点类型为text
    69. if (node.nodeType === 3) {
    70. if (reg.test(node.nodeValue)) {
    71. var name = RegExp.$1; // 获取匹配到的字符串
    72. name = name.trim();
    73. // node.nodeValue = vm[name]; // 将data的值赋值给该node
    74. new Watcher(vm, node, name);
    75. }
    76. }
    77. }
    78. function Watcher(vm, node, name) {
    79. Dep.target = this;
    80. this.name = name;
    81. this.node = node;
    82. this.vm = vm;
    83. this.update();
    84. Dep.target = null;
    85. }
    86. Watcher.prototype = {
    87. update: function () {
    88. this.get();
    89. this.node.nodeValue = this.value;
    90. },
    91. // 获取data中的属性值
    92. get: function () {
    93. this.value = this.vm[this.name]; // 触发相应属性的get
    94. }
    95. }
    96. function Dep () {
    97. this.subs = [];
    98. }
    99. Dep.prototype = {
    100. addSub: function (sub) {
    101. this.subs.push(sub);
    102. },
    103. notify: function () {
    104. this.subs.forEach(function (sub) {
    105. sub.update();
    106. });
    107. }
    108. }
    109. function Vue(options) {
    110. this.data = options.data;
    111. var data = this.data;
    112. observe(data, this);
    113. var id = options.el;
    114. var dom = nodeToFragment(document.getElementById(id), this);
    115. // 编译完成后,将dom返回到app中。
    116. document.getElementById(id).appendChild(dom);
    117. }
    118. var vm = new Vue({
    119. el: 'app',
    120. data: {
    121. text: 'hello world'
    122. }
    123. });
    124. script>
    125. body>
    126. html>

     

    给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

    地址:前端面试题库

  • 相关阅读:
    android junit 单元测试与输出日志信息查看处理
    解决老版本Oracle VirtualBox 此应用无法在此设备上运行问题
    人大女王金融硕士——不要成为群羊中盲从的羊,别人疯狂你要冷静
    [谷粒商城笔记]07、Linux环境-虚拟机网络设置
    年薪30万+的HR这样做数据分析!(附关键指标&免费模版)
    day10_面向对象_抽象_接口
    uniapp自定义顶部导航栏
    学 Python 都用来干嘛的?
    10.25verilog复习,代码规范复盘,触发器复习
    Eigen稀疏矩阵操作
  • 原文地址:https://blog.csdn.net/weixin_42981560/article/details/126873233