1. 程式人生 > 其它 >Vue原始碼解讀之InitState

Vue原始碼解讀之InitState

前面我們講到了_init函式的執行流程,簡單回顧下:

  • 初始化生命週期-initLifecycle
  • 初始化事件-initEvents
  • 初始化渲染函式-initRender
  • 呼叫鉤子函式-beforeCreate
  • 初始化依賴注入-initInjections
  • 初始化狀態資訊-initState
  • 初始化依賴提供-initProvide
  • 呼叫鉤子函式-created
    一共經過上面8步,init函式執行完成,開始mount渲染。

初始化狀態資訊

本章咱們主要講解initState函式的處理過程,咱們先看下init的主函式

function initState(vm: Component) {
    vm._watchers = []
    const 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)
    }
}

看上面程式碼,先聲明瞭一個_watchers的空陣列;然後依次判斷傳遞進來的options是否包含系列引數;依次執行initProps、initMethods、initData、initComputed、initWatch。

initProps

initProps函式主要是處理傳進來的props物件,但是這個props物件是在上一篇文章中講到的normalizeProps函式處理後的物件,不是傳遞進來的原物件。來看下initProps的程式碼:

function initProps(vm: Component, propsOptions: Object) {
    const propsData = vm.$options.propsData || {}
    const props = vm._props = {}
    const keys = vm.$options._propKeys = []
    const isRoot = !vm.$parent
    if (!isRoot) {
        toggleObserving(false)
    }
    for (const key in propsOptions) {
        keys.push(key)
        const value = validateProp(key, propsOptions, propsData, vm)
        defineReactive(props, key, value)
        if (!(key in vm)) {
            proxy(vm, `_props`, key)
        }
    }
    toggleObserving(true)
}

上面程式碼解讀:

  • 第一步獲取了propsData;
  • 第二步給當前例項添加了_props屬性,新增了一個props引用,指向了_props屬性;
  • 第三步給當前例項增加了_propKeys屬性,新增了一個keys的引用,指向了_propKeys屬性;
  • 第四步判斷了是否需要進行監聽;
  • 遍歷normalizeProps函式處理後的物件propsOptions;
    • 儲存key
    • 校驗props格式
    • 為當前key定義響應式的屬性:defineReactive
    • 把當前key的訪問方式提高到例項上面:proxy,即可以vm.name來訪問vm._props.name
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
    }
    Object.defineProperty(target, key, sharedPropertyDefinition)
}
  • 開啟監聽。

initMethods

initMethods方法是用來處理傳遞進來的methods引數,把methods繫結到當前例項上面

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

上面程式碼解讀:

  • 第一步獲取了props;
  • 第二步遍歷methods;
    • 判斷當前method是否是函式,不是函式則在開發環境下報警
    • 判斷props是否已經有了當前method的key,如有則在開發環境下報警
    • 判斷當前method是否已經在vm上面了,並且以$或_開頭,如是,則在開發環境下報警
    • 為當前例項新增方法;

initData

initData方法是用來處理傳遞進來的data引數,新增監聽

function initData(vm: Component) {
    let data = vm.$options.data
    data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
    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)) {
            // 實現代理,可以this.massage 來進行訪問this._data.message
            proxy(vm, `_data`, key)
        }
    }
    observe(data, true /* asRootData */)
}

上面程式碼解讀:

  • 第一步獲取傳遞進來的data,判斷data是否為函式,是函式則執行函式獲取當前物件,否則直接讀取當前物件;
  • 第二步,獲取上一步的data所有的key,賦值給keys;
  • 第三步獲取props;
  • 第四步獲取methods;
  • 第五步,迴圈keys;
    • 判斷是否和methods裡面是否重複,重複則開發環境進行報警
    • 判斷是否和props裡面是否重複,重複則開發環境進行報警
    • 判斷如不是以_或$開頭的key,則進行代理處理,把當前key的訪問方式提高到例項上面:proxy,即可以vm.name來訪問vm._datas.name
  • 對當前data物件進行observe處理,暫時先不用關注observe,後面會講到是做什麼的。

參考 Vue面試題詳細解答


initComputed

initComputed是用來處理傳進來的computed引數

function initComputed(vm: Component, computed: Object) {
    const watchers = vm._computedWatchers = Object.create(null)
    const isSSR = isServerRendering()

    for (const key in computed) {
        const userDef = computed[key]
        const getter = typeof userDef === 'function' ? userDef : userDef.get
        if (!isSSR) {
            watchers[key] = new Watcher(
                vm,
                getter || noop,
                noop,
                {
                    lazy: true
                }
            )
        }
        if (!(key in vm)) {
            defineComputed(vm, key, userDef)
        }
    }
}

initComputed方法解讀:

  • 第一步為例項增加了_computedWatchers屬性,宣告引用watchers;
  • 獲取是否是服務端渲染-isSSR;
  • 遍歷computed;
    • 獲取使用者定義的內容-userDef
    • 根據使用者定義的內容來獲取當前屬性key的getter函式
    • 為當前key增加Watcher,暫時不用關注Watcher後面會講到
    • 呼叫defineComputed,引數為當前例項,當前屬性key和userDef
      下面來看下defineComputed的實現:
