Vue 源碼學(xué)習(xí):入口樊破、數(shù)據(jù)響應(yīng)化愉棱、虛擬DOM

Vue 源碼學(xué)習(xí)

new Vue() 入口

src\platforms\web\entry-runtime-with-compiler.js 擴展$mount

src\platforms\web\runtime\index.js 實現(xiàn)$mount

src\core\index.js initGlobalAPI 實現(xiàn)全局 api

src\core\instance\index.js Vue 構(gòu)造函數(shù)

// Vue構(gòu)造函數(shù) new Vue()
function Vue(options) {
  this._init(options)
}

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

export default Vue

initMixin(vue)

實現(xiàn)_init

// ---------------------- src\core\instance\init.js ----------------------

// 初始化
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

initLifecycle(vm)

把組件實例里面用到的常用屬性初始化,比如$parent,$root,$children

// ---------------------- src\core\instance\lifecycle.js ----------------------
vm.$parent = parent
vm.$root = parent ? parent.$root : vm

vm.$children = []
vm.$refs = {}

vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false

initEvents(Vue)

父組件傳遞的需要處理的事件 ps:事件的監(jiān)聽者實際是子組件

// ---------------------- src\core\instance\events.js ----------------------
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
  updateComponentListeners(vm, listeners)
}

initRender(Vue)

$slots $scopedSlots 初始化

$createElement 函數(shù)聲明

$attrs/$listeners 響應(yīng)化

// ---------------------- src\core\instance\render.js ----------------------
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = (vm.$vnode = options._parentVnode) // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
// 處理插槽
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject

// 把createElement函數(shù)掛載到當(dāng)前組件上哲戚,編譯器使用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

// 用戶編寫的渲染函數(shù)使用這個
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data

/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
} else {
  defineReactive(
    vm,
    '$attrs',
    (parentData && parentData.attrs) || emptyObject,
    null,
    true
  )
  defineReactive(
    vm,
    '$listeners',
    options._parentListeners || emptyObject,
    null,
    true
  )
}

initInjections(Vue)

Inject 響應(yīng)化

// src\core\instance\inject.js

initState(Vue)

執(zhí)行各種數(shù)據(jù)狀態(tài)初始化奔滑,包括數(shù)據(jù)響應(yīng)化等

// ---------------------- src\core\instance\state.js ----------------------
vm._watchers = []
// 初始化所有屬性
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
// 初始化回調(diào)函數(shù)
if (opts.methods) initMethods(vm, opts.methods)
// data數(shù)據(jù)響應(yīng)化
if (opts.data) {
  initData(vm)
} else {
  observe((vm._data = {}), true /* asRootData */)
}
//   computed初始化
if (opts.computed) initComputed(vm, opts.computed)
//   watch初始化
if (opts.watch && opts.watch !== nativeWatch) {
  initWatch(vm, opts.watch)
}

initProvide(Vue)

Provide 注入

// src\core\instance\inject.js

stateMixin(Vue)

定義只讀屬性$data 和$props

定義$set 和$delete

定義$watch

// ---------------------- src\core\instance\state.js ----------------------
const dataDef = {}
dataDef.get = function() {
  return this._data
}
const propsDef = {}
propsDef.get = function() {
  return this._props
}

Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)

Vue.prototype.$set = set
Vue.prototype.$delete = del

Vue.prototype.$watch = function(
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {}

eventsMixin(Vue)

實現(xiàn)事件相關(guān)實例 api:$on,$emit,$off,$once

// ---------------------- src\core\instance\events.js ----------------------
const hookRE = /^hook:/
Vue.prototype.$on = function(
  event: string | Array<string>,
  fn: Function
): Component {}

Vue.prototype.$once = function(event: string, fn: Function): Component {}

Vue.prototype.$off = function(
  event?: string | Array<string>,
  fn?: Function
): Component {}

Vue.prototype.$emit = function(event: string): Component {}

lifecycleMixin(Vue)

實現(xiàn)組件生命周期相關(guān)的三個核心實例 api:_update,$forceUpdate,$destroy

// ---------------------- src\core\instance\lifecycle.js ----------------------
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}

Vue.prototype.$forceUpdate = function() {}

Vue.prototype.$destroy = function() {}

renderMixin(Vue)

實現(xiàn)$nextTick 及_render 函數(shù)

// ---------------------- src\core\instance\render.js ----------------------
Vue.prototype.$nextTick = function(fn: Function) {}

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

  if (_parentVnode) {
    vm.$scopedSlots = normalizeScopedSlots(
      _parentVnode.data.scopedSlots,
      vm.$slots,
      vm.$scopedSlots
    )
  }

  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    // There's no need to maintain a stack because all render fns are called
    // separately from one another. Nested component's render fns are called
    // when parent component is patched.
    currentRenderingInstance = vm
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    handleError(e, vm, `render`)
    // return error render result,
    // or previous vnode to prevent render error causing blank component
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
    } else {
      vnode = vm._vnode
    }
  } finally {
    currentRenderingInstance = null
  }
  // if the returned array contains only a single node, allow it
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }
  // return empty vnode in case the render function errored out
  if (!(vnode instanceof VNode)) {
    if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
    }
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

數(shù)據(jù)響應(yīng)式

