本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
这是源码共读的第23期,链接: 为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘!
1. 环境源码准备
// index.js
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>
const vm = new Vue({
data: {
name: 'hello'
},
methods: {
sayName(){
console.log('log>>>', this.name);
}
}
})
console.log(vm.name);
console.log(vm.sayName());
</script>
全局安装http-server, 启动index.html;
npm i -g http-server
http-server .
通过 http://localhost:8082/ 打开 index.html 页面
2. 调试
1)F12 打开调试, source面板,第二行 打上断点
2)刷新页面 F11 进入函数,此时断点就走进了Vue构造函数
3. 走进Vue源码
3.1 Vue构造函数
function Vue (options) {
// options是一个对象,就是创建 vm实例时传入的,
// options对象具有data、props、watch、computed、methods等一些属性
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
if(!this instanceof Vue) 判断是不是用了new关键词调用构造函数
3.2 _init初始化函数
var uid$3 = 0;
function initMixin(Vue){
Vue.prototype._init = function(options){
var vm = this;
vm._uid = uid$3++;
// a flag to avoid this being observed
vm._isVue = true
// merge options
if(options && options._isComponent){
initInternalComponent(vm, options)
} else {
// mergeOptions: 第一个参数是父, 第二个参数是, 第三个参数是vm实例
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm)
}
}
// expose real self
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjection(vm); // resolve injections before data/props
initState(vm); // 初始化状态
initProvide(vm); // resolve provide after data/props
callHook(vm, 'create')
}
3.3 initState 初始化状态
这个函数主要实现功能是:
// 初始化 props
// 初始化 methods
// 监测数据
// 初始化 computed
// 初始化 watch
// Firefox has a "watch" function on Object.prototype...
var nativeWatch = ({}).watch;
function initState(vm){
vm._watchers = [];
var opts = vm.$options;
if(opts.props) {initProps(vm, opt.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)
}
}
3.4 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
)
}
// 判断每一项是不是 和props 冲突了
if(props && hasOwn(props, key)){
warn(
("Method \"" + key + "\" has already been defined as a prop."),vm
);
}
// 判断每一项是不是已经在 new Vue 中存在, 且方法名是保留的 _或者 $(js中一般指内部变量标识)开头
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)
}
}
3.4.1 initMethods中使用的工具函数
- hasOwn 是否是自身属性
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key){
return hasOwnProperty.call(obj, key)
}
- isReserved 是否是内部私有保留的字符串$和_开头
function isReserved(str){
// str转为字符串后的首个字符
var c = (str + '').chartCode(0)
return c === 0 0x24 || c === 0x5F
}
- noop 空函数
function noop(a,b,c){}
- bind 改变 this 指向
function pollyfillBind(fn, ctx){
function boundFn(a){
// l 存在 且 l > 1, 传的是数组
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 = Object.prototype.bind ? nativeBind : pollyfillBind;
initMethods函数是遍历vm.options.methods中每一项,传入到 methods 对象,并且使用 bind 绑定函数的this为vm,也就是new Vue的实例对象。
这就是为什么可以通过 this 直接访问到 methods 里面的函数的原因
3.5 initData 初始化data
function initData(vm){
var data = vm.$options.data;
// 给 _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
// keys为 data 数据的属性名
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]
{
// 判断 data 中定义的数据 是否和 props 中冲突
if(props && hasOwn(props, key)){
warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
}
}
// 判断 data 中定义的数据 是否和 methods 中冲突
if(methods && hasOwn(methods, keys)){
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
} else if(!isReserved(key)){
// 定义 _data 并使用内部定义的 proxy 方法代理所有属性(实际上是将属性代理到vm实例上,支持this.attr这种形式的支持调用)
proxy(vm, '_data', key)
}
}
// 观察data中所有属性,实现data响应式的具体处理
observe(data, true, /* asRootData*/)
}
3.5.1 initData 中使用到的工具函数
- getData 获取数据
// 是函数时调用函数,执行获取对象
function getData(data, vm){
pushTarget();
try{
return data.call(vm, vm)
} catch(e){
handleError(e, vm, "data()")
} finally{
popTarget()
}
}
Dep.target = null;
var targetStack = [];
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
- 判断是否是纯对象
var _toString = Object.prototype.toString
function isPlainObject (obj) {
return _toString.call(obj) === '[object Object]'
}
- proxy 代理 其实就是用 Object.definePreperty 定义对象
// 还有其他属性,value: 获取属性时返回的值, writable: 是否可写
var sharePropertyDefinition = {
enumerable: true, // 可否通过for...in 遍历
configurable: true, // 可否修改、删除属性
get: noop,
set: noop
}
function proxy(target, sourceKey, key){
sharedPropertyDefinition.get = function proxyGetter(){
// 访问的是this._data 中的 key
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter(val){
// 访问的是this._data 中的 key
this[sourceKey][key] = val
}
// target: 属性所在的对象, key: 属性名 sharedPropertyDefinition: 一个对象, 对key进行描述
Object.defineProperty(target, key, sharedPropertyDefinition)
}
4. observe 监听数据变化的
function observe(value, asRootData){
// 如果不是对象或者是虚拟dom对象则返回
if(!isObject(value) || value instance of VNode){
return
}
var ob;
// 存在 __ob__ 属性则代表该对象之前已经观察过了
if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observe){
ob = value.__ob__;
} else if(
shouldObserve && // // 当前允许观察
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) && // 只允许对数组和简单对象进行观察
// 该对象是可开展的,即可以给它添加新属性 且它不能是Vue实例
Object.isExtensible(value) && !value._isVue){
ob = new Observer(value)
}
// 统计有多少个Vue实例对象将该对象作为根数据
if(asRootData && ob){
ob.vmCount++;
}
return ob;
}
observe 方法会判断一个对象是否已经被观察过,如果没有,那么当该数据是数组或简单对象的话会给它创建一个 observer 实例,接下来看 Observer 类
export class Observer {
value;
dep;
vmCount; // 使用该对象作为根数据的vm数量
constructor(value) {
// 目标对象
this.value = value
// 实例化一个依赖收集对象
this.dep = new Dep()
// vm数量
this.vmCount = 0
// 给目标对象添加一个被观察过了的标志
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 如果浏览器支持使用__proto__属性
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
}
Observer 构造函数定义了几个变量,然后给目标对象添加了一个被观察到标志位,使用了 def 方法,这个方法也是源码中很常见的一个方法
export function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,// 是否可枚举
writable: true,// 可写
configurable: true// 可配置、可删除
})
}
5. 简化版实现
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)
}
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)
}
}
function initMethods(vm, methods){
for(var key in methods){
vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm)
}
}
function proxy(target, sourceKey, key){
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
sharedPropertyDefinition.get = function proxyGetter(){
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter(val){
this[sourceKey][key] = val
}
Object.defineProperty(target,key,sharedPropertyDefinition)
}
function noop(a,b,c){}
}
var p = new Person({
data: {
name: '多啦A梦'
},
methods: {
getName(){
console.log('name>>>', this.name)
}
}
})
思考:
- 如何理解data.call(vm, vm)
data = vm.$options.data
改变this指向,将 data函数的this指向组件实例vm: data 改为 第一个vm是为了执行函数上下文, 并将 第二个 vm 作为参数传入。
总结:
this能够直接访问到methods里面函数的原因: 通过bind将this指向改成了new Vue的实例vm
bind(methods[key], vm)
this能够直接访问到data里面数据的原因:data中数据会在vm实例上的_data中又存储一份, 当访问this.xxx访问的是Object.defineProperty代理后的this._data.xxx
Object.defineProperty(target, key, sharedPropertyDefinition)
// sharedPropertyDefinition.get --> this[_data][key]
// Object.defineProperty(vm, key, sharedPropertyDefinition)
so this.XXX 指向this._data.XXX
-
vue中vm指向问题
a. 被vue所管理的函数,再写成普通函数后,里面的this指向才是vm或者组件实例对象 b. 而不被vue管理的函数,例如 ajax 回调函数等,需要写成箭头函数,这样里面的 this 才指向 vm 或组件实例对象