2
今天我们来学习下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学习系列项目地址

本文源码已收入到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


VioletJack
931 声望161 粉丝

专注于Vue前端开发的学习和分享