Vue2源码系列-render函数

简介: 前面我们对 Vue 实例化的大致流程进行了梳理。现在我们再具体看看初始化中的 initRender 的处理,通过本篇文章可以学习到 Vue 的 render 函数处理逻辑。

前面我们对 Vue 实例化的大致流程进行了梳理。现在我们再具体看看初始化中的 initRender 的处理,通过本篇文章可以学习到 Vuerender 函数处理逻辑。


初始化渲染函数


上篇文章我们分析了初始化逻辑


let uid = 0
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    vm._uid = uid++
    //...
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    // 挂载节点
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
复制代码

其中的 initRender(vm) 我们没有进行深入分析,那是为了留给今天

export function initRender (vm: Component) {
  // ...
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  // 为实例添加了两个方法 其中只有一个参数不同
  // 我们主要分析 $createElement
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  // ... 
}
复制代码

我们可以看到,除去我省略的(分别和 slot / $attrs|listeners 相关 )不影响主流程的代码,initRender 仅仅为实例添加了方法 $createElement 并透传了参数


挂载实例


初始化之后,我们来到 _init 函数的最后一行


// 挂载节点
if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}
复制代码


这不是 $mount 方法么,激动啊,终于要开始渲染了么。问题是我们没看到 $mount 在哪里定义的呀


挂载入口


我们前面说过,在溯源得到 Vue 函数的时候,发现在溯源链路上不同的文件或多或少都有对 Vue 进行改造,或添加修改静态方法,或修改原型方法。既然没有看到 $mount 方法,那我们再从入口开始。


努力的人运气都不会太差,恰好 entry-runtime-with-compiler.js 中就找到了 $mount


// 先缓存原有的 $mount 函数
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  // 挂载根节点提示
  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
  // 判断是否有 render 函数,没有的话会调用 compileToFunctions 将 template 编译成 render 函数
  // 我们选择自己写 render 函数 就不用分析 compileToFunctions 过程了 偷懒就是这么简单
  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)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}
复制代码


这里的 $mount 方法主要是对配置 options 进行判断处理,如果定义了 render 方法,则啥事没有。如果没有则继续判断是否定义了 el,有则获取元素的内容 innerHTML 作为 template。最终通过 compileToFunctions 生成 options.render


还有个注意点是,前面我们缓存了 $mount 方法,最后我们又调用了刚才缓存的 $mount。其实这边的函数仅仅是对 options.render 进行了判断或生成。这样做的好处是将不同的逻辑分散在不同的文件模块中,很好地进行解耦,最后通过装饰器的效果实现整体代码,这点很值得我们学习和思考。当我们不知道一个函数到底该叫什么,或者到底属于哪部分时,不妨将其拆分解耦,再通过装饰器效果添加逻辑,这样各部分代码就不耦合杂糅。


$mount


我们继续往上溯源寻找我们的 $mount 真面目,在 plateform/web/runtime/index.js 找到了它


这时候突有所悟,这段代码在 web 目录下找到的,不就说明是和平台相关的么,前面解耦的作用不就体现的淋漓尽致,不管我的 $mount 是如何基于平台渲染的,都不用管我外层 render 函数的定义,他们的逻辑相互分离,简直妙不可言


// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 这边对 el 还有个兼容处理 开发者可以自己配置 dom 节点 或者让框架帮你去查找 dom 节点
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
复制代码

mountComponent


