vue源码探究(第四弹)

作者: zhangjingbibibi | 来源:发表于2018-11-22 19:33 被阅读2次

    vue源码探究(第四弹)

    结束了上一part的数据代理,这一部分主要讲讲vue的模板解析,感觉这个有点难理解,而且内容有点多,hhh。

    模板解析

    废话不多说,先从简单的入手。

    按照之前的套路,先举一个例子🌰:

    <div id="test">
      <p>{{name}}</p>
    </div>
    <script type="text/javascript" src="js/mvvm/compile.js"></script>
    <script type="text/javascript" src="js/mvvm/mvvm.js"></script>
    <script type="text/javascript" src="js/mvvm/observer.js"></script>
    <script type="text/javascript" src="js/mvvm/watcher.js"></script>
    <script type="text/javascript">
      new MVVM({
        el: '#test',
        data: {
          name: '喵喵喵'
        }
      })
      // 这时候,我们的页面还是渲染出 喵喵喵
    </script>
    

    接下来讲讲内部的相关实现:

    我们的MVVM中的构造函数中有什么东西,可以解析我们的模板呢?

    // 创建一个用来编译模板的compile对象
    this.$compile = new Compile(options.el || document.body, this)
    

    什么是Compile?

    一行一行注释着解读

    function Compile(el, vm) {
      // 保存vm
      this.$vm = vm;
      // 保存el元素
      this.$el = this.isElementNode(el) ? el : document.querySelector(el);
      // 如果el元素存在
      if (this.$el) {
        // 1. 取出el中所有子节点, 封装在一个framgment对象中
        // 这里的node2Fragment 就是将node -> 放入 Fragment中,documentFragment将node进行批量处理
        this.$fragment = this.node2Fragment(this.$el);
        // 2. 编译fragment中所有层次子节点
        this.init();
        // 3. 将fragment添加到el中
        this.$el.appendChild(this.$fragment);
      }
    }
    
    Compile.prototype = {
      node2Fragment: function (el) {
        var fragment = document.createDocumentFragment(),
          child;
    
        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
          fragment.appendChild(child);
        }
    
        return fragment;
      },
    
      init: function () {
        // 编译fragment
        this.compileElement(this.$fragment);
      },
    
      compileElement: function (el) {
        // 得到所有子节点
        var childNodes = el.childNodes,
          // 保存compile对象
          me = this;
        // 遍历所有子节点
        [].slice.call(childNodes).forEach(function (node) {
          // 得到节点的文本内容
          var text = node.textContent;
          // 正则对象(匹配大括号表达式)
          var reg = /\{\{(.*)\}\}/;  // {{name}}
          // 这里提出一个问题,为什么这里的正则匹配要用/\{\{(.*)\}\}/,而不是/\{\{.*\}\}/呢?
          // 其实/\{\{.*\}\}/就可以匹配到{{xxx}},这里加一个()的意义是,用于.$1,来取得{{}}中的值,eg:name
          // 如果是元素节点
          if (me.isElementNode(node)) {
            // 编译元素节点的指令属性
            me.compile(node);
            // 如果是一个大括号表达式格式的文本节点
          } else if (me.isTextNode(node) && reg.test(text)) {
            // 编译大括号表达式格式的文本节点
            me.compileText(node, RegExp.$1); // RegExp.$1: 表达式   name
          }
          // 如果子节点还有子节点
          if (node.childNodes && node.childNodes.length) {
            // 递归调用实现所有层次节点的编译
            me.compileElement(node);
          }
        });
      },
    
      compile: function (node) {
        // 得到所有标签属性节点
        var nodeAttrs = node.attributes,
          me = this;
        // 遍历所有属性
        [].slice.call(nodeAttrs).forEach(function (attr) {
          // 得到属性名: v-on:click
          var attrName = attr.name;
          // 判断是否是指令属性
          if (me.isDirective(attrName)) {
            // 得到表达式(属性值): test
            var exp = attr.value;
            // 得到指令名: on:click
            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) {
        // 调用编译工具对象解析
        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 = {
      // 解析: v-text/{{}}
      text: function (node, vm, exp) {
        this.bind(node, vm, exp, 'text');
      },
      // 解析: v-html
      html: function (node, vm, exp) {
        this.bind(node, vm, exp, 'html');
      },
    
      // 解析: v-model
      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;
        });
      },
    
      // 解析: v-class
      class: function (node, vm, exp) {
        this.bind(node, vm, exp, 'class');
      },
    
      // 真正用于解析指令的方法
      bind: function (node, vm, exp, dir) {
        /*实现初始化显示*/
        // 根据指令名(text)得到对应的更新节点函数
        // 取到一个object的属性,有2个方法,一个是obj. 一个是obj[]
        // 当我们要取得属性是一个变量的时候,使用obj[]
        var updaterFn = updater[dir + 'Updater'];
        // 如果存在调用来更新节点
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));
    
        // 创建表达式对应的watcher对象
        new Watcher(vm, exp, function (value, oldValue) {/*更新界面*/
          // 当对应的属性值发生了变化时, 自动调用, 更新对应的节点
          updaterFn && updaterFn(node, value, oldValue);
        });
      },
    
      // 事件处理
      eventHandler: function (node, vm, exp, dir) {
        // 得到事件名/类型: click
        var eventType = dir.split(':')[1],
          // 根据表达式得到事件处理函数(从methods中): test(){}
          fn = vm.$options.methods && vm.$options.methods[exp];
        // 如果都存在
        if (eventType && fn) {
          // 绑定指定事件名和回调函数的DOM事件监听, 将回调函数中的this强制绑定为vm
          node.addEventListener(eventType, fn.bind(vm), false);
        }
      },
    
      // 得到表达式对应的value
      _getVMVal: function (vm, exp) {
        // 这里为什么要forEach呢?
        // 如果你的exp是a.b.c.c.d呢 就需要forEach 如果只是一层 当然不需要遍历啦
        var val = vm._data;
        exp = exp.split('.');
        exp.forEach(function (k) {
          val = val[k];
        });
        return val;
      },
    
      _setVMVal: function (vm, exp, value) {
        var val = vm._data;
        exp = exp.split('.');
        exp.forEach(function (k, i) {
          // 非最后一个key,更新val的值
          if (i < exp.length - 1) {
            val = val[k];
          } else {
            val[k] = value;
          }
        });
      }
    };
    
    // 包含多个用于更新节点方法的对象
    var updater = {
      // 更新节点的textContent
      textUpdater: function (node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
      },
    
      // 更新节点的innerHTML
      htmlUpdater: function (node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
      },
    
      // 更新节点的className
      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;
      },
    
      // 更新节点的value
      modelUpdater: function (node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
      }
    };
    
    

    最后

    未完待续...
    接下来,还有一个更有趣的东西

    下一章继续~

    相关文章

      网友评论

        本文标题:vue源码探究(第四弹)

        本文链接:https://www.haomeiwen.com/subject/xgrrqqtx.html