今天咱们来学习下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