Vue原始碼簡析(版本vue-2.4.4)

litongqian發表於2018-05-04

在開始之前,閱讀原始碼你需要有紮實的基本功,還要有耐心,能把握全域性,不要扣細節!

一、先看看目錄

├── build --------------------------------- 構建相關的檔案
├── dist ---------------------------------- 構建後檔案的輸出目錄
├── examples ------------------------------ 存放使用Vue開發的的例子
├── flow ---------------------------------- 型別宣告,使用開源專案 [Flow](https://flowtype.org/)
├── package.json -------------------------- 專案依賴
├── test ---------------------------------- 包含所有測試檔案
├── src ----------------------------------- 這個是我們最應該關注的目錄,包含了原始碼
│   ├──platforms --------------------------- 包含平臺相關的程式碼
│   │   ├──web -----------------------------  包含了不同構建的包的入口檔案
│   │   |   ├──entry-runtime.js ---------------- 執行時構建的入口,輸出 dist/vue.common.js 檔案,不包含模板(template)到render函式的編譯器,所以不支援 `template` 選項,我們使用vue預設匯出的就是這個執行時的版本。大家使用的時候要注意
│   │   |   ├── entry-runtime-with-compiler.js -- 獨立構建版本的入口,輸出 dist/vue.js,它包含模板(template)到render函式的編譯器
│   ├── compiler -------------------------- 編譯器程式碼的存放目錄,將 template 編譯為 render 函式
│   │   ├── parser ------------------------ 存放將模板字串轉換成元素抽象語法樹的程式碼
│   │   ├── codegen ----------------------- 存放從抽象語法樹(AST)生成render函式的程式碼
│   │   ├── optimizer.js ------------------ 分析靜態樹,優化vdom渲染
│   ├── core ------------------------------ 存放通用的,平臺無關的程式碼
│   │   ├── observer ---------------------- 反應系統,包含資料觀測的核心程式碼
│   │   ├── vdom -------------------------- 包含虛擬DOM建立(creation)和打補丁(patching)的程式碼
│   │   ├── instance ---------------------- 包含Vue建構函式設計相關的程式碼
│   │   ├── global-api -------------------- 包含給Vue建構函式掛載全域性方法(靜態方法)或屬性的程式碼
│   │   ├── components -------------------- 包含抽象出來的通用元件
│   ├── server ---------------------------- 包含服務端渲染(server-side rendering)的相關程式碼
│   ├── sfc ------------------------------- 包含單檔案元件(.vue檔案)的解析邏輯,用於vue-template-compiler包
│   ├── shared ---------------------------- 包含整個程式碼庫通用的程式碼複製程式碼

二、Vue 的建構函式是什麼樣的

使用 new 操作符來呼叫 VueVue是一個建構函式,瞭解了目錄結構,下面來看下下入口檔案

開啟package.jsonVue原始碼簡析(版本vue-2.4.4)

當我們執行npm run dev,看看幹了啥,rollup也是類似webpack的打包工具,根據

TARGET=web-full-dev

去build/config.js查詢
Vue原始碼簡析(版本vue-2.4.4)

開啟入口檔案找到了web/entry-runtime-with-compiler.js 

Vue原始碼簡析(版本vue-2.4.4)

依照以上查詢路徑,我們找到了Vue建構函式

Vue原始碼簡析(版本vue-2.4.4)

定義了建構函式,引入依賴,呼叫初始化函式,最後匯出Vue

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue複製程式碼

開啟這五個檔案,找到相應的方法,你會發現,這些方法的作用,就是在 Vue 的原型 prototype 上掛載方法或屬性

1. 先進入 initMixin(Vue),在prototype上掛載

Vue.prototype._init = function (options) {} 
複製程式碼

2. 進入 stateMixin(Vue),在prototype上掛載 

Vue.prototype.$data 
Vue.prototype.$props 
Vue.prototype.$set = set 
Vue.prototype.$delete = del 
Vue.prototype.$watch = function(){} 
複製程式碼

3. 進入eventsMixin(Vue),在prototype上掛載 

Vue.prototype.$on 
Vue.prototype.$once 
Vue.prototype.$off 
Vue.prototype.$emit
複製程式碼

4.進入lifecycleMixin(Vue),在prototype上掛載 

Vue.prototype._update 
Vue.prototype.$forceUpdate 
Vue.prototype.$destroy  複製程式碼

5. 最後進入renderMixin(Vue),在prototype上掛載 

Vue.prototype.$nextTick 
Vue.prototype._render 
Vue.prototype._o = markOnce 
Vue.prototype._n = toNumber 
Vue.prototype._s = toString 
Vue.prototype._l = renderList 
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual 
Vue.prototype._i = looseIndexOf 
Vue.prototype._m = renderStatic 
Vue.prototype._f = resolveFilter 
Vue.prototype._k = checkKeyCodes 
Vue.prototype._b = bindObjectProps 
Vue.prototype._v = createTextVNode 
Vue.prototype._e = createEmptyVNode 
Vue.prototype._u = resolveScopedSlots 
Vue.prototype._g = bindObjectListeners
複製程式碼

根據上面的查詢路徑下一步到src/core/index.js

引入依賴,在Vue上掛載靜態方法和屬性

  1. import { initGlobalAPI } from './global-api/index'
    import { isServerRendering } from 'core/util/env'
    
    initGlobalAPI(Vue)
    
    Object.defineProperty(Vue.prototype, '$isServer', {
      get: isServerRendering
    })
    
    Object.defineProperty(Vue.prototype, '$ssrContext', {
      get () {
        /* istanbul ignore next */
        return this.$vnode && this.$vnode.ssrContext
      }
    })
    
    Vue.version = '__VERSION__'
    
    export default Vue複製程式碼

進入initGlobalAPI(Vue),在Vue上掛載靜態屬性和方法

Vue.config Vue.util = util
Vue.set = set 
Vue.delete = del 
Vue.nextTick = util.nextTick 
Vue.options = { 
 components: { KeepAlive }, 
 directives: {}, 
 filters: {}, 
 _base: Vue 
} 
Vue.use 
Vue.mixin 
Vue.cid = 0 
Vue.extend 
Vue.component = function(){} 
Vue.directive = function(){} 
Vue.filter = function(){} 
複製程式碼

接著掛載

Vue.prototype.$isServer 
Vue.version = '__VERSION__'
複製程式碼


Vue原始碼簡析(版本vue-2.4.4)

根據上面的查詢路徑下一步到runtime/index.js,安裝平臺特有的工具

Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// 安裝平臺特定的 指令 和 元件
Vue.options = {
    components: {
        KeepAlive,
        Transition,
        TransitionGroup
    },
    directives: {
        model,
        show
    },
    filters: {},
    _base: Vue
}
Vue.prototype.__patch__
Vue.prototype.$mount複製程式碼

根據上面的查詢路徑最後一步到web/entry-runtime-with-compiler.js

  1. 快取來自 web-runtime.js 檔案的 $mount 函式,const mount = Vue.prototype.$mount
  2. 在 Vue 上掛載 compile然後覆蓋覆蓋了 Vue.prototype.$mount
  3. Vue.compile = compileToFunctions 就是將模板 template 編譯為render函式。

到這整個Vue建構函式就還原了


三、通過一個例子說明整個流程

index.html

 <!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue.js grid component example</title>
    </head>
  <body>
    <div id="app">
        <ol>
          <li v-for="todo in todos">
            {{ todo.text }}
          </li>
        </ol>
        <Child></Child>
    </div>
  </body>
</html>
複製程式碼

grid.js

let Child = {
	data: function() {
	  return {child: '你好哈'}
	},
	template: '<div>{{child}}</div>'
}
new Vue({
	el: '#app',
	data: {
		todos: [
                    {text: '學習 JavaScript'}, 
                    {text: '學習 Vue'}, 
                    {text: '整個牛專案'}]
	},
	components: {'Child': Child}
})複製程式碼

想要知道Vue都幹了什麼,首先檢視建構函式function Vue (options) { this._init(options) }

  • new Vue({//傳入上面的內容 }) ,首先進入Vue.prototype._init,建構函式第一個掛載的方法

Vue.prototype._init = function (options) {
    const vm= this
    vm._uid = uid++
    let startTag, endTag
    vm._isVue = true

    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    vm._renderProxy = vm
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) 
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
複製程式碼

_init() 方法在一開始的時候,在 this 物件上定義了兩個屬性:_uid_isVue,然後判斷有沒有定義 options._isComponent這裡會走 else 分支,也就是這段程式碼:

vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )mergeOptions使用策略模式合併傳入的options和Vue.options複製程式碼

mergeOptions使用策略模式合併傳入的options和Vue.options合併後的程式碼結構, 可以看到通過合併策略components,directives,filters繼承了全域性的, 這就是為什麼全域性註冊的可以在任何地方使用,因為每個例項都繼承了全域性的, 所以都能找到

Vue原始碼簡析(版本vue-2.4.4)

      接著呼叫了initLifecycleinitEventsinitRender、initState,且在 initState 前後分別回撥了生命週期鉤子 beforeCreatecreated,看到這裡,也就明白了為什麼 created 的時候不能操作DOM了。因為這個時候還沒有渲染真正的DOM元素到文件中。created 僅僅代表資料狀態的初始化完成。重點看下initState()

initState (vm) {
  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)
  }
}複製程式碼

