Vue源碼解析(一)data屬性映射和methods函數引用的重定義

使用Vue框架進行開發時,我們在option的data和methods中定義屬性和方法,在調用時直接使用 vm.attr 或 vm.func()的形式,而不是用vm.data.attr或vm.methods.func()的方式。

項目的git地址: https://github.com/xubaodian/SimuVue.git ,後續會持續更新,分析Vue的源碼,爭取實現一個乞丐版的Vue。
我們傳入Vue的options對象一般爲以下這種形式,

{
  data: {
    name: 'xxx'
  },
  mounted() {
    //調用方法,沒有使用this.methods.getInfo();
    this.getInfo();
  },
  methods: {
    getInfo() {
      //獲取屬性,沒有使用this.data.name
      this.name = 'xxxx2314';
      //操作等等....
    }
  },
  computed: {
    getName() {
      return this.name;
    }
  },
  watch: {
    'name'(val, oldVal) {
      //這是操作
    }
  }
}

在vue實例中,我們無論data還是method,都直接調用,這是因爲一下vue初始化時做了下面兩點操作:

1、給data中的屬性做了代理,所有訪問和設置vm[key]時,最終操作的是vm._data[key],而Vue在初始化時,會vm._data其實是options中data的引用。

2、methods中的所有方法都直接在vue實例重新定義了引用。

看下我的實現代碼,是對Vue源碼的精簡,如下:

//vue構造函數
class Vue {
  constructor(options) {
    //$options存儲構造選項
    this.$options = options || {};
    //data保存構造設置中的data,暫時忽略data爲函數的情況
    let data = options.data;
    this._data = data;
    //初始化
    this._init();
  }

  _init() {
    //映射key
    mapKeys(this);
    //在vue實例上重新定義方法的引用
    initMethods(this, this.$options.methods)
  }
}


//重新定義方法的引用,注意修改調用函數時的上下文環境,這裏用bind,當然也可以用apply和call
function initMethods (vm, methods) {
  for (const key in methods) {
    vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm);
  }
}

//重新定義data的get和set
function mapKeys(vm) {
  let data = vm._data;
  if (null !== data && typeof data === 'object') {
    const keys = Object.keys(data);
    let i = keys.length;
    while (i-- >= 0) {
      //所有屬性的操作就重新定向到了_data上
      proxy(vm, `_data`, keys[i]);
    }
  }
}

//使用defineProperty重新定義get和set
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)
}

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

//空函數,佔位用
function noop () {}


//使用
let options = {
    data: {
        name: 'xxx',
        age: 18
    },
    methods: {
        sayName() {
            console.log(this.name);
        }
    }
}


let vm = new Vue(options);

vm.sayName();//控制檯打印了xxx,可以把代碼直接複製出去試一下

上面代碼就完成了屬性的重新映射和方法的引用重新定義。

看下vue中源碼,,如下,我做了註釋,應該比較好懂:

簡單說明一下,源碼中使用了flow作爲js代碼的靜態檢查工具,原理和typescript類似,所以代碼看起來會有些不同,不影響整體閱讀

//初始化,參數是vue實例
function initData (vm: Component) {
  //獲取options中的
  let data = vm.$options.data
  //設置vm._data,判斷data是obj還是函數
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && 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
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    //這是在開發環境打印的一些提示不用關心
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      //代理訪問,這就是爲何操作vm[key]被定位到vm._data[key]的原因
      proxy(vm, `_data`, key)
    }
  }


  const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

//代理函數
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  //利用defineProperty設置對象的get和set,操作屬性時,target[key]會映射到target[sourceKey][key]
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

//方法映射
function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    //這些都是開發環境的提示信息,可以忽略
    if (process.env.NODE_ENV !== 'production') {
      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)
  }
}

有疑問可以給我留言,或發郵件至[email protected],歡迎大家來討論

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章