Vue源碼解析02-數據響應式

Vue源碼解析02-數據響應式

開篇之前先了解幾個相關概念

MVC模式

模式簡介

MVC的全稱是Model(模型)-View(視圖)-Controller(控制器)

MVC

  • Model:這是數據層,存儲項目所需的數據。Model的作用是返回或者更新數據。在應用中常用於數據庫存儲數據。

  • View:視圖層,用於想用戶顯示數據。View本身不顯示任何數據,而是Controller或者Model讓View顯示數據。

  • Controller:控制層,MVC的核心部分。控制器相當於用戶和系統的鏈接,接收用戶的輸入,處理完成後,要求Model更新數據。

MVP模式

MVP模式將Controller更名爲Presenter,同時改變了通訊方向。全稱是Model(模型)-Presenter(呈現器)-View(視圖)。

MVP

  • MVP中的數據通信均是雙向的。

  • Model:數據層。

  • View:視圖層。

  • Presenter:Presenter層。Pressenter作爲View和Model的中間層起到了橋樑的作用。Presenter從Model層獲取數據通過接口發送給View層展示。View層將用戶操作發送給Presenter,藉由Presenter將數據發送給Model進行數據更新。

MVVM模式

MVVP模式將Presenter更名爲View-Model(對應MVC中的C-Controller),基本上於MVP模式一致。但是MVVM採用的是雙向數據綁定,View的變動自動反應到ViewModel上。

MVVM

  • Model:數據層。

  • View:視圖層。

  • ViewModel:在vue中指的是vue的實例對象,是一個公開公共屬性和命令的抽象view;

  • View中的變化會自動更新到ViewModel,ViewModel的變化也會自動反應到View視圖中。這種自動更新是通過vue中的Observer觀察者實現的。

Vue的雙向數據綁定原理

數據雙向綁定的意思是view的變化能反應到ViewModel中,ViewModel中的變化能同步更新View視圖。

Vue雙向數據綁定的原理是數據劫持+訂閱發佈模式實現

數據劫持

數據劫持指的是在訪問或者修改某個屬性時,通過Object.defineProperty()或者Proxy對象攔截這個行爲,擴展額外的操作和行爲然後返回結果。

  • Vue2中使用的是Object.defineProperty(),Vue3中使用的是Proxy對象的方式。

訂閱發佈模式

定義:對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有與該對象產生依賴關係的對象都會接收到通知。

訂閱發佈模式

  • 優點:耦合性低,便於維護

  • 缺點:創建訂閱者可能會消耗一定時間和內存,但是訂閱事件不一定會發生,訂閱者則會一直存在於內存中。

Vue的雙向數據綁定源碼解析

源碼分析入口點

在之前的源碼分析中我們知道,Vue的構造函數中實現了一個_init()方法,這個方法是用來初始化一些選項的:

    function Vue (options) {
        if (process.env.NODE_ENV !== 'production' &&
            !(this instanceof Vue)
        ) {
            warn('Vue is a constructor and should be called with the `new` keyword')
        }
        //這裏初始化了Vue傳入的選項
        this._init(options)
    }

上面的_init(options)方法的實現在src/core/instance/init.js中的initMixin(Vue)中,分析其中的源碼,我們可以根據方法名稱做一下簡單的判斷:

    export function initMixin (Vue: Class<Component>) {
        Vue.prototype._init = function (options?: Object) {
            const vm: Component = this
            // a uid
            vm._uid = uid++

            let startTag, endTag
            /* istanbul ignore if */
            if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            startTag = `vue-perf-start:${vm._uid}`
            endTag = `vue-perf-end:${vm._uid}`
            mark(startTag)
            }

            // 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
            )
            }
            /* istanbul ignore else */
            if (process.env.NODE_ENV !== 'production') {
            initProxy(vm)
            } else {
            vm._renderProxy = vm
            }
            // expose real self
            vm._self = vm
            // 初始化聲明週期,跟生命週期有關
            initLifecycle(vm)
            // 初始化事件,實現處理父組件傳遞的監聽事件的監聽器
            initEvents(vm)
            // 初始化渲染器$slots scopedSlots、_c、$createElement
            initRender(vm)
            // 調用生命週期鉤子函數
            callHook(vm, 'beforeCreate')
            // 獲取注入的數據
            initInjections(vm) // resolve injections before data/props
            // 初始化狀態props、methods、data、computed、watch
            initState(vm)
            // 提供數據
            initProvide(vm) // resolve provide after data/props
            // 調用生命週期鉤子函數
            callHook(vm, 'created')

            /* istanbul ignore if */
            if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            vm._name = formatComponentName(vm, false)
            mark(endTag)
            measure(`vue ${vm._name} init`, startTag, endTag)
            }

            // 如果存在el則執行$mount
            if (vm.$options.el) {
            vm.$mount(vm.$options.el)
            }
        }
    }

