1. 程式人生 > >Vue原始碼解讀之資料繫結

Vue原始碼解讀之資料繫結

原文地址:https://banggan.github.io/2019/01/08/Vue原始碼解讀之資料繫結/

從最開始vue初始化到渲染的整個流程如下:new Vue----一系列的初始化----$mount做掛載—如果是帶編譯的版本就compile,沒有就跳過—render函式—生成vnode—patch方法—渲染成DOM,如下圖:
在這裡插入圖片描述

何為資料驅動

資料驅動就是指檢視由資料來生成,在傳統的前端開發中,大多是利用jQuery直接對DOM進行修改,而在Vue開發中,對檢視的修改,不會直接操作DOM,而是通過修改資料來實現。當互動較為複雜的時候,我們只需要關心資料,所有的邏輯都是通過對DOM的修改實現,而不用接觸DOM,這樣不僅僅簡化程式碼,讓程式碼邏輯更為清晰也有利於程式碼的維護。

new Vue的實際過程

從入口程式碼開始分析,我們先來分析 new Vue 背後發生了哪些事情。我們都知道,new 關鍵字在 Javascript 語言中代表例項化是一個物件,而 Vue 實際上是一個類,類在 Javascript 中是用 Function 來實現的,來看一下原始碼,在src/core/instance/index.js 中。

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')
  }
  this._init(options)
}

可以看到 Vue 只能通過 new 關鍵字初始化,然後會呼叫 this._init 方法

Vue.prototype._init = function (options?: Object) { //掛載在原型上的init方法
  const vm: Component = this
 .........
       // merge options  合併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
    )
  }
  .......
  // expose real self         一系列的初始化
  vm._self = vm               
  initLifecycle(vm)             //生命週期初始化
  initEvents(vm)       // 事件
  initRender(vm)   
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)  //對data。props等的初始化,最終通過poxy(vm,'_data;,key)代理掛載到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)
  }    
  if (vm.$options.el) {     //判斷有沒有el 如果有
    vm.$mount(vm.$options.el)  //通過$mount做掛載 目的就是把模板渲染成最終的DOM
  }
}

Vue 初始化主要就幹了幾件事情,合併配置,初始化生命週期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。

Vue 例項掛載的實現

Vue 中我們是通過 $mount 例項方法去掛載 vm 的,先來看一下 src/platform/web/entry-runtime-with-compiler.js 檔案中定義:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  //如果el為bady或者element  報警告   vue不可直接掛載在bady或者element 會直接覆蓋
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options  
  // resolve template/el and convert to render function
  if (!options.render) {     //是否定義render方法
    let template = options.template
    if (template) {     //是否定義render方法
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } 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)     //拿到物件的outerhtml
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
 // 編譯相關的內容,會轉換為rander函式
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)   //原型上的mount方法
}

這段程式碼首先快取了原型上的 $mount 方法,再重新定義該方法。首先,它對 el 做了限制,Vue 不能掛載在 body、html 這樣的根節點上。接下來的是很關鍵的邏輯 —— 如果沒有定義 render 方法,則會把 el 或者 template 字串轉換成 render 方法。這裡我們要牢記,在 Vue 2.0 版本中,所有 Vue 的元件的渲染最終都需要 render 方法,無論我們是用單檔案 .vue 方式開發元件,還是寫了 el 或者 template 屬性,最終都會轉換成 render 方法,那麼這個過程是 Vue 的一個“線上編譯”的過程,它是呼叫 compileToFunctions 方法實現的,編譯過程我們之後會介紹。最後,呼叫原先原型上的 $mount 方法掛載。

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

