大前端進階-讀懂vuejs源碼2

前文中,已經分析了在vuejs源碼中是如何定義Vue類,以及如何添加實例屬性和靜態方法:大數據進階-讀懂vuejs源碼1vue

Vue實例化時調用_init,本文將深刻該方法內部作了哪些事情及vuejs如何實現數據響應式。node

Vue初始化

core/instance/index.js文件中定義了Vue的構造函數:react

function Vue (options) {
  // 執行_init方法,此方法在initMixin中定義
  this._init(options)
}

_init方法定義在core/instance/init.js中:web

Vue.prototype._init = function (options?: Object) {
    // 。。。
    // 1. 合併options
    if (options && options._isComponent) {
        // 此處有重要的事情作。
        initInternalComponent(vm, options)
    } else {
        vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
        )
    }
    // 2. 初始化屬性
    // 初始化$root,$parent,$children
    initLifecycle(vm)
    // 初始化_events
    initEvents(vm)
    // 初始化$slots/$scopedSlots/_c/$createElement/$attrs/$listeners
    initRender(vm)
    // 執行生命週期鉤子
    callHook(vm, 'beforeCreate')
    // 註冊inject成員到vue實例上
    initInjections(vm) // resolve injections before data/props
    // 初始化_props/methods/_data/computed/watch
    initState(vm)
    // 初始化_provided
    initProvide(vm) // resolve provide after data/props
    // 執行生命週期鉤子
    callHook(vm, 'created')

    // 3. 調用$mount方法
    if (vm.$options.el) {
        vm.$mount(vm.$options.el)
    }
}

在合併options的時候,若是options表示一個組件(_isComponent)則調用了initInternalComponent函數:express

export function initInternalComponent(vm: Component, options: InternalComponentOptions) {
    // 此處保留組件之間的父子關係,
    const parentVnode = options._parentVnode
    opts.parent = options.parent
    opts._parentVnode = parentVnode
    //...
}

此方法中設置了組件之間的父子關係,在後續的註冊及渲染組件的時候會用到。segmentfault

initProvide

定義在core/instance/inject.js文件中。數組

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

在上面的代碼中能夠看出,若是provide是一個函數,那麼會調用這個函數,並將this指向vm實例。因爲initProvide在_init方法中最後被調用,所以可以訪問到實例的屬性。閉包

initInjections

定義在core/instance/inject.js文件中。app

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    // 遍歷result屬性,利用Object.defineProperty將其添加到vue實例上
    // ...
  }
}

此方法調用resolveInject方法獲取全部inject值。框架

export function resolveInject(inject: any, vm: Component): ?Object {
    if (inject) {
        const result = Object.create(null)
        const keys = hasSymbol
            ? Reflect.ownKeys(inject)
            : Object.keys(inject)

        for (let i = 0; i < keys.length; i++) {
            // ....
            const provideKey = inject[key].from
            let source = vm
            while (source) {
                if (source._provided && hasOwn(source._provided, provideKey)) {
                    result[key] = source._provided[provideKey]
                    break
                }
                source = source.$parent
            }
            // ...
        }
        return result
    }
}

在resolveInject方法中會從當前實例出發,延着parent一直向上找,直到找到_provided中存在。

總結

此時整個Vue定義和初始化流程能夠總結爲以下:

image.png

數據響應式

vuejs框架的整個數據響應式實現過程比較複雜,代碼散落在各個文件中。咱們都知道,在定義組件的時候,組件會自動將data屬性中的數據添加上響應式監聽,所以咱們從_init方法中調用initState函數開始。

啓動監聽

在initState函數中:

