vue源码调试之路——初始化数据

156 阅读4分钟

前言:本人前端菜鸟,有问题欢迎指出啊!!!!!不急眼,不杠,虚心接受一切建议!!!

最近一直在熬项目,文章也是断断续续的写。vue我用到现在也用了一年多了,期间断断续续的看过很多大佬写的源码文章,什么响应式原理,computed原理,什么依赖收集等等。看是看了,但是自己没有真正的调试过源码。感觉摸索摸索着也应该可以开始干一下了。我会从最基本的开始,基本按照我自己的思路写清楚,文笔不会太生涩,基本都是大白话,毕竟我肚子里也没啥墨水,哈哈哈哈。这个应该也会是系列文,希望我自己可以写下去。

在阅读源码之前我们需要了解一些在源码中经常出现的工具类函数,不然直接看有些方法可能看不懂。差不多有三百多行的基础函数,这一块的代码在vue.js最上头,算是平常项目中比较常见的,我这里贴了一大半,也添加了写注释,源码阅读碰到的时候就可以看下。(不过我觉得这里面的方法还是实用的!判空,判断数据类型,转数据类型用的最多)

//冻结一个对象,被冻结的对象最外层是无法修改
var emptyObject = Object.freeze({});

// These helpers produce better VM code in JS engines due to their
// explicitness and function inlining.
function isUndef (v) {
  return v === undefined || v === null
}

function isDef (v) {
  return v !== undefined && v !== null
}

function isTrue (v) {
  return v === true
}

function isFalse (v) {
  return v === false
}

/**
 * Check if value is primitive.
 */
function isPrimitive (value) {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    // $flow-disable-line
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

/**
 * Quick object check - this is primarily used to tell
 * Objects from primitive values when we know the value
 * is a JSON-compliant type.
 */
// 判断对象
function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

/**
 * Get the raw type string of a value, e.g., [object Object].
 */
  // 判断数据类型 返回值:String,Number,Boolean,Function,Object,Array
var _toString = Object.prototype.toString;

function toRawType (value) {
  return _toString.call(value).slice(8, -1)
}

/**
 * Strict object type check. Only returns true
 * for plain JavaScript objects.
 */
//判断是否是纯函数
function isPlainObject (obj) {
  return _toString.call(obj) === '[object Object]'
}

//判断正则
function isRegExp (v) {
  return _toString.call(v) === '[object RegExp]'
}

/**
 * Check if val is a valid array index.
 * isFinite函数用来检查其参数是否是无穷大,也可以理解味是否是一个有限数值
 * 返回值:如果参数是NaN,字符串,正无穷大或者负无穷大,会返回false,其他返回true
 */
function isValidArrayIndex (val) {
  var n = parseFloat(String(val));
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}

//判断是否是Promise对象
function isPromise (val) {
  return (
    isDef(val) &&
    typeof val.then === 'function' &&
    typeof val.catch === 'function'
  )
}

/**
 * Convert a value to a string that is actually rendered.
 * 将数据转成字符串
 */
function toString (val) {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}

/**
 * Convert an input value to a number for persistence.
 * If the conversion fails, return original string.
 * 转数字,Number
 */
function toNumber (val) {
  var n = parseFloat(val);
  return isNaN(n) ? val : n
}

/**
 * Make a map and return a function for checking if a key
 * is in that map.
 * 传入一个逗号分割的字符串,讲字符串转成一个对象,每个key值味true,最后返回判断当前入参是否包含在str内
 * 返回值:true/false
 */
function makeMap (
  str,
  expectsLowerCase
) {
  var map = Object.create(null);
  var list = str.split(',');
  for (var i = 0; i < list.length; i++) {
    map[list[i]] = true;
  }
  return expectsLowerCase
    ? function (val) { return map[val.toLowerCase()]; }
    : function (val) { return map[val]; }
}

/**
 * Check if a tag is a built-in tag.
 */
var isBuiltInTag = makeMap('slot,component', true);

/**
 * Check if an attribute is a reserved attribute.
 */
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');

/**
 * 从数组中删除一个数据.
 */
function remove (arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

/**
 * 判断当前key是否是当前对象的属性.
 */
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}

/**
 * 创建一个纯对象n.
 */
function cached (fn) {
  var cache = Object.create(null);
  return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
  })
}

/**
 * .
 */
var camelizeRE = /-(\w)/g;
var camelize = cached(function (str) {
  return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});

/**
 * Capitalize a string.
 */
var capitalize = cached(function (str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
});

/**
 * Hyphenate a camelCase string.
 */
var hyphenateRE = /\B([A-Z])/g;
var hyphenate = cached(function (str) {
  return str.replace(hyphenateRE, '-$1').toLowerCase()
});

/**
 * Simple bind polyfill for environments that do not support it,
 * e.g., PhantomJS 1.x. Technically, we don't need this anymore
 * since native bind is now performant enough in most browsers.
 * But removing it would mean breaking code that was able to run in
 * PhantomJS 1.x, so this must be kept for backward compatibility.
 */

/* istanbul ignore next */
/**
 * 在当前不支持bind的时候,转成call/apply
 * */
function polyfillBind (fn, ctx) {
  function boundFn (a) {
    var l = arguments.length;
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }

  boundFn._length = fn.length;
  return boundFn
}