$mount 方法支援傳入 2 個引數,第一個是 el,它表示掛載的元素,可以是字串,也可以是 DOM 物件,如果是字串在瀏覽器環境下會呼叫 query 方法轉換成 DOM 物件的。第二個引數是和服務端渲染相關,在瀏覽器環境下我們不需要傳第二個引數。
$mount 方法實際上會去呼叫 mountComponent 方法,這個方法定義在 src/core/instance/lifecycle.js 檔案中:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el    //  做一個快取 
  if (!vm.$options.render) {   //  如果沒有render  建立一個VNode
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {    // 用了template  用了runtime版本;
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',  //沒有template和render函式
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')
  let updateComponent
  /* istanbul ignore if */  開發環境又配置了performance 的時候
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)
      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {    //   渲染方法  監聽到執行的過程,修改的時候也是觸發這個方法
      vm._update(vm._render(), hydrating)    //執行vm._render(), vm._update()
    }
  }

  // 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
  //   渲染watcher watcher也是一個類   noop是空函式
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        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
}

從上面的程式碼可以看到,mountComponent 核心就是先呼叫 vm._render 方法先生成虛擬 Node,再例項化一個渲染Watcher,在它的回撥函式中會呼叫 updateComponent 方法,最終呼叫 vm._update 更新 DOM。
Watcher 在這裡起到兩個作用,一個是初始化的時候會執行回撥函式,另一個是當 vm 例項中的監測的資料發生變化的時候執行回撥函式
函式最後判斷為根節點的時候設定 vm._isMounted 為 true, 表示這個例項已經掛載了,同時執行 mounted 鉤子函式。 這裡注意 vm.$vnode 表示 Vue 例項的父虛擬 Node,所以它為 Null 則表示當前是根 Vue 的例項。

vm_render()

Vue 的 _render 方法是例項的一個私有方法,它用來把例項渲染成一個虛擬 Node。它的定義在 src/core/instance/render.js 檔案中:

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

  // reset _rendered flag on slots for duplicate slot check
  if (process.env.NODE_ENV !== 'production') {
    for (const key in vm.$slots) {
      // $flow-disable-line
      vm.$slots[key]._rendered = false
    }
  }

  if (_parentVnode) {
    vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
  }

  // 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 {
    vnode = render.call(vm._renderProxy, vm.$createElement)  
    //vm._renderProxy其實就是vm本身
  } 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') {
      if (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
      }
    } else {
      vnode = vm._vnode
    }
  }
  // return empty vnode in case the render function errored out
  if (!(vnode instanceof VNode)) {
    if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
      warn(
        'Multiple root nodes returned from render function. Render function ' +
        'should return a single root node.',
        vm
      )
    }
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

這段程式碼最關鍵的是 render 方法的呼叫,我們在平時的開發工作中手寫 render 方法的場景比較少,而寫的比較多的是 template 模板,在之前的 mounted 方法的實現中,會把 template 編譯成 render 方法
在 Vue 的官方文件中介紹了 render 函式的第一個引數是 createElement,那麼結合之前的例子:

<div id="app">
  {{ message }}
</div>

相當於我們編寫如下 render 函式:

render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  }, this.message)
}

再回到 _render 函式中的 render 方法的呼叫:

vnode = render.call(vm._renderProxy, vm.$createElement)

可以看到,render 函式中的 createElement 方法就是 vm.$createElement 方法:

export function initRender (vm: Component) {
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

實際上,vm. c r e a t e E l e m e n t i n i t R e n d e r v m . createElement 方法定義是在執行 initRender 方法的時候,可以看到除了 vm. createElement 方法,還有一個 vm._c 方法,它是被模板編譯成的 render 函式使用,而 vm.$createElement 是使用者手寫 render 方法使用的, 這倆個方法支援的引數相同,並且內部都呼叫了 createElement 方法。
vm._render 最終是通過執行 createElement 方法並返回的是 vnode,它是一個虛擬 Node。

Virtual DOM

Virtual DOM 就是用一個原生的 JS 物件去描述一個 DOM 節點,真正的 DOM 元素是非常龐大的,因為瀏覽器的標準就把 DOM 設計的非常複雜。當我們頻繁的去做 DOM 更新,會產生一定的效能問題。所以它比建立一個 DOM 的代價要小很多。在 Vue.js 中,Virtual DOM 是用 VNode 這麼一個 Class 去描述,它是定義在 src/core/vdom/vnode.js 中的。

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  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
  .......

可以看到 Vue.js 中的 Virtual DOM 的定義還是略微複雜一些的,因為它這裡包含了很多 Vue.js 的特性。其實 VNode 是對真實 DOM 的一種抽象描述,它的核心定義標籤名、資料、子節點、鍵值等,其它屬性都是都是用來擴充套件 VNode 的靈活性以及實現一些特殊 feature 的。由於 VNode 只是用來對映到真實 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常輕量和簡單的。Virtual DOM 除了它的資料結構的定義,對映到真實的 DOM 實際上要經歷 VNode 的 create、diff、patch 等過程。

createElement

Vue.js 利用 createElement 方法建立 VNode,它定義在 src/core/vdom/create-elemenet.js 中:

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {   // 對引數個數不一致的處理
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

createElement 方法實際上是對 _createElement 方法的封裝,它允許傳入的引數更加靈活,在處理這些引數後,呼叫真正建立 VNode 的函式 _createElement:

export function _createElement (  //五個引數
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {   //對data做校驗   data不能是響應式
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()     // 相當於一個註釋結點
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {   判斷data.is   做相應的處理
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()   // 不是真值 返回註釋結點
  }
  // warn against non-primitive key  對key的一個判斷
 ..........
  }
  if (normalizationType === ALWAYS_NORMALIZE) {    
    children = normalizeChildren(children)   // 如果其中一個子節點為array 遞迴 
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)  //對children做遍歷  是一個一維陣列  陣列每一個都是一個vnode
  }
  let vnode, ns
  if (typeof tag === 'string') {          //   tag為一個string
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {   
      // platform built-in elements
      vnode = new VNode(   //  建立一個vnode
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component  元件的解析
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

_createElement 方法有 5 個引數,context 表示 VNode 的上下文環境,它是 Component 型別;tag 表示標籤,它可以是一個字串,也可以是一個 Component;data 表示 VNode 的資料,它是一個 VNodeData 型別;children 表示當前 VNode 的子節點,它是任意型別的,它接下來需要被規範為標準的 VNode 陣列;normalizationType 表示子節點規範的型別,型別不同規範的方法也就不一樣,它主要是參考 render 函式是編譯生成的還是使用者手寫的。
createElement 函式的流程略微有點多,我們接下來主要分析 2 個重點的流程 —— children 的規範化以及 VNode 的建立。

children 的規範化

由於 Virtual DOM 實際上是一個樹狀結構,每一個 VNode 可能會有若干個子節點,這些子節點應該也是 VNode 的型別。_createElement 接收的第 4 個引數 children 是任意型別的,因此我們需要把它們規範成 VNode 型別。
這裡根據 normalizationType 的不同,呼叫了 normalizeChildren(children) 和 simpleNormalizeChildren(children) 方法,它們的定義都在 src/core/vdom/helpers/normalzie-children.js 中:

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

simpleNormalizeChildren 方法呼叫場景是 render 函式當函式是編譯生成的。理論上編譯生成的 children 都已經是 VNode 型別的,但這裡有一個例外,就是 functional component 函式式元件返回的是一個數組而不是一個根節點,所以會通過 Array.prototype.concat 方法把整個 children 陣列打平,讓它的深度只有一層。
normalizeChildren 方法的呼叫場景有 2 種,一個場景是 render 函式是使用者手寫的,當 children 只有一個節點的時候,Vue.js 從介面層面允許使用者把 children 寫成基礎型別用來建立單個簡單的文字節點,這種情況會呼叫 createTextVNode 建立一個文字節點的 VNode;另一個場景是當編譯 slot、v-for 的時候會產生巢狀陣列的情況,會呼叫 normalizeArrayChildren 方法,接下來看一下它的實現:

function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // merge adjacent text nodes
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        res.push(c)
      }
    }
  }
  return res
}

normalizeArrayChildren 接收 2 個引數,children 表示要規範的子節點,nestedIndex 表示巢狀的索引,因為單個 child 可能是一個數組型別。 normalizeArrayChildren 主要的邏輯就是遍歷 children,獲得單個節點 c,然後對 c 的型別判斷,如果是一個數組型別,則遞迴呼叫 normalizeArrayChildren; 如果是基礎型別,則通過 createTextVNode 方法轉換成 VNode 型別;否則就已經是 VNode 型別了,如果 children 是一個列表並且列表還存在巢狀的情況,則根據 nestedIndex 去更新它的 key。這裡需要注意一點,在遍歷的過程中,對這 3 種情況都做了如下處理:如果存在兩個連續的 text 節點,會把它們合併成一個 text 節點。
經過對 children 的規範化,children 變成了一個型別為 VNode 的 Array。

vnode 建立

回到 createElement 函式,規範化 children 後,接下來會去建立一個 VNode 的例項:

let vnode, ns
if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // platform built-in elements
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // component
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    // unknown or unlisted namespaced elements
    // check at runtime because it may get assigned a namespace when its
    // parent normalizes children
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}