export function initState (vm: Component) {
  // ...
  if (opts.data) {
    // 處理data數據
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // ...
}

options中的data數據會交由initData方法處理:

function initData(vm: Component) {
    // ... 1. 獲取data數據,若是data是一個函數,但沒有返回值,會提示錯誤。
    // ... 2. 遍歷data全部的屬性,首先判斷在props和methods是否同名,而後將其代理到vue實例上。
    // 3. 添加響應式數據監聽
    observe(data, true /* asRootData */)
}

添加監聽

observe函數

定義在core/observer/index.js文件中:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 經過__ob__屬性判斷該屬性是否添加過響應式監聽
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // 若是添加過,不作處理,直接返回
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 建立Observer實例,其爲響應式的核心
    ob = new Observer(value)
  }
  // 經過vmCount能夠判斷某個響應式數據是不是根數據,能夠理解爲data屬性返回的對象是根數據,若是data對象的某個屬性也是一個對象,那麼就再也不是根數據。
  // vmCount屬性後續會被用到
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

該方法的核心就是爲data數據建立Observer實例ob, ob對象會爲data添加getter/setter方法,其能夠用來收集依賴並在變化的時候觸發dom更新。

Observer類

定義在core/observer/index.js文件中,在其構造函數中,根據傳入data的類型(Array/Object),分別進行處理。

Object
constructor(value: any) {
    this.value = value
    // Observer實例上包含dep屬性,這個屬性後續會有很大做用,有些沒法監聽的數據變化能夠由此屬性完成
    this.dep = new Dep()
    this.vmCount = 0
    // 爲data添加__ob__屬性
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
        // ... 處理數組
    } else {
        // 處理對象
        this.walk(value)
    }
}
  • walk

遍歷data的全部屬性,調用defineReactive函數添加getter/setter。

walk(obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
        // 添加數據攔截
        defineReactive(obj, keys[i])
    }
}
  • defineReactive

數據響應式實現的核心方法,原理是經過Object.defineProperty爲data添加getter/setter攔截,在攔截中實現依賴收集和觸發更新。

export function defineReactive(
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
) {
    // 1. 建立閉包做用域內的Dep對象,用於收集觀察者,當數據發生變化的時候,會觸發觀察者進行update
    const dep = new Dep()
    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }
    // 2. 獲取對象描述中原有的get和set方法
    const getter = property && property.get
    const setter = property && property.set
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }
    let childOb = !shallow && observe(val)
    // 3. 添加getter/setter
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val
            // 靜態屬性target存儲的是當前觀察者。
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    // 將觀察者添加到Obsetver實例屬性dep中。
                    childOb.dep.depend()
                    if (Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val
            /* eslint-disable no-self-compare */
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            // ... 一些判斷,省略
            // 當賦值的時候,若是值爲對象,須要爲新賦值的對象添加響應式
            childOb = !shallow && observe(newVal)
            // 調用set就是爲屬性賦值,賦值說明有新的變化,因此要觸發更新
            dep.notify()
        }
    })
}

整個defineReactive有兩個地方比較難以理解:

  1. 經過Dep.target獲取依賴

因爲這個地方涉及到後面的編譯部分,因此咱們把這部分邏輯單獨拿出來,用一段簡短的代碼來描述整個過程,以下:

// 模擬Dep
let Dep = {}
Dep.target = null

// 模擬變化數據
let data = {
    foo: 'foo'
}

Object.defineProperty(data, 'foo', {
    get() {
        if (Dep.target) {
            console.log(Dep.target)
        }
    }
})

// 模擬編譯 {{foo}}
// 1. 解析到template中須要foo屬性的值
const key = 'foo'
// 2. 在foo屬性對應的值渲染到頁面以前,爲Dep.target賦值
Dep.target = () => {
    console.log('觀察foo的變化')
}
// 3. 獲取foo屬性的值,此時會觸發get攔截
const value = data[key]
// 4. 獲取完成後,須要將Dep.target的值從新賦值null,這樣下一輪解析的時候,可以存儲新的觀察者
Dep.target = null
  1. 在閉包做用域內已經包含了Dep對象,在set中經過此對象的notify方法觸發更新,爲何還須要在get方法中,將依賴添加到Observer對象的實例屬性dep中。

其實,這是爲了方便在其餘手動觸發更新,因爲defineReactive方法內部的dep對象是閉包做用域,在外部沒法直接訪問,只能經過賦值方式觸發。

