一点一点理解vue

178 阅读9分钟

Vue双向数据绑定(渣渣版)

1. 双向数据绑定的基本过程

双向数据绑定一般来说,常见的我们可以知道是使用object.defineProperty结合发布订阅者模式来实现的。理解双向数据绑定原理的难点一般是在于这个发布订阅者模式,对于对象属性特性的拦截器一般来说不会有多大的理解困难。只是在vue3.0版本中针对属性拦截器的一些缺点比如数组的监控不足,是使用了proxy代理来进行拦截。这里不展开讲,只是围绕vue讲一下我自己对双向数据绑定过程的理解。
首先先看一下双向数据绑定的使用,如下所示:

    <div id="app">  
        <h2>{{title}}</h2>  
        <input v-model="name">  
        <h1>{{name}}</h1>  
        <button v-on:click="clickMe">click me!</button>  
    </div>  
  1. 文本类型的单向绑定,这里的{{title}}、{{name}}属于文本类的绑定,这是一种单向的数据绑定,即数据改变会更新到视图,但是视图无法执行更新操作。
  2. input中的v-model绑定,这个绑定是经常在开发中使用的用户表单控件,最典型的双向绑定的模式:
    1. 视图到数据的绑定:在input元素对象上加入了DOM2级监听事件addEventListener, 然后当用户在视图层更改数据时就会触发input事件(在用户输入时触发):
    2. 数据到视图的绑定:
      (1)通过 Object.defineProperty(obj, name, handler配置对象) 来对vue实例中的data属性特性进行监听,一旦出现更改就会触发setter钩子函数(触发钩子函数的作用就是为了达到监听的作用)
      (2)钩子函数的触发达到了监听的作用,但是监听的目的是为了告知使用到这个data.name属性的DOM节点去更新对应的视图,如何告知就使用一个watcher来告知双向数据绑定的比较难理解的点就是这个抽象的watcher。可以这么理解一个使用data.name数据的DOM节点就需要一个watcher观察者,这个观察者起到一个连接的作用:1. 连接DOM节点:这个观察者Wacher里面有一个update函数,函数内部执行callback--更新视图函数(获取DOM节点,更改input节点的value值)。2.连接监听器Object.defineProperty(),通过监听器的钩子函数内部触发这个watcher的update函数,执行内部的视图渲染。
 node.addEventListener('input', function(e) {
  // 这里双向数据绑定中从视图到数据主要是通过事件的触发来执行的
  // (2)从视图到数据的更新---
  //  1.通过对input标签进行事件的监听来执行,主要是通过新输入的值然后替换为vue实例中的data属性中值
  //  注:当替换了data属性值的同时又触发了监听器observer,这个时候又会执行一次数据到视图的更新,这里
  //虽然这个input对象已经在视图上进行了更改,但是其他绑定这个data属性的试图把并没有完成更新,因此每一次执行
  //视图到属性的更新的时候都必然会触发一次数据到视图的更新过程!!!
          var newValue = e.target.value;
          if (val === newValue) {
              return;
          }
          self.vm[exp] = newValue;
          val = newValue;
      });
  1. 事件的监听绑定,这里与上面不同的是这里绑定的不是data属性而是methods属性
<script type="text/javascript">
     new SelfVue({
        el: '#app',
        data: {
            title: 'hello world',
            name: '11'
        },
        methods: {
            clickMe: function () {
                this.title = 'hello world';
            }
        },
        mounted: function () {
            window.setTimeout(() => {
                this.title = '你好';
            }, 1000);
        }
    });
</script>

2. 双向数据绑定的基本组成

1. 对象data的属性监听器--Observer

function Observer(data) {
    this.data = data;
    this.walk(data);
}

Observer.prototype = {
    walk: function(data) {
        var self = this;
        Object.keys(data).forEach(function(key) {
            self.defineReactive(data, key, data[key]);
        });
    },
    defineReactive: function(data, key, val) {
        var dep = new Dep();
        var childObj = observe(val);
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function getter () {
                if (Dep.target) {
                    dep.addSub(Dep.target);
                }
                return val;
            },
            set: function setter (newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                dep.notify();
            }
        });
    }
};

