vue源码分析

vue源码分析

分析vue作为一个MVVM框架的基本实现原理

一. 数据代理

  1. 数据代理: 通过一个对象代理对另一个对象(在前一个对象内部)中属性的操作(读/写)
  2. vue 数据代理: 通过 vm 对象来代理 data 对象中所有属性的操作
// 相当于vue的构造函数
function MVVM(options) {
  //将配置对象保存到vm
  this.$options = options;
  //将data对象保存到vm和变量data中
  var data = (this._data = this.$options.data);
  //保存vm到变量me中
  var me = this;
  //遍历data中所有的属性
  Object.keys(data).forEach(function (key) {
    //key是data的某个属性名:name
    //对指定的属性实现代理
    me._proxy(key);
  });
}

MVVM.prototype = {
  //这个方法是在原型上面的,来实现数据代理
  //实现指定属性代理的方法
  _proxy: function (key) {
    //保存vm
    var me = this;
    //给vm添加指定属性名的属性
    Object.defineProperty(me, key, {
      configurable: false, //不能重新定义
      enumerable: true, //可以枚举遍历
      //当通过vm.xxx读取属性值时调用,从data中获取对应的属性值返回 代理读操作
      get: function proxyGetter() {
        return me._data[key];
      },
      //当通过vm.xxx=value时,value被保存到data中对应的属性上
      set: function proxySetter(newVal) {
        me._data[key] = newVal;
      },
    });
  },
};

第一个:尽量将当前断点执行掉,当碰到其他断点暂停
第二个:当步执行,执行下一条语句,一句句执行
第三个:进入函数内部执行
在这里插入图片描述

二. 模板解析

1.模板解析的基本流程

  • 将 el 的所有子节点取出, 添加到一个新建的文档 fragment 对象中
  • 对 fragment 中的所有层次子节点递归进行编译解析处理
    对大括号表达式文本节点进行解析
    对元素节点的指令属性进行解析
    事件指令解析 * 一般指令解析
  • 将解析后的 fragment 添加到 el 中显示

2.模板解析(三种):
2.1大括号表达式解析
(1) 根据正则对象得到匹配出的表达式字符串: 子匹配/RegExp.$1 name (取出name)
(2) 从 data 中取出表达式对应的属性值 (value)
(3) 将属性值设置为文本节点的 textContent(textContent)
2.2事件指令解析:
(1) 从指令名中取出事件名
(2) 根据指令的值(表达式)从 methods 中得到对应的事件处理函数对象
(3) 给当前元素节点绑定指定事件名和回调函数的 dom 事件监听
(4) 指令解析完后, 移除此指令属性
2.3一般指令解析:
(1) 得到指令名和指令值(表达式) text/html/class msg/myClass
(2) 从 data 中根据表达式得到对应的值
(3) 根据指令名确定需要操作元素节点的什么属性

  • v-text—textContent 属性
  • v-html—innerHTML 属性
  • v-class–className 属性

(4) 将得到的表达式的值设置到对应的属性上
(5) 移除元素的指令属性
compile.js

// 相当于vue的构造函数
function MVVM(options) {
  //创建了一个编译对象,用来编译解析模板
  this.$compile = new Compile(options.el || document.body, this);
}
function Compile(el, vm) {
  // 保存vm到compile对象
  this.$vm = vm;
  //将el对应的元素对象保存到compile对象中
  // 保存el元素,是一个dom元素,通过isElementNode寻找是否是一个元素节点,不是的话再去寻找
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  // 如果el元素存在
  if (this.$el) {
    //编译模板的整体三步
    //1. 取出el元素中所有子节点保存到一个fragment对象中(转移)
    this.$fragment = this.node2Fragment(this.$el);
    //2. 编译fragment中所有层次的节点(编译)
    this.init();
    //3. 将编译好的fragment添加到页面的el元素中(添加回去)
    this.$el.appendChild(this.$fragment);
  }
}

