html:
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"/> <title>組件頁面渲染的基本過程</title> </head> <body> <div id="app"></div> </body> </html>
main.js:
import Vue from "vue"; import Home from "./home.vue"; new Vue({ el: "#app", template: "<Home/>", components: { Home } });
home.vue
<template> <div class="home"> <a>{{text}}</a> </div> </template> <script> export default { name: "home", data() { return { text: '測試' }; }, mounted() { }, methods: { } }; </script>
1.項目運行編譯時,home.vue中的template會被vue-loader編譯成render函數,並添加到組件選項對象裏。組件經過components引用該組件選項,建立組件(Vue實例),渲染頁面(多個組件引用同一組件時,引用的是相同的對象,解釋data爲何要是函數)。main.js根組件template轉化成render函數是由vue插件中的編譯器實現的。javascript
// 編譯器生成的render函數: var render = function() { var _vm = this var _h = _vm.$createElement var _c = _vm._self._c || _h return _c( "div", { staticClass: "home" }, [ _c("a", [_vm._v(_vm._s(_vm.text))] ) ] ) }
// 編譯後的組件選項對象: { beforeCreate: [] beforeDestroy: [] data: function() {} methods: {} mounted: function() {} name: "home" render: function() {} staticRenderFns: [] __file: "src/page/home/index.vue" _compiled: true }
2.組件渲染頁面時會先調用render函數,render函數返回組件內標籤節點(VNode實例)。每一個標籤(包括文本和組件標籤等)會建立一個節點,先建立子標籤的節點,再將它添加父節點的children數組中,造成與標籤結構相同的樹形結構。css
// 節點構造函數 var VNode = function VNode ( tag, data, children, text, elm, context, componentOptions, asyncFactory ) { this.tag = tag;// 標籤名 this.data = data;// 節點數據,包括節點的生命週期函數 this.children = children;// 當節點是原生標籤節點,保存它的子節點 this.text = text;// 爲文本節點或者註釋節點時的文本內容 this.elm = elm;// 節點對應的DOM,組件節點則對應的是該組件內原生根標籤DOM this.ns = undefined; this.context = context;// 節點對應的組件 this.fnContext = undefined; this.fnOptions = undefined; this.fnScopeId = undefined; this.key = data && data.key;// key值 this.componentOptions = componentOptions;// 緩存組件標籤信息:包括組件名稱,組件選項、標籤上的props、標籤上的事件、以及組件標籤內的子節點。 this.componentInstance = undefined;// 組件標籤節點對應的組件(Vue實例) 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; };
// 調用render函數,生成組件模板對應的節點 Vue.prototype._render = function () { var vm = this; var ref = vm.$options; var render = ref.render; var _parentVnode = ref._parentVnode; ... vm.$vnode = _parentVnode; // render self var vnode; ... currentRenderingInstance = vm; vnode = render.call(vm._renderProxy, vm.$createElement);//返回組件模板造成節點(組件內根標籤節點) ... currentRenderingInstance = null; ... vnode.parent = _parentVnode;// 設置組件根標籤節點的parent爲當前組件節點。 return vnode };
// 根據標籤建立節點 function _createElement ( context, tag, data, children, normalizationType ) { if (isDef(data) && isDef((data).__ob__)) { ... return createEmptyVNode() } ... if (!tag) { return createEmptyVNode() } ... if (typeof tag === 'string') { vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ); } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {/* 從組件實例option的components中尋找該組件標籤信息(組件對象) */ vnode = createComponent(Ctor, data, context, children, tag);// 組件標籤節點 } else { vnode = new VNode( tag, data, children, undefined, undefined, context ); } } else { vnode = createComponent(tag, data, context, children);// 標籤節點 } 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() } }
3.若是標籤是組件標籤,經過components獲取的組件選項對象,並使用extend方法生成組件的構造函數,將構造函數和組件選項保存在組件標籤節點上。html
4.render函數生成組件內標籤對應的節點,並設置根節點的parent指向當前組件節點。將節點做爲新節點,傳入到patch方法中,組件頁面初始更新時,不存在舊節點,直接根據新節點建立DOM。vue
// 組件頁面更新 Vue.prototype._update = function (vnode, hydrating) { var vm = this; var prevEl = vm.$el; var prevVnode = vm._vnode; var restoreActiveInstance = setActiveInstance(vm); vm._vnode = vnode; ... if (!prevVnode) {// 組件初次渲染 // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);// 將組件內根標籤DOM賦值給實例的$el屬性 } else { // updates vm.$el = vm.__patch__(prevVnode, vnode);// 將組件內根標籤DOM賦值給實例的$el屬性 } restoreActiveInstance(); ... };
5.在patch方法中,根據節點建立DOM,並在節點上保存它的DOM引用,再根據節點的children值,建立子節點的DOM,再添加到父節點的DOM中,完成組件的渲染。java
// 根據節點類型(原生標籤或者組件標籤),建立組件或者DOM function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { ... vnode.isRootInsert = !nested; // for transition enter check if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {// 若是vnode是組件節點,建立組件 return } var data = vnode.data; var children = vnode.children; var tag = vnode.tag; if (isDef(tag)) { ... vnode.elm = vnode.ns // 建立DOM ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode); setScope(vnode);// 添加DOM屬性,構造css做用域 { createChildren(vnode, children, insertedVnodeQueue);// 建立子節點的DOM if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue); } insert(parentElm, vnode.elm, refElm);// 添加父節點的DOM中 } ... } else if (isTrue(vnode.isComment)) {// 註釋節點 vnode.elm = nodeOps.createComment(vnode.text); insert(parentElm, vnode.elm, refElm);// 添加DOM } else {// 文本節點 vnode.elm = nodeOps.createTextNode(vnode.text); insert(parentElm, vnode.elm, refElm);// 添加DOM } }
6.若是節點包含組件構造器信息(組件節點),會先使用構造器建立組件,調用組件render方法,執行以上操做,生成組件內標籤對應的節點,再根據節點生成DOM,並將根標籤節點的DOM保存在組件上,而後添加到父節點的DOM上,完成組件的渲染。DOM添加到頁面的過程是從下往上依次添加,DOM添加到父級DOM中,父級DOM添加到它的父級DOM中,迭代添加,最後將最上級的DOM添加到頁面。node
// 生成組件,並將組件內根標籤DOM添加到父級DOM中: function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { var i = vnode.data; if (isDef(i)) { var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */);// 調用節點生命週期函數init,生成組件 } if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue); insert(parentElm, vnode.elm, refElm);// 組件內根標籤DOM添加到父級DOM中 if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true } } }
// 在節點生命週期init建立組件 function init (vnode, hydrating) { ... var child = vnode.componentInstance = createComponentInstanceForVnode(// 建立組件 vnode, activeInstance// 父組件 ); child.$mount(hydrating ? vnode.elm : undefined, hydrating);//渲染頁面 }