【若川视野 x 源码共读】第23期 | 为什么 Vue2 this 能够直接获取到 data 和 methods

197 阅读4分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

学习目标

  1. 学习调试 vue2 源码
  2. data 中的数据为什么可以用 this 直接获取到
  3. methods 中的方法为什么可以用 this 直接获取到
  4. 学习源码中优秀代码和思想,投入到自己的项目中

示例

let vm = new Vue({
    data() {
        return {
            name: 'bss'
        }
    },
    methods: {
        show() {
            console.log(this.name)
        }
    }
})
console.log(vm.name)
console.log(vm.show())

调试方案

  1. 安装http-server包 用于搭建本地服务
npm i http-server -g
  1. 新建index.html 引入vue.js 并添加示例代码
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>
   let vm = new Vue({
    data() {
        return {
            name: 'bss'
        }
    },
    methods: {
        show() {
            console.log(this.name)
        }
    }
})
console.log(vm.name)
console.log(vm.show())
</script>
  1. 进入index.html所在目录 运行如下命令
http-server .
更换端口
http-server -p 8081 .
  1. 此时打开http://localhost:8080/就可以看到index.html页面了
  2. 打开浏览器的开发者工具 -> source -> 在new Vue()这里打上断点, 如图:

image.png 6. 刷新页面 执行调试 下面为常用调试按钮(更全调试技巧

image.png

源码解析

1. Vue函数定义

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

利用instanceof判断是否用new调用

是的话执行示例方法_init初始化Vue示例

否则给出警告

2. _init方法初始化

大致的内容如下

image.png

其他初始化内容可略过,大致包含实例的属性、生命周期、事件、渲染、调用生命周期及各种数据源

  • vm被赋值为当前实例 vm = this

  • 注意options参数的处理

    整合实例vm的构造函数options实例vm的属性并挂在vm.$options

  • 关注initState(vm)

3. initState(vm)

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);
    }
  }
  • 获取vm.$options
  • 初始化props
  • 初始化methods
  • 初始化data并做数据监听observe()
// data是否为空都会执行observe
observe(data ? data : vm._data, true /* asRootData */)
  • 初始化computed
  • 初始化watch

4. initMethods(vm, opts.methods)

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);
    }
  }
  1. 遍历methods对象

  2. 不是函数则给出警告

  3. props中存在重复的method的key给出警告

  4. method的key与Vue实例的保留属性重名给出警告

  5. 判断method的值

    是函数则改变this指向 通过bind函数返回一个新函数 并赋值给vm对应的key

    否则vm对应的key赋值为空

hasOwn
 var hasOwnProperty = Object.prototype.hasOwnProperty;
  function hasOwn (obj, key) {
    return hasOwnProperty.call(obj, key)
  }
  
// 示例 
var obj = {
  p: 123
};
obj.hasOwnProperty('p') // true

isReserved 是否为保留属性
// 检查字符串是否以$或_开头
  function isReserved (str) {
    var c = (str + '').charCodeAt(0);
    return c === 0x24 || c === 0x5F
  }
bind绑定this
  function polyfillBind (fn, ctx) {
    function boundFn (a) {
      var l = arguments.length; // vm的method的参数个数
      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;

5. initData(vm)

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 */);
  }
  1. 将data转为对象并更新到vm._data
  2. 如果不是普通对象则给出警告
  3. 遍历判断data的key是否与methodsprops的key重名 重名则给出警告
  4. 不是保留属性则进行代理proxy
  5. 最后对data进行监听observe
data处理
function getData (data, vm) {
    pushTarget();
    try {
      // 绑定上下文为vm data函数的参数为vm 执行结果返回
      return data.call(vm, vm) 
    } catch (e) {
      handleError(e, vm, "data()");
      // 返回空对象
      return {}
    } finally {
      popTarget();
    }
  }
  
var data = vm.$options.data; 
// 如果data是函数则getData()返回对象 否则直接返回data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};

由以上分析可以得知,我们在data()函数可以取到methods并执行(因为methods的初始化在data之前)例如:

let vm = new Vue({
    data(vm) {
        return {
            age: vm.showAge()
        }
    },
    methods: {
        show() {
            console.log(this.age)
        },
        showAge() {
            return 28
        }
    }
})
console.log(vm.show())
proxy代理

实现通过this.data[key]真正访问的是this._data[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);
  }
defineProperty
  • Object.defineProperty(obj, prop, descriptor):通过描述对象,定义某个属性。
  • Object.getOwnPropertyDescriptor():获取某个属性的描述对象。
  • Object.defineProperties():通过描述对象,定义多个属性。

descriptor包含的属性如下:

value——当试图获取属性时所返回的值。
writable——该属性是否可写。
enumerable——该属性在for in循环中是否会被枚举。
configurable——该属性是否可被删除。
set()——该属性的更新操作所调用的函数。
get()——获取属性值时所调用的函数。

// 使用示例
var obj = {};
Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  }
});
const descriptor1 = Object.getOwnPropertyDescriptor(obj, 'property1');
console.log(descriptor1)

observe后续再做分析 此处暂时忽略

手动编写代码实现示例效果

function noop (a, b, c) {}
let sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
  };
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); 
}

function Vue1(options) {
const vm = this
vm.$options = options
initState(vm)
}
const initState = function(vm) {
    initMethods(vm)
    initData(vm)
}
const initMethods = function(vm) {
const methods = vm.$options.methods
for(key in methods) {
    vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm)
} 
}

const initData = function(vm) {
let data = vm.$options.data
    data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {}
    let keys = Object.keys(data);
    let i = keys.length
    while(i--) {
        let key = keys[i]
        proxy(vm, "_data", key);
    }
}

将开头的示例改为:

    data(vm) {
        return {
            name: 'bss',
            age: vm.showAge()
        }
    },
    methods: {
        show() {
            console.log(this.name) // bss
            console.log(this.age) // 28
        },
        showAge() {
            return 28
        }
    }
})
console.log(vm.name) // bss
console.log(vm.show())

ok 正常打印 实现完成✌️

总结

  • 要灵活掌握this的使用和Object.defineProperty
  • 源码的阅读是对提升编码的有效途径之一,学习不在于急于求成,而在于细嚼慢咽。
  • 源码听起来好像很难,但是如果你真的静下心来看,一点点的去打通,再难的问题都不是问题。
  • 一个大的项目,要学会拆解学习,分成小项目,阶段性完成,这样就会比较容易,最终实现大的目标。