挂载阶段 — vm._render()

最后更新于:2022-04-02 08:12:49

>[success] # vm._render() ~~~ 1.刚才分析了挂载阶段,发现想得到这个虚拟dom就需要,渲染函数vm._render()得到一份最新的VNode节点树 ,现在就需要看看这个vm._render 是个什么? 2.通过代码分析,首先在'core\instance\index.js' ,通过方法renderMixin(Vue)声明注册了'_render', renderMixin 方法在'core\instance\render.js' ~~~ * renderMixin 源码内容 ~~~js export function renderMixin (Vue: Class) { Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) } /*_render渲染函数,返回一个VNode节点*/ Vue.prototype._render = function (): VNode { const vm: Component = this const { render, staticRenderFns, _parentVnode } = vm.$options if (vm._isMounted) { // clone slot nodes on re-renders /*在重新渲染时会克隆槽位节点 不知道是不是因为Vnode必须必须唯一的原因,网上也没找到答案,此处存疑。*/ for (const key in vm.$slots) { vm.$slots[key] = cloneVNodes(vm.$slots[key]) } } /*作用域slot*/ vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject if (staticRenderFns && !vm._staticTrees) { /*用来存放static节点,已经被渲染的并且不存在v-for中的static节点不需要重新渲染,只需要进行浅拷贝*/ vm._staticTrees = [] } // 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 { /*调用render函数,返回一个VNode节点*/ vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render function`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { vnode = vm.$options.renderError ? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) : vm._vnode } else { vnode = vm._vnode } } // return empty vnode in case the render function errored out /*如果VNode节点没有创建成功则创建一个空节点*/ if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode } ~~~ >[info] ## 正式分析 ~~~ 1.这是一个粗略看源码做了什么的解析文章,因此我不会去分析源码内部,进过上面的源码内容,整个核心 的地方在这里 /*调用render函数,返回一个VNode节点*/ vnode = render.call(vm._renderProxy, vm.$createElement) 2.现在有了新的疑问'$createElement'是哪里来的,在回到'initMixin'方法文件对应位置'vue-src\core\instance\init.js' ,里面有初始化render,的方法initRender(vm) 3.其实这里我不太懂,但是感觉领悟到,并不是所有方法都要挂载到构造函数上,让构造函数变得无限臃肿 有时候可以将这些方法方法实例上 ~~~ ~~~js /*初始化render*/ export function initRender(vm: Component) { vm._vnode = null // the root of the child tree vm._staticTrees = null const parentVnode = vm.$vnode = vm.$options._parentVnode // the placeholder node in parent tree 父树中的占位符节点 const renderContext = parentVnode && parentVnode.context vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext) vm.$scopedSlots = emptyObject //将createElement fn绑定到此实例 //这样我们就可以在里面得到正确的呈现上下文。 //args顺序:tag、data、children、normalizationType、alwaysNormalize //内部版本由从模板编译的呈现函数使用 /*将createElement函数绑定到该实例上,该vm存在闭包中,不可修改,vm实例则固定。这样我们就可以得到正确的上下文渲染*/ vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) //标准化总是应用于公共版本,用于 //用户编写的渲染函数。 /*常规方法被用于公共版本,被用来作为用户界面的渲染方法*/ vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) } ~~~ >[danger] ##### 在整个代码分析前 需要理解一段代码 [render](https://cn.vuejs.org/v2/api/#render) [官网对render介绍](https://cn.vuejs.org/v2/api/#render) ~~~ 1.我们在vue 使用render 时候一般可能会这么写 new Vue({ render: function (createElement) { return createElement('h1', this.blogTitle) } }).$mount("#app"); 2.可以看到在'renderMixin' 中我们取出opition中的render,并且利用call 的形式改变指向和传参,我这不是 细读的版本所以'vm._renderProxy' 就理解成当前vue实例指向,这里传入'vm.$createElement' 做参数,这个方法 也说过是在'initRender' 中声明的 vnode = render.call(vm._renderProxy, vm.$createElement) 3.这么绕写个简单的小demo 给解开,其实option 中render参数,是一个回调函数,真正帮助我们解析内容的是 'createElement '这个函数 ~~~ ~~~ const opition = { render: function (createElement) { return createElement('h1', 'www') } } const createElement = (a, b, c) => { console.log(a, b, c) } opition.render.call(this, createElement) 打印结果: h1 www undefined ~~~ >[danger] ##### initRender -- createElement ~~~ 1.如果使用的是模板方式' 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)' 2.根据上面写的小demo 也可以知道真正帮我们解析vnode其实是createElement 3.现在分析createElement这个方法,被定义在'\core\vdom\create-element.js' 4.'Array.isArray(data) || isPrimitive(data)' 用来判读是否传了参数data,如果没传data这个参数 其他参数都往上提一层 5.关于'alwaysNormalize' 这个 参数在初始化render 时候如果用的是'模板方法'就是false 是render 函数就是true 这里在后续_createElement中对'children' 参数处理有讲究 ~~~ ~~~ export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode { /*兼容不传data的情况*/ if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } /*如果alwaysNormalize为true,则normalizationType标记为ALWAYS_NORMALIZE*/ if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } /*创建虚拟节点*/ return _createElement(context, tag, data, children, normalizationType) } ~~~ >[danger] ##### _createElement [更多详细内容参考这里](https://ustbhuangyi.github.io/vue-analysis/v2/data-driven/create-element.html#children-%E7%9A%84%E8%A7%84%E8%8C%83%E5%8C%96) ~~~ 1.上面可以看到对参数进行整理后,会调用'_createElement'方法这个方法做了什么? 2.关于'normalizationType ' 判读这里做个说明 2.1.simpleNormalizeChildren 方法调用场景是 render 函数是编译生成的。 2.2.normalizeChildren 方法的调用场景有 2 种,一个场景是 render 函数是用户手写的,当 children 只有 一个节点的时候,Vue.js 从接口层面允许用户把 children 写成基础类型用来创建单个简单的文本节点,这 种情况会调用 createTextVNode 创建一个文本节点的 VNode;另一个场景是当编译 slot、v-for 的时候会产 生嵌套数组的情况,会调用 normalizeArrayChildren 方法 ~~~ ~~~js /*创建VNode节点*/ export function _createElement ( context: Component, tag?: string | Class | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode { /* 如果data未定义(undefined或者null)或者是data的__ob__已经定义(代表已经被observed,上面绑定了Oberver对象), https://cn.vuejs.org/v2/guide/render-function.html#约束 那么创建一个空节点 */ 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() } /*如果tag不存在也是创建一个空节点*/ if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // 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 } if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === 'string') { let Ctor /*获取tag的名字空间*/ ns = config.getTagNamespace(tag) /*判断是否是保留的标签*/ if (config.isReservedTag(tag)) { // platform built-in elements /*如果是保留的标签则创建一个相应节点*/ vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component /*从vm实例的option的components中寻找该tag,存在则就是一个组件,创建相应节点,Ctor为组件的构造类*/ vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children /*未知的元素,在运行时检查,因为父组件可能在序列化子组件的时候分配一个名字空间*/ vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor /*tag不是字符串的时候则是组件的构造类*/ vnode = createComponent(tag, data, context, children) } if (isDef(vnode)) { /*如果有名字空间,则递归所有子节点应用该名字空间*/ if (ns) applyNS(vnode, ns) return vnode } else { /*如果vnode没有成功创建则创建空节点*/ return createEmptyVNode() } } ~~~
';