Vue 一大特點是數(shù)據(jù)響應(yīng)式,數(shù)據(jù)的變化會作用于 UI 而不用進行 DOM 操作惫恼。原理上講档押,是利用了 JS 語言特性Object.defineProperty(),通過定義對象屬性 setter 方法攔截對象屬性變更祈纯,從而將數(shù)值的變化轉(zhuǎn)換為 UI 的變化令宿。

具體實現(xiàn)是在 Vue 初始化時,會調(diào)用 initState腕窥,它會初始化 data粒没,props 等,這里著重關(guān)注 data 初始化簇爆。

// ---------------------- src\core\instance\state.js ----------------------
export function initState(vm: Component) {
  const opts = vm.$options

  if (opts.data) {
    initData(vm) // 初始化數(shù)據(jù)
  } else {
    observe((vm._data = {}), true /* asRootData */)
  }
}

initData()

將 data 數(shù)據(jù)響應(yīng)化

function initData(vm: Component) {
  // 獲取數(shù)據(jù)
  let data = vm.$options.data
  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
      )
  }
  // 代理數(shù)據(jù)
  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 (props && hasOwn(props, key)) {
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // 數(shù)據(jù)響應(yīng)化
  observe(data, true /* asRootData */)
}

observe()

返回一個 Observer 實例

// ---------------------- src\core\observer\index.js ----------------------
export function observe(value: any, asRootData: ?boolean): Observer | void {
  // 只對Object進行處理
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  // 有則返回癞松,沒有新建
  let ob: Observer | void
  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
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

class Observer

根據(jù)數(shù)據(jù)類型執(zhí)行對應(yīng)的響應(yīng)化操作

export class Observer {
  value: any
  dep: Dep // 保存數(shù)組類型數(shù)據(jù)的依賴
  vmCount: number // number of vms that have this object as root $data

  constructor(value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this) // 在getter中可以通過__ob__獲取ob實例
    if (Array.isArray(value)) {
      // 數(shù)組響應(yīng)化
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      // 對象響應(yīng)化
      this.walk(value)
    }
  }

  /**
   * 遍歷對象所有屬性并轉(zhuǎn)換為getter/setter
   */
  walk(obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * 對數(shù)組每一項執(zhí)行響應(yīng)化
   */
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive()

定義對象屬性的 getter/setter,getter 負責(zé)收集添加依賴入蛆,setter 負責(zé)通知更新

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep() // 一個key對應(yīng)一個Dep實例

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  // 遞歸執(zhí)行子對象響應(yīng)化
  let childOb = !shallow && observe(val)
  // 定義當(dāng)前對象getter/setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      // getter被調(diào)用時若存在依賴則追加
      if (Dep.target) {
        dep.depend()
        // 若存在子observer响蓉,則依賴也追加到子ob
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value) // 數(shù)組需要特殊處理
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      /* eslint-enable no-self-compare */
      // #7981: for accessor properties without setter

      // 更新值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 遞歸更新子對象
      childOb = !shallow && observe(newVal)
      // 通知更新
      dep.notify()
    }
  })
}

class Dep

負責(zé)管理一組 Watcher,包括 watcher 實例的增刪及通知更新

// ---------------------- src\core\observer\dep.js ----------------------
export default class Dep {
  static target: ?Watcher // 依賴收集時的watcher引用
  id: number
  subs: Array<Watcher> // watcher數(shù)組

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

  // 添加watcher實例
  addSub(sub: Watcher) {
    this.subs.push(sub)
  }

  // 刪除watcher實例
  removeSub(sub: Watcher) {
    remove(this.subs, sub)
  }

  // watcher和dep相互保存引用
  depend() {
    if (Dep.target) {
      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++) {
      subs[i].update()
    }
  }
}

class Watcher

負責(zé)管理一組 Watcher哨毁,包括 watcher 實例的增刪及通知更新

// ---------------------- src\core\observer\watcher.js ----------------------
export default class Watcher {
  addDep(dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // watcher保存dep引用
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      // dep添加watcher
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  update() {
    // 更新邏輯
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 默認lazy和sync都是false枫甲,所以會走該邏輯
      queueWatcher(this)
    }
  }
}

vue 中的數(shù)據(jù)響應(yīng)化使用了觀察者模式:

  • defineReactive 中的 getter 和 setter 對應(yīng)著訂閱和發(fā)布應(yīng)為
  • Dep 的角色相當(dāng)于主題 Subject,維護訂閱者扼褪、通知觀察者更新
  • Watcher 的角色相當(dāng)于觀察者 Observer想幻,執(zhí)行更新
  • 但是 vue 里面的 Observer 不是上面說的觀察者,它和 data 中對象一一對應(yīng)话浇,有內(nèi)嵌的對象就會有 child Observer 與之對應(yīng)

$watch

$watch 是和數(shù)據(jù)響應(yīng)機制息息相關(guān)的一個 API脏毯,它指定一個監(jiān)控表達式,當(dāng)數(shù)值發(fā)生變化的時候執(zhí)行回調(diào)函數(shù)幔崖,我們來看一下它的實現(xiàn)

// src\core\instance\state.js
// stateMixin()
Vue.prototype.$watch = function(
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  // 對象形式回調(diào)的解析
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  // 創(chuàng)建Watcher監(jiān)視數(shù)值變化
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 若有immediate選項立即執(zhí)行一次cb
  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()
  }
}