若是在Observer對象上保存一份,那麼就能夠經過data.__ob__.dep的方式訪問到,直接手動調用notify方法就能夠觸發更新,在Vue.set方法內部實現就能夠這種觸發更新方式。

Array

衆所周知,Object.defineProperty是沒法監控到經過push,pop等方法改變數組,此時,vuejs經過另一種方式實現了數組響應式。該方式修改了數組原生的push,pop等方法,在新定義的方法中,經過調用數組對象的__ob__屬性的notify方法,手動觸發更新。

Observer構造函數中:

if (Array.isArray(value)) {
    if (hasProto) {
        // 支持__proto__,那麼就經過obj.__proto__的方式修改原型
        protoAugment(value, arrayMethods)
    } else {
        // 不支持,就將新定義的方法遍歷添加到數組對象上,這樣能夠覆蓋原型鏈上的原生方法
        copyAugment(value, arrayMethods, arrayKeys)
    }
    // 遍歷數組項,若是某項是對象,那麼爲該對象添加響應式
    this.observeArray(value)
}

其中arrayMethods就是從新定義的數組操做方法。

  • arrayMethods

定義在core/Observer/array.js文件中,該文件主要做了兩件事情:

  1. 建立新的集成自Array.prototype的原型對象arrayMethods。
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
  1. 在新的原型對象上,添加自定義方法覆蓋原生方法。
// 定義全部會觸發更新的方法
const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]


methodsToPatch.forEach(function (method) {
    // 獲取Array中原生的同名方法
    const original = arrayProto[method]
    // 經過Object.defineProperty爲方法調用添加攔截
    def(arrayMethods, method, function mutator(...args) {
        // 調用原生方法獲取本該獲得的結果
        const result = original.apply(this, args)
        const ob = this.__ob__
        let inserted
        // push,unshift,splice三個方法會向數組中插入新值,此處根據狀況獲取新插入的值
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
        }
        // 若是新插入的值是對象,那麼須要爲對象添加響應式,處理邏輯和data處理邏輯類似
        if (inserted) ob.observeArray(inserted)
        // 手動觸發更新
        ob.dep.notify()
        return result
    })
})

從上面的處理邏輯能夠看出,下面的數組操做能夠觸發自動更新:

// 修改數組項
[].push(1)
[].pop()
[].unshift(1)
[].shift()
[].splice()
// 修改數組項順序
[].sort()
[].reverse()

而下面的操做不能觸發:

// 修改數組項
[1, 2][0] = 3
[1, 2].length = 0

Dep

在添加數據監聽的過程當中用到了Dep類,Dep類至關於觀察者模式中的目標,用於存儲全部的觀察者和發生變化時調用觀察者的update方進行更新。

export default class Dep {
    // 當前須要添加的觀察者
    static target: ?Watcher;
    // id,惟一標識
    id: number;
    // 存儲全部的觀察者
    subs: Array<Watcher>;

    constructor() {
        this.id = uid++
        this.subs = []
    }

    // 添加觀察者
    addSub(sub: Watcher) {
        this.subs.push(sub)
    }

    // 移除觀察者
    removeSub(sub: Watcher) {
        remove(this.subs, sub)
    }

    // 調用觀察者的addDep方法,將目標添加到每個觀察者中,觀察者會調用addSub方法
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }

    // 將觀察者排序,而後依次調用update
    notify() {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
            // subs aren't sorted in scheduler if not running async
            // we need to sort them now to make sure they fire in correct
            // order
            subs.sort((a, b) => a.id - b.id)
        }
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}

Watcher

Watcher類是觀察者模式中的觀察者,當Dep觸發變化的時候,會調用內部存儲的全部Watcher實例的update方法進行更新操做。

在vuejs中,Watcher可大體分爲三種:Computed Watcher, 用戶Watcher(偵聽器)和渲染Watcher(觸發Dom更新)。

Watcher類包含大量的實例成員,在構造函數中,主要邏輯以下:

constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options ?: ? Object,
    isRenderWatcher ?: boolean
) {
    // ... 根據參數爲實例成員賦值
    // 調用get方法
    this.value = this.lazy
        ? undefined
        : this.get()
}

