我爱编程

关于 Vue 的双向绑定和虚拟 DOM

2018-05-28  本文已影响0人  _月光临海

参考链接 《剖析Vue原理&实现双向绑定MVVM》

做了进一步的解释,代码读着费劲的话可以先看下每个文件最上的关于整个文件的内容概括。当然,代码才是最好的注释。


整体流程概括

实例化 MVVM 的过程中做了以下三件事:

1. 传入一个 options 配置对象,类似这种,MVVM 的参数就是:

  var vm = new MVVM({
      el: '#mvvm-app',
      data: {
          someStr: 'hello ',
          className: 'btn',
          htmlStr: '<span style="color: #f00;">red</span>',
          child: {
              someStr: 'World !'
          }
      },

      computed: {
          getHelloWord: function() {
              return this.someStr + this.child.someStr;
          }
      },

      methods: {
          clickBtn: function(e) {
              var randomStrArr = ['childOne', 'childTwo', 'childThree'];
              this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
          }
      }
  });

2. 实例化 Observer ,用于重写 options.data 所有属性的访问器属性,即 get 和 set 方法,重写的目的是:
a. 通过重写的 get 实现:哪里读取数据,就把哪里当做一个 Watcher,并将它存到 dep.subs
b. 通过重写的 set 实现:修改 data 数据时调用 notice() 通知所有观察者进行更新 update()
3. 实例化 Compile , 用于编译 options.el ,效果是:
a. 找出 options.el 中引用到 options.data 的部分,每找到一处就实例化一个观察者 Watcher,而 Watcher 本身定义有一个 update() 方法
b. 初始化渲染,初始化时会读取 options.data 的数据用于渲染视图,也就将所有 Watcher 存入了 dep.subs


html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MVVM</title>
</head>
<body>

<div id="mvvm-app">
    <input type="text" v-model="someStr">
    <input type="text" v-model="child.someStr">
    <!-- <p v-class="className" class="abc">
        {{someStr}}
        <span v-text="child.someStr"></span>
    </p> -->
    <p>{{getHelloWord}}</p>
    <p v-html="htmlStr"></p>
    <button v-on:click="clickBtn">change model</button>
</div>

<!--<script src="http://cdn.bootcss.com/vue/1.0.25/vue.js"></script>-->
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
    var vm = new MVVM({
        el: '#mvvm-app',
        data: {
            someStr: 'hello ',
            className: 'btn',
            htmlStr: '<span style="color: #f00;">red</span>',
            child: {
                someStr: 'World !'
            }
        },

        computed: {
            getHelloWord: function() {
                return this.someStr + this.child.someStr;
            }
        },

        methods: {
            clickBtn: function(e) {
                var randomStrArr = ['childOne', 'childTwo', 'childThree'];
                this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
            }
        }
    });

    vm.$watch('child.someStr', function() {

    });
</script>

</body>
</html>

mvvm.js

/*
    实例化操作:
        通过 observe() 传入 MVVM 实例对象的 data 和 MVVM 实例本身,返回 new Observer(data)
        实例化 Compile
        
    通俗的讲:
        实例化 MVVM 的过程中做了以下几件事:
            传入一个 options 配置对象
            实例化 Observer ,用于重写 options.data 所有属性,重写的目的是:
                哪里读取数据,就把哪里当做一个 Watcher,将它存到 dep.subs 中
                修改 data 数据时通过 notice() 通知所有观察者进行更新 update()
            实例化 Compile , 用于编译 options.el ,效果是:
                找出 options.el 中引用到 options.data 的部分,每找到一处就实例化一个观察者 Watcher,而 Watcher 本身定义有一个 update() 方法
                初始化渲染,初始化时会读取 options.data 的数据用于渲染视图,也就将所有 Watcher 存入了 dep.subs
*/