接着来到 mountComponent 函数在 core/instance/lifecycle.js,探探其逻辑。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 跳过render检查
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  // 好熟悉得生命周期钩子beforeMount
  callHook(vm, 'beforeMount')
  // 跳过开发环境带性能监测得代码
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`
      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)
      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    // 这个函数函数实现了渲染逻辑,是我们分析重点
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  // 此处watcher用于订阅数据改变时调用 updateComponent 回调函数,是我们响应式的原理
  // 当然创建Watcher实例的时候也会触发一次 updateComponent 函数
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // 又是熟悉得钩子
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
复制代码

通过分析可得 mountComponent 主要就是创建了 Watcher 实例。首次及当数据变化时调用 updateComponent 实现渲染。我们就来分析下 updateComponent 的逻辑

vm._update(vm._render())
复制代码

代码虽短,但逻辑不少,我们先看看 vm._render()

不好,又遇到难题了,我们前面分析了 vm.$options.render 的来源,怎么这边又来了个 vm._render,这又是个啥


renderMixin


寻根溯源,在 core/instance/index.js 我们找到这么一段代码

// ...
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
复制代码


没错 renderMixin 就是我们的猎物,我们看看其中代码实现

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)
  const { render, _parentVnode } = vm.$options
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  // 重点关注
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    // ...
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      currentRenderingInstance = vm
      // 生产环境 vm._renderProxy === vm
      // 可以看出_render的主要逻辑还是执行options.render函数 不过多加了异常处理提示
      // 当然有个重点是传入了vm.$createElement作为render函数的第一个参数
      // $createElement 的定义我们在initRender中有分析 接下来我们就看看render函数的执行了
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      // 异常逻辑暂不分析
      // ...
    } finally {
      currentRenderingInstance = null
    }
    // ...
    return vnode
  }
}
复制代码

中场总结



我们先梳理下上半篇分析出来的成果


  1. 在 Vue 实例初始化的时候 initRender 函数为实例添加了 $createElement 函数
  2. 初始化的最终一步是执行 $mount函数,$mount 包括两部分,其中最外层主要校验并生成 render 函数

  3. 在内层的 $mount 逻辑最终执行的是 mountComponent 函数
  4. mountComponent 的重点是创建 Watcher 实例并执行 updateComponent
  5. updateComponent 的逻辑在于执行 vm._update(vm._render(), hydrating)
  6. vm._render() 实际就是执行在 renderMixin 中定义的 _render
  7. _render 函数最终就是将步骤①中定义的 $createElement 作为参数传递给 options.render 并执行 render

render


接下来我们来分析 options.render($createElement) 的实现

为方便分析,我们先在项目中配置 render 函数来创建实例


new Vue({
  el: '#app',
  render: $createElement => $createElement('h1', {style: {color: 'red'}}, 'hello world')
})
复制代码


不出意外,大屏幕显示的是红色的标题 hello world


接下来就是分析 $createElement 的逻辑了


刚才有分析到 $createElement 主要是透传参数给 createElement

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
复制代码


所以我们接下来的重点是分析 createElement


createElement

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 如果data是数组或者非对象类型数据
  // 则默认为data位置就是子节点 而实际data置为undefined
  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 各参数的含义了,分别是 标签 数据 子节点 归一化类型。对参数进行简单处理转化后,接着执行 _createElement


_createElement

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // 响应式数据不能作为data
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // v-bind:is逻辑
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  // 无tag创建空节点
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // key值为对象类型时提示
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // slot相关
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 子节点扁平化处理,列如[a, [b, c]] => [a, b, c]
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 后文将单独分析
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  // ns 的逻辑暂时可以不去了解
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // ..
      // 创建Vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  // 返回VNode
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
复制代码


通过 _createElement 分析,我们可以得知 _createElement 的主要任务在于实例化 VNode 对象并返回。其中 VNode 就是我们常说的虚拟DOM,而不同的 VNode 对象拥有 children 属性构成一颗虚拟树。具体 VNode 创建过程及实例,我们将通过专门的文章分析,在此明白返回的是个 VNode 实例即可。


normalizeChildren


前面在 _createElement 函数创建虚拟节点之前,还有个子节点扁平化的过程

children = normalizeChildren(children)
复制代码


我们来看看 normalizeChildren 的处理逻辑

export function normalizeChildren (children: any): ?Array<VNode> {
  // 如果子节点是非对象数据 非返回单元素数组 值为文本虚拟节点
  // 否则返回 normalizeArrayChildren(children)
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}
复制代码


看来逻辑主要在 normalizeArrayChildren(children),我们接着进行分析

normalizeArrayChildren


函数的判断逻辑比较长。但本着分析主要流程的心态,我们大致了解下处理逻辑即可

function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  // 返回值为数组
  const res = []
  let i, c, lastIndex, last
  // 对数组进行遍历
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    // last为返回值末端节点
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  nested
    if (Array.isArray(c)) {
      // 遇到嵌套则递归调用normalizeArrayChildren拍拍平数组
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // merge adjacent text nodes
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        // 将节点添加进返回数组中
        res.push.apply(res, c)
      }
    // 判断文本节点
    } else if (isPrimitive(c)) {
      // 文本节点处理
      if (isTextNode(last)) {
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        // 非文本和数组则添加进返回数组中
        res.push(c)
      }
    }
  }
  return res
}
复制代码


可以看出 normalizeArrayChildren 的主要逻辑就是创建返回数组,遍历 children 数组。


  • 如果是节点数组,则递归调用 normalizeArrayChildren 来拍平子节点数组

  • 如果是非数组非对象数据,则为其创建文本虚拟节点,文本节点还涉及了合并文本的逻辑(这边我没发现怎么创建这样的数据,所以暂不分析)

  • 否则正常节点将节点添加至返回值即可


所以 normalizeArrayChildren 的返回值将为 [VNode, VNode, VNode, VNode] 这样的虚拟节点数组。


有个奇怪的问题,就是我们刚才分析是先分析 _createElement 创建虚拟节点,而创建虚拟节点之前先拍平数组。那我怎么说 normalizeArrayChildren 函数返回的是虚拟节点数组呢?


其实涉及到JS基础问题

new Vue({
  el: '#app',
  render: $createElement => $createElement('h1', {style: {color: 'red'}}, [$createElement('span', 'hello world')]),
})
复制代码


缓过神了吧,我们创建的 render 函数本身就是个函数嵌套函数的函数,所以实际运行中先会调用 $createElement 创建子节点 span 再创建父节点 h1


所以在我们在调用 normalizeChildren 拍平数组前,chilren 已经是经过 createElement 处理后形如 [VNode, VNode, [VNode, VNode]] 的节点数据了。

至此 render 函数就已经分析完了。


结语


我们今天分析了 render 函数的入口及执行逻辑。很遗憾的是我们的节点还是没有渲染到浏览器中,我们只是创建了 VNode 数据。等后文我们再去分析 Vnode 的实现及创建逻辑,以及 vm_update 是如何将 VNode 渲染为真实 DOM 的。


最后啰嗦一句,贴的代码比较多。分析的有不对的地方希望帮忙指正,有不清楚的地方也可以提出来,大家一起交流~



相关文章
|
1月前
|
前端开发 JavaScript 开发者
|
19天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
22天前
|
JavaScript API
vue3知识点:ref函数
vue3知识点:ref函数
30 2
|
23天前
|
JavaScript API
vue3知识点:自定义hook函数
vue3知识点:自定义hook函数
26 2
|
22天前
|
API
vue3知识点:reactive函数
vue3知识点:reactive函数
25 1
|
29天前
|
JavaScript
|
1月前
|
JavaScript
vue 组件中的 data 为什么是一个函数 ?
【10月更文挑战第8天】 在 Vue 组件中,`data` 被定义为一个函数而非普通对象,以确保每个组件实例拥有独立的数据空间,避免数据混乱。这种方式还支持数据的响应式更新、组件的继承与扩展,并有助于避免潜在问题,提升应用的可靠性和性能。
19 2
|
1月前
|
JavaScript 开发者
Vue Render函数
【10月更文挑战第11天】 Vue 的 Render 函数提供了一种强大而灵活的方法来创建虚拟 DOM 节点,使开发者能够更精细地控制组件的构建过程。通过 `createElement` 参数,可以动态生成各种元素和组件,实现复杂逻辑和高级定制。尽管使用 Render 函数需要更多代码和对虚拟 DOM 的深入理解,但它在处理复杂场景时展现出巨大的优势。
15 2
|
1月前
|
JavaScript
vue 函数化组件
【10月更文挑战第1天】 Vue.js 的函数化组件通过设置 `functional: true`,使其无状态和无实例,从而减少渲染开销。通过 `render` 函数的 `context` 参数传递数据。示例中,`smart-item` 组件根据 `data.type` 动态选择并渲染 `ImgItem`、`VideoItem` 或 `TextItem` 组件。根实例 `app` 通过按钮切换不同类型的组件数据。函数化组件适用于程序化选择组件和操作传递数据的场景。
|
7天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。