本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
调试:
1. F11进入函数内部
2. F8跳转到下一个断点
开始调试
- 新建index.html文件,在new Vue 处设置断点,按f11进入函数内部。
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script> <script> const vm = new Vue({ data: { name: '我是若川', }, methods: { sayName(){ console.log(this.name); } }, }); console.log(vm.name); console.log(vm.sayName()); </script> - 跳转到Vue的构造函数
这里检测了Vue构造函数是否在this的原型链上,不是则报错。function Vue (options) { if (!(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); } - 在this._init(options)上打上断点,按f8进入这个断点,再按f11进入这个函数内部。
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); } }; }
根据resolve injections before data/props和resolve provide after data/props这两句注释知道,初始化data和methods的函数应该是initState,打上断点,按f8和f11进入到这个函数内部。
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
看到initMethods和initData,分别给这两行代码打上断点,先进入initMethods。
function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
{
if (typeof methods[key] !== 'function') {
warn(
"Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
"Did you reference the function correctly?",
vm
);
}
if (props && hasOwn(props, key)) {
warn(
("Method \"" + key + "\" has already been defined as a prop."),
vm
);
}
if ((key in vm) && isReserved(key)) {
warn(
"Method \"" + key + "\" conflicts with an existing Vue instance method. " +
"Avoid defining component methods that start with _ or $."
);
}
}
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
}
}
initMethods做了什么
1. 遍历了methods对象。
2. 判断methods的键值是否是函数,否则报错。
3. 判断methods里函数名是否有和prop的属性同名,否则报错。
4. 判断methods的函数名是否在vue的实例上并且是否为保留字($,_),否则报错。
5. 最后用bind将函数绑定在vue的实例上。
initMethods用到的hasOwn
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}
判断对象本身是否有某个属性,而不是去原型链上查找。
initMethods用到的isReserved
/**
* Check if a string starts with $ or _
*/
function isReserved (str) {
//charCodeAt()返回指定位置的字符的Unicode编码
var c = (str + '').charCodeAt(0);
return c === 0x24 || c === 0x5F
}
判断str是否以_,$开头。
initMethods用到的bind
/* istanbul ignore next */
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;
为了兼容,实现了polyfillBind,防止原生的bind失效
initMethods用到的noop
function noop (a, b, c) {}
在initMethods中methods的key不是函数时,给实例上的key赋值为一个空函数
按f8和f11跳转到initData内部
initData
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// observe data
observe(data, true /* asRootData */);
}
initData做了什么
1. 遍历data的key
2. 判断是否有和methods的属性重名,有则报错
3. 判断是否有和props的属性重名,有则报错
4. `isReserved`确定不是保留字以后用proxy处理,做一层代理,参数分别为vue的实例,_data字符串、data里的键名
5. 监听data,让它成为响应式数据
initData里的getData
function getData (data, vm) {
// #7573 disable dep collection when invoking data getters
pushTarget();
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, "data()");
return {}
} finally {
popTarget();
}
}
通过getData获取data
在proxy(vm, "_data", key);上打上断点,进入proxy函数
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);
}
经过 Object.defineProperty的处理,这样就能够通过this.xxx访问this._data[xxx]了。
简单的实现通过this访问data和methods
/**
* 像vue一样 访问this下的属性简化版
*/
function noop(a,b,c){}
var sharedPropertyDefinition = {
enumerable:true,
configurable:true,
get:noop,
set:noop,
}
/**
* 用Object.defineProperty代理访问定义的属性
* @param {*} target
* @param {*} sourceKey
* @param {*} key
*/
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)
}
/**
* 初始化data;
* @param {*} vm
*/
function initData(vm){
const data = vm._data = vm.$options.data;
const keys = Object.keys(data)
var i = keys.length;
while(i--){
var key = keys[i];
proxy(vm,'_data',key)
}
}
/**
* 初始化methods
* @param {*} vm,methods
*/
function initMethods(vm,methods){
for (const key in methods) {
vm[key] = typeof methods[key]!=='function'? noop:methods[key].bind(vm)
}
}
function Person(options){
let vm = this;
vm.$options = options;
var opts = vm.$options;
if(opts.data){
initData(vm)
}
if(opts.methods){
initMethods(vm,opts.methods)
}
}
const p = new Person({
data:{
name:'sam'
},
methods:{
sayName(){
console.log(this.name);
}
}
})
console.log(p.name);//sam
p.sayName()//sam
with在渲染中的作用
function generate (
ast,
options
) {
var state = new CodegenState(options);
// fix #11483, Root level <script> tags should not be rendered.
var code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
function generate$1 (
ast,
options
) {
var state = new CodegenState(options);
var code = ast ? genSSRElement(ast, state) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
with语句动态地将this插入了作用域链,模版语法里的变量会去this里面寻找它的值,所以不用写this也是可以的。
参考链接:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/with
总结
1. 调试技能看源码真的好用,以后看源码要多用起来。
2. 阅读大量的源码,可以选择其中的一个小功能进行,这样既能够了解源码,并且能提升自信。
3. 使用Object.defineProperty改进了项目中挂载在vue.prototype的属性,防止被篡改。
4. vue2中的methods的属性和props的属性不能同名,data的属性不能和methods和props中的属性同名。