1. 程式人生 > >Vue.js原始碼學習四 —— 渲染 Render 初始化過程學習

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 的程式碼:

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 方法:

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 函式,找到該函式原始碼:

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物件。

initRender

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

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 方法:

// 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 建立了一個空的 VNode

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

createComponent 建立了一個元件,最終也將返回一個 VNode 物件。

// 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學習系列專案地址

關於作者

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

相關推薦

Vue.js原始碼學習 —— 渲染 Render 初始過程學習

今天我們來學習下Vue的渲染 Render 原始碼~ 還是從初始化方法開始找程式碼,在 src/core/instance/index.js 中,先執行了 renderMixin 方法,然後在Vue例項化的時候執行了 vm._init 方法,在這個 v

mybatis原始碼學習——Configuration類及其初始過程、TypeHandler、TypeAlias

Configuration類是Mybatis中的特別核心的一個類,主要用來進行Mybatis執行過程中的各項引數的設定。第一次Debug原始碼時,會感覺到什麼配置都需要在Configuration中設定,多次Debug之後,發現確實如此,這就是Mybatis中的

spring MVC初始過程學習筆記1

load cati 過程 mage 筆記 ngx 名稱 spring -s 如果有錯誤請指正~ 1.springmvc容器和spring的關系? 1.1 spring是個容器,主要是管理bean,不需要servlet容器就可以啟動,而springMVC實現了servl

dubbo原始碼分析-消費端啟動初始過程-筆記

消費端的程式碼解析是從下面這段程式碼開始的 <dubbo:reference id="xxxService" interface="xxx.xxx.Service"/> ReferenceBean(afterPropertiesSet) ->getObject() ->ge

從底層原始碼淺析Mybatis的SqlSessionFactory初始過程

目錄 搭建原始碼環境 POM依賴 測試SQL Mybatis全域性配置檔案 UserMapper介面 UserMapper配置 User實體 Main方法 快速進入Debug跟蹤 原始碼分析準備 原始碼分析

rocketmq之原始碼分析broker入口BrokerController初始過程(十六)

接著上一章的BrokerController的基礎功能講,本章主要介紹的是BrokerController的初始化操作,在初始化的

Vue.js原始碼 生命週期 LifeCycle 學習

callHook 。  我們來看看 callHook 程式碼: export function callHook (vm: Component, hook: string) {   const handlers = vm.$options[hook] // 獲取Vue選項中的

Vue.js原始碼學習一 —— 資料選項 State 學習

關於Vue原始碼學習的部落格, HcySunYang的Vue2.1.7原始碼學習是我所見過講的最清晰明瞭的部落格了,非常適合想了解Vue原始碼的同學入手。本文是在看了這篇部落格之後進一步的學習心得。 注意:本文所用Vue版本為 2.5.13 P

Vue.js 原始碼學習筆記

最近饒有興致的又把最新版 Vue.js 的原始碼學習了一下,覺得真心不錯,個人覺得 Vue.js 的程式碼非常之優雅而且精闢,作者本身可能無 (bu) 意 (xie) 提及這些。那麼,就讓我來吧:) 程式結構梳理 Vue.js 是一個非常典型的 MVVM 的程式結

Vue學習原始碼分析--從template到DOM(Vue.js原始碼角度看內部執行機制)(九)

從new一個Vue物件開始 let vm = new Vue({ el: '#app', /*some options*/ }); 很多同學好奇,在new一個Vue物件的時候,內部究竟發生了什麼? 究竟Vue.js是如何將data中的資

Vue.js原始碼學習三 —— 事件 Event 學習

早上好!繼續學習Vue原始碼~這次我們來學習 event 事件。 原始碼簡析 其實看了前兩篇的同學已經知道原始碼怎麼找了,這裡再提一下。 先找到Vue核心原始碼index方法 src/core/instance/index.js func

Vue.js 原始碼學習五 —— provide 和 inject 學習

早上好!繼續開始學習Vue原始碼吧~ 在 Vue.js 的 2.2.0+ 版本中新增加了 provide 和 inject 選項。他們成對出現,用於父級元件向下傳遞資料。 下面我們來看看原始碼~ 原始碼位置 和之前一樣,初始化的方法都是在 V

Vuevue.js聲明式渲染

這一 logs 類型檢測 body 表達式 頁面 渲染 strong setter Html: <div id="app"> {{ message }} </div> Vue: var app = new Vue({ el: ‘#

從零開始學習比特幣開發()--網路初始,載入區塊鏈和錢包,匯入區塊啟動節點

寫在前面: 本篇文章接續 從零開始學習區塊鏈技術(三)-接入比特幣網路的關鍵步驟解析、建立比特幣錢包,以及重要rpc指令 從零開始學習區塊鏈技術(二)–如何接入比特幣網路以及其原理分析 以及從零開始學習區塊鏈技術(一)–從原始碼編譯比特幣 如果這篇文章看不明白,請務必先閱讀之前的文章

vue.js 中 :is 與 is 的用法和區別,學習全域性與區域性註冊元件

  vue中 is用來動態切換元件,詳細請看示例:(順便講解父向子元件的傳遞資訊) html: <div id="app">   <!--         1.在這裡呼叫元件。   &

spring原始碼學習之路---深度分析IOC容器初始過程(三)

分析FileSystemXmlApplicationContext的建構函式,到底都做了什麼,導致IOC容器初始化成功。 public FileSystemXmlApplicationContext(String[] configLocations, boolean ref

Vue.js 原始碼解析

介紹 Vue.js原始碼分析,記錄了個人學習Vue.js原始碼的過程中的一些心得以及收穫。以及對於Vue框架,周邊庫的一些個人見解。 在學習的過程中我為Vue.js(2.3.0)、Vuex(2.4.0)、Vue-router(3.0.1)加上了註釋,分別在資料夾vue-src、vuex-sr

說說 Vue.js 中的條件渲染指令

1 應用於單個元素 Vue.js 中的條件渲染指令可以根據表示式的值,來決定在 DOM 中是渲染還是銷燬元素或元件。 html: <div id="app"> <p v-if="type===1">拌麵</p> <

10分鐘快速精通rollup.js——Vue.js原始碼打包原理深度分析

前言 本教程是rollup.js系列教程的最後一篇,我將基於目前非常流行的Vue.js框架,深度分析Vue.js原始碼打包過程,讓大家深入理解複雜的前端框架是如何利用rollup.js進行打包的。通過這一篇教程的學習,相信大家可以更好地應用rollup.js為自己的專案服務。 說明:本教程基於Vue

從template到DOM(Vue.js原始碼角度看內部執行機制)

寫在前面 這篇文章算是對最近寫的一系列Vue.js原始碼的文章(https://github.com/answershuto/learnVue)的總結吧,在閱讀原始碼的過程中也確實受益匪淺,希望自己的這些產出也會對同樣想要學習Vue.js原始碼的小夥伴有所幫助。之前這篇文章同樣在我司(大搜車)的