/**
 * bind
 * **/
function nativeBind (fn, ctx) {
  return fn.bind(ctx)
}

/***
 * bind方法
 * */
var bind = Function.prototype.bind
  ? nativeBind
  : polyfillBind;

/**
 * Convert an Array-like object to a real Array.
 *
 */
function toArray (list, start) {
  start = start || 0;
  var i = list.length - start;
  var ret = new Array(i);
  while (i--) {
    ret[i] = list[i + start];
  }
  return ret
}

准备环境调试

新建一个文件夹vue-data,文件夹内新建index.html。先简单的写一个例子,安装live-server或者http-server启动服务。打开控制台,打上断点,开始调试!当页面进入到vue.js就开始成功的第一步!

注意,这次的源码调试,我都是在浏览器上打断点进行的!!!也可以自己复制一份vue.js代码放到项目中方便查看,方式有多种。我这边是在浏览器上打断点,在编辑器中查看具体代码,并填写注释。

image.png

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>

  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const vm = new Vue({
      data: {
        name: '我是33'
      },
      methods: {
        sayName() {
          console.log(this.name)
        }
      }
    })
    console.log(vm.name)
    console.log(vm.sayName())
  </script>
</body>

</html>

我们点击下一步,在代码执行到new Vue()的时候,就会进入devtools.emit('init',Vue),初始化数据。全局搜索function Vue(),就可以看到在Vue方法里对数据进行初始化了。_init*方法在最初加载vue.js文件时在initMixin注册。

function Vue (options) {
    if (!(this instanceof Vue)) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
}

initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
function initMixin(Vue) {
    Vue.prototype._init = function (options) {
      var vm = this;
      // a uid
      vm._uid = uid$3++;

      var startTag, endTag;
      /* istanbul ignore if */
      if (config.performance && mark) {
        startTag = "vue-perf-start:" + (vm._uid);
        endTag = "vue-perf-end:" + (vm._uid);
        mark(startTag);
      }

      // a flag to avoid this being observed
      vm._isVue = true;
      // merge options
      if (options && options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options);
      } else {
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        );
      }
      /* istanbul ignore else */
      {
        initProxy(vm);
      }
      // expose real self
      vm._self = vm;
      initLifecycle(vm);
      initEvents(vm);
      initRender(vm);
      callHook(vm, 'beforeCreate');
      initInjections(vm); // resolve injections before data/props
      initState(vm);
      initProvide(vm); // resolve provide after data/props
      callHook(vm, 'created');

      /* istanbul ignore if */
      if (config.performance && mark) {
        vm._name = formatComponentName(vm, false);
        mark(endTag);
        measure(("vue " + (vm._name) + " init"), startTag, endTag);
      }

      if (vm.$options.el) {
        vm.$mount(vm.$options.el);
      }
    };
  }

initMixin

我们在浏览器initMixin方法里打断点看下执行情况,可以看到option就是我们传入的数据date,methods。

image.png

mergeOptions的作用是将options,vm这两个数据合并,并创建了一个实例,这样$options就可以继承所有的方法。 这些方法一看就能猜出是一堆初始化数据的方法了,

  initLifecycle(vm);  
  initEvents(vm);
  initRender(vm);
  callHook(vm, 'beforeCreate');
  initInjections(vm); // resolve injections before data/props
  initState(vm);
  initProvide(vm); // resolve provide after data/props
  callHook(vm, 'created');
  

initState

然后继续打断点进去看,先看initState(vm)。先判断vm中是否有props、method、data、computed、watch,再分别初始化。 image.png

initMethods

继续寻找initMethods打断点查看,methods里面包含了所有的方法,for in对methods循环,将每个方法挂载再vm上。同时在props里判断,是否有同名的属性。方法名是不是$或者_开头的。需要注意的是这里用到了bind,上面的工具类函数里对bind自定义封装了下(因为bind是在ECMA-262 第五版才被加入,所以不是所有的浏览器都可以运行),在当前浏览器不支持bind的情况下,根据参数长度用apply,call来改变方法指向。所有遍历的方法都通过bind将this指向了vm,所以我们可以直接this.xxx来调用方法


vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);


function polyfillBind(fn, ctx) {
    function boundFn(a) {
      var l = arguments.length;
      return l
        ? l > 1
          ? fn.apply(ctx, arguments)
          : fn.call(ctx, a)
        : fn.call(ctx)
    }

    boundFn._length = fn.length;
    return boundFn
}

function nativeBind(fn, ctx) {
return fn.bind(ctx)
}

var bind = Function.prototype.bind
? nativeBind
: polyfillBind;

image.png

initData

initData和method一样,先是判断vm.$option.data是否是一个function,用getData获取data,判断返回的是否是对象,否则发出警告。在循环data对象所有属性,同时还要判断不能和props,methods同名。

proxy(vm, "_data", key)将原先需要通过vm.$options.data才能获取到的数据可以直接通过vm.xxx获取到。 Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

最后用observe将data变成可观察的数据

function proxy(target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter() {
      return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter(val) {
      this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
}

image.png

总结

总结就三个字,我很菜。要补充的东西还有很多,今天就只能先写到这里了,有问题欢迎指出啊!!!!