源码揭秘为什么 Vue2 this 能够直接获取到 data 和 methods

2022-04-15 0 1,146
目录
  • 1. 示例:this 能够直接获取到 data 和 methods
  • 2. 准备环境调试源码一探究竟
    • 2.1 Vue 构造函数
    • 2.2 _init 初始化函数
    • 2.3 initState 初始化状态
    • 2.4 initMethods 初始化方法
      • 2.4.1 bind 返回一个函数,修改 this 指向
    • 2.5 initData 初始化 data
      • 2.5.1 getData 获取数据
      • 2.5.2 proxy 代理
      • 2.5.3 Object.defineProperty 定义对象属性
    • 2.6 文中出现的一些函数,最后统一解释下
      • 2.6.1 hasOwn 是否是对象本身拥有的属性
      • 2.6.2 isReserved 是否是内部私有保留的字符串$  和 _ 开头
  • 3. 最后用60余行代码实现简化版
    • 4. 总结

      1. 示例:this 能够直接获取到 data 和 methods

      举例:

      const vm = new Vue({
          data: {
              name: '我是若川',
          },
          methods: {
              sayName(){
                  console.log(this.name);
              }
          },
      });
      console.log(vm.name); // 我是若川
      console.log(vm.sayName()); // 我是若川
      
      

      这样是可以输出我是若川的。好奇的人就会思考为啥 this 就能直接访问到呢。

      那么为什么 this.xxx 能获取到data里的数据,能获取到 methods 方法。
      我们自己构造写的函数,如何做到类似Vue的效果呢。

      function Person(options){
      
      }
      
      const p = new Person({
          data: {
              name: '若川'
          },
          methods: {
              sayName(){
                  console.log(this.name);
              }
          }
      });
      
      console.log(p.name);
      // undefined
      console.log(p.sayName());
      // Uncaught TypeError: p.sayName is not a function
      

      如果是你,你会怎么去实现呢。带着问题,我们来调试 Vue2源码学习。

      2. 准备环境调试源码一探究竟

      可以在本地新建一个文件夹examples,新建文件index.html文件。
      <body></body>中加上如下js。

      <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>
      
      

      再全局安装npm i -g http-server启动服务。

      npm i -g http-server
      cd examples
      http-server .
      // 如果碰到端口被占用,也可以指定端口
      http-server -p 8081 .
      
      

      这样就能在http://localhost:8080/打开刚写的index.html页面了。

      调试:在 F12 打开调试,source 面板,在例子中const vm = new Vue({打上断点。

      源码揭秘为什么 Vue2 this 能够直接获取到 data 和 methods

      刷新页面后按F11进入函数,这时断点就走进了 Vue 构造函数。

      2.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);
      }
      // 初始化
      initMixin(Vue);
      stateMixin(Vue);
      eventsMixin(Vue);
      lifecycleMixin(Vue);
      renderMixin(Vue);
      
      

      值得一提的是:if (!(this instanceof Vue)){} 判断是不是用了 new 关键词调用构造函数。
      一般而言,我们平时应该不会考虑写这个。
      当然看源码库也可以自己函数内部调用 new 。但 vue 一般一个项目只需要 new Vue() 一次,所以没必要。
      而 jQuery 源码的就是内部 new ,对于使用者来说就是无new构造。

      jQuery = function( selector, context ) {
        // 返回new之后的对象
        return new jQuery.fn.init( selector, context );
      };
      
      

      因为使用 jQuery 经常要调用。
      其实 jQuery 也是可以 new 的。和不用 new 是一个效果。

      调试:继续在this._init(options);处打上断点,按F11进入函数。

      2.2 _init 初始化函数

      进入 _init 函数后,这个函数比较长,做了挺多事情,我们猜测跟datamethods相关的实现在initState(vm)函数里。

      // 代码有删减
      function initMixin (Vue) {
          Vue.prototype._init = function (options) {
            var vm = this;
            // a uid
            vm._uid = uid$3++;
      
            // 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
              );
            }
      
            // 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');
          };
      }
      
      

      调试:接着我们在initState(vm)函数这里打算断点,按F8可以直接跳转到这个断点,然后按F11接着进入initState函数。

      2.3 initState 初始化状态

      从函数名来看,这个函数主要实现功能是:

      • 初始化 props
      • 初始化 methods
      • 监测数据
      • 初始化 computed
      • 初始化 watch
      function initState (vm) {
          vm._watchers = [];
          var opts = vm.$options;
          if (opts.props) { initProps(vm, opts.props); }
          // 有传入 methods,初始化方法
          if (opts.methods) { initMethods(vm, opts.methods); }
          // 有传入 data,初始化 data
          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);
          }
      }
      
      

      我们重点来看初始化 methods,之后再看初始化 data

      调试:initMethods 这句打上断点,同时在initData(vm)处打上断点,看完initMethods函数后,可以直接按F8回到initData(vm)函数。继续按F11,先进入initMethods函数。

      2.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
                );
              }
              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函数,主要有一些判断。

      • 判断 methods 中的每一项是不是函数,如果不是警告。
      • 判断 methods 中的每一项是不是和 props 冲突了,如果是,警告。
      • 判断 methods 中的每一项是不是已经在 new Vue实例 vm 上存在,而且是方法名是保留的 _ $ (在JS中一般指内部变量标识)开头,如果是警告。

      除去这些判断,我们可以看出initMethods函数其实就是遍历传入的methods对象,并且使用bind绑定函数的this指向为vm,也就是new Vue的实例对象。

      这就是为什么我们可以通过this直接访问到methods里面的函数的原因。

      我们可以把鼠标移上 bind 变量,按alt键,可以看到函数定义的地方,这里是218行,点击跳转到这里看 bind 的实现。

      2.4.1 bind 返回一个函数,修改 this 指向

      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;
      

      简单来说就是兼容了老版本不支持 原生的bind函数。同时兼容写法,对参数多少做出了判断,使用callapply实现,据说是因为性能问题。
      如果对于callapplybind的用法和实现不熟悉,能否模拟实现JS的callapply方法

      调试:看完了initMethods函数,按F8回到上文提到的initData(vm)函数断点处。

      2.5 initData 初始化 data

      • initData 函数也是一些判断。主要做了如下事情:
      • 先给 _data 赋值,以备后用。
      • 最终获取到的 data 不是对象给出警告。
      • 遍历 data ,其中每一项:
      • 如果和 methods 冲突了,报警告。
      • 如果和 props 冲突了,报警告。
      • 不是内部私有的保留属性,做一层代理,代理到 _data 上。
      • 最后监测 data,使之成为响应式的数据。
      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 */);
      }
      
      

      2.5.1 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();
          }
      }
      
      

      2.5.2 proxy 代理

      其实就是用 Object.defineProperty 定义对象
      这里用处是:this.xxx 则是访问的 this._data.xxx。

      /**
         * Perform no operation.
         * Stubbing args to make Flow happy without leaving useless transpiled code
         * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
         */
      function noop (a, b, c) {}
      var 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);
      }
      
      

      2.5.3 Object.defineProperty 定义对象属性

      • Object.defineProperty 算是一个非常重要的API。还有一个定义多个属性的API:Object.defineProperties(obj, props) (ES5)
      • Object.defineProperty 涉及到比较重要的知识点,面试也常考。
      • value——当试图获取属性时所返回的值。
      • writable——该属性是否可写。
      • enumerable——该属性在for in循环中是否会被枚举。
      • configurable——该属性是否可被删除。
      • set() —该属性的更新操作所调用的函数。
      • get() —获取属性值时所调用的函数。

      2.6 文中出现的一些函数,最后统一解释下

      2.6.1 hasOwn 是否是对象本身拥有的属性

      调试模式下,按alt键,把鼠标移到方法名上,可以看到函数定义的地方。点击可以跳转。
      /**
         * Check whether an object has the property.
         */
      var hasOwnProperty = Object.prototype.hasOwnProperty;
      function hasOwn (obj, key) {
        return hasOwnProperty.call(obj, key)
      }
      
      hasOwn({ a: undefined }, 'a') // true
      hasOwn({}, 'a') // false
      hasOwn({}, 'hasOwnProperty') // false
      hasOwn({}, 'toString') // false
      // 是自己的本身拥有的属性,不是通过原型链向上查找的。
      

      2.6.2 isReserved 是否是内部私有保留的字符串$  和 _ 开头

      /**
         * Check if a string starts with $ or _
         */
      function isReserved (str) {
        var c = (str + '').charCodeAt(0);
        return c === 0x24 || c === 0x5F
      }
      isReserved('_data'); // true
      isReserved('$options'); // true
      isReserved('data'); // false
      isReserved('options'); // false
      
      

      3. 最后用60余行代码实现简化版

      function noop (a, b, c) {}
      var 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 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 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: '若川'
          },
          methods: {
              sayName(){
                  console.log(this.name);
              }
          }
      });
      
      console.log(p.name);
      // 未实现前: undefined
      // '若川'
      console.log(p.sayName());
      // 未实现前:Uncaught TypeError: p.sayName is not a function
      // '若川'
      
      

      4. 总结

      本文涉及到的基础知识主要有如下:

      • 构造函数
      • this 指向
      • callbindapply
      • Object.defineProperty

      本文源于解答源码共读群友的疑惑,通过详细的描述了如何调试 Vue 源码,来探寻答案。
      解答文章开头提问:
      通过this直接访问到methods里面的函数的原因是:因为methods里的方法通过 bind 指定了this为 new Vue的实例(vm)。
      通过 this 直接访问到 data 里面的数据的原因是:data里的属性最终会存储到new Vue的实例(vm)上的 _data对象中,访问 this.xxx,是访问Object.defineProperty代理后的 this._data.xxx。
      Vue的这种设计,好处在于便于获取。也有不方便的地方,就是propsmethods 和 data三者容易产生冲突。
      文章整体难度不大,但非常建议读者朋友们自己动手调试下。调试后,你可能会发现:原来 Vue 源码,也没有想象中的那么难,也能看懂一部分。
      启发:我们工作使用常用的技术和框架或库时,保持好奇心,多思考内部原理。能够做到知其然,知其所以然。就能远超很多人。
      你可能会思考,为什么模板语法中,可以省略this关键词写法呢,内部模板编译时其实是用了with。有余力的读者可以探究这一原理。

      到此这篇关于源码揭秘为什么 Vue2 this 能够直接获取到 data 和 methods的文章就介绍到这了,更多相关Vue2 this 直接获取到 data 和 methods内容请搜索NICE源码以前的文章或继续浏览下面的相关文章希望大家以后多多支持NICE源码!

      免责声明:
      1、本网站所有发布的源码、软件和资料均为收集各大资源网站整理而来;仅限用于学习和研究目的,您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。 不得使用于非法商业用途,不得违反国家法律。否则后果自负!

      2、本站信息来自网络,版权争议与本站无关。一切关于该资源商业行为与www.niceym.com无关。
      如果您喜欢该程序,请支持正版源码、软件,购买注册,得到更好的正版服务。
      如有侵犯你版权的,请邮件与我们联系处理(邮箱:skknet@qq.com),本站将立即改正。

      NICE源码网 JavaScript 源码揭秘为什么 Vue2 this 能够直接获取到 data 和 methods https://www.niceym.com/24828.html