通過分析initMixin(Vue)我們可以得到一個大概的結果,那就是在Vue初始化的時候我們完成了生命週期、事件、渲染器、狀態的初始化,同時還獲取了祖先組件注入的數據,同時爲後代組件的注入提供了數據。
其中initState(vm)方法則是對數據還有其他東西的一些初始化操作,跳入該方法中查看一下其內容src/core/instance/state.js

  • 我們逐行分析一下該方法的實現把:
    export function initState (vm: Component) {
        // 在當前實例中創建了一個watcher的空數組
        vm._watchers = []
        // 保存了當前Vue實例的選項options
        const opts = vm.$options
        // 如果選項中的props存在的化則初始化props
        if (opts.props) initProps(vm, opts.props)
        // 如果選項中methods存在則初始化方法
        if (opts.methods) initMethods(vm, opts.methods)
        // 如果選項中的data存在則進行data的初始化操作
        // data的處理,響應化處理
        if (opts.data) {
            // 一般情況下我們初始化Vue實例的時候都會傳入data,所以大部分情況是走這個方法的
            // 初始化data
            initData(vm)
        } else {
            // 數據響應化
            observe(vm._data = {}, true /* asRootData */)
        }
        // 初始化computed
        if (opts.computed) initComputed(vm, opts.computed)
        if (opts.watch && opts.watch !== nativeWatch) {
            // 初始化watch
            initWatch(vm, opts.watch)
        }
    }

初始化數據initData

分析上面這段代碼我們可以知道,在initState(vm)中是對數據進行了初始化的(先考慮傳入data的情況),那麼我們繼續順着代碼往下看:

    function initData (vm: Component) {
        // 取出data
        let data = vm.$options.data
        // 判斷data是否爲方法,如果是一個方法,則處理完畢後返回
        data = vm._data = typeof data === 'function'
            ? getData(data, vm)
            : data || {}
            //如果data不是一個object則警告
        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
        // 分別取出data的key,選項中的props、methods
        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)) {
                proxy(vm, `_data`, key)
            }
        }
        // observe data
        // 數據的響應化,遍歷開始
        observe(data, true /* asRootData */)
    }
  • 上面代碼,我們發現了一個核心的方法:observe(),這個方法曾在initState()中出現過,所以,我們初始化數據的方法歸根結底會落在observe()上面代碼,我們發現了一個核心的方法:observe

observe:返回一個Observer

廢話不多說,直接上代碼:from src/core/observer/index.js

    //該方法,接收了vue實例中的data數據,和一個boolean,返回了一個Observer(觀察者)實例
    export function observe (value: any, asRootData: ?boolean): Observer | void {
        //如果data數據不是一個對象或者是一個虛擬domVNode,直接結束
        if (!isObject(value) || value instanceof VNode) {
            return
        }
        let ob: Observer | void
        // 如果當前對象已經存在observer則返回
        if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
            // 已經存在的Observer則會保存在value.__ob__中
            ob = value.__ob__
        } else if (
            shouldObserve &&
            !isServerRendering() &&
            (Array.isArray(value) || isPlainObject(value)) &&
            Object.isExtensible(value) &&
            !value._isVue
        ) {// 否則,我們要新創建一個Observer將其返回
            ob = new Observer(value)
        }
        if (asRootData && ob) {
            ob.vmCount++
        }
        // 無論結果如何,最終都會返回一個observer
        return ob
    }
  • 通過分析上述代碼我們可以得出一個結論**observe()**的作用就是返回一個Observer的實例。

Observer:判斷data類型做處理

所以接下來我們要看一下Observer的構造方法完成了什麼事情fromsrc/core/observer/index.js

    //Observer的構造方法
    // 接收傳入的Vue中的data數據
    constructor (value: any) {
        this.value = value
        // 新建了一個Dep的實例,這個Dep是用來做依賴收集的,後面會用到
        this.dep = new Dep()
        //當前的vmCount數量
        this.vmCount = 0
        // 給當前data對象定義了一個__ob__屬性
        def(value, '__ob__', this)
        // 判斷當前對象是否爲數組,如果是數組單獨處理,
        if (Array.isArray(value)) {
            if (hasProto) {
                protoAugment(value, arrayMethods)
            } else {
                copyAugment(value, arrayMethods, arrayKeys)
            }
            this.observeArray(value)
        } else {// 如果不是數組的話
            //普通對象則用walk遍歷
            this.walk(value)
        }
    }

    walk (obj: Object) {
        // 拿出obj(data對象)的key
        const keys = Object.keys(obj)
        // 對所有的key進行遍歷
        for (let i = 0; i < keys.length; i++) {
            // 實現數據響應式
            // 傳入data對象和當前key,進行響應化處理
            defineReactive(obj, keys[i])
        }
    }

    /**
    * Observe a list of Array items.
    */
    observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i])
        }
    }
  • 分析Observer的構造函數,我們知道了Observer的核心功能:判斷data對象是不是一個數組,根據判斷進行不同的響應化處理

