前言
本文主要想看一下render是如何调用的,也就是render→VNode的过程。
流程总览
在 Vue 2 完整版中,实例化Vue后,调用(被重载的)$mount方法时就开始进行模板解析,整合了各种配置之后,调用baseCompile开始模板编译,生成render和staticRenderFns。render调用后生成虚拟 DOMVNode,staticRenderFns记录了静态节点的构造函数。
graph LR
A["(new Vue(...)).$mount(...)"] --> |call|B[compileToFunctions]
createCompilerCreator\nargs:baseCompile -->|return| createCompiler -->|return|B
B --> |"call & return"|baseCompile --> |return|C["{ render, staticRenderFns }"]
接着调用用于挂载节点的Vue.prototype.$mount方法,最后在_render中调用render函数
graph LR
A["(new Vue(...)).$mount(...)"] --> |"①"|B[compileToFunctions]
C --> D
A --> |"②"|D["options=this.$options\n options.render=render\n options.staticRenderFns=staticRenderFns"]
A-->|"③"|E[Vue.prototype.$mount] -->|call| F[mountComponent] -->|call|G[updateComponent]-->|call|H["_render"]-->|call|I["render"]
B --> |return|C["{ render, staticRenderFns }"]
函数入口
src/core/instance/render.ts,_render,略过作用域插槽的逻辑:
ts- Vue.prototype._render = function (): VNode {
- // ...作用域插槽
-
- // set parent vnode. this allows render functions to have access
- // to the data on the placeholder node.
- vm.$vnode = _parentVnode!
- // render self
- let vnode
- try {
- setCurrentInstance(vm)
- currentRenderingInstance = vm
- vnode = render.call(vm._renderProxy, vm.$createElement)
- } catch (e: any) {
- handleError(e, vm, `render`)
- vnode = vm._vnode
- } finally {
- currentRenderingInstance = null
- setCurrentInstance()
- }
- if (isArray(vnode) && vnode.length === 1) {
- vnode = vnode[0]
- }
- if (!(vnode instanceof VNode)) {
- vnode = createEmptyVNode()
- }
- vnode.parent = _parentVnode
- return vnode
- }
render函数被调用,vm._renderProxy是vm生产模式本身,开发模式下会附上错误输出的钩子,vm.$createElement用于生成VNode。如果使用render选项或者在setup中返回render的话,我们会这样子写:
ts- render (h) {
- return h('div',{
- // 给div绑定class属性
- class: {
- classTest: true,
- },
- // 给div绑定样式
- style: {
- width: '200px',
- height: '200px',
- },
- // 给div绑定点击事件
- on: {
- click: () => {
- console.log('点击事件')
- }
- },
- })
- }
类似于:
html- <template>
- <div
- :class="{ classTest: true }"
- :style="{ width: '200px', height: '200px' }"
- @click="clickHandler"
- ></div>
- </template>
- <script>
- export default {
- methods: {
- clickHandler: () => {
- console.log('点击事件')
- }
- }
- }
- </script>
vm.$createElement就是上面的h函数。
ts- // src/core/instance/render.ts
- vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
标准化输入
createElement先对输入的格式进行标准化:
ts- // src/core/vdom/create-element.ts
- export function createElement(
- context: Component,
- tag: any,
- data: any,
- children: any,
- normalizationType: any,
- alwaysNormalize: boolean
- ): VNode | Array<VNode> {
- if (isArray(data) || isPrimitive(data)) {
- normalizationType = children
- children = data
- data = undefined
- }
- if (isTrue(alwaysNormalize)) {
- normalizationType = ALWAYS_NORMALIZE
- }
- return _createElement(context, tag, data, children, normalizationType)
- }
- export function _createElement(
- context: Component,
- tag?: string | Component | Function | Object,
- data?: VNodeData,
- children?: any,
- normalizationType?: number
- ): VNode | Array<VNode> {
- if (isDef(data) && isDef((data as any).__ob__)) {
- return createEmptyVNode()
- }
- // 处理 component 标签
- if (isDef(data) && isDef(data.is)) {
- tag = data.is
- }
- if (!tag) {
- // in case of component :is set to falsy value
- return createEmptyVNode()
- }
- // support single function children as default scoped slot
- // ... 对应在 h 函数传入插槽的情况
- if (isArray(children) && isFunction(children[0])) {
- data = data || {}
- data.scopedSlots = { default: children[0] }
- children.length = 0
- }
- if (normalizationType === ALWAYS_NORMALIZE) {
- children = normalizeChildren(children)
- } else if (normalizationType === SIMPLE_NORMALIZE) {
- // 模板生成的 render 函数一般调用这个
- children = simpleNormalizeChildren(children)
- }
- // ...
- }
下面来看看normalizeChildren和simpleNormalizeChildren两个函数,这两个函数做的事情很简单,下面代码就不用细看了:
ts- // 模板生成的 render 函数一般调用这个
- export function simpleNormalizeChildren(children: any) {
- for (let i = 0; i < children.length; i++) {
- if (isArray(children[i])) {
- return Array.prototype.concat.apply([], children)
- }
- }
- return children
- }
- // <template>, <slot>, v-for 或者手写的 render, JSX 调用这个
- export function normalizeChildren(children: any): Array<VNode> | undefined {
- return isPrimitive(children)
- ? [createTextVNode(children)]
- : isArray(children)
- ? normalizeArrayChildren(children)
- : undefined
- }
- function isTextNode(node): boolean {
- return isDef(node) && isDef(node.text) && isFalse(node.isComment)
- }
- function normalizeArrayChildren(
- children: any,
- nestedIndex?: string
- ): Array<VNode> {
- const res: VNode[] = []
- let i, c, lastIndex, last
- for (i = 0; i < children.length; i++) {
- c = children[i]
- if (isUndef(c) || typeof c === 'boolean') continue
- lastIndex = res.length - 1
- last = res[lastIndex]
- // nested
- if (isArray(c)) {
- 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].text)
- c.shift()
- }
- res.push.apply(res, c)
- }
- } else if (isPrimitive(c)) {
- if (isTextNode(last)) {
- // merge adjacent text nodes
- // this is necessary for SSR hydration because text nodes are
- // essentially merged when rendered to HTML strings
- res[lastIndex] = createTextVNode(last.text + c)
- } else if (c !== '') {
- // convert primitive to vnode
- 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
- }
normalizeChildren和simpleNormalizeChildren都会把多层数组的子节点平铺。normalizeChildren此外还会创建文本类型的VNode,合并相邻文本,以及为没有key的子节点增加key。
创建 VNode
继续看_createElement:
ts- export function _createElement(
- context: Component,
- tag?: string | Component | Function | Object,
- data?: VNodeData,
- children?: any,
- normalizationType?: number
- ): VNode | Array<VNode> {
- // ...
-
- let vnode, ns
- if (typeof tag === 'string') {
- let Ctor
- ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
- if (config.isReservedTag(tag)) {
- // 原生标签,div 之类的
- vnode = new VNode(
- config.parsePlatformTagName(tag),
- data,
- children,
- undefined,
- undefined,
- context
- )
- } else if (
- (!data || !data.pre) &&
- // 这里的 resolveAsset 会去组件实例的 components 中获得 tag (这时候是自定义组件名)对应的组件构造函数或者对象
- isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
- ) {
- // 组件
- vnode = createComponent(Ctor, data, context, children, tag)
- } else {
- // 传入的标签不知道是什么
- vnode = new VNode(tag, data, children, undefined, undefined, context)
- }
- } else {
- // 给 h 函数传入组件的构造函数或者对象的情况
- vnode = createComponent(tag as any, data, context, children)
- }
- if (isArray(vnode)) {
- return vnode
- } else if (isDef(vnode)) {
- if (isDef(ns)) applyNS(vnode, ns)
- if (isDef(data)) registerDeepBindings(data)
- return vnode
- } else {
- return createEmptyVNode()
- }
- }
我们不难猜出createComponent是处理自定义组件的,new VNode创建普通的标签。
先看VNode,new VNode(...)创建出一个空壳 VNode,这也展示了 Vue 的虚拟 DOM 的数据结构:
ts- export default class VNode {
- //...
- constructor(
- tag?: string,
- data?: VNodeData,
- children?: Array<VNode> | null,
- text?: string,
- elm?: Node,
- context?: Component,
- componentOptions?: VNodeComponentOptions,
- asyncFactory?: Function
- ) {
- this.tag = tag
- this.data = data
- this.children = children
- this.text = text
- this.elm = elm
- this.ns = undefined
- this.context = context
- this.fnContext = undefined
- this.fnOptions = undefined
- this.fnScopeId = undefined
- this.key = data && data.key
- this.componentOptions = componentOptions
- this.componentInstance = undefined
- this.parent = undefined
- this.raw = false
- this.isStatic = false
- this.isRootInsert = true
- this.isComment = false
- this.isCloned = false
- this.isOnce = false
- this.asyncFactory = asyncFactory
- this.asyncMeta = undefined
- this.isAsyncPlaceholder = false
- }
- }
然后createComponent:
ts- export function createComponent(
- Ctor: typeof Component | Function | ComponentOptions | void,
- data: VNodeData | undefined,
- 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 as typeof Component)
- }
- if (typeof Ctor !== 'function') {
- return
- }
-
- // ... 异步组件
-
- data = data || {}
- // ... 合并继承而来的属性
- // ... v-model
- // 获取组件的 props
- const propsData = extractPropsFromVNodeData(data, Ctor, tag)
- // ... functional 组件,就是一种用于没有实例和状态的组件,选项加上 functional: true 即可,
- // 可用于增强组件功能或者作为性能更好的纯展示组件
- // extract listeners, since these needs to be treated as
- // child component listeners instead of DOM listeners
- const listeners = data.on
- // replace with listeners with .native modifier
- // so it gets processed during parent component patch.
- data.on = data.nativeOn
- // ... 抽象组件,就是一种没有生命周期的组件,选项加上 abstract: true 即可,
- // 可用于增强组件功能
- // 创建 init(渲染子组件), destory(发起销毁生命周期), insert(发起挂载生命周期) 等钩子
- installComponentHooks(data)
- // 最后创建 VNode
- // @ts-expect-error
- const name = getComponentName(Ctor.options) || tag
- const vnode = new VNode(
- // @ts-expect-error
- `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
- data,
- undefined,
- undefined,
- undefined,
- context,
- // @ts-expect-error
- { Ctor, propsData, listeners, tag, children },
- asyncFactory
- )
- return vnode
- }
感觉这里只是对组件做了初始化的处理,组件的具体逻辑就不展开了…