通過initData看Vue的資料響應系統,由於只是傳了data,所以執行initData(vm)

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]
      proxy(vm, `_data`, key)
  }
  observe(data, true /* asRootData */)
}
複製程式碼

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)
}複製程式碼

取出data中的key,進行迴圈,通過proxy代理,可以直接通過 this.屬性 訪問data中的值,在例項物件上對資料進行代理,這樣我們就能通過this.todos 來訪問 data.todos 了 接下啦,正式的資料響應系統observe(data, true /* asRootData */),將資料通過Object.defineProperty進行get,set處理,使得資料響應

class Observer {
  constructor(value) {
    this.value = value 
    this.dep = new Dep() 
    this.vmCount = 0 
    def(value, '__ob__', this) 
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  walk(obj: Object) {
    const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
} 
複製程式碼

Observer 類中,我們使用 walk 方法對資料data的屬性迴圈呼叫 defineReactive 方法,defineReactive 方法很簡單,僅僅是將資料data的屬性轉為訪問器屬性,並對資料進行遞迴觀測,否則只能觀測資料data的直屬子屬性。這樣我們的第一步工作就完成了,當我們修改或者獲取data屬性值的時候,通過 getset 即能獲取到通知。

function defineReactive (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  const dep = new Dep()
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      val = newVal
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
複製程式碼

  1. 其中 let childOb = !shallow && observe(val),進行遞迴呼叫,將所有data資料包含子集進行get set化,進行響應
  2. 其中在Observe類中,如果屬性是陣列,會進行改造

    if (Array.isArray(value)) {
          const augment = hasProto
            ? protoAugment
            : copyAugment
          augment(value, arrayMethods, arrayKeys)
          this.observeArray(value)
        } else {
          this.walk(value)
        }export const arrayMethods = Object.create(arrayProto)
    ;[
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    .forEach(function (method) {
      const original = arrayProto[method]
      def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        const ob = this.__ob__
        let inserted
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args
            break
          case 'splice':
            inserted = args.slice(2)
            break
        }
        if (inserted) ob.observeArray(inserted)
        // notify change
        ob.dep.notify()
        return result
      })
    })
    複製程式碼

重寫陣列中的上述方法,當在對陣列進行這些操作時,ob.dep.notify(),通知相應的改變


initData(vm)執行完成,相應系統就完成了,這時候執行callHook(vm, 'created') 觸發created,繼續回到_init(),執行到 vm.$mount(vm.$options.el)

Vue.prototype._init = function (options) {
    const vm= this
    vm._uid = uid++
    let startTag, endTag
    vm._isVue = true

    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    vm._renderProxy = vm
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) 
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }複製程式碼

