zoukankan      html  css  js  c++  java
  • Vue相关原理以及手写一个MyVue

    一、Vue基本原理

    1. 建立虚拟DOM Tree,通过document.createDocumentFragment(),遍历指定根节点内部节点,根据{{ prop }}、v-model等规则进行compile;
    2. 通过Object.defineProperty()进行数据变化拦截;
    3. 截取到的数据变化,通过发布者-订阅者模式,触发Watcher,从而改变虚拟DOM中的具体数据;
    4. 通过改变虚拟DOM元素值,从而改变最后渲染dom树的值,完成双向绑定

    Vue的模式是m-v-vm模式,即(model-view-modelView),通过modelView作为中间层(即vm的实例),进行双向数据的绑定与变化。

    而实现这种双向绑定的关键就在于:Object.defineProperty订阅——发布者模式这两点。

    二、双向绑定

    1.Object.defineProperty

    Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
    语法:Object.defineProperty(obj, prop, descriptor)

    var obj = {};
    Object.defineProperty(obj,'hello',{
      get:function(){
        //我们在这里拦截到了数据
        console.log("get方法被调用");
      },
      set:function(newValue){
        //改变数据的值,拦截下来额
        console.log("set方法被调用");
      }
    });
    obj.hello//输出为“get方法被调用”,输出了值。
    obj.hello = 'new Hello';//输出为set方法被调用,修改了新值

     获取对象属性值触发get、设置对象属性值触发set,因此我们可以想象到数据模型对象的属性设置和读取可以驱动view层的数据变化,view的数据变化传递给数据模型对象,在set里面可以做很多事情。

    可以从这里看到,这是在对更底层的对象属性进行编程。简单地说,也就是我们对其更底层对象属性的修改或获取的阶段进行了拦截(对象属性更改的钩子函数)。

    2.基础数据双向绑定的实现

    <input class="inp-text" type="text">
    <div class="text-box"></div>

    通过对象底层属性的set和get进行数据拦截

         var obj = {};
            Object.defineProperty(obj,'hello',{
                  get:function(){
                    //我们在这里拦截到了数据
                    console.log("get方法被调用");
                },
                set:function(newValue){
                    //改变数据的值,拦截下来额
                    console.log("set方法被调用");
                    document.getElementById('test').value = newValue;
                    document.getElementById('test1').innerHTML = newValue;
                }
            });
            //obj.hello;
            //obj.hello = '123';
            document.getElementById('test').addEventListener('input',function(e){
                obj.hello = e.target.value;//触发它的set方法
            })

    3.Vue初始化(虚拟节点的产生与编译)

    html:

    <div id="mvvm">
        <input v-model="text" id="test"></input>{{text}}
        <div id="test1">{{text}}</div>
    </div>

    3.1 Vue的虚拟节点容器

    function nodeContainer(node, vm, flag){
      var flag = flag || document.createDocumentFragment();
    
      var child;
      while(child = node.firstChild){
        compile(child, vm);
        flag.appendChild(child);
        if(child.firstChild){
          // flag.appendChild(nodeContainer(child,vm));
          nodeContainer(child, vm, flag);
        }
      }
      return flag;
    }

    这里几个注意的点:

    1. while(child = node.firstChild)把node的firstChild赋值成while的条件,可以看做是遍历所有的dom节点。一旦遍历到底了,node的firstChild就会未定义成undefined就跳出while。
    2. document.createDocumentFragment();是一个虚拟节点的容器树,可以存放我们的虚拟节点。
    3. 上面的函数是个迭代,一直循环到节点的终点为止。 
    说明:appendChild 方法具有可移动性,将el中的节点移动到了fragment当中大家可以在循环后打印一下node.firstChild,会发现是Null

    3.2编译函数

    //编译
    function compile(node, vm){
      var reg = /{{(.*)}}/g;//匹配双绑的双大括号
      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;
            node.value = vm.data[name];//讲实例中的data数据赋值给节点
            //node.removeAttribute('v-model');
          }
        }
      }
      //如果节点类型为text
      if(node.nodeType === 3){
        
        if(reg.test(node.nodeValue)){
          // console.dir(node);
          var name = RegExp.$1;//获取匹配到的字符串
          name = name.trim();
          node.nodeValue = vm.data[name];
        }
      }
    }

    代码解释:

    1. 当nodeType为1的时候,表示是个元素。同时我们进行判断,如果节点中的指令含有v-model这个指令,那么我们就初始化,进行对节点的值的赋值。
    2. 如果nodeType为3的时候,也就是text节点属性。表示你的节点到了终点,一般都是节点的前后末端。我们常常在这里定义我们的双绑值。此时一旦匹配到了双绑(双大括号),即进行值的初始化。

    3.3Vue的节点初始化编译

    function Vue(options){
      this.data = options.data;
      
      var id = options.el;
      var dom = nodeContainer(document.getElementById(id),this);
      document.getElementById(id).appendChild(dom);  
    }
    
    //随后使用他
    var Demo = new Vue({
      el:'mvvm',
      data:{
        text:'HelloWorld',
        d:'123'
      }
    })

    至此,初始化已经完成

    4.Vue声明响应式

    4.1 定义Vue的data的属性响应式

    function defineReactive (obj, key, value){
      Object.defineProperty(obj,key,{
        get:function(){
          console.log("get了值"+value);
          return value;//获取到了值
        },
        set:function(newValue){
          if(newValue === value){
            return;//如果值没变化,不用触发新值改变
          }
          value = newValue;//改变了值
          console.log("set了最新值"+value);
        }
      })
    }

    这里的obj我们这定义为vm实例或者vm实例里面的data属性。

    PS:这里强调一下,defineProperty这个方法,不仅可以定义obj的直接属性,比如obj.hello这个属性。也可以间接定义属性比如:obj.middle.hello。这里导致的效果就是两者的hello属性都被定义成响应式了。

    4.2用下列的observe方法循环调用响应式方法。

    function observe (obj,vm){
      Object.keys(obj).forEach(function(key){
        defineReactive(vm,key,obj[key]);
      })
    }

    4.3然后再Vue方法中初始化:

    function Vue(options){
      this.data = options.data;
      var data = this.data;
      -------------------------
      observe(data,this);//这里调用定义响应式方法
      -------------------------
      var id = options.el;
      var dom = nodeContainer(document.getElementById(id),this);
      document.getElementById(id).appendChild(dom); //把虚拟dom渲染上去 
    }

    4.4在编译方法中v-model属性找到的时候去监听:

    function compile(node, vm){
      var reg = /{{(.*)}}/g;
      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;
            -------------------------//这里新添加的监听
            node.addEventListener('input',function(e){
              console.log(vm[name]);
              vm[name] = e.target.value;//改变实例里面的值
            });
            -------------------------
            node.value = vm[name];//讲实例中的data数据赋值给节点
            //node.removeAttribute('v-model');
          }
        }
      }
    }

    以上我们实现了,你再输入框里面输入,同时触发getter&setter,去改变vm实例中data的值。也就是说MVVM的图例中经过getter&setter已经成功了。接下去就是订阅——发布者模式。

     

    5.订阅——发布者模式

    5.1每个订阅者对象内部声明一个update方法来触发订阅属性。再声明一个发布者,去触发发布消息,通知的方法

    var sub1 = {
        update: function () {
            console.log(1);
        },
    };
    var sub2 = {
        update: function () {
            console.log(2);
        },
    };
    var sub3 = {
        update: function () {
            console.log(3);
        },
    };
    function Dep() {
        this.subs = [sub1, sub2, sub3]; //把三个订阅者加进去
    }
    Dep.prototype.notify = function () {
        //在原型上声明“发布消息”方法
        this.subs.forEach(function (sub) {
            sub.update();
        });
    };
    
    /* var dep = new Dep();
    //pub.publish();
    dep.notify(); */
    
    var dep = new Dep();
    var pub = {
        publish: function () {
            dep.notify();
        },
    };
    pub.publish(); //我们也可以声明另外一个中间对象,这里的结果是跟上面一样的

    到这,我们已经实现了:

    1. 修改输入框内容 => 触发修改vm实例里的属性值 => 触发set&get方法
    2. 订阅成功 => 发布者发出通知notify() => 触发订阅者的update()方法

    接下来重点要实现的是:如何去更新视图,同时把订阅——发布者模式进去watcher观察者模式?

     5.2观察者模式

    先定义发布者:
    function Dep(){
      this.subs = [];
    }
    Dep.prototype ={
      add:function(sub){//这里定义增加订阅者的方法
        this.subs.push(sub);
      },
      notify:function(){//这里定义触发订阅者update()的通知方法
        this.subs.forEach(function(sub){
          console.log(sub);
          sub.update();//下列发布者的更新方法
        })
      }
    }

    再定义观察者(订阅者):

    function Watcher(vm,node,name){
      Dep.global = this;//这里很重要!把自己赋值给Dep函数对象的全局变量
      this.name = name;
      this.node = node;
      this.vm = vm;
      this.update();
      Dep.global = null;//这里update()完记得清空Dep函数对象的全局变量
    }
    Watcher.prototype.update = function(){
        this.get();
        switch (this.node.nodeType) { //这里去通过判断节点的类型改变视图的值
          case 1: 
            this.node.value = this.value;
            break;
          case 3:
            this.node.nodeValue = this.value;
            break;
          default: break;
        };
    }
    Watcher.prototype.get = function(){
        this.value = this.vm[this.name];//这里把this的value值赋值,触发data的defineProperty方法中的get方法!
    }

    以上需要注意的点:

    1. 在Watcher函数对象的原型方法update里面更新视图的值(实现watcher到视图层的改变)。
    2. Watcher函数对象的原型方法get,是为了触发defineProperty方法中的get方法!
    3. 在new一个Watcher的对象的时候,记得把Dep函数对象赋值一个全局变量,而且及时清空。至于为什么这么做,我们接下来看。
    function defineReactive (obj, key, value){
      var dep = new Dep();//这里每一个vm的data属性值声明一个新的订阅者
      Object.defineProperty(obj,key,{
        get:function(){
          console.log(Dep.global);
          -----------------------
          if(Dep.global){//这里是第一次new对象Watcher的时候,初始化数据的时候,往订阅者对象里面添加对象。第二次后,就不需要再添加了
            dep.add(Dep.global);
          }
          -----------------------
          return value;
        },
        set:function(newValue){
          if(newValue === value){
            return;
          }
          value = newValue;
          dep.notify();//触发了update()方法
        }
      })
    }

    这里有一点需要注意:

    在上述圈起来的地方:if(Dep.global)是在第一次new Watcher()的时候,进入update()方法,触发这里的get方法。这里非常的重要的一点!在此时new Watcher()只走到了this.update();方法,此刻没有触发Dep.global = null函数,所以值并没有清空,所以可以进到dep.add(Dep.global);方法里面去。

    而第二次后,由于清空了Dep的全局变量,所以不会触发add()方法。

    紧接着在text节点和绑定了的input节点(别忘记了这个节点)new Watcher的方法来触发以上的内容:

    // 如果节点为input
        if(node.nodeType === 1){ 
            ...........
            ----------
            new Watcher(vm,node,name) // 别忘记给input添加观察者模式
            ----------
    
        }
    //如果节点类型为text
      if(node.nodeType === 3){
        
        if(reg.test(node.nodeValue)){
          // console.dir(node);
          var name = RegExp.$1;//获取匹配到的字符串
          name = name.trim();
          // node.nodeValue = vm[name];
          -------------------------
          new Watcher(vm,node,name);//这里到了一个新的节点,new一个新的观察者
          -------------------------
        }
      }

    至此,vue双向绑定已经简单的实现。

    以下全部原理demo代码仅供参考:

    html:

    <div id="mvvm">
        <input v-model="text" id="test"></input>{{text}}
        <div id="test1">{{text}}</div>
    </div>

    js:

    // 定义发布者
    function Dep() {
        this.subs = [];
    }
    Dep.prototype = {
        add: function (sub) {
            //这里定义增加订阅者的方法
            this.subs.push(sub);
        },
        notify: function () {
            //这里定义触发订阅者update()的通知方法
            this.subs.forEach(function (sub) {
                sub.update(); //下列发布者的更新方法
            });
        },
    };
    // 定义订阅者
    function Watcher(vm, node, name) {
        Dep.global = this; //这里很重要!把自己赋值给Dep函数对象的全局变量
        this.name = name;
        this.node = node;
        this.vm = vm;
        this.update();
        Dep.global = null; //这里update()完记得清空Dep函数对象的全局变量
    }
    Watcher.prototype.update = function () {
        this.get();
        switch (
            this.node.nodeType //这里去通过判断节点的类型改变视图的值
        ) {
            case 1:
                this.node.value = this.value;
                break;
            case 3:
                this.node.nodeValue = this.value;
                break;
            default:
                break;
        }
    };
    Watcher.prototype.get = function () {
        this.value = this.vm[this.name]; //这里把this的value值赋值,触发data的defineProperty方法中的get方法!
    };
    function defineReactive(obj, key, value) {
        var dep = new Dep(); //这里每一个vm的data属性值声明一个新的订阅者
        Object.defineProperty(obj, key, {
            get: function () {
                // console.log("get了值"+value);
                if (Dep.global) {
                    //这里是第一次new对象Watcher的时候,初始化数据的时候,往订阅者对象里面添加对象。第二次后,就不需要再添加了
                    dep.add(Dep.global);
                }
                return value; //获取到了值
            },
            set: function (newValue) {
                if (newValue === value) {
                    return; //如果值没变化,不用触发新值改变
                }
                value = newValue; //改变了值
                // console.log("set了最新值"+value);
                dep.notify(); //触发了update()方法
            },
        });
    }
    function observe(obj, vm) {
        Object.keys(obj).forEach(function (key) {
            defineReactive(vm, key, obj[key]);
        });
    }
    function nodeContainer(node, vm, flag) {
        var flag = flag || document.createDocumentFragment();
        var child;
        while ((child = node.firstChild)) {
            compile(child, vm);
            flag.appendChild(child);
            if (child.firstChild) {
                // flag.appendChild(nodeContainer(child,vm));
                nodeContainer(child, vm, flag);
            }
        }
        return flag;
    }
    /* 
        1.while(child = node.firstChild)把node的firstChild赋值成while的条件,可以看做是遍历所有的dom节点。一旦遍历到底了,node的firstChild就会未定义成undefined就跳出while。
        2.document.createDocumentFragment();是一个虚拟节点的容器树,可以存放我们的虚拟节点。
        3.上面的函数是个迭代,一直循环到节点的终点为止。 
    */
    //编译
    function compile(node, vm) {
        var reg = /{{(.*)}}/g; //匹配双绑的双大括号
        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;
                    //这里新添加的监听
                    node.addEventListener("input", function (e) {
                        vm[name] = e.target.value; //改变实例里面的值
                    });
                    node.value = vm.data[name]; //讲实例中的data数据赋值给节点
                    //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.data[name];
                new Watcher(vm,node,name);//这里到了一个新的节点,new一个新的观察者
            }
        }
    }
    function Vue(options) {
        this.data = options.data;
        var data = this.data;
        observe(data, this); //这里调用定义响应式方法
        var id = options.el;
        var dom = nodeContainer(document.getElementById(id), this);
        document.getElementById(id).appendChild(dom);
    }
    //随后使用他
    var Demo = new Vue({
        el: "mvvm",
        data: {
            text: "HelloWorld",
            d: "123",
        },
    });
    View Code

    三.总结

    从上可以看出,大概的过程是这样的:

    1. 定义Vue对象,声明vue的data里面的属性值,准备初始化触发observe方法。
    2. 在Observe定义过响应式方法Object.defineProperty()的属性,在初始化的时候,通过Watcher对象进行addDep的操作。即每定义一个vue的data的属性值,就添加到一个Watcher对象到订阅者里面去。
    3. 每当形成一个Watcher对象的时候,去定义它的响应式。即Object.defineProperty()定义。这就导致了一个Observe里面的getter&setter方法与订阅者形成一种依赖关系。
    4. 由于依赖关系的存在,每当数据的变化后,会导致setter方法,从而触发notify通知方法,通知订阅者我的数据改变了,你需要更新。
    5. 订阅者会触发内部的update方法,从而改变vm实例的值,以及每个Watcher里面对应node的nodeValue,即视图上面显示的值。
    6. Watcher里面接收到了消息后,会触发改变对应对象里面的node的视图的value值,而改变视图上面的值。
    7. 至此,视图的值改变了。形成了双向绑定MVVM的效果。

    参考:https://segmentfault.com/a/1190000016434836?utm_source=sf-similar-article

    说明:ES6版本Vue原理demo代码:

    younghxp/MyVue (github.com)

  • 相关阅读:
    今天开始用 VSU 2010
    Visual Studio 2010 模型设计工具 基本应用
    Asp.Net访问Oracle 数据库 执行SQL语句和调用存储过程
    Enterprise Library 4.1 Security Block 快速使用图文笔记
    解决“System.Data.OracleClient 需要 Oracle 客户端软件 8.1.7 或更高版本。”(图)
    一个Oracle存储过程示例
    Enterprise Library 4.1 Application Settings 快速使用图文笔记
    Oracle 10g for Windows 简体中文版的安装过程
    Oracle 11g for Windows 简体中文版的安装过程
    Oracle 9i 数据库 创建数据库 Net 配置 创建表 SQL查询 创建存储过程 (图)
  • 原文地址:https://www.cnblogs.com/younghxp/p/15005546.html
Copyright © 2011-2022 走看看