Vue.js源碼學習四 —— 渲染 Render 初始化過程學習

今天咱們來學習下Vue的渲染 Render 源碼~

仍是從初始化方法開始找代碼,在 src/core/instance/index.js 中,先執行了 renderMixin 方法,而後在Vue實例化的時候執行了 vm._init 方法,在這個 vm._init 方法中執行了 initRender 方法。renderMixininitRender 都在 src/core/instance/render.js 中,咱們來看看代碼:前端

renderMixin

首先來跟一下 renderMixin 的代碼:vue

export function renderMixin (Vue: Class<Component>) {
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

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

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

    vm.$vnode = _parentVnode
    let vnode
    try {
      // 執行 vue 實例的 render 方法
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      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
      }
    }
    // 返回空vnode避免render方法報錯退出
    if (!(vnode instanceof VNode)) {
      vnode = createEmptyVNode()
    }
    // 父級Vnode
    vnode.parent = _parentVnode
    return vnode
  }
}

源碼執行了 installRenderHelpers 方法,而後定義了 Vue 的 $nextTick_render 方法。
先來看看 installRenderHelpers 方法:node

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber // 數字
  target._s = toString // 字符串
  target._l = renderList // 列表
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
}

這就是 Vue 的各種渲染方法了,從字面意思中能夠知道一些方法的用途,這些方法用在Vue生成的渲染函數中。具體各個渲染函數的實現先不提~以後會專門寫博客學習。
$nextTick 函數中執行了 nextTick 函數,找到該函數源碼:webpack

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

如今來講關鍵的 _render 方法,關鍵在這個 try...catch 方法中,執行了Vue實例中的 render 方法生成一個vnode。若是生成失敗,會試着生成 renderError 方法。若是vnode爲空,則爲vnode傳一個空的VNode,最後返回vnode對象。git

initRender

接下來看下 render 的初始化過程:github

export function initRender (vm: Component) {
  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 方法綁定到這個實例,這樣咱們就能夠在其中獲得適當的 render context。
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // 規範化一直應用於公共版本,用於用戶編寫的 render 函數。
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  // 父級組件數據
  const parentData = parentVnode && parentVnode.data
  // 監聽事件
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}

在 initRender 方法中,爲Vue的實例方法添加了幾個屬性值,最後定義了 $attrs$listeners 的監聽方法。
看下 createElement 方法:web

// src/core/vdom/create-element.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 方法,因爲該方法太長,就不貼出來費篇幅了,代碼看這裏。最終返回一個 VNode 對象,VNode 對象由 createEmptyVNodecreateComponent 方法獲得的。
createEmptyVNode 建立了一個空的 VNodevue-router

// src/core/vdom/vnode.js
export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

createComponent 建立了一個組件,最終也將返回一個 VNode 對象。vuex

// src/core/vdom/create-component.js
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }
  const baseCtor = context.$options._base
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  if (typeof Ctor !== 'function') {
    return
  }

  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}
  resolveConstructorOptions(Ctor)
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  const listeners = data.on
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  mergeHooks(data)
  // 建立組件的 VNode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}

初次渲染過程

既然是初次渲染,確定會觸發 mounted 生命週期鉤子。因此咱們從 mount 找起。在源碼中定義了兩次 $mount 方法,第一次返回了 mountComponent 方法;第二次定義了 Vue 實例的 $options 選項中的一些數據,而後再執行第一次的 $mount 方法,即執行 mountComponent 方法。前端工程師

// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
// src/platforms/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)

  if (el === document.body || el === document.documentElement) {
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  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 {
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating)
}

這裏須要注意的是 compileToFunctions 方法,該方法的做用是將 template 編譯爲 render 函數。
compileToFunctions 方法是一個編譯的過程,暫且不論。抓住主線,看渲染。因此去看看 mountComponent 方法:

// src/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
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

能夠看到,在 beforeMount 和 mounted 生命週期之間的代碼:建立一個更新方法,而後建立一個Watcher監聽該方法。

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

  new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)

new Watcher 監聽了 updateComponent 方法後,會當即執行 updateComponent 方法。在 updateComponent 方法中,咱們以前提到 _render 方法最終返回一個編譯過的 VNode 對象,即虛擬 DOM,這裏咱們就看看 _update 方法。

// src/core/instance/lifecycle.js
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    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) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      vm.$options._parentElm = vm.$options._refElm = null
    } 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 (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }

從註釋能夠看出,初次渲染會走到 vm.__patch__ 方法中,這個方法就是比對虛擬 DOM ,局部更新 DOM 的方法,關於虛擬 DOM 和 VNode 節點,以後再聊。

小結一下

  • 經過 renderMixin 方法來定義一些渲染屬性。
  • initRender 定義了各種渲染選項,而且對一些屬性進行監聽。
  • $mount 方法執行了 mountComponent 方法,監聽
    updateComponent 方法並執行 _update 方法。
  • _update 方法中執行 __patch__ 方法渲染 VNode。

最後

這裏簡單理了理 render 渲染的代碼流程,更深刻的關於虛擬 DOM 的內容在下一篇中繼續研究~
這裏再提出幾個問題,以後學習和解決:

  • template 的具體編譯細節
  • 已知 data 數據監測,如何在改變數據後對改變界面的顯示。
  • 深刻理解虛擬 DOM 的原理
  • 學習全局 API 的源碼
  • 瞭解各種工具類
  • 瞭解 AST 語法樹是什麼~

計劃3月底完成Vue源碼的系統學習,以後轉戰vue-router、vuex、vuxt、 devtools、webpack、vue-loader,今年目標把Vue全家老少、親戚朋友都學習一遍!加油!

Vue.js學習系列

鑑於前端知識碎片化嚴重,我但願可以系統化的整理出一套關於Vue的學習系列博客。

Vue.js學習系列項目地址

本文源碼已收入到GitHub中,以供參考,固然能留下一個star更好啦^-^。
https://github.com/violetjack/VueStudyDemos

關於做者

VioletJack,高效學習前端工程師,喜歡研究提升效率的方法,也專一於Vue前端相關知識的學習、整理。
歡迎關注、點贊、評論留言~我將持續產出Vue相關優質內容。

新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571...
CSDN: http://blog.csdn.net/violetja...
簡書: http://www.jianshu.com/users/...
Github: https://github.com/violetjack

相關文章
相關標籤/搜索