Watcher 構(gòu)造函數(shù)

主要解析監(jiān)聽的表達式食店,并觸發(fā)依賴收集

// src\core\observer\watcher.js
export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // 組件保存render watcher
    if (isRenderWatcher) {
      vm._watcher = this
    }
    // 組件保存非render watcher
    vm._watchers.push(this)

    // options

    // parse expression for getter
    // 將表達式解析為getter函數(shù)
    // 如果是函數(shù)則直接指定為getter渣淤,那什么時候是函數(shù)?
    // 答案是那些和組件實例對應(yīng)的Watcher創(chuàng)建時會傳遞組件更新函數(shù)updateComponent
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 這種是$watch傳遞進來的表達式叛买,它們需要解析為函數(shù)
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' &&
          warn(
            `Failed watching path: "${expOrFn}" ` +
              'Watcher only accepts simple dot-delimited paths. ' +
              'For full control, use a function instead.',
            vm
          )
      }
    }
    // 若非延遲watcher砂代,立即調(diào)用getter
    this.value = this.lazy ? undefined : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   *
   * 模擬getter蹋订,重新收集依賴
   */
  get() {
    // Dep.target = this
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 從組件中獲取到value同時觸發(fā)依賴收集
      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
      // deep watching率挣,遞歸觸發(fā)深層屬性
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
}

數(shù)組響應(yīng)化

數(shù)組數(shù)據(jù)變化的偵測跟對象不同,我們操作數(shù)組通常使用 push露戒、pop椒功、splice 等方法,此時沒有辦法得知數(shù)組變化智什。所以 vue 中采取的策略是攔截這些方法并通知 dep动漾。

攔截器

為數(shù)組原型中的 7 個可以改變內(nèi)容的方法定義攔截器

// src\core\observer\array.js
import { def } from '../util/index'

// 數(shù)組原型
const arrayProto = Array.prototype
// 修改后的數(shù)組
export const arrayMethods = Object.create(arrayProto)

// 7個待修改方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 *
 * 攔截這些方法,額外發(fā)送變更通知
 */
methodsToPatch.forEach(function(method) {
  // cache original method
  // 原始數(shù)組方法
  const original = arrayProto[method]
  // 修改這些方法的descriptor
  def(arrayMethods, method, function mutator(...args) {
    // 原始操作
    const result = original.apply(this, args)
    // 獲取ob實例用于發(fā)送通知
    const ob = this.__ob__
    // 三個能新增元素的方法特殊處理
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 若有新增則做響應(yīng)處理
    if (inserted) ob.observeArray(inserted)
    // notify change
    // 通知更新
    ob.dep.notify()
    return result
  })
})

覆蓋數(shù)組原型

Observer 中覆蓋數(shù)組原型

// src\core\observer\index.js
// class Observer constructor()
if (Array.isArray(value)) {
  // 覆蓋數(shù)組原型
  protoAugment(value, arrayMethods) // value.__proto__ = arrayMethods

  this.observeArray(value)
}

依賴收集

defineReactive 中數(shù)組的特殊處理

// src\core\observer\index.js
// defineReactive()
// getter中處理
if (Array.isArray(value)) {
  dependArray(value)
}

// 數(shù)組中所有項添加依賴荠锭,將來數(shù)組里面就可以通過__ob__.dep發(fā)送通知
function dependArray(value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

數(shù)據(jù)響應(yīng)式處理中的各種角色可以通過動畫再捋一下

理解響應(yīng)式原理的實現(xiàn)旱眯,我們可以知道一下注意事項:

  • 對象各屬性初始化時進行一次響應(yīng)化處理,以后再動態(tài)設(shè)置是無效的
data: {
  obj: {
    foo: 'foo'
  }
}

// 無效
this.obj.bar = 'bar'
// 有效
this.$set(this.obj, 'bar', 'bar')
  • 數(shù)組是通過方法攔截實現(xiàn)響應(yīng)化處理证九,不通過方法操作數(shù)組也是無效的
data: {
  items: ['foo', 'bar']
}
// 無效
this.items[0] = 'hello'
this.items.length = 0
//有效
this.$set(this.items, 0, 'hello')
this.items.splice(0, 2)

Vue 異步更新隊列

Vue 在更新 DOM 時是異步執(zhí)行的删豺。只要偵聽到數(shù)據(jù)變化,Vue 將開啟一個隊列愧怜,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更呀页。如果同一個 watcher 被多次觸發(fā),指揮被推入到隊列中一次拥坛。這種在緩沖時去除重復(fù)數(shù)據(jù)對于避免不必要的計算和 DOM 操作是非常重要的蓬蝶。
然后,在下一個的事件循環(huán)“tick”中猜惋,Vue 刷新隊列并執(zhí)行實際(已去重的)工作丸氛。Vue 在內(nèi)部對異步隊列嘗試使用原生的Promise.then()MutationObserversetImmediate著摔,如果執(zhí)行環(huán)境不支持缓窜,則會采用setTimeout(fn, 0)代替。

如果項獲取更新后 DOM 狀態(tài)梨撞,可以在數(shù)據(jù)變化之后使用Vue.nextTick(cb)雹洗,這樣回調(diào)函數(shù)會在 DOM 更新完成后被調(diào)用。

queueWatcher

執(zhí)行 watcher 入隊操作卧波,若存在重復(fù) id 則跳過

// src\core\observer\watcher.js
// update()
queueWatcher(this)

// src\core\observer\scheduler.js
// watcher入隊
export function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    // id不存在才會入隊
    has[id] = true
    if (!flushing) {
      // 沒有在執(zhí)行刷新則進入隊尾
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      // 若已刷新时肿,按id順序插入到隊列
      // 若已經(jīng)過了,則下次刷新立即執(zhí)行
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    // 刷新隊列
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

// src\core\observer\scheduler.js
// nextTick(flushSchedulerQueue)
// 按照特定異步策略執(zhí)行隊列刷新操作
export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve
  // 注意cb不是立即執(zhí)行港粱,而是加入到回調(diào)數(shù)組螃成,等待調(diào)用
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx) // 真正執(zhí)行cb
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 沒有出在掛起狀態(tài)則開始異步執(zhí)行過程
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

let timerFunc

// nextTick異步行為利用微任務(wù)隊列旦签,可通過Promise或MutationObserver交互
// 首選Promise,次選MutationObserver
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)

    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  // 不能用Promise時:PhantomJS寸宏,iOS7宁炫,Android 4.4
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 回退到 setImmediate 它利用的是宏任務(wù)
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 最后選擇 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

