引言
通过Vue源码探秘(九)(createComponent)的分析我们知道,当我们通过 createComponent 创建了组件 VNode,接下来会走到 vm._update,执行 vm.__patch__ 把 VNode 转换成真正的 DOM 节点。
之前分析的是针对一个普通的 VNode 节点,接下来我们来看看组件的 VNode 会有哪些不一样的地方。
patch 函数的核心步骤是调用 createElm 函数来创建节点,这一节我们再次回顾这个函数,看它是怎么处理组件的 VNode 的。
createComponent这一节我们依然围绕上一节的例子来分析:
import Vue from "vue"; import App from "./App.vue"; var app = new Vue({ el: "#app", // 这里的 h 是 createElement 方法 render: h => h(App) });回顾一下 createElm 的实现,它的定义在 src/core/vdom/patch.js 中,在函数的一开始有这么一段代码:
// src/core/vdom/patch.js function createElm( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // ... if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return; } // .. }这里判断如果调用 createComponent 函数返回 true ,则结束执行 createElm 函数。传给 createComponent 函数的 vnode 参数是组件 VNode,因此 createComponent 函数返回 true ,不会再往下执行。来看 createComponent 函数的定义:
// src/core/vdom/patch.js function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data; if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive; if (isDef((i = i.hook)) && isDef((i = i.init))) { i(vnode, false /* hydrating */); } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue); insert(parentElm, vnode.elm, refElm); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true; } } }函数一开始的 isReactivated 和 Keep-alive 相关,暂时不展开讲。然后接下来的 if 语句的意思是判断 vnode.data.hook.init 是否存在,这里vnode 是一个组件 VNode,那么条件满足,并且得到 i 就是 init 钩子函数。
回顾Vue源码探秘(九)(createComponent),在执行 createComponent 函数的时候会调用 installComponentHooks 函数给 vnode.data.hook 安装四个钩子函数。回顾 init 钩子函数的代码,它被定义在 src/core/vdom/create-component.js 文件中:
// src/core/vdom/create-component.js init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }if 语句依然是 Keep-alive 相关,我们先跳过。else 逻辑调用了 createComponentInstanceForVnode 函数创建一个Vue实例,传入 vnode、activeInstance 两个参数。activeInstance 是指什么呢,在 src/core/instance/lifecycle.js 文件中有这么几行代码:
// src/core/instance/lifecycle.js export let activeInstance: any = null; export function lifecycleMixin(Vue: Class<Component>) { Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) { // ... const restoreActiveInstance = setActiveInstance(vm); // setActiveInstance内:const prevActiveInstance = activeInstance // activeInstance = vm vm._vnode = vnode; // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); } else { // updates vm.$el = vm.__patch__(prevVnode, vnode); } restoreActiveInstance(); // setActiveInstance返回:activeInstance = prevActiveInstance // ... }; // ... }这里面在前后分别调用了setActiveInstance(vm)和他的返回值,我把函数内部的逻辑揉进了整体代码逻辑中。
可以看到,activeInstance 是一个全局变量,在调用 __patch__ 前先用 prevActiveInstance 保存 activeInstance ,然后将当前实例 vm 赋给 activeInstance ,在执行完 __patch__ 后再恢复 activeInstance 原来的值。那为什么要这样做呢,我们带着这个疑问继续往下看。
我们回过来继续看 createComponentInstanceForVnode 函数的代码:
// src/core/vdom/create-component.js export function createComponentInstanceForVnode( vnode: any, // we know it's MountedComponentVNode but flow doesn't parent: any // activeInstance in lifecycle state ): Component { const options: InternalComponentOptions = { _isComponent: true, _parentVnode: vnode, parent }; // check inline-template render functions const inlineTemplate = vnode.data.inlineTemplate; if (isDef(inlineTemplate)) { options.render = inlineTemplate.render; options.staticRenderFns = inlineTemplate.staticRenderFns; } return new vnode.componentOptions.Ctor(options); }这里定义了一个options,将 vnode作为 _parentVnode,将 activeInstance作为 parent。后面和 inline-template 相关的先略过。最后通过构造函数 vnode.componentOptions.Ctor 创建一个对象并返回,并传入 options 作为参数。
这里的 vnode.componentOptions.Ctor 对应的就是子组件的构造函数,回顾上一节,我们知道它实际上是继承于 Vue 的一个构造器 Sub,相当于 new Sub(options)。
回顾 Vue.extend 函数是怎么定义子构造函数的:
const Sub = function VueComponent(options) { this._init(options); };这里子构造函数继承了 Vue.prototype 上的 _init 函数,所以 createComponentInstanceForVnode 函数最后就是将 options 传给了 Vue.prototype._init 函数并执行。
来看下init方法:
// src/core/instance/init.js Vue.prototype._init = function(options?: Object) { // ... // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options); } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ); } // ... };这一段是合并 options 的操作,现在我们传入的 options 的 _isComponent 属性为 true,会走 if 逻辑调用 initInternalComponent 函数。简单看下initInternalComponent的实现:
// src/core/instance/init.js export function initInternalComponent( vm: Component, options: InternalComponentOptions ) { const opts = (vm.$options = Object.create(vm.constructor.options)); // doing this because it's faster than dynamic enumeration. const parentVnode = options._parentVnode; opts.parent = options.parent; opts._parentVnode = parentVnode; const vnodeComponentOptions = parentVnode.componentOptions; opts.propsData = vnodeComponentOptions.propsData; opts._parentListeners = vnodeComponentOptions.listeners; opts._renderChildren = vnodeComponentOptions.children; opts._componentTag = vnodeComponentOptions.tag; if (options.render) { opts.render = options.render; opts.staticRenderFns = options.staticRenderFns; } }initInternalComponent 函数的作用就是往 vm.$options 上添加属性,这里要重点关注的属性是 _parentVnode 和 parent ,这两个分别对应一开始在 init 钩子函数传入的 VNode 和 activeInstance参数。
回到 _init 函数,来看最后一段代码:
Vue.prototype._init = function(options?: Object) { // ... if (vm.$options.el) { vm.$mount(vm.$options.el); } };这一段代码也是组件实例与普通实例不同之处,由于组件没有 el ,所以不会执行 if 语句中的逻辑。重新回到钩子函数 init:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( /* ... */ ) { // ... } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }在调用 createComponentInstanceForVnode 函数创建了组件实例后,调用了组件实例的 $mount 方法。
回顾 Vue源码探秘(四)(实例挂载$mount) ,$mount 方法分为原型定义和重写两部分,重写部分就是多了将 template 转换为 render 函数的步骤,而组件在编译时就生成了 render 函数,不会执行重写部分,只执行了原型定义的 $mount 函数。回顾原型上定义的 $mount 函数:
// 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); };$mount 函数最终调用了 mountComponent 函数。我们知道 mountComponent 函数会创建一个 Watcher 对象并调用 updateComponent函数,进而执行vm._render() 方法:
Vue.prototype._render = function(): VNode { const vm: Component = this; const { render, _parentVnode } = vm.$options; // 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 { vnode = render.call(vm._renderProxy, vm.$createElement); } catch (e) { // ... } // set parent vnode.parent = _parentVnode; return vnode; };可以看到 _render 函数拿到 vm.$options._parentVNode ,也就是占位符 VNode,对应例子里面的 App 组件的 VNode,将它赋值给 vm.$vnode。之后通过组件的渲染函数 render 创建渲染 vnode,并把 _parentVnode 赋给了 vnode.parent。
之后创建出来的渲染 vnode 传给了 _update 函数:
// src/core/instance/lifecycle.js Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) { const vm: Component = this; const prevEl = vm.$el; const prevVnode = vm._vnode; const prevActiveInstance = activeInstance; activeInstance = vm; vm._vnode = vnode; // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); } 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 parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el; } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. };也就是说我们绕了一大圈又回到了 _update 函数。这是因为 Vue 的初始化是深度优先搜索的过程,Vue 实例引用了组件,如果组件中又引用了组件,那么它就会一直执行上述流程直到一个 vm 实例完成它的所有子树的 patch 或者 update 过程。
_update 过程中有几个关键的代码,首先 vm._vnode = vnode 的逻辑,这个 vnode 是通过 vm._render() 返回的组件渲染 VNode,vm._vnode 和 vm.$vnode 的关系就是一种父子关系,用代码表达就是 vm._vnode.parent === vm.$vnode。
回顾这一节的内容,在实例化子组件的时候,我们需要知道这个子组件的父实例是谁,把它存入 vm.$options 中,后面调用 initLifecycle 函数的时候再把它的父实例保存到 vm.$parent ,同时通过 parent.$children.push(vm) 来把子组件的 vm 存储到父实例的 $children 中,通过这些操作建立父子关系。
activeInstance 的作用就体现在这里,在 vm._update 的过程中,把当前的 vm 赋值给 activeInstance,同时 prevActiveInstance 保留上一次的 activeInstance 。当一个 vm 实例完成它的所有子树的 patch 或者 update 过程后,activeInstance 通过 prevActiveInstance 又回到它的父实例,这样就完美地保证了在整个深度遍历过程中,在实例化子组件的时候能传入当前子组件的父 Vue 实例,并在 initLifecycle 的过程中,通过 vm.$parent 把这个父子关系保留。
回到 _update 函数,这里又再次调用了 __patch__方法,然后又再次执行 patch 方法当中的 createElm 方法,就又回到本节开头提到的判断:
function createElm( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // ... if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return; } const data = vnode.data; const children = vnode.children; const tag = vnode.tag; if (isDef(tag)) { if (process.env.NODE_ENV !== "production") { if (data && data.pre) { creatingElmInVPre++; } if (isUnknownElement(vnode, creatingElmInVPre)) { warn( "Unknown custom element: <" + tag + "> - did you " + "register the component correctly? For recursive components, " + 'make sure to provide the "name" option.', vnode.context ); } } vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode); setScope(vnode); /* istanbul ignore if */ if (__WEEX__) { // ... } else { createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue); } insert(parentElm, vnode.elm, refElm); } if (process.env.NODE_ENV !== "production" && data && data.pre) { creatingElmInVPre--; } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text); insert(parentElm, vnode.elm, refElm); } else { vnode.elm = nodeOps.createTextNode(vnode.text); insert(parentElm, vnode.elm, refElm); } }这一次传给 createComponent 函数的 vnode 是渲染 VNode 而不是组件 VNode ,因此会继续往下执行。
往下的逻辑之前在分析元素节点 VNode 时已经分析过了,而其中有一点区别就是执行 insert(parentElm, vnode.elm, refElm) 这条语句时,由于 parentElm 参数对应的是 vm.$el ,而组件实例是没有 $el 的,因此这里的 parentElm 是 undefined ,再来看 insert 函数的定义:
// src/core/vdom/patch.js function insert(parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (nodeOps.parentNode(ref) === parent) { nodeOps.insertBefore(parent, elm, ref); } } else { nodeOps.appendChild(parent, elm); } } }可以看到,没有 parentElm 的话是不会执行插入操作的,那插入操作是在哪里执行的呢,我们回顾 createComponent 函数:
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data; if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive; if (isDef((i = i.hook)) && isDef((i = i.init))) { i(vnode, false /* hydrating */); } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue); insert(parentElm, vnode.elm, refElm); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true; } } }执行完 initComponent 函数后又执行了 insert 函数,插入过程就是在这里执行的,因为这里的 parentElm 参数是有值的。
由于整个过程是一个深度遍历的过程,如果组件中又嵌套子组件,那么它会递归调用 createComponent 函数完成子组件的一系列过程,因此整个 DOM 的插入顺序是先子后父。
总结本节带大家梳理了一个组件的 VNode 创建、初始化、渲染的完整流程。组件的 patch 过程相对于普通元素的 patch 来说复杂了许多,这部分需要反复翻看,通过断点调试等方法来加深理解。
在对组件化的实现有一个大概了解后,接下来我们来介绍一下这其中的一些细节。我们知道编写一个组件实际上是编写一个 JavaScript 对象,对象的描述就是各种配置,之前我们提到在 _init 的最初阶段执行的就是 merge options 的逻辑:
// merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options); } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ); }那么下一节我们从源码角度来分析合并配置的过程。
---来自腾讯云社区的---前端森林
微信扫一扫打赏
支付宝扫一扫打赏