這裡先對 tag 做判斷,如果是 string 型別,則接著判斷如果是內建的一些節點,則直接建立一個普通 VNode,如果是為已註冊的元件名,則通過 createComponent 建立一個元件型別的 VNode,否則建立一個未知的標籤的 VNode。 如果是 tag 一個 Component 型別,則直接呼叫 createComponent 建立一個元件型別的 VNode 節點。對於 createComponent 建立元件型別的 VNode 的過程,我們之後會去介紹,本質上它還是返回了一個 VNode。
那麼至此,我們大致瞭解了 createElement 建立 VNode 的過程,每個 VNode 有 children,children 每個元素也是一個 VNode,這樣就形成了一個 VNode Tree,它很好的描述了我們的 DOM Tree。
回到 mountComponent 函式的過程,我們已經知道 vm._render 是如何建立了一個 VNode,接下來就是要把這個 VNode 渲染成一個真實的 DOM 並渲染出來,這個過程是通過 vm._update 完成的。

vm_update

Vue 的 _update 是例項的一個私有方法,它被呼叫的時機有 2 個,一個是首次渲染,一個是資料更新的時候。_update 方法的作用是把 VNode 渲染成真實的 DOM

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = 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)
  }
  activeInstance = prevActiveInstance
  // 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.
}

_update 的核心就是呼叫 vm.patch 方法,它的定義在 src/platforms/web/runtime/patch.js中:

export const patch: Function = createPatchFunction({ nodeOps, modules })

該方法的定義是呼叫 createPatchFunction 方法的返回值,這裡傳入了一個物件,包含 nodeOps 引數和 modules 引數。其中,nodeOps 封裝了一系列 DOM 操作的方法,modules 定義了一些模組的鉤子函式的實現。
createPatchFunction 內部定義了一系列的輔助方法,最終返回了一個 patch 方法,這個方法就賦值給了 vm._update 函式裡呼叫的 vm.patch,它接收 4個引數,oldVnode 表示舊的 VNode 節點,它也可以不存在或者是一個 DOM 物件;vnode 表示執行 _render 後返回的 VNode 的節點;hydrating 表示是否是服務端渲染;removeOnly 是給 transition-group 用的,patch 的邏輯看上去相對複雜
確定了這些入參後,我們回到 patch 函式的執行過程,看幾個關鍵步驟。

