• vue学习之响应式原理的demo实现


    Vue.js 核心:
    1、响应式的数据绑定系统
    2、组件系统。

    访问器属性

    访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过 defineProperty() 方法单独定义。

           var obj = { };
    
           // 为obj定义一个名为 hello 的访问器属性
    
           Object.defineProperty(obj, "hello", {
    
             get: function () {return sth},
    
             set: function (val) {/* do sth */}
    
           })
    
           obj.hello // 可以像普通属性一样读取访问器属性
    

    访问器属性的"值"比较特殊,读取或设置访问器属性的值,实际上是调用其内部特性:get和set函数。

           obj.hello // 读取属性,就是调用get函数并返回get函数的返回值
    
           obj.hello = "abc" // 为属性赋值,就是调用set函数,赋值其实是传参 
    

    get 和 set 方法内部的 this 都指向 obj,这意味着 get 和 set 函数可以操作对象内部的值。另外,访问器属性的会"覆盖"同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略。

    预期达到的效果:
    1、随文本框输入文字的变化,span 中会同步显示相同的文字内容;
    2、在js或控制台显式的修改 obj.hello 的值,视图会相应更新。这样就实现了 model => view 以及 view => model 的双向绑定。

    模型图:

    子任务:

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

    1、输入框以及文本节点与 data 中的数据绑定

    这里需要对 DOM 进行编译,这里引入一个知识点:DocumentFragment。

    DocumentFragment

    DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到 DOM 中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用 DocumentFragment 处理节点,速度和性能远远优于直接操作 DOM。Vue 进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过 append 方法,DOM 中的节点会被自动删除)到 DocumentFragment 中,经过一番处理后,再将 DocumentFragment 整体返回插入挂载目标。

    <!DOCTYPE html>
    <html>
    <head>
        <title></title>
    </head>
    <body>
        <div id="app">
            <input type="text" id="a">
            <span id="b"></span>
        </div>
    
    <script type="text/javascript">
        var dom = nodeToFragment(document.getElementById('app'));
        console.log(dom);
    
        function nodeToFragment (node) {
            var flag = document.createDocumentFragment();
            var child;
            while (child = node.firstChild) {
                flag.append(child);  // 劫持node的所有子节点
            }
            return flag;
        }
    
        document.getElementById('app').appendChild(dom);  // 返回到app中
    </script>
    </body>
    </html>
    
    

    数据初始化绑定

    function compile (node, vm) {
        var reg = /{{(.*)}}/;
        // 节点类型为元素
        if (node.nodeType === 1) {
            var attr = node.attributes;
    
            // 解析属性
            for (var i = 0; i < attr.length; i++) {
                if (attr[i].nodeName == 'v-model') {
                    var name = attr[i].nodeValue;  // 获取v-model绑定的属性名
                    node.value = vm.data[name];  // 将data的值赋值给该node
                    node.removeAttribute('v-model');
                }
            }
        }
    
        // 节点类型为text
        if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                var name = RegExp.$1;  // 获取匹配到的字符串
                name = name.trim();
                node.nodeValue = vm.data[name];
            }
        }
    }
    
    function nodeToFragment (node, vm) {
        var flag = document.createDocumentFragment();
        var child;
    
        while (child  = node.firstChild) {
            compile(child, vm);
            flag.append(child);
        }
        return flag;
    }
    
    
    function Vue (options) {
        this.data = options.data;
        var id = options.el;
        var dom = nodeToFragment(document.getElementById(id), this);
    
        // 编译完成后,将dom返回到app中
        document.getElementById(id).appendChild(dom);
    }
    
    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello world'
        }
    })
    
    

    效果

    响应式的数据绑定

    2、输入框内容变化时,data 中的数据同步变化。即 view => model 的变化。

    思路:当我们在输入框输入数据的时候,首先触发 input 事件(或者 keyup、change 事件),在相应的事件处理程序中,我们获取输入框的 value 并赋值给 vm 实例的 text 属性。我们利用 defineProperty 将 data 中的 text 设置为 vm 的访问器属性,因此给 vm.text 赋值,就会触发 set 方法。这里set主要做了跟新属性值得操作。

        function defineReactive (obj, key, val) {
    
          Object.defineProperty(obj, key, {
            get: function () {
              return val
            },
            set: function (newVal) {
              if (newVal === val) return
              val = newVal;
         	  console.log(val);  // console
            }
          });
        }
    
        function observe (obj, vm) {
          Object.keys(obj).forEach(function (key) {
            defineReactive(vm, key, obj[key]);
          });
        }
    
        function Vue (options) {
          this.data = options.data;
          var data = this.data;
    
          observe(data, this);
    
          var id = options.el;
          var dom = nodeToFragment(document.getElementById(id), this);
    
          // 编译完成后,将dom返回到app中
          document.getElementById(id).appendChild(dom); 
        }
    
        function compile (node, vm) {
          var reg = /{{(.*)}}/;
          // 节点类型为元素
          if (node.nodeType === 1) {
            var attr = node.attributes;
            // 解析属性
            for (var i = 0; i < attr.length; i++) {
              if (attr[i].nodeName == 'v-model') {
                var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                node.addEventListener('input', function (e) {
                  // 给相应的data属性赋值,进而触发该属性的set方法
                  vm[name] = e.target.value;
                });
                node.value = vm[name]; // 将data的值赋给该node
                
                node.removeAttribute('v-model');
              }
            };
    
            new Watcher(vm, node, name, 'input');
          }
          // 节点类型为text
          if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
              var name = RegExp.$1; // 获取匹配到的字符串
              name = name.trim();
              node.nodeValue = vm[name];
            }
          }
        }
    

    第二部完成效果

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

    data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化。

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

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

    	var pub = {
    		publish: function() {
    			def.notify():
    		}
    	}
    	// 三个订阅者subscribers
    	var sub1 = {updata: function () {console.log(1)} }
    	var sub2 = {updata: function () {console.log(2)} }
    	var sub3 = {updata: function () {console.log(3)} }
    
    	// 一个主题对象
    	funciton Dep () {
    		this.subs = [sub1, sub2, sub3];
    	}
    	Dep.prototype.notify = function () {
    		this.subs.forEach(function (sub) {
    			sub.update();
    		})
    	}
    	// 发布者发布消息,主题对象执行notify方法,然后会触发订阅者实现更函数
    	var dep = new Dep();
    	pub.publish();  // 1, 2, 3
    

    set在这里的作用是:作为发布者发出通知,而文本节点在这里是订阅者,收到消息之后执行相应的更新操作。

    双向绑定的实现

    每当 new 一个 Vue,主要做了两件事:
    1.是监听数据:observe(data),
    2.第二个是编译 HTML:nodeToFragement(id)。

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

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

    现在效果:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的 set 方法。

    下一步实现:发出通知 dep.notify() => 触发订阅者的 update 方法 => 更新视图。

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

    在编译 HTML 过程中,为每个与 data 关联的节点生成一个 Watcher。
    Watcher函数实现思路:

        function Watcher (vm, node, name, nodeType) {
          Dep.target = this;
          this.name = name;
          this.node = node;
          this.vm = vm;
          this.update();
          Dep.target = null;
        }
    
        Watcher.prototype = {
          update: function () {
            this.get();
    	    this.node.nodeValue = this.value;
          },
          // 获取data中的属性值
          get: function () {
            this.value = this.vm[this.name]; // 触发相应属性的get
          }
        }
    

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

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

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

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

        function defineReactive (obj, key, val) {
    
          var dep = new Dep();  // !!
    
          Object.defineProperty(obj, key, {
            get: function () { 
              // 添加订阅者watcher到主题对象Dep  // !!
              if (Dep.target) dep.addSub(Dep.target);  // !!
              return val
            },
            set: function (newVal) {
              if (newVal === val) return
              val = newVal;
              // 作为发布者发出通知
              dep.notify();
            }
          });
        }
        function Dep () {
          this.subs = []
        }
    
        Dep.prototype = {
          addSub: function(sub) {
            this.subs.push(sub);
          },
    
          notify: function() {
            this.subs.forEach(function(sub) {
              sub.update();
            });
          }
        };
    

    最终效果:
    文本内容会随输入框内容同步变化,在控制器中修改 vm.text 的值,会同步反映到文本内容中。

    感悟

    1.异步更新带来的数据响应式误解

    <div id="app">
            <h2>{{dataObj.text}}</h2>
    </div>
    
    
    
    new Vue({
                el: '#app',
                data: {
                    dataObj: {}
                },
                ready: function () {
                    var self = this;
    
                    /**
                     * 异步请求模拟
                     */
                    setTimeout(function () {
                        self.dataObj = {}; 
                        self.dataObj['text'] = 'new text';
                    }, 3000);
                }
            })
    

    上面的代码非常简单,我们都知道vue中在data里面声明的数据才具有响应式的特性,所以我们一开始在data中声明了一个dataObj空对象,然后在异步请求中执行了两行代码,如下:

    self.dataObj = {}; 
    self.dataObj['text'] = 'new text';
    

    模板更新了,应该具有响应式特性,如果这么想那么你就已经走入了误区,一开始我们并没有在data中声明.text属性,所以该属性是不具有响应式的特性的。

    但模板切切实实已经更新了,这又是怎么回事呢?

    那是因为vue的dom更新是异步的,即当setter操作发生后,指令并不会立马更新,指令的更新操作会有一个延迟,当指令更新真正执行的时候,此时.text属性已经赋值,所以指令更新模板时得到的是新值。

    具体流程如下所示:

    self.dataObj = {};发生setter操作
    vue监测到setter操作,通知相关指令执行更新操作
    self.dataObj['text'] = 'new text';赋值语句
    指令更新开始执行

    所以真正的触发更新操作是self.dataObj = {};这一句引起的,所以单看上述例子,具有响应式特性的数据只有dataObj这一层,它的子属性是不具备的。

    2.Vue 不允许在已经创建的实例上动态添加新的根级响应式属性(root-level reactive property)。然而它可以使用Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上或者使用$set

  • 相关阅读:
    Activity 横竖屏生命周期
    gradle wrapper, gradle ,gradle plugin 之间的关系
    《构建之法》第八、九章学习总结
    《构建之法》第六、七章学习总结
    《构建之法》第三、四、五章学习总结
    《构建之法》第一、二章学习总结
    SQL练习50题(基于MySQL)后25题
    SQL练习50题(基于MySQL)前25题
    轮播2-css
    轮播1-animate-匀速
  • 原文地址:https://www.cnblogs.com/Yfling/p/7168219.html
Copyright © 2020-2023  润新知