function observe(value, vm) {
    if (!value || typeof value !== 'object') {
        return;
    }
    return new Observer(value);
};

function Dep () {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
Dep.target = null;

2. 订阅通知器--Watcher

function Watcher(vm, exp, cb) {//这里vm为vue实例,exp为data中的属性名,cb为对应的获取dom节点更改视图值的函数
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    this.value = this.get();  // 将自己添加到订阅器的操作
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this;  // 缓存自己
        var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null;  // 释放自己,因为每一个节点都要使用watcehr,所以每次使用完以后需要释放自己
        return value;
    }
};

3. 视图渲染器--Compiler

function Compile(el, vm) {
    this.vm = vm;
    this.el = document.querySelector(el); //querySelector(el) 指匹配指定选择器的第一个元素,此时el为元素节点。如果你需要返回所有的元素,使用 querySelectorAll() 方法替代。
    this.fragment = null;
    this.init();
}

Compile.prototype = {
    init: function () { //首先判断挂载点是否存在,如果存在则建立文档节点
        if (this.el) {
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        } else {
            console.log('Dom元素不存在');
        }
    },
    nodeToFragment: function (el) { 
    //遍历这个挂载点看看这个挂载点有多少子节点,都放在文档片段中
        var fragment = document.createDocumentFragment(); // 创建一个新的空白的文档片段
        var child = el.firstChild;
        while (child) {
            // 将Dom元素移入fragment中
            fragment.appendChild(child);
            child = el.firstChild
        }
        return fragment;
    },
    compileElement: function (el) {
        // 1.获取文档节点的所有子节点,得到的是一个类数组对象
        // 2.然后对这个childNode类数组变换为数组然后进行遍历检查各个子节点的节点类型nodeType进行不同的渲染函数
        // 3.进行渲染:
        //    3.1如果nodeType值为1:表示该子节点为元素节点(元素节点例如<div class>),重点掌握对属性的解析
        var childNodes = el.childNodes;
        var self = this;
        [].slice.call(childNodes).forEach(function(node) {
            var reg = /\{\{(.*)\}\}/;
            var text = node.textContent;
        // 如果是节点对象类型那么就可能涉及双向数据绑定中的两个方向,即双向数据绑定的全部过程包括监听器、观察通知器以及compile渲染器
        // 如果是{{}}类型那么就只有数据到视图的更新,此时建立对应的new Watcher然后通过watcher触发updateText函数来更新视图
            if (self.isElementNode(node)) {  
                self.compile(node);
            } else if (self.isTextNode(node) && reg.test(text)) {
                self.compileText(node, reg.exec(text)[1]);
            }

            if (node.childNodes && node.childNodes.length) {
                self.compileElement(node);
            }
        });
    },
    compile: function(node) {
        var nodeAttrs = node.attributes; // 重点掌握对属性的解析
        // 1.返回当前节点的所有属性节点组成一个类数组对象
        // 2.然后进行遍历,遍历的每一项为属性节点对象(具体参见属性节点对象的相关方法: name、value)
        // 3.根据每个属性节点对象对应的属性名称attrName(包括自定义的属性也在里面,因此可以解析到v-model)
        // 4.然后根据属性节点名称判断是不是vue的指令v-,如果是进入vue属性的相关操作:
        //   4.1 通过属性名name来截取第二个以后的字符串获取真正的属性名actual_attribute,然后在通过value获取属性值
        //   4.2 将真正的属性名按照类型的不同进行分类:时间类on或者model绑定类等,如果为双向绑定命令属性model则进入下一步
        //      4.2.1 解析model属性compileModel(node节点对象, self.vm-vue实例, exp属性值, dir真正的属性名)
        var self = this;
        Array.prototype.forEach.call(nodeAttrs, function(attr) {
            var attrName = attr.name;
            if (self.isDirective(attrName)) {
                var exp = attr.value;
                var dir = attrName.substring(2);
                if (self.isEventDirective(dir)) {  // 事件指令
                    self.compileEvent(node, self.vm, exp, dir);
                } else {  // v-model 指令
                    self.compileModel(node, self.vm, exp, dir);
                    //传入拥有v-model属性的节点对象,vue实例对象,v-model的属性值exp,
                }
                node.removeAttribute(attrName);
            }
        });
    },
    compileText: function(node, exp) {
        var self = this;
        var initText = this.vm[exp];
        this.updateText(node, initText);
        new Watcher(this.vm, exp, function (value) {
            self.updateText(node, value);
        });
    },
    compileEvent: function (node, vm, exp, dir) {
        var eventType = dir.split(':')[1];
        var cb = vm.methods && vm.methods[exp];

        if (eventType && cb) {
            node.addEventListener(eventType, cb.bind(vm), false);
        }
    },
    compileModel: function (node, vm, exp, dir) {
        //(属性节点对象,vue实例,属性名,属性值)
        // 开始对
        var self = this;
        var val = this.vm[exp];//v-mode的属性值message对应的是在vue实例中的data对象的属性名message,然后获取真正的属性值
        this.modelUpdater(node, val);
        //这样就可以通过v-mode的属性值对应vue中data属性名的属性值,然后将
        new Watcher(this.vm, exp, function (value) {
            self.modelUpdater(node, value);
            //在input元素中通过js操作动态的修改value值
            //这里新建了一个watcher来负责监视这个属性,如果data中这个属性值变化那么就会触发
            //watcher中的更新函数,更新函数就会将新的值传入到这个回调中,通过更改input的value来更新视图
            //到这里就已经是实现了双向数据绑定中的数据到视图的更新

            //小结:简单说一下
            // (1) 数据到视图的更新---
            //     1.首先建立监听器observer,作用是如果更新了data的数据就可以通知watcher来执行更新函数。
            //     2.然后建立观察通知器watcher,作用是监听器拦截到更改从操作之后,通过遍历Dep中的watcher更新函数
            //来执行视图更新函数,在这里每一个watcher的回调函数中都会自动对应更新自己的节点node。
            //     3.构建渲染解析器compiler,因为观察者之所以能观察到对应的与data属性绑定的节点,主要是通过解析器
            //解析出v-model属性,然后进入对应节点对象v-model属性的渲染器(在对应的渲染函数中进行new watcher的创建)
            //这样在执行渲染时才能传入对应的节点对象input,执行渲染函数node.value=newValue进而更新视图中input输入框的值
            // 注:根据模板解析中的节点中属性是否为v-model来确定是否要建立对应的new Watcher
        });
        node.addEventListener('input', function(e) {
            // 这里双向数据绑定中从视图到数据主要是通过事件的触发来执行的
            // (2)从视图到数据的更新---
            //     1.通过对input标签进行事件的监听来执行,主要是通过新输入的值然后替换为vue实例中的data属性中值
            //    注:当替换了data属性值的同时又触发了监听器observer,这个时候又会执行一次数据到视图的更新,这里
            //虽然这个input对象已经在视图上进行了更改,但是其他绑定这个data属性的试图把并没有完成更新,因此每一次执行
            //视图到属性的更新的时候都必然会触发一次数据到视图的更新过程!!!
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }
            self.vm[exp] = newValue;
            val = newValue;
        });
    },
    updateText: function (node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },
    modelUpdater: function(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    },
    isDirective: function(attr) {
        return attr.indexOf('v-') == 0;
    },
    isEventDirective: function(dir) {
        return dir.indexOf('on:') === 0;
    },
    isElementNode: function (node) {
        return node.nodeType == 1;
    },
    isTextNode: function(node) {
        return node.nodeType == 3;
    }
}

4. vue构造函数

function SelfVue (options) {
    var self = this;
    this.data = options.data;
    this.methods = options.methods;

    Object.keys(this.data).forEach(function(key) {
        self.proxyKeys(key);
    });

    observe(this.data);
    new Compile(options.el, this);
    options.mounted.call(this); // 所有事情处理好后执行mounted函数
}

SelfVue.prototype = {
    proxyKeys: function (key) {
        var self = this;
        Object.defineProperty(this, key, {
            enumerable: false,
            configurable: true,
            get: function getter () {
                return self.data[key];
            },
            set: function setter (newVal) {
                self.data[key] = newVal;
            }
        });
    }
}