Vue.js原始碼學習四 —— 渲染 Render 初始化過程學習
今天我們來學習下Vue的渲染 Render 原始碼~
還是從初始化方法開始找程式碼,在 src/core/instance/index.js
中,先執行了 renderMixin
方法,然後在Vue例項化的時候執行了 vm._init
方法,在這個 vm._init
方法中執行了 initRender
方法。renderMixin
和 initRender
都在 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 物件由 createEmptyVNode
或 createComponent
方法得到的。
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
Vue之vue.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原始碼的小夥伴有所幫助。之前這篇文章同樣在我司(大搜車)的