function MVVM(options) {
    this.$options = options || {};
    var data = this._data = this.$options.data;
    var me = this;

    // 数据代理
    // 实现 vm.xxx -> vm._data.xxx
    Object.keys(data).forEach(function(key) {
        me._proxyData(key);
    });

    this._initComputed();
    
    observe(data, this);
    
    this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
    $watch: function(key, cb, options) {
        new Watcher(this, key, cb);
    },

    _proxyData: function(key, setter, getter) {
        var me = this;
        setter = setter || 
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];
            },
            set: function proxySetter(newVal) {
                me._data[key] = newVal;
            }
        });
    },

    _initComputed: function() {
        var me = this;
        var computed = this.$options.computed;
        if (typeof computed === 'object') {
            Object.keys(computed).forEach(function(key) {
                Object.defineProperty(me, key, {
                    get: typeof computed[key] === 'function' 
                            ? computed[key] 
                            : computed[key].get,
                    set: function() {}
                });
            });
        }
    }
};

observer.js

/*
    实例化操作:
        将 MVVM 的 data 属性传进 Observer
        通过 walk 方法遍历 data 对象的所有属性
        使用 Object.defineProperty 重写 data 对象所有属性的 get 和 set 方法
            get 方法中判断 Dep.target 是否存有 Watcher 的实例,如果有就存入 Dep 实例的属性 subs 数组中
            set 方法通过调用 dep.notify() 方法去触发所有存入 subs 中观察者上的 update 方法
                Watcher 上定义的 update 方法用于更新视图数据
*/

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

Observer.prototype = {
    walk: function(data) {
        var me = this;
        Object.keys(data).forEach(function(key) {
            me.convert(key, data[key]);
        });
    },
    convert: function(key, val) {
        this.defineReactive(this.data, key, val);
    },

    defineReactive: function(data, key, val) {
        var dep = new Dep();
        var childObj = observe(val);

        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            get: function() {
                if (Dep.target) {
                    dep.depend();
                }
                return val;
            },
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // 新的值是object的话,进行监听
                childObj = observe(newVal);
                // 通知订阅者
                dep.notify();
            }
        });
    }
};

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

    return new Observer(value);
};


var uid = 0;

function Dep() {
    this.id = uid++;
    this.subs = [];
}

Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },

    depend: function() {
        Dep.target.addDep(this);
    },

    removeSub: function(sub) {
        var index = this.subs.indexOf(sub);
        if (index != -1) {
            this.subs.splice(index, 1);
        }
    },

    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

Dep.target = null;

compile.js

/*
    实例化操作:
    获取构造函数 MVVM 上传入的配置对象 options 中 el 挂载的元素 #mvvm-app ,将该元素下的所有节点存到 Compile 实例的 $fragment 上
    对 Compile 实例进行初始化,即编译上述的文档片段
    初始化:
        遍历文档片段的节点,根据节点类型进行判断
        如果是文本节点并且是 {{ xxx }} 格式,则取到 xxx 的值,然后调用 bind 方法,下面会说 bind 方法做了什么
        如果是元素节点,则取出节点上所有的属性,找到 v- 开头的指令,通过匹配 on 区分事件指令和普通指令
            如果是事件指令,比如 button 上的 v-on:click = "clickBtn" :
                则在 MVVM 传入的配置对象 options 中找到 methods 属性上对应的 clickBtn 方法
                通过 addEventListener 将 click 事件,结合 clickBtn 方法绑定到 button 上
            如果是普通指令,比如 v-html = "htmlStr"。注意,data 的 htmlStr 属性值为 '<span style="color: #f00;">red</span>':
                则调用 compileUtil 对象上的 html 方法,而 compileUtil 上的每个方法都调用 compileUntil 自身定义的 bind 方法,
                bind 方法做了两件事:
                    调用 updater 对象上对应的 htmlUpdater 方法并传入该元素节点和 htmlStr 的值
                        htmlUpdater 方法将元素节点的 innerHTML = htmlStr
                    实例化 Watcher,传入 MVVM 实例,htmlStr,数据更新时的 callback。(注意:通过 new 的实例化操作会调用一次 Watcher 方法)
            
    将编译过后的文档片段插入 #mvvm-app
*/


function Compile(el, vm) {
/*  
    el: #mvvm-app
    vm: MVVM 实例
*/  
    this.$vm = vm;
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}