宏任務(wù)和微任務(wù)

虛擬 DOM

虛擬 DOM(Virtual DOM)是對 DOM 的 JS 抽象表示,它們是 JS 對象氮凝,能夠描述 DOM 結(jié)構(gòu)和關(guān)系羔巢。

優(yōu)點

虛擬 DOM 輕量、快速罩阵,當(dāng)他們發(fā)生變化時通過新舊虛擬 DOM 比對可以得到最小 DOM 操作量竿秆,從而提升性能和用戶體驗。本質(zhì)上時使用 JavaScript 運算成本替換 DOM 操作的執(zhí)行成本稿壁,前者運算速度要比后者快得多幽钢,這樣做很劃算,因此才會有虛擬 DOM傅是。

Vue 1.0 中有細粒度的數(shù)據(jù)變化偵測匪燕,每一個屬性對應(yīng)一個 Watcher 實例,因此它是不需要虛擬 DOM 的喧笔,但是細粒度造成了大量開銷帽驯,這對于大型項目來說是不可接受的。因此溃斋,Vue 2.0 選擇了中等粒度的解決方案界拦,每一個組件對應(yīng)一個 Watcher 實例,這樣狀態(tài)變化時只能通知到組件梗劫,再通過引入虛擬 DOM 去進行對比和渲染享甸。

實現(xiàn)

虛擬 DOM 整體流程

mountComponent

vdom 樹首頁生成、渲染發(fā)生在 mountComponent 中

// core/instance/lifecycle.js
export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  } else {
    // 調(diào)用 _render() 返回結(jié)果
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate')
        }
      }
    },
    true /* isRenderWatcher */
  )
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
_render

_render 生成虛擬 dom

// core/instance/render.js
// renderMixin()
Vue.prototype._render = function(): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  if (_parentVnode) {
    vm.$scopedSlots = normalizeScopedSlots(
      _parentVnode.data.scopedSlots,
      vm.$slots,
      vm.$scopedSlots
    )
  }

  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    // There's no need to maintain a stack because all render fns are called
    // separately from one another. Nested component's render fns are called
    // when parent component is patched.
    currentRenderingInstance = vm
    // 組件實例作為上下文梳侨,獲取 $createElement() 結(jié)果
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    handleError(e, vm, `render`)
    // return error render result,
    // or previous vnode to prevent render error causing blank component
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
      try {
        vnode = vm.$options.renderError.call(
          vm._renderProxy,
          vm.$createElement,
          e
        )
      } catch (e) {
        handleError(e, vm, `renderError`)
        vnode = vm._vnode
      }
    } else {
      vnode = vm._vnode
    }
  } finally {
    currentRenderingInstance = null
  }
  // if the returned array contains only a single node, allow it
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }
  // return empty vnode in case the render function errored out
  if (!(vnode instanceof VNode)) {
    if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
    }
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}
  • createElement

    真正用來創(chuàng)建 vnode 的函數(shù)是 createElement蛉威,src\core\vdom\create-element.js

  • createComponent

    用于創(chuàng)建組件并返回 VNode,src\core\vdom\create-component.js

  • VNode

_render 返回的一個 VNode 實例走哺,它的 children 還是 VNode蚯嫌,最終構(gòu)成一個樹,就是虛擬 DOM 樹

// src\core\vdom\vnode.js
// VNode對象共有6種類型:元素丙躏、組件择示、函數(shù)式組件、文本晒旅、注釋和克隆節(jié)點
// 靜態(tài)節(jié)點可作為克隆節(jié)點栅盲,因為不會有變化 <h1>Hello</h1>
export default class VNode {
  tag: string | void // 節(jié)點標(biāo)簽,文本及注釋沒有
  data: VNodeData | void // 節(jié)點數(shù)據(jù)废恋,文本及注釋沒有
  children: ?Array<VNode> // 子元素
  text: string | void // 文本及注釋的內(nèi)容谈秫,元素文本
  elm: Node | void
  ns: string | void
  context: Component | void // rendered in this component's scope
  key: string | number | void
  componentOptions: VNodeComponentOptions | void
  componentInstance: Component | void // component instance 組件實例
  parent: VNode | void // component placeholder node
}
_update