進入$mount會先獲取掛載el節點,然後先判斷有沒有傳入render方法,沒有在去找有沒有傳入template,

本例都沒有,就會去取
getOuterHTML(el)作為當期模板

Vue.prototype.$mount = function (
  el,
  hydrating
){
  el = el && query(el)
  const options = this.$options
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}
複製程式碼

有了模板經過compileToFunctions,將模板編譯為ast語法樹,經過靜態優化, 最後處理成render函式,本例中的render函式,使用了with(this), 將this作用域提前, {{ todo.text }} 所以我們可以直接在模板中使用屬性,不加this! 當然加上this也是可以的 解除了我的很多迷惑,為什麼模板中可以不加this(react中的render是要使用this的), 其中的v-for轉換成_l,根據之前的Vue.prototype._l = renderList

function() {
    with (this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        }, [_c('ol', _l((this.todos), function(todo) {
            return _c('li', [_v("\n            " + _s(todo.text) + "\n          ")])
        })), _v(" "), _c('child')], 1)
    }
}複製程式碼

  • 生成了render函式後接著進入mountComponent,
  • 首先呼叫了
    beforeMount函式,
  • 接著執行vm._watcher = new Watcher(vm, updateComponent, noop)
  • 最後callHook(vm, 'mounted'),執行mounted,所以在mounted執行之前就已經掛載到dom上