Compile.prototype = {
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(),
            child;
        
        // 将原生节点拷贝到fragment (拷一个少一个,所以一直都是 firstChild)
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }

        return fragment;
    },

    init: function() {
        this.compileElement(this.$fragment);
    },

    compileElement: function(el) {
        var childNodes = el.childNodes,
            me = this;

        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;

            if (me.isElementNode(node)) {
                me.compile(node);

            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }

            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    compile: function(node) {
        var nodeAttrs = node.attributes,
            me = this;

        [].slice.call(nodeAttrs).forEach(function(attr) {
        /*  attr:   
            type = "text"
            v-model = "someStr"
            v-model = "child.someStr"
            v-html = "htmlStr"
            style = "color:#f00"
            v-on:click = "clickBtn"
        */  
            var attrName = attr.name;           //  type , v-model , v-html , v-on:click ...
            if (me.isDirective(attrName)) {     //  如果是  v- 指令
                var exp = attr.value;               //  someStr , child.someStr , htmlStr , clickBtn
                var dir = attrName.substring(2);    //  model , html , on:click
                // 事件指令
                if (me.isEventDirective(dir)) {
                /*  
                    node:   <buttom>change model</bottom>
                    me.$vm: mvvm 实例
                    exp:    clickBtn
                    dir:    on:click
                */  
                    compileUtil.eventHandler(node, me.$vm, exp, dir);   
                // 普通指令
                } else {
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }

                node.removeAttribute(attrName);
            }
        });
    },

    compileText: function(node, exp) {
        compileUtil.text(node, this.$vm, exp);
    },

    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;
    }
};

// 指令处理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },

    html: function(node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },

    model: function(node, vm, exp) {
        this.bind(node, vm, exp, 'model');

        var me = this,
            val = this._getVMVal(vm, exp);
        node.addEventListener('input', function(e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }

            me._setVMVal(vm, exp, newValue);
            val = newValue;
        });
    },

    class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },

    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));
        
        new Watcher(vm, exp, function(value, oldValue) {
            updaterFn && updaterFn(node, value, oldValue);
        });
    },

    // 事件处理
    eventHandler: function(node, vm, exp, dir) {
    /*  
        node:   <buttom>change model</bottom>
        me.$vm: mvvm 实例
        exp:    clickBtn
        dir:    on:click
    */  
        var eventType = dir.split(':')[1],
            fn = vm.$options.methods && vm.$options.methods[exp];

        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false);
        }
    },

    _getVMVal: function(vm, exp) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function(k) {
            val = val[k];
        });
        
        return val;
    },

    _setVMVal: function(vm, exp, value) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function(k, i) {
            // 非最后一个key,更新val的值
            if (i < exp.length - 1) {
                val = val[k];
            } else {
                val[k] = value;
            }
        });
    }
};


var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },

    htmlUpdater: function(node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },

    classUpdater: function(node, value, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');

        var space = className && String(value) ? ' ' : '';

        node.className = className + space + value;
    },

    modelUpdater: function(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    }
};

watcher.js

/*
    实例化操作:
        调用 get() 方法,将自身的实例存入 Dep.target 
*/

function Watcher(vm, expOrFn, cb) {
    this.cb = cb;
    this.vm = vm;
    this.expOrFn = expOrFn;
    this.depIds = {};

    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
    } else {
        this.getter = this.parseGetter(expOrFn);
    }

    this.value = this.get();
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.get();
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    addDep: function(dep) {
        // 1. 每次调用run()的时候会触发相应属性的getter
        // getter里面会触发dep.depend(),继而触发这里的addDep
        // 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已
        // 则不需要将当前watcher添加到该属性的dep里
        // 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里
        // 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性
        // 则需要将当前watcher(child.name)加入到新的 child.name 的dep里
        // 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
        // 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
        // 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
        // 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
        // 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
        // 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
        // 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);
            this.depIds[dep.id] = dep;
        }
    },
    get: function() {
        Dep.target = this;
        var value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    },

    parseGetter: function(exp) {
        if (/[^\w.$]/.test(exp)) return;    //  如果这个表达式 不是字母,点,美元符 就返回 

        var exps = exp.split('.');

        return function(obj) {
            for (var i = 0, len = exps.length; i < len; i++) {
                if (!obj) return;
                obj = obj[exps[i]];
            }
            return obj;
        }
    }
};

虚拟 DOM

上一篇下一篇

猜你喜欢

热点阅读