_update 負責(zé)更新 dom扒寄,核心是調(diào)用 __patch__

// src\core\instance\lifecycle.js
// lifecycleMixin()
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}
__patch__

__patch__ 是在平臺特有代碼中指定的

// src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
patch

實際就是 createPatchFunction 的返回值,傳遞 nodeOps 和 modules拟烫,這里主要是為了跨平臺

// src\platforms\web\runtime\patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
  • nodeOps

定義各種原生 dom 基礎(chǔ)操作方法

// src\platforms\web\runtime\node-ops.js
  • modules

modules 定義了虛擬 dom 更新 => dom 操作轉(zhuǎn)換方法

import baseModules from 'core/vdom/modules/index'
// export default [
//   ref,
//   directives
// ]
import platformModules from 'web/runtime/modules/index'
// export default [
//   attrs,
//   klass,
//   events,
//   domProps,
//   style,
//   transition
// ]

const modules = platformModules.concat(baseModules)

patch 詳解

Vue 使用的 patching 算法基于 Snabbdom

patch 將新老 VNode 節(jié)點進行比對(diff 算法)该编,然后根據(jù)比較結(jié)果進行最小量 DOM 操作,而不是將整個視圖根據(jù)新的 VNode 重繪硕淑。

那么 patch 如何工作的呢课竣?

首先說一下 patch 的核心 diff 算法:通過同層的樹節(jié)點進行比較而非對樹進行逐層搜索遍歷的方式,所以時間復(fù)雜度只有 O(n)喜颁,是一種相當(dāng)高效的算法稠氮。

同層級只做三件事:增刪改。具體規(guī)則是:new VNode 不存在就刪半开;old VNode 不存在就增;都存在就比較類型赃份,類型不同直接替換寂拆、類型相同執(zhí)行更新

return function patch(oldVnode, vnode, hydrating, removeOnly) {
  // vnode新節(jié)點不存在就刪
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // oldVnode不存在則創(chuàng)建新節(jié)點
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    // 標(biāo)記oldVnode是否有nodeType,true為一個DOM元素
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      // 是同一個節(jié)點的時候做更新
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      // 帶編譯器版本才會出現(xiàn)的情況:傳了DOM元素進來
      if (isRealElement) {
        // mounting to a real element
        // create an empty node and replace it
        // 掛載一個真實元素抓韩,創(chuàng)建一個空的VNode節(jié)點替換它
        oldVnode = emptyNodeAt(oldVnode)
      }

      // replacing existing element
      // 取代現(xiàn)有元素
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // create new node

      // update parent placeholder node element, recursively

      // destroy old node
      // 移除老節(jié)點
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        // 調(diào)用destroy鉤子
        invokeDestroyHook(oldVnode)
      }
    }
  }

  // 調(diào)用insert鉤子
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

patchVnode

兩個 VNode 類型相同纠永,就執(zhí)行更新操作,包括三種類型操作:屬性更新 PROPS谒拴、文本更新 TEXT尝江、子節(jié)點更新 REORDER

patchVnode 具體規(guī)則如下:

  • 如果新舊 VNode 都是靜態(tài)的,同時它們的 key 相同(代表同一節(jié)點)英上,并且新的 VNode 是 clone 或者是標(biāo)記了 v-once炭序,那么只需要替換 elm 以及 componentInstance 即可
  • 新老節(jié)點均有 children 子節(jié)點,則對子節(jié)點進行 diff 操作苍日,調(diào)用 updateChildren惭聂,這個 updateChildren 也是 diff 的核心
  • 如果老節(jié)點沒有子節(jié)點而新節(jié)點存在子節(jié)點,先清空老節(jié)點 DOM 的文本內(nèi)容相恃,然后為當(dāng)前 DOM 節(jié)點加入子節(jié)點
  • 當(dāng)新節(jié)點沒有子節(jié)點而老節(jié)點有子節(jié)點的時候辜纲,則移除該 DOM 節(jié)點的所有子節(jié)點
  • 當(dāng)新老節(jié)點都無子節(jié)點的時候,只是文本的替換
function patchVnode(
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // 兩個VNode節(jié)點相同則直接返回
  if (oldVnode === vnode) {
    return
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  const elm = (vnode.elm = oldVnode.elm)

  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }

  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  // 如果新舊VNode都是靜態(tài)的拦耐,同時它們的key相同(代表同一節(jié)點)
  // 并且新的VNode是clone或者是標(biāo)記了once(標(biāo)記v-once屬性耕腾,只渲染一次)
  // 那么只需要替換elm以及componentInstance即可
  if (
    isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  // 如果存在data.hook.prepatch則要先執(zhí)行
  let i
  const data = vnode.data
  if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
    i(oldVnode, vnode)
  }

  const oldCh = oldVnode.children
  const ch = vnode.children
  // 執(zhí)行屬性、事件杀糯、樣式等等更新操作
  if (isDef(data) && isPatchable(vnode)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode)
  }

  // 開始判斷children的各種情況
  // VNode節(jié)點沒有text文本時
  if (isUndef(vnode.text)) {
    // 新老節(jié)點均有children子節(jié)點扫俺,則對子節(jié)點進行diff操作,調(diào)用updateChildren
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch)
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      // 如果老節(jié)點沒有子節(jié)點而新節(jié)點存在子節(jié)點火脉,清空elm的文本內(nèi)容牵舵,然后為當(dāng)前節(jié)點加入子節(jié)點
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      // 如果新節(jié)點沒有子節(jié)點而老節(jié)點存在子節(jié)點柒啤,則移除所有elm的子節(jié)點
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      // 當(dāng)新老節(jié)點都不存在子節(jié)點,則在此分支中清空elm文本
      nodeOps.setTextContent(elm, '')
    }
    // VNode節(jié)點有text文本時
  } else if (oldVnode.text !== vnode.text) {
    // 新老節(jié)點text不一樣時畸颅,直接替換這段文本
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode)
  }
}

