MVVM的实现原理入门了解3-- 模板解析与watcher(订阅者)的创建

在MVVM.js文件中第三步执行的是

this.$compile = new Compile(options.el || document.body, this)

那么执行compile.js文件

function Compile(el, vm) {
    this.$vm = vm; // vm表示mvvm中的this 使其保存到Compile实例上
    // vm中有el属性  判断el是元素节点还是其他节点 如果是元素节点 返回自身 如果不是使用querySelector进行查找
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    // 判断this.$el
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el); //创建文档碎片 将el中的内容插入到documentFragment中
        this.init();// 进行模板解析
        this.$el.appendChild(this.$fragment); //将编译完毕的文档碎片内容 从新插入到this.$el所对应的DOM元素中
    }
}

Compile.prototype = {
    // 创建文档碎片节点
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(),
            child;
        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        //返回文档碎片节点
        return fragment;
    },
    //编译元素
    init: function() {
        this.compileElement(this.$fragment); //用于编译传入的在内存中的fragment元素
    },

    compileElement: function(el) {
        var childNodes = el.childNodes, //获取文档碎片的子节点
            me = this; //Compile实例
        //使文档碎片节点改为数组的形式 然后遍历数组
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent; // 使text都为node中的文本内容
            var reg = /\{\{(.*)\}\}/; // 定义正则表达式 用于匹配{{ data中的属性 }}

            if (me.isElementNode(node)) { //判断这个节点是否为元素节点
                me.compile(node); // 如果是元素节点 判断指令类型

            } else if (me.isTextNode(node) && reg.test(text)) { // 判断这个节点是否是文本节点 并且带有带括号的文本内容
                me.compileText(node, RegExp.$1.trim()); // 解析node中 正则表达式中的子表达式的内容 也就是 {{ 内容 }}
            }

            if (node.childNodes && node.childNodes.length) { //判断node是否有子节点并且子节点的个数不为0
                me.compileElement(node); //递归执行
            }
        });
    },
    //进行编译节点
    compile: function(node) {
        //获取节点的所用属性内容
        var nodeAttrs = node.attributes,
            me = this;
        // 将属性节点转为数组 并遍历
        [].slice.call(nodeAttrs).forEach(function(attr) {
            //用attrName 保存attr.name 属性名
            var attrName = attr.name;
            if (me.isDirective(attrName)) {
                var exp = attr.value; //data中的属性名
                var dir = attrName.substring(2);
                // 事件指令
                if (me.isEventDirective(dir)) {
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                    // 普通指令
                } else {
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }

                node.removeAttribute(attrName);
            }
        });
    },
    compileText: function(node, exp) {
        // 编译指令 使用工具库的text方法 进行解析 传入节点 vm 正则表达式
        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;
    }
};

// 编译指令处理集合
// 指令集合使用的都是bind绑定的
var compileUtil = {
    text: function(node, vm, exp) { // text指令
        // node vm实例 exp属性名  指令名
        this.bind(node, vm, exp, 'text'); //执行bind
    },

    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) {
//  updaterFn用于对{{}} 以及 各种指令的解析
        //updater为一个对象 调用 dir为指令名 结果为 var updaterFn = updater['textUpdater']
        //得到更新节点的函数
        var updaterFn = updater[dir + 'Updater']; //调用updater对象中的对应属性
        //updaterFn存在 并且 调用updaterFn 传入 node , this._getVMVal(vm,exp)的结果
        //调用函数更新节点
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));
        // 监视数据变化
//####################################################
//  watcher 用于更新显示视图
        // watcher中 传递参数 vm实例 exp指令表达式 回调函数新值与旧值
        new Watcher(vm, exp, function(value, oldValue) {
            // 回调函数 更新exp指令表达式中相对应的data属性时 节点发生改变
            updaterFn && updaterFn(node, value, oldValue);
        });
    },

    // 事件处理
    eventHandler: function(node, vm, exp, dir) {
        var eventType = dir.split(':')[1],
            fn = vm.$options.methods && vm.$options.methods[exp];

        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false);
        }
    },
    // 从vm中得到表达式所对应的值
    _getVMVal: function(vm, exp) {
        var val = vm; //val为 MVVM实例化后的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 = {
    //更新节点中的 TEXT
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },
    //更新节点中的 HTML
    htmlUpdater: function(node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },
    //更新节点的class
    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;
    },
    // model指令
    modelUpdater: function(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    }
};