在get方法中,獲取初始值並將自身添加到Dep.target。

get() {
    // 1. 和下面的popTarget相對應,這裏主要是爲Dep.target賦值
    // 因爲存在組件之間的父子關係,因此在pushTarget中還會將當前對象存放到隊列中,方便處理完成子組件後繼續處理父組件
    pushTarget(this)
    let value
    const vm = this.vm
    try {
        // 2. 獲取初始值,並觸發get監聽,Dep會收集該Watcher
        value = this.getter.call(vm, vm)
    } catch (e) {
        if (this.user) {
            handleError(e, vm, `getter for watcher "${this.expression}"`)
        } else {
            throw e
        }
    } finally {
        // 實現deep深度監聽
        if (this.deep) {
            traverse(value)
        }
        // 3. 將Dep.target值變爲null
        popTarget()
        this.cleanupDeps()
    }
    return value
}
addDep

addDep方法用於將當前Watcher實例添加到Dep中。

addDep(dep: Dep) {
    const id = dep.id
    // 確保不會重複添加
    if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
            // 調用dep的addSub方法,將Watcher實例添加到Dep中
            dep.addSub(this)
        }
    }
}
update

update主要處理兩種狀況:

  1. 若是是用戶添加的監聽器,在變化的時候會執行run方法。
  2. 若是是渲染Dom時添加的,在變化的時候會執行queueWatcher函數,在queueWatcher函數中,經過隊列的方式批量執行更新。
update() {
    if (this.lazy) {
        this.dirty = true
    } else if (this.sync) {
        // 用戶添加的監聽器會執行run方法
        this.run()
    } else {
        // 觸發dom更新會執行此方法, 以隊列方式執行update更新
        queueWatcher(this)
    }
}
run

run方法主要用於在數據變化後,執行用戶傳入的回調函數。

run() {
    if (this.active) {
        // 1. 經過get方法獲取變化後的值
        const value = this.get()
        if (
            value !== this.value ||
            isObject(value) ||
            this.deep
        ) {
            // 2. 獲取初始化時保存的值做爲舊值
            const oldValue = this.value
            this.value = value
            if (this.user) {
                try {
                    // 3. 調用用戶定義的回調函數
                    this.cb.call(this.vm, value, oldValue)
                } catch (e) {
                    handleError(e, this.vm, `callback for watcher "${this.expression}"`)
                }
            } else {
                this.cb.call(this.vm, value, oldValue)
            }
        }
    }
}

生成渲染Watcher

在查找編譯入口那部分講到了platforms/web/runtime/index.js文件定義了$mount方法,此方法用於首次渲染Dom。

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

其內部執行了mountComponent函數。

mountComponent

定義在core/instance/lifecycle.js文件中,該函數主要執行三塊內容:

  1. 觸發beforeMount,beforeUpdatemounted生命週期鉤子函數。
  2. 定義updateComponent方法。
  3. 生成Watcher實例,傳入updateComponent方法,此方法會在首次渲染和數據變化的時候被調用。
export function mountComponent(
    vm: Component,
    el: ?Element,
    hydrating?: boolean
): Component {
    // ... 1. 觸發生命週期鉤子
    // 2. 定義updateComponent方法
    let updateComponent
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        updateComponent = () => {
            // ...
            vm._update(vnode, hydrating)
            // ...
        }
    } else {
        updateComponent = () => {
            vm._update(vm._render(), hydrating)
        }
    }

    // 生成watcher實例
    new Watcher(vm, updateComponent, noop, {
        before() {
            if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, 'beforeUpdate')
            }
        }
    }, true /* isRenderWatcher */)
    hydrating = false

    // ... 觸發生命週期鉤子
    return vm
}

_update, _render

_update, _render是Vue的實例方法, _render方法用於根據用戶定義的render或者模板生成的render生成虛擬Dom。_update方法根據傳入的虛擬Dom,執行patch,進行Dom對比更新。

總結

至此,響應式處理的整個閉環脈絡已經摸清。

image.png

相關文章
相關標籤/搜索