今天咱們來學習下Vue的渲染 Render 源碼~
仍是從初始化方法開始找代碼,在 src/core/instance/index.js
中,先執行了 renderMixin
方法,而後在Vue實例化的時候執行了 vm._init
方法,在這個 vm._init
方法中執行了 initRender
方法。renderMixin
和 initRender
都在 src/core/instance/render.js
中,咱們來看看代碼:前端
首先來跟一下 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
接下來看下 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 對象由 createEmptyVNode
或 createComponent
方法獲得的。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 的內容在下一篇中繼續研究~
這裏再提出幾個問題,以後學習和解決:
計劃3月底完成Vue源碼的系統學習,以後轉戰vue-router、vuex、vuxt、 devtools、webpack、vue-loader,今年目標把Vue全家老少、親戚朋友都學習一遍!加油!
鑑於前端知識碎片化嚴重,我但願可以系統化的整理出一套關於Vue的學習系列博客。
本文源碼已收入到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