前言:本人前端菜鸟,有问题欢迎指出啊!!!!!不急眼,不杠,虚心接受一切建议!!!
最近一直在熬项目,文章也是断断续续的写。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代码放到项目中方便查看,方式有多种。我这边是在浏览器上打断点,在编辑器中查看具体代码,并填写注释。
<!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。
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,再分别初始化。
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;
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);
}
总结
总结就三个字,我很菜。要补充的东西还有很多,今天就只能先写到这里了,有问题欢迎指出啊!!!!