const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  // patch existing root node
  patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
  if (isRealElement) {
    // mounting to a real element
    // check if this is server-rendered content and if we can perform
    // a successful hydration.
    if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
      oldVnode.removeAttribute(SSR_ATTR)
      hydrating = true
    }
    if (isTrue(hydrating)) {
      if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
        invokeInsertHook(vnode, insertedVnodeQueue, true)
        return oldVnode
      } else if (process.env.NODE_ENV !== 'production') {
        warn(
          'The client-side rendered virtual DOM tree is not matching ' +
          'server-rendered content. This is likely caused by incorrect ' +
          'HTML markup, for example nesting block-level elements inside ' +
          '<p>, or missing <tbody>. Bailing hydration and performing ' +
          'full client-side render.'
        )
      }
    }      
    // either not server-rendered, or hydration failed.
    // create an empty node and replace it
    oldVnode = emptyNodeAt(oldVnode)
  }

  // replacing existing element
  const oldElm = oldVnode.elm
  const parentElm = nodeOps.parentNode(oldElm)

  // create new node 建立新結點
  createElm(
    vnode,
    insertedVnodeQueue,
    // extremely rare edge case: do not insert if old element is in a
    // leaving transition. Only happens when combining transition +
    // keep-alive + HOCs. (#4590)
    oldElm._leaveCb ? null : parentElm,
    nodeOps.nextSibling(oldElm)
  )
}

由於我們傳入的 oldVnode 實際上是一個 DOM container,所以 isRealElement 為 true,接下來又通過 emptyNodeAt 方法把 oldVnode 轉換成 VNode 物件,然後再呼叫 createElm 方法,這個方法在這裡非常重要,來看一下它的實現:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check    建立子元件
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }    
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    } 
    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

createElm 的作用是通過虛擬節點建立真實的 DOM 並插入到它的父節點中。 我們來看一下它的一些關鍵邏輯,createComponent 方法目的是嘗試建立子元件,在當前這個 case 下它的返回值為 false;接下來判斷 vnode 是否包含 tag,如果包含,先簡單對 tag 的合法性在非生產環境下做校驗,看是否是一個合法標籤;然後再去呼叫平臺 DOM 的操作去建立一個佔位符元素。
接下來呼叫 createChildren 方法去建立子元素:

createChildren(vnode, children, insertedVnodeQueue)
function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children)
    }
    for (let i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

createChildren 的邏輯很簡單,實際上是遍歷子虛擬節點,遞迴呼叫 createElm,這裡要注意的一點是在遍歷過程中會把 vnode.elm 作為父容器的 DOM 節點佔位符傳入。

接著再呼叫 invokeCreateHooks 方法執行所有的 create 的鉤子並把 vnode push 到 insertedVnodeQueue 中。

 if (isDef(data)) {
  invokeCreateHooks(vnode, insertedVnodeQueue)
}    
function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

最後呼叫 insert 方法把 DOM 插入到父節點中,因為是遞迴呼叫,子元素會優先呼叫 insert,所以整個 vnode 樹節點的插入順序是先子後父。來看一下 insert 方法,它的定義在 src/core/vdom/patch.js 上。

insert(parentElm, vnode.elm, refElm)
function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (ref.parentNode === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}

insert 邏輯很簡單,呼叫一些 nodeOps 把子節點插入到父節點中,這些輔助方法定義在 src/platforms/web/runtime/node-ops.js 中:

export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

其實就是呼叫原生 DOM 的 API 進行 DOM 操作,在 createElm 過程中,如果 vnode 節點不包含 tag,則它有可能是一個註釋或者純文字節點,可以直接插入到父元素中。首次渲染我們呼叫了 createElm 方法,這裡傳入的 parentElm 是 oldVnode.elm 的父元素,在我們的例子是 id 為 #app div 的父元素,也就是 Body;實際上整個過程就是遞迴建立了一個完整的 DOM 樹並插入到 Body 上。
嗯,終於大概對著學習文件理了一遍。有點簡陋,可能很多細節沒有注意到