updateChildren

updateChildren 主要作用是比對新舊兩個 VNode 的 children 得出具體 DOM 操作担巩。執(zhí)行一個雙循環(huán)是傳統(tǒng)方式,vue 中針對 web 場景特點做了特別的算法優(yōu)化:



在新老兩組 VNode 節(jié)點的左右頭尾兩側(cè)都有一個變量標(biāo)記没炒,在遍歷過程中這幾個變量都會向中間靠攏涛癌。當(dāng)
oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 時結(jié)束循環(huán)。

下面是遍歷規(guī)則:

首先送火,oldStartVnode拳话、oldEndVnode 與 newStartVnode、newEndVnode 兩兩交叉比較种吸,共有 4 種比較方法弃衍。
當(dāng) oldStartVnode 和 newStartVnode 或者 oldEndVnode 和 newEndVnode 滿足 sameVnode,直接將該 VNode 節(jié) 點進行 patchVnode 即可坚俗,不需再遍歷就完成了一次循環(huán)镜盯。如下圖,

如果 oldStartVnode 與 newEndVnode 滿足 sameVnode猖败。說明 oldStartVnode 已經(jīng)跑到了 oldEndVnode 后面去了速缆, 進行 patchVnode 的同時還需要將真實 DOM 節(jié)點移動到 oldEndVnode 的后面

如果 oldEndVnode 與 newStartVnode 滿足 sameVnode,說明 oldEndVnode 跑到了 oldStartVnode 的前面恩闻,進行
patchVnode 的同時要將 oldEndVnode 對應(yīng) DOM 移動到 oldStartVnode 對應(yīng) DOM 的前面艺糜。

如果以上情況均不符合,則在 old VNode 中找與 newStartVnode 滿足 sameVnode 的 vnodeToMove幢尚,若存在執(zhí)行
patchVnode破停,同時將 vnodeToMove 對應(yīng) DOM 移動到 oldStartVnode 對應(yīng)的 DOM 的前面。

當(dāng)然也有可能 newStartVnode 在 old VNode 節(jié)點中找不到一致的 key侠草,或者是即便 key 相同卻不是 sameVnode辱挥,這 個時候會調(diào)用 createElm 創(chuàng)建一個新的 DOM 節(jié)點。

至此循環(huán)結(jié)束边涕,但是我們還需要處理剩下的節(jié)點晤碘。

當(dāng)結(jié)束時 oldStartIdx > oldEndIdx,這個時候舊的 VNode 節(jié)點已經(jīng)遍歷完了功蜓,但是新的節(jié)點還沒有园爷。說明了新的
VNode 節(jié)點實際上比老的 VNode 節(jié)點多,需要將剩下的 VNode 對應(yīng)的 DOM 插入到真實 DOM 中式撼,此時調(diào)用
addVnodes童社。

但是,當(dāng)結(jié)束時 newStartIdx > newEndIdx 時著隆,說明新的 VNode 節(jié)點已經(jīng)遍歷完了扰楼,但是老的節(jié)點還有剩余呀癣,需要 從文檔中刪 的節(jié)點刪除。

