• 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)

  • 相关阅读:
    (转)Silverlight从程序集读取xaml文件
    阻止jQuery事件冒泡
    如何避免JQuery Dialog的内存泄露(转)
    VS2010 好用的javascript扩展工具
    C#计时器
    Silverlight初体验之设置启动页面
    javascript调用asp.net后置代码方法
    应用程序工程文件组织重要性
    javascript中字符串转化为json对象
    jQuery EasyUI
  • 原文地址:https://www.cnblogs.com/younghxp/p/15005546.html
Copyright © 2020-2023  润新知