1. 程式人生 > 程式設計 >原始碼揭祕為什麼 Vue2 this 能夠直接獲取到 data 和 methods

原始碼揭祕為什麼 Vue2 this 能夠直接獲取到 data 和 methods

目錄
  • 1. 示例:this 能夠直接獲取到 data 和 methods
  • 2. 準備環境除錯原始碼一探究竟
    • 2.1 建構函式
    • 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. 準備環境除錯原始碼YQBssn一探究竟

      可以在本地新建一個資料夾examples,新建檔案index.html檔案。
      <body></body>中加上如下。

      <script src="https://unpkg.com/[email protected]/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() 一次,所以沒必要。
      而 原始碼的就是內部 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,optwww.cppcns.comions);
            } 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
              );
            } ewww.cppcns.comlse 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,set: noop
      };
      function proxy (target,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

      本文源於解答源YQBssn碼共讀群友的疑惑,通過詳細的描述瞭如何除錯 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內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!