Compile.prototype = {
  //将节点转化为Fragment
  node2Fragment: function (el) {
    //1. 创建空的fragment
    var fragment = document.createDocumentFragment(),
      child;
    //2. 把页面el里面所有的子节点都转移到fragment中
    while ((child = el.firstChild)) {
      fragment.appendChild(child);
    }
    //3. 返回fragment
    return fragment;
  },

  init: function () {
    // 编译指定元素(所有层次的子节点)
    this.compileElement(this.$fragment);
  },
  //编译el里面的所有子节点
  compileElement: function (el) {
    // 取出最外层所有子节点
    var childNodes = el.childNodes,
      // 保存compile对象(this是compile实例)
      me = this;
    //遍历所有子节点(text/element),[].slice.call:将伪数组转化成数组
    [].slice.call(childNodes).forEach(function (node) {
      // 得到节点的文本内容
      var text = node.textContent;
      // 正则对象,来匹配大括号表达式
      var reg = /\{\{(.*)\}\}/; // {{name}}
      // 判断节点是否为元素节点
      if (me.isElementNode(node)) {
        // 编译元素节点(解析指令,查看是哪个指令的)
        me.compile(node);
      }
      // 判断节点是否为大括号表达式的文本节点
      else if (me.isTextNode(node) && reg.test(text)) {
        //编译大括号表达式文本节点,RegExp.$1是取出来的匹配的表达式name
        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)得到对应的更新节点函数(得到更新节点的函数,dir是指令名)
    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中得到表达式所对应的函数(事件回调函数)
      // 根据表达式得到事件处理函数(从methods中): test(){}
      fn = vm.$options.methods && vm.$options.methods[exp];
    // 如果都存在
    if (eventType && fn) {
      //给节点绑定指定事件名和回调函数(强制绑定this为vm)的DOM事件监听
      // 绑定指定事件名和回调函数的DOM事件监听, 将回调函数中的this强制绑定为vm
      node.addEventListener(eventType, fn.bind(vm), false);
    }
  },

  // 从vm中得到表达式所对应的值
  _getVMVal: function (vm, exp) {
    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 = {
  // 更新节点的textUpdater属性值
  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) {
    //静态class属性值
    var className = node.className;
    //将静态class属性值与动态class值进行合并后设置为新的className的属性值
    node.className = className + (className ? " " : "") + value;
  },

  // 更新节点的value属性值
  modelUpdater: function (node, value, oldValue) {
    node.value = typeof value == "undefined" ? "" : value;
  },
};

三. 数据绑定

1.数据绑定
一旦更新了 data 中的某个属性数据, 所有界面上直接使用或间接使用了此属性的节点都会更新
2.数据劫持
(1) 数据劫持是 vue 中用来实现数据绑定的一种技术
(2) 基本思想: 通过 defineProperty()来监视 data 中所有属性(任意层次)数据的变化, 一旦变化就去更新界面
在上面数据代理中是给vm的xxx添加get和set的方法,而这里是给data中的属性绑定get和set的方法,当我们使用this.xxx=999,去更改xxx的值,此时vm中set会去改变data中xxx的值
在这里插入图片描述
在这里插入图片描述

总结

MVVM分为两个部分:初始化和更新
初始化 会创建observer和complie,observer是给data中所有层次的属性都使用数据劫持的方法使用(object.definedProperty)添加get和set方法。同时给每一个属性创建一个Dep;
Compile中去解析指令或大括号表达式之后调用updater方法实现初始化视图。同时也给每一个表达式创建一个watcher,watcher会与dep建立关系,把watcher保存到dep的中。
更新阶段: 会触发observer中对应属性的set方法调用,之后会去通知dep,此时里面中有对应的watcher,再去通知所有相关的watcher,watcher调用updater方法去更新视图。
在这里插入图片描述

v-model,双向数据绑定

  1. 双向数据绑定是建立在单向数据绑定(model==>View)的基础之上的
  2. 双向数据绑定的实现流程:
    a. 在解析 v-model 指令时, 给当前元素添加 input 监听
    b. 当 input 的 value 发生改变时, 将最新的值赋值给当前表达式所对应的 data 属性

在input标签中输入内容,下面视图也会变化
在这里插入图片描述
要怎么做才能把input里面数据同步到data中?(实际上就是监听,要明白它是怎么去解析v-model的)
以上(2.a、2.b)

<input type="text" v-model="msg" />
//相当于(vue写法)
<input type="text" :value="msg" @input="handleInput" />
 handleInput(e) {
      this.msg = e.target.value;
      console.log(e.target.value);
    },
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值