function updateChildren(
  parentElm,
  oldCh,
  newCh,
  insertedVnodeQueue,
  removeOnly
) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  // 確保移除元素在過度動畫過程中待在正確的相對位置弦赖,僅用于<transition-group>
  const canMove = !removeOnly

  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh)
  }

  // 循環(huán)條件:任意起始索引超過結(jié)束索引就結(jié)束
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 分別比較oldCh以及newCh的兩頭節(jié)點4種情況项栏,判定為同一個VNode,則直接patchVnode即可
      patchVnode(
        oldStartVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      )
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      patchVnode(
        oldStartVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      )
      canMove &&
        nodeOps.insertBefore(
          parentElm,
          oldStartVnode.elm,
          nodeOps.nextSibling(oldEndVnode.elm)
        )
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Vnode moved left
      patchVnode(
        oldEndVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      )
      canMove &&
        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      // 生成一個哈希表蹬竖,key是舊VNode的key沼沈,值是該VNode在舊VNode中索引
      if (isUndef(oldKeyToIdx))
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      // 如果newStartVnode存在key并且這個key在oldVnode中能找到則返回這個節(jié)點的索引
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) {
        // New element
        // 沒有key或者是該key沒有在老節(jié)點中找到則創(chuàng)建一個新的節(jié)點
        createElm(
          newStartVnode,
          insertedVnodeQueue,
          parentElm,
          oldStartVnode.elm,
          false,
          newCh,
          newStartIdx
        )
      } else {
        // 獲取同key的老節(jié)點
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          // 如果新VNode與得到的有相同key的節(jié)點是同一個VNode則進行patchVnode
          patchVnode(
            vnodeToMove,
            newStartVnode,
            insertedVnodeQueue,
            newCh,
            newStartIdx
          )
          // 因為已經(jīng)patchVnode進去了,所以將這個老節(jié)點賦值undefined币厕,
          // 之后如果還有新節(jié)點與該節(jié)點 key相同可以檢測出來提示已有重復(fù)的key
          oldCh[idxInOld] = undefined
          // 當(dāng)有標(biāo)識位canMove實可以直接插入oldStartVnode對應(yīng)的真實DOM節(jié)點前面
          canMove &&
            nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // same key but different element. treat as new element
          // 當(dāng)新的VNode與找到的同樣key的VNode不是sameVNode的時候
          //(比如說tag不一樣或者是有不一樣 type的input標(biāo)簽)列另,創(chuàng)建一個新的節(jié)點
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          )
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  if (oldStartIdx > oldEndIdx) {
    // 全部比較完成以后,發(fā)現(xiàn)oldStartIdx > oldEndIdx的話旦装,說明老節(jié)點已經(jīng)遍歷完了页衙,
    // 新節(jié)點比老節(jié)點 多,所以這時候多出來的新節(jié)點需要一個一個創(chuàng)建出來加入到真實DOM中
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(
      parentElm,
      refElm,
      newCh,
      newStartIdx,
      newEndIdx,
      insertedVnodeQueue
    )
  } else if (newStartIdx > newEndIdx) {
    // 如果全部比較完成以后發(fā)現(xiàn)newStartIdx > newEndIdx同辣,
    // 則說明新節(jié)點已經(jīng)遍歷完了拷姿,老節(jié)點多余新節(jié) 點
    // 這個時候需要將多余的老節(jié)點從真實DOM中移除
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  }
}

屬性相關(guān) dom 操作

原理是將屬性相關(guān) dom 操作按 vdom hooks 歸類,在 patchVnode 時一起執(zhí)行

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction(backend) {
  let i, j
  const cbs = {}

  const { modules, nodeOps } = backend

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  function patchVnode(...) {
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
  }

模板編譯

模板編譯的主要目標(biāo)是:將模板(tempalte)轉(zhuǎn)換為渲染函數(shù)(render)

模板編譯的由來

Vue2.0 需要用到 VNode 描述視圖以及各種交互旱函,手寫顯然不切實際,因此用戶只需要編寫類似 HTML 代碼的 Vue 模板描滔,通過編譯器將模板轉(zhuǎn)換為可返回 VNode 的 render 函數(shù)棒妨。

體驗?zāi)0寰幾g

帶編譯器的版本中,可以使用 template 或 el 的方式聲明模板

<div id="demo">
  <h1>Vue.js測試</h1>
  <p>{{foo}}</p>
</div>
<script>
  // 使用el方式
  const app = new Vue({
    data: { foo: 'foo' },
    el: '#demo'
  })
  // 然后輸出渲染函數(shù)
  console.log(app.$options.render)
</script>

輸出結(jié)果大致如下:

?unction anonymous() {
    with (this) {
        return _c('div', { attrs: { "id": "demo" } }, [
        _c('h1', [_v("Vue.js測試")]),
        _v(" "),
        _c('p', [_v(_s(foo))])
        ])
    }
}

元素節(jié)點使用 createElement 創(chuàng)建含长,別名_c

本文節(jié)點使用 createTextVNode 創(chuàng)建券腔,別名_v

表達式先使用 toString 格式化,別名_s

模板編譯過程

實現(xiàn)模板編譯共有三個階段:解析拘泞、優(yōu)化和生成

  • 解析 - parse
    src/compiler/parser/index.js - parse

解析器將模板解析為抽象語法樹 AST纷纫,只有將模板解析成 AST 后,才能基于它做優(yōu)化或者生成代碼字符串陪腌。

調(diào)試查看得到的 AST辱魁,結(jié)構(gòu)如下:

解析器內(nèi)部分了 HTML 解析器、文本解析器和過濾器解析器诗鸭,最主要是 HTML 解析器染簇,核心算法說明:

// src/compiler/parser/index.js
parseHTML(tempalte, {
  start(tag, attrs, unary) {}, // 遇到開始標(biāo)簽的處理
  end() {}, // 遇到結(jié)束標(biāo)簽的處理
  chars(text) {}, // 遇到文本標(biāo)簽的處理
  comment(text) {} // 遇到注釋標(biāo)簽的處理
})
  • 優(yōu)化 - optimize
    優(yōu)化器的作用是在 AST 中找出靜態(tài)子樹并打上標(biāo)記。靜態(tài)子樹是在 AST 中永遠不變的節(jié)點强岸,如純文本節(jié)點锻弓。

標(biāo)記靜態(tài)子樹的好處:

每次重新渲染,不需要為靜態(tài)子樹創(chuàng)建新節(jié)點

虛擬 DOM 中 patch 時蝌箍,可以跳過靜態(tài)子樹

標(biāo)記過程有兩步:

  1. 找出靜態(tài)節(jié)點并標(biāo)記
  2. 找出靜態(tài)根節(jié)點并標(biāo)記

代碼實現(xiàn)