當data爲對象時

  • 當data爲對象時,walk()方法遍歷data的key,執行defineReactive()方法,下面看一下defineReactive方法的實現,在src/core/observer/index.js
export function defineReactive (
  obj: Object,// 接收一個object,就是vue實例的data
  key: string,// data的key
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 這裏傳進來的是data對象,所以沒有一個data對象就有一個dep與之對應
  const dep = new Dep()

// 查看對象上是否有該屬性,如果沒有則停止執行
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

// 獲取屬性的getter和setter
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  // 如果當前值是一個對象,則遞歸調用
  let childOb = !shallow && observe(val)
  // 定義數據的攔截
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 依賴收集,這裏的Dep.target是watcher的實例
      if (Dep.target) {
        dep.depend()// 追加依賴關係,簡單來說就是將watcher加入到dep中,但是實際操作要複雜一點
        // 如果childOb存在,說明該屬性是一個對象
        if (childOb) {
          // 繼續追加依賴
          childOb.dep.depend()
          // 如果是數組,繼續處理
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
        // 如果getter存在則調用getter否則返回當前val
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 如果新值和老值相同
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      // 如果setter存在則執行setter執行更新,否則用新值覆蓋老值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        // 更新本地的val
        val = newVal
      }
      // 如果新傳入的val是一個數組的化,則遞歸進行響應話處理observe
      childOb = !shallow && observe(newVal)
      // 通知更新
      dep.notify()
    }
  })
}
  • 簡單總結一下defineReactive()
    ♣ 通過Object.defineProperty方法對data屬性的set和get方法進行數據劫持
    ♣ 創建Dep實例,每有一個data屬性則有一個dep與之對應
    ♣ 擴展了data屬性的get方法,將Dep.target靜態屬性中的watcher加入到dep實例中(依賴收集過程)
    ♣ 擴展了data屬性中的set方法,當數據被更新時,執行dep.notify()方法通知數據更新

  • 所以我們順着代碼看一下依賴收集的過程和通知更新的方法

依賴收集

依賴收集的過程通過**dep.depend()**完成,我們來看一下它的實現from src\core\observer\dep.js,由於代碼較多,我們只粘貼關鍵代碼

export default class Dep{
    static target: ?Watcher;// 靜態屬性中的watcher實例
    id: number;
    subs: Array<Watcher>;// 維護了一個watcher數組
    depend () {
      // 這裏的Dep.target是watcher的實例,
    if (Dep.target) {
      // 建立和watcher之間的關係,將當前Dep實例加入watcher中
      Dep.target.addDep(this)
    }
  }
  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++) {
      // 通知watcher進行數據更新
      subs[i].update()
    }
  }
}
    // 將當前watcher實例賦值到Dep.target的靜態屬性上
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  // 將watcher實例賦值到Dep的靜態屬性上
  Dep.target = target
}


  • 總結dep的作用:維護了一個watcher數組,實現了watcher的增加刪除和通知更新

上面代碼中的Dep.target.addDep(this),是將當前的Dep實例加入到了watcher實例中,這裏有一個細節:理論上是每有一個Data則有一個Dep,當同一個Data多次被調用的時候,只需要創建多個watcher對其進行監聽,然後Dep進行依賴收集,通知watcher更新,所以理論上Dep和watcher是一對多的關係.
但是上面的代碼是將Dep實例添加到了Wtacher中,所以這就形成了多對多的關係.出現這種情況是因爲真正使用的時候,有的時候一個組件

Watcher的實現

這裏的Watcher主要是講Render Watcher,組件實例化的時候會產生一個Watcher的實例,在組件$mount過程中的mountComponent()方法中new Watcher:
這裏只粘貼部分核心代碼

// 定義組件更新函數
    // _render()執行可以獲得虛擬dom,VNode
    // _update()將虛擬DOM轉換爲真實DOM
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    // 創建Watcher實例
  //vm:當前vue實例
  //updateComponent:組件更新函數
  // noop:
  // {}:回調函數
  //true:是否是瀏覽器的watcher

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