模板解析的理解

模板解析分为三步进行

  1. 创建文档碎片节点 document.createDocumentFragment
  2. 将el对应的节点内的所有子节点appendChild(剪切操作)到documentFragment中进行处理
  3. 将处理好的documentFragment中所有的子节点重新appendChild到el对应的节点中

compile.js文件中对应的 是在Compile.prototype.compile为主要进行模板解析的方法 

  1. 取出所有文档碎片节点的内容 也就是文档碎片的子节点
  2. 将取出子节点的伪数组转为数组  使用[].slice.call(伪数组) 或者是 Array.from() 然后进行遍历
  3. 对每一个子节点进行操作 主要为解析 节点属性的v-指令、事件指令以及节点内容的{{}}表达式 获取指令名称(var dir = attrName.substring(2); 用于剪切on或者v-)   并调用compileUtil进行操作

compile的文件中还包括  compileUtil 这个对象 这个对象主要用于对指令,更新节点的内容、属性,事件指令等操作

  1. 对应v-指令 由于普通指令传递的参数都为(node, vm, exp)且都是调用bind方法进行的操作 因此普通指令的核心为bind方法
  2. 事件指令主要调用addEventListener进行操作、更新操作主要使用innerHTML、innerText、className等操作自行查看即可

对应普通指令中使用的bind方法 理解

  1. bind方法中 首先调用视图更新 也就是 updater对象中的对应方法
  2. 创建watcher并传递vm实例、data中的属性名(exp)、回调函数用于更新视图

Watcher的实例化并传参

    watcher.js文件中的内容

function Watcher(vm, expOrFn, cb) {
    this.cb = cb; //用于更新界面的回调
    this.vm = vm; //vm实例
    this.expOrFn = expOrFn; //对应的data中的属性值 表达式
    this.depIds = {};   //相关的dep的容器对象
    // 表达式的解析
    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
    } else {
        this.getter = this.parseGetter(expOrFn.trim());
    }

    this.value = this.get(); //返回data中的属性对应的value
}

Watcher.prototype = {
    // 数据更新时调用
    update: function() {
        this.run();
    },
    run: function() {
        // 让value等于 this.get
        var value = this.get(); //获取最新的值
        var oldVal = this.value;//获取旧值
        if (value !== oldVal) {// 如果新的值与旧的值不相等
            this.value = value; //让value为新的值
            // cb.call 是执行Watcher中的cb形参
            // 调用回调函数
            this.cb.call(this.vm, value, oldVal); //更新界面的回调函数
        }
    },
    addDep: function(dep) {
        //判断depId容器中是否该dep.id的属性名 如果没有进入 有不进入 也就是说判断watcher与dep的关系是否建立
        // 有为true 没有为false
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);  //调用dep的addSub方法 传入 watcher
            this.depIds[dep.id] = dep; //让depIds容器中的dep.id属性名的属性值为 dep
            // depIds:{ dep.id:dep }  此时也就是将 dep保存到了 watcher中
        }
    },
    //获取data中 对应的属性值 会导致observer中get调用
    get: function() {
        // 将dep.target = this 也就是说 将当前的watcher 赋值给 Dep.target
        Dep.target = this;
        // 获取值时会导致observer给data中的属性值添加的get方法执行
        var value = this.getter.call(this.vm, this.vm); //获取data中属性的值
        // 执行完毕后将 Dep.target清空·
        Dep.target = null;
        //返回value值
        return value;
    },
    //这个方法是解析传递的data中的属性值 并返回
    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;
        }
    }
};

由于compile.js文件中bind方法的调用导致了 Watcher的实例化

watcher方法的使用:

  1. 进行初始化 保存回调cb,vm实例,exp(data中对应的属性值),创建自身属性depIds对象,this.value = this.get();
  2. 因为this.value = this.get() 导致了Watcher.prototype.get方法被调用 
    parseGetter方法主要用于解析传递的data中的属性值 防止传递的为对象形式
  3. 由于get方法的调用导致dep与watcher产生一系列的关系
posted @ 2019-04-21 11:00  嘿!巴扎嘿  阅读(30)  评论(0)    收藏  举报