所以重點 vm._watcher = new Watcher(vm, updateComponent, noop)

function mountComponent (
  vm,
  el,
  hydrating
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }

  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
複製程式碼

檢視Watcher程式碼

class Watcher {
  constructor (
    vm,
    expOrFn,
    cb,
    options
  ) {
    this.vm = vm
    vm._watchers.push(this)

    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    return value
  }
  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(this)
      }
    }
  }
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}
複製程式碼

執行建構函式由於 this.lazy=false; this.value = this.lazy ? undefined : this.get();

執行get方法 其中pushTarget(this),給Dep.target新增靜態屬性this(當前new Watcher()例項 )

 function pushTarget (_target) { 

 if (Dep.target) targetStack.push(Dep.target) 

 Dep.target = _target

 }

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    return value
  }
複製程式碼

接著執行 this.getter.call(vm, vm)複製程式碼
this.getter就是
updateComponent = () => {
vm._update(vm._render(), hydrating)
}      複製程式碼
  1. 開始先呼叫vm._render()

  Vue.prototype._render = function (){
    const vm = this
    const {
      render,
      staticRenderFns,
      _parentVnode
    } = vm.$options

    let vnode = render.call(vm._renderProxy, vm.$createElement)
    return vnode
  }
複製程式碼

開始執行之前編譯好的render函式了,在執行render函式時,通過獲取todos屬性等,觸發相應

function() {
    with (this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        }, [_c('ol', _l((this.todos), function(todo) {
            return _c('li', [_v("\n            " + _s(todo.text) + "\n          ")])
        })), _v(" "), _c('child')], 1)
    }
}複製程式碼

的get方法,這個時候Dep.target已經存在靜態屬性,Watcher例項了

所以相應的dep例項就會收集對應的Watcher例項了

複製程式碼

執行完之後返回vnode,

Vue原始碼簡析(版本vue-2.4.4)


updateComponent = () => {
vm._update(vm._render(), hydrating)
} 
其中vm._render()執行render函式返回vnode作為引數
接下來執行vm._update
這是首次渲染,所以執行
vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false,
        vm.$options._parentElm,
        vm.$options._refElm
      )複製程式碼

Vue.prototype._update = function (vnode, hydrating) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    if (!prevVnode) {
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      vm.$options._parentElm = vm.$options._refElm = null
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }
複製程式碼

vm.__patch__( vm.$el, vnode, hydrating, false, vm.$options._parentElm, vm.$options._refElm ) 根據vnode中樹,建立對應元素,插入到父節點中,通過對vnode遞迴迴圈建立所有子節點 插入到父節點中 其中如果遇到子元素是元件,例如本例中Child,會建立對應VueComponent的例項,執行 和new Vue()一樣的流程

如果還沒有 prevVnode 說明是首次渲染,直接建立真實DOM。如果已經有了 prevVnode 說明不是首次渲染,那麼就採用 patch 演算法進行必要的DOM操作。這就是Vue更新DOM的邏輯。只不過我們沒有將 virtual DOM 內部的實現。

當改變屬性值時,會觸發對應的屬性的set方法,由於之前執行render的時候觸發了get,收集了對應的Watcher,所以改變值時觸發set,通知之前收集的Watcher例項執行,重新計算render方法進行patch操作

最後盜取一張圖:

Vue原始碼簡析(版本vue-2.4.4)


寫了半天,實在寫不下去了,以後有好的語言,再來整理吧!



相關文章