function defineComputed(target: any, key: string, userDef: Object | Function) {
    const shouldCache = !isServerRendering()
    if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = shouldCache
            ? createComputedGetter(key)
            : createGetterInvoker(userDef)
        sharedPropertyDefinition.set = noop
    } else {
        sharedPropertyDefinition.get = userDef.get
            ? shouldCache && userDef.cache !== false
                ? createComputedGetter(key)
                : createGetterInvoker(userDef.get)
            : noop
        sharedPropertyDefinition.set = userDef.set || noop
    }
    Object.defineProperty(target, key, sharedPropertyDefinition)
}

defineComputed

defineComputed方法解讀:

  • 判斷是否需要使用cache,非server端渲染,使用cache,即瀏覽器情況下都是true;
  • 分情況討論:
    • userDef為函式時,呼叫createComputedGetter函式生成get函式,set函式為空函式
    • userDef不為函式時,get函式為createComputedGetter或者createGetterInvoker生成的函式;
  • 呼叫Object.defineProperty為當前例項新增定義屬性;

createGetterInvoker

下面來看下createGetterInvoker:

function createGetterInvoker(fn) {
    return function computedGetter() {
        return fn.call(this, this)
    }
}

上面程式碼直接返回了一個函式,函式內部呼叫的是傳遞進來的fn函式,fn函式是從defineComputed傳進來的,值為userDef或者userDef.get。

createComputedGetter

下面來看下createComputedGetter:

function createComputedGetter(key) {
    return function computedGetter() {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate()
            }
            if (Dep.target) {
                watcher.depend()
            }
            return watcher.value
        }
    }
}

上面程式碼返回了一個computedGetter的函式,函式內部分析:

  • 獲取了在initComputed函式裡面宣告的_computedWatchers,
  • watcher肯定是有值的,dirty屬性的值在此處也相當於lazy屬性,因為建立watcher的時候傳的是true,所以此處也是true;
  • 執行watcher.evaluate,該方法會獲取當前watcher的value,並且把dirty屬性變為false;
  • 判斷Dep.target,然後呼叫watcher的收集依賴;
  • 返回watcher.value;

initWatch

initWatch是用來處理傳進來的watch引數。

function initWatch(vm: Component, watch: Object) {
    for (const key in watch) {
        const handler = watch[key]
        if (Array.isArray(handler)) {
            for (let i = 0; i < handler.length; i++) {
                createWatcher(vm, key, handler[i])
            }
        } else {
            createWatcher(vm, key, handler)
        }
    }
}

initWatch函式解讀:
遍歷watch,根據key獲取handler,handler為陣列遍歷執行createWatcher,不為陣列直接執行createWatcher;
來看下createWatcher:

createWatcher

function createWatcher(vm: Component, expOrFn: string | Function, handler: any, options?: Object) {
    if (isPlainObject(handler)) {
        options = handler
        handler = handler.handler
    }
    if (typeof handler === 'string') {
        handler = vm[handler]
    }
    return vm.$watch(expOrFn, handler, options)
}

createWatcher程式碼解讀:

  • 判斷handler是否為物件,如果為物件,則把當前handler作為options,options.handler作為handler;
  • 判斷handler是否為字串,字串的話,則直接獲取例項的handler方法;
  • 呼叫$watch返回;
    綜上分析,watch的傳參可以分為以下幾種:
watch: {
    telephone: function (newValue, oldValue) {
      console.log('telephone')
    },
    name: 'printName',
    message: ['printName', 'printValue'],
    address: [{
      handler: function (newValue, oldValue) {
        console.log('address')
      }
    }]
  },
  methods: {
    printName(newValue, oldValue) {
      console.log('printName')
    },
    printValue(newValue, oldValue) {
      console.log('printValue')
    }
  }
  • 第一種:直接傳方法;
  • 第二種:傳遞方法的字串名稱;
  • 第三種:傳遞方法的字串名稱陣列;
  • 第四種:傳遞一個包含handler屬性的物件陣列;
    接下來咱們看下$watch方法的實現

$watch

現在咱們來看下watch的實現,watch是Vue原型上的方法,主流程篇簡單提了一下,流程圖上面看到$watch是在statesMixin函式裡面給Vue掛載到原型物件上的。

Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options?: Object): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
        try {
            cb.call(vm, watcher.value)
        } catch (error) {
            handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
        }
    }
    return function unwatchFn() {
        watcher.teardown()
    }
}

上面程式碼就是$watch函式的實現,咱們一步步來看下。

  • 引數,包含3個,第一個就是需要watch的key,比如上面例子程式碼的name;第二個就是回撥函式,當name屬性改變的時候會呼叫此回撥函式;第三個引數為options,顧名思義,就是配置資訊;
  • 第一步:例項vm的宣告;
  • 第二步:判斷cb是否為物件,如果是則呼叫上面的createWatcher;
  • 第三步:options檢測是否有值,無值則賦值為空物件;
  • 第四步:設定options.user為true,即這是使用者所定義和呼叫觸發的;
  • 第五步:建立Watcher;
  • 第六步:如果是立即呼叫,則呼叫cb,即回撥函式;
  • 返回一個函式,此函式為watcher的銷燬函式。
    上面就是整個initWatch的呼叫過程。