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

100 阅读3分钟

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

调试:

    1. F11进入函数内部
    2. F8跳转到下一个断点

开始调试

  1. 新建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>
    
  2. 跳转到Vue的构造函数
      function Vue (options) {
       if (!(this instanceof Vue)
       ) {
         warn('Vue is a constructor and should be called with the `new` keyword');
       }
       this._init(options);
     }
    
    这里检测了Vue构造函数是否在this的原型链上,不是则报错。
  3. 在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/propsresolve 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);
    }
  }

看到initMethodsinitData,分别给这两行代码打上断点,先进入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中的属性同名。