// src/compiler/optimizer.js - optimize
export function optimize(root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  markStatic(root)
  // second pass: mark static roots.
  markStaticRoots(root, false)
}

標(biāo)記結(jié)束

  • 代碼生成 - generate
    將 AST 轉(zhuǎn)換成渲染函數(shù)中的內(nèi)容青灼,即代碼字符串暴心。

generate 方法生成渲染函數(shù)

// src/compiler/codegen/index.js - generate
export function generate(
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

生成的 code

"_c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("Vue.js測試")]),_v(" "),_c('p',[_v(_s(foo))])])"

v-if、v-for

著重觀察幾個結(jié)構(gòu)性指令的解析過程

// 解析v-if杂拨,parser/index.js
function processIf(el) {
  const exp = getAndRemoveAttr(el, 'v-if') // 獲取v-if=“exp"中exp并刪除v-if屬性
  if (exp) {
    el.if = exp // 為ast添加if表示條件
    addIfCondition(el, {
      // 為ast添加ifConditions表示各種情況對應(yīng)結(jié)果
      exp: exp,
      block: el
    })
  } else {
    // 其他情況處理
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}
// 代碼生成专普,codegen/index.js
function genIfConditions(
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  const condition = conditions.shift() // 每次處理一個條件
  if (condition.exp) {
    // 每種條件生成一個三元表達式
    return `(${condition.exp})?${genTernaryExp(
      condition.block
    )}:${genIfConditions(conditions, state, altGen, altEmpty)}`
  } else {
    return `${genTernaryExp(condition.block)}`
  }
  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp(el) {}
}

插槽

普通插槽是在父組件編譯和渲染階段生成 vnodes ,數(shù)據(jù)的作用域是父組件扳躬,子組件渲染的時候直接拿到這些渲染好的 vnodes 脆诉。

作用域插槽,父組件在編譯和渲染階段并不會直接生成 vnodes 贷币,而是在父節(jié)點保留一個 scopedSlots 對象击胜,存儲著不同名稱的插槽以及它們對應(yīng)的渲染函數(shù),只有在編譯和渲染子組件階段才會執(zhí)行這個渲染函數(shù)生成vnodes 役纹,由于是在子組件環(huán)境執(zhí)行的偶摔,所以對應(yīng)的數(shù)據(jù)作用域是子組件實例。

簡單地說促脉,兩種插槽的目的都是讓子組件 slot 占位符生成的內(nèi)容由父組件來決定辰斋,但數(shù)據(jù)的作用域會根據(jù)它們 vnodes 渲染時機不同而不同。

解析相關(guān)代碼:

// processSlotContent:處理<template v-slot:xxx="yyy">
const slotBinding = getAndRemoveAttrByRegex(el, slotRE) // 查找v-slot:xxx
if (slotBinding) {
  const { name, dynamic } = getSlotName(slotBinding) // name是xxx
  el.slotTarget = name // xxx賦值到slotTarget
  el.slotTargetDynamic = dynamic
  el.slotScope = slotBinding.value || emptySlotScopeToken // yyy賦值到slotScope
}
// processSlotOutlet:處理<slot>
if (el.tag === 'slot') {
  el.slotName = getBindingAttr(el, 'name') // 獲取slot的name并賦值到slotName
}

生成相關(guān)代碼:

// genScopedSlot:這里把slotScope作為形參轉(zhuǎn)換為工廠函數(shù)返回內(nèi)容
const fn =
  `function(${slotScope}){` +
  `return ${
    el.tag === 'template'
      ? el.if && isLegacySyntax
        ? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`
        : genChildren(el, state) || 'undefined'
      : genElement(el, state)
  }}`
// reverse proxy v-slot without scope on this.$slots
const reverseProxy = slotScope ? `` : `,proxy:true`
return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瘸味,一起剝皮案震驚了整個濱河市明肮,隨后出現(xiàn)的幾起案子震束,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嫂粟,死亡現(xiàn)場離奇詭異酥郭,居然都是意外死亡茵瀑,警方通過查閱死者的電腦和手機昆咽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來尘奏,“玉大人滩褥,你說我怎么就攤上這事§偶樱” “怎么了瑰煎?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長琢感。 經(jīng)常有香客問我丢间,道長,這世上最難降的妖魔是什么驹针? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任烘挫,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘饮六。我一直安慰自己其垄,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布卤橄。 她就那樣靜靜地躺著绿满,像睡著了一般。 火紅的嫁衣襯著肌膚如雪窟扑。 梳的紋絲不亂的頭發(fā)上喇颁,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音嚎货,去河邊找鬼橘霎。 笑死,一個胖子當(dāng)著我的面吹牛殖属,可吹牛的內(nèi)容都是我干的姐叁。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼洗显,長吁一口氣:“原來是場噩夢啊……” “哼外潜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起挠唆,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤处窥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后玄组,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碧库,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年巧勤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弄匕。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡颅悉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出迁匠,到底是詐尸還是另有隱情剩瓶,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布城丧,位于F島的核電站延曙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏亡哄。R本人自食惡果不足惜枝缔,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧愿卸,春花似錦灵临、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至发钝,卻和暖如春顿涣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酝豪。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工涛碑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人寓调。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓锌唾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親夺英。 傳聞我的和親對象是個殘疾皇子晌涕,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容