上面的updateComponent()函數調用了Vue._render函數,最終會調用data屬性的get函數,最終完成依賴收集。

  • Watcher對象的實現:

只粘貼部分核心代碼

    constructor(){
        // watcher創建的時候會執行當前watcher實例的get函數,這樣會出發依賴收集的過程
    this.value = this.lazy
      ? undefined
      : this.get()
    }

    /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    //將當前watcher實例賦值到Dep.target靜態屬性中
    pushTarget(this)
    let value
    // 當前vue實例
    const vm = this.vm
    try {
      // getter函數是上面的updateComponent()函數,會觸發依賴收集過程
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      // 清空Dep.target靜態屬性中的watcher實例
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
   //依賴收集
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // 維護了一個depid映射關係
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      // 如果當前dep裏面沒有watcher,則將該watcher加入到dep中建立聯繫
      if (!this.depIds.has(id)) {
          // dep中維護了一個watcher的數組
        // 將當前watcher加入到dep中的watcher數組中,實現dep對watcher的收集
        dep.addSub(this)
      }
    }
  }
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
   // 實現數據更新
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
        // 將當前watcher實例push到更新隊列中實現數據的更新
      queueWatcher(this)
    }
  }
  • 總結watcher的作用:解析傳入的updateComponent更新函數並進行依賴收集。每個組件都會有一個Watcher與之對應,數值變化會觸發更新函數進行重新渲染。

當data爲數組時,進行數組響應化處理

根據Observer的構造方法得知,當data爲數組時

    constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 給當前對象定義一個__ob__屬性
    def(value, '__ob__', this)
    // 判斷當前data對象是否爲數組
    if (Array.isArray(value)) {
      if (hasProto) {
          // 覆蓋數組的原型方法
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 如果是數組則進行數組響應化的處理
      this.observeArray(value)
    } else {
      //普通對象則用walk遍歷
      this.walk(value)
    }
  }
  // 數組響應化處理
  observeArray (items: Array<any>) {
      //遍歷數組
    for (let i = 0, l = items.length; i < l; i++) {
        // 取出數組的每一項進行響應化處理
      observe(items[i])
    }
  }

上述代碼的核心功能主要是,protoAugment()方法擴展了當前data數組的原型方法,arrayMethods
直接上核心代碼

    //src/core/observer/array.js:
    const arrayProto = Array.prototype
    export const arrayMethods = Object.create(arrayProto)
    const methodsToPatch = [
        'push',
        'pop',
        'shift',
        'unshift',
        'splice',
        'sort',
        'reverse'
    ]

    /**
    * Intercept mutating methods and emit events
    */
    methodsToPatch.forEach(function (method) {
        // cache original method
        // 取出數組的原型方法
        const original = arrayProto[method]
        // 攔截,添加額外行爲
        // arrayMethods:數組的原型對象,定義特殊方法
        def(arrayMethods, method, function mutator (...args) {
            // 執行原先的任務
            const result = original.apply(this, args)
            // 額外任務:通知更新
            // 從this.__ob__中取出觀察者
            const ob = this.__ob__
            let inserted
            // 以下三個操作需要額外處理
            // 如果是新添加的元素,還需要額外的做響應化的處理
            switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
            }
            // 如果inserted存在說明元素是新添加的,額外響應化的處理
            if (inserted) ob.observeArray(inserted)
            // notify change
            // 核心:添加通知更新方法,每一個ob中都有一個dep和這個對象或者數組對應
            ob.dep.notify()
            return result
        })
    })
  • 總結一下數組的響應化實現:通過擴展了數組原型的七個方法,實現了數組每一項的響應化,從而實現數組的響應化。

♣ 注意,通過上面的代碼我們可以看出,只有通過擴展的這七個方法才能實現數組的響應化:pop、push、shift、unshift、splice、sort、reverse

總結

因爲是自己邊分析源碼邊寫的一些東西,所以可能有點亂。爲了捋清思路做了張圖片,聊勝於無吧:

源碼加載運行流程
數據響應化

下面附上一張官方數據響應化的工作流程圖:

官方
圖片來源:https://vuejs.org/v2/guide/reactivity.html

響應式的基本機制:

  • 通過Object.defineProperty()進行數據劫持,擴展對象屬性的set和get方法
  • watcher執行getter方法觸發對象屬性的get方法進行依賴收集
  • 輸入寫入時觸發對象屬性的set方法,dep發佈通知,watcher進行數據更新

附上一張我理解的數據響應化流程圖:
數據響應化流程

以上,根據自己對源碼的理解,和網上一些大神的分析整理出來的,如有不對的地方,歡迎各位大神指正。

發佈了18 篇原創文章 · 獲贊 2 · 訪問量 3504
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章