身爲原來的jquery,angular使用者。後面接觸了react和vue。漸漸的喜歡上了vue。抱着學習的態度呀。看看源碼。果真菜要付出代價。一步步單步調試。頭好疼。看到哪裏記到哪裏。來一點點心得。錯誤的地方請幫我指出來。謝謝。最近看的是vue component部分。javascript
先上一段最簡單的代碼,來剖析component機制:
<div id="app"> <div>{{a}}</div> <input v-model="heihei"> <button v-on:click="click1"> click1 </button> <my-component> <div slot='dudu'>111</div> <Child>{{a}}</Child> </my-component> <button @click.stop="click2"> click2 </button> </div> </body> <script src="vue.js"></script> <script type="text/javascript"> var Child = { template: '<div>A custom component!</div>' } Vue.component('my-component', { name: 'my-component', template: '<div>A custom component!<Child></Child><slot></slot></div>', components: { Child:Child }, created(){ console.log(this); }, mounted(){ console.log(this); } }) new Vue({ el: '#app', data: function () { return { heihei:{name:3333}, a:1 } }, components: { Child:Child }, created(){ }, methods: { click1: function(){ console.log(this); }, click2: function(){ alert('click2') } } }) </script>
咱們按照瀏覽器的思惟逐行來。執行到腳本時。首先執行了html
Vue.component('my-component', { name: 'my-component', template: '<div>A custom component!<Child></Child><slot></slot></div>', components: { Child:Child }, created(){ console.log(this); }, mounted(){ console.log(this); } })
咱們來看看這個函數經歷了什麼:
vue.js初始化的時候。調用了initGlobalAPI(vue),爲vue掛上了工具函數vue.component
通過initGlobalAPI(vue)中的initAssetRegisters (Vue) 後。變爲vue
vue.component = function ( id, definition ) { if (!definition) { return this.options[type + 's'][id] } else { /* istanbul ignore if */ { if (type === 'component' && config.isReservedTag(id)) { warn( 'Do not use built-in or reserved HTML elements as component ' + 'id: ' + id ); } } if (type === 'component' && isPlainObject(definition)) { definition.name = definition.name || id; definition = this.options._base.extend(definition); } if (type === 'directive' && typeof definition === 'function') { definition = { bind: definition, update: definition }; } this.options[type + 's'][id] = definition;//全局的組件或者指令和過濾器。統一掛在vue.options上。等待init的時候利用策略合併侵入實例。供實例使用 return definition } };
this.options._base在initGlobalAPI(vue)中爲Vue.options._base = Vue;
so vue.component調用了vue.extend。找到了源頭。咱們來好好看看這個vue.extend這個function。代碼以下:java
Vue.extend = function (extendOptions) { extendOptions = extendOptions || {}; var Super = this; var SuperId = Super.cid; var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}); if (cachedCtors[SuperId]) { return cachedCtors[SuperId]//若是組件已經被緩存在extendOptions上則直接取出 } var name = extendOptions.name || Super.options.name; { if (!/^[a-zA-Z][\w-]*$/.test(name)) { warn( 'Invalid component name: "' + name + '". Component names ' + 'can only contain alphanumeric characters and the hyphen, ' + 'and must start with a letter.'//校驗組件名 ); } } var Sub = function VueComponent (options) { this._init(options); }; Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub;//將vue上原型的方法掛在Sub.prototype中,Sub的實例同時也繼承了vue.prototype上的全部屬性和方法。 Sub.cid = cid++; Sub.options = mergeOptions( Super.options, extendOptions//經過vue的合併策略合併添加項到新的構造器上 ); Sub['super'] = Super;緩存父構造器 // For props and computed properties, we define the proxy getters on // the Vue instances at extension time, on the extended prototype. This // avoids Object.defineProperty calls for each instance created. if (Sub.options.props) { initProps$1(Sub); } if (Sub.options.computed) { //處理props和computed相關響應式配置項 initComputed$1(Sub); } // allow further extension/mixin/plugin usage Sub.extend = Super.extend; Sub.mixin = Super.mixin; Sub.use = Super.use; // create asset registers, so extended classes // can have their private assets too. //在新的構造器上掛上vue的工具方法 ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type]; }); // enable recursive self-lookup if (name) { Sub.options.components[name] = Sub; } // keep a reference to the super options at extension time. // later at instantiation we can check if Super's options have // been updated. Sub.superOptions = Super.options; Sub.extendOptions = extendOptions; Sub.sealedOptions = extend({}, Sub.options); // cache constructor cachedCtors[SuperId] = Sub;//緩存組件構造器在extendOptions上 return Sub }; } function initProps$1 (Comp) { var props = Comp.options.props; for (var key in props) { proxy(Comp.prototype, "_props", key); } } function initComputed$1 (Comp) { var computed = Comp.options.computed; for (var key in computed) { defineComputed(Comp.prototype, key, computed[key]); } }
總的來講vue.extend是返回了一個帶有附加配置相的新的vue的構造器。在函數中,構造器叫作Sub,等待render時候初始化。
通過vue.component的調用。vue增長了一個全局組件my-component;此時vue.options.component以下圖:node
前三個是vue內置的三個組件,在initgloabalapi的時候初始化。
至此全局組件建立完成。全局組件放置在最底層。在之後的策略合併裏會在子組件中的component項的__proto__中。react
上圖:jquery
vue官方的生命週期圖,其實也就是vue組件的構成的生命週期。沿着new Vue()咱們來大概看看這些生命週期在什麼階段ios
Vue.prototype._init = function (options) { var vm = this; // a uid vm._uid = uid$1++; var startTag, endTag; /* istanbul ignore if */ if ("development" !== 'production' && config.performance && mark) { startTag = "vue-perf-init:" + (vm._uid); endTag = "vue-perf-end:" + (vm._uid); mark(startTag); } // a flag to avoid this being observed vm._isVue = true; // 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 ); } /* istanbul ignore else */ { initProxy(vm);//屬性代理,即vm.xx = vm.data.xx } // expose real self vm._self = vm; initLifecycle(vm);//初始化生命週期狀態變量,創建子父關係初始值,如$children,$parent. initEvents(vm);// 初始化事件 initRender(vm);//初始化render核心函數_$createElement和$slots $scopedSlots等 callHook(vm, 'beforeCreate'); initInjections(vm); // resolve injections before data/props initState(vm);//利用數據劫持作響應式 initProvide(vm); //resolve provide after data/props callHook(vm, 'created'); /* istanbul ignore if */ if ("development" !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false); mark(endTag); measure(((vm._name) + " init"), startTag, endTag); } if (vm.$options.el) { vm.$mount(vm.$options.el);//若是有el配置相則主動掛載。觸發以後的compile.render } };
介紹了大概的_init函數,咱們繼續往下看程序的運行。完成了vue.component()以後。開始執行new vue(),建立實例。
對照_init函數。咱們知道它分別進行了對傳入參數的合併。初始化實例參數。建立響應的響應式。最後掛載:vm.$mount(vm.$options.el);
簡單說說掛載。好吧。咱們仍是往方法裏面看,掛載的時候發生了什麼:算法
// public mount method Vue$3.prototype.$mount = function ( el, hydrating ) { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating) }; var mount = Vue$3.prototype.$mount//緩存mount,用來觸發render Vue$3.prototype.$mount = function (//核心mount用來構建render函數 el, hydrating ) { el = el && query(el); /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { "development" !== 'production' && warn( "Do not mount Vue to <html> or <body> - mount to normal elements instead."//檢測,排除不可掛載的元素 ); return this } var options = this.$options; // resolve template/el and convert to render function if (!options.render) { var template = options.template;//假如輸入的是template模版時。 if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template); /* istanbul ignore if */ if ("development" !== 'production' && !template) { warn( ("Template element not found or is empty: " + (options.template)), this ); } } } else if (template.nodeType) { template = template.innerHTML;//輸入的是dom節點時 } else { { warn('invalid template option:' + template, this); } return this } } else if (el) { template = getOuterHTML(el);//若是是一個id,如這次初始化掛載的id=app,會取到id=app的html } if (template) { /* istanbul ignore if */ if ("development" !== 'production' && config.performance && mark) { mark('compile'); } var ref = compileToFunctions(template, { shouldDecodeNewlines: shouldDecodeNewlines,//核心compile函數。用於生成render函數。這裏不細說 delimiters: options.delimiters }, this); var render = ref.render; var staticRenderFns = ref.staticRenderFns; options.render = render;//掛載render到實例options中。待調用 options.staticRenderFns = staticRenderFns;//靜態的元素區分開。提高性能,後續虛擬dom樹比較時,不會比較靜態節點 /* istanbul ignore if */ if ("development" !== 'production' && config.performance && mark) { mark('compile end'); measure(((this._name) + " compile"), 'compile', 'compile end'); } } } return mount.call(this, el, hydrating)//利用緩存的mount調用準備好的render };
$mount方法的核心其實就是準備好組件的render函數。這裏最核心的一個方法就是:api
var ref = compileToFunctions(template, { shouldDecodeNewlines: shouldDecodeNewlines,//核心compile函數。用於生成render函數。這裏不細說 delimiters: options.delimiters }, this);
compileToFunctions這個函數中主要作了兩件事:
1:對模版進行compile(按標籤解析,生成ast(抽象語法樹)
2:利用generate(ast, options),生成render函數語法
咱們來看看最後實例生成的render函數:
沒有錯就是這個樣子,頗有感受。生成的render函數保存在options中,等待調用
好吧。開始調用吧。
mount.call(this, el, hydrating)=》mountComponent(this, el, hydrating)=》updateComponent = function () { vm._update(vm._render(), hydrating); };=》vm._watcher = new Watcher(vm, updateComponent, noop);
new Watcher中會主動調用updateComponent去touch依賴(給頁面中引用過的data中的變量假如監聽)正式調用render函數。既然都說了。那就來看看render函數:
Vue.prototype._render = function () { var vm = this; var ref = vm.$options; var render = ref.render; var staticRenderFns = ref.staticRenderFns; var _parentVnode = ref._parentVnode; if (vm._isMounted) { // clone slot nodes on re-renders for (var key in vm.$slots) { vm.$slots[key] = cloneVNodes(vm.$slots[key]); } } vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject; if (staticRenderFns && !vm._staticTrees) { vm._staticTrees = []; } // set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode; // render self var vnode; try { vnode = render.call(vm._renderProxy, vm.$createElement);//核心函數,調用render } catch (e) { handleError(e, vm, "render function"); // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ { vnode = vm.$options.renderError ? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) : vm._vnode; } } // return empty vnode in case the render function errored out if (!(vnode instanceof VNode)) { if ("development" !== '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 };
_render()間接調用了vnode = render.call(vm._renderProxy, vm.$createElement);
而後結合render函數。看看發生了什麼。vm.$createElement是核心的建立虛擬dom的函數。
繼續看看核心構建虛擬dom函數:
function createElement ( context, tag, data, children,//children是該元素下的全部子元素 normalizationType, alwaysNormalize ) { 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) } function _createElement ( context, tag, data, children, normalizationType ) { if (isDef(data) && isDef((data).__ob__)) { "development" !== '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() } 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); } var vnode, ns; if (typeof tag === 'string') { var Ctor; 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 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 vnode = createComponent(tag, data, context, children); } if (vnode !== undefined) { if (ns) { applyNS(vnode, ns); } return vnode } else { return createEmptyVNode() } }
這裏其實咱們不難看出vue在構造虛擬dom時。遞歸的去調用createElement去生成虛擬dom樹。當children是組件或者是普通元素時。作不一樣的處理。這裏咱們關注的是。當子元素是組建時。這裏調用了
vnode = createComponent(tag, data, context, children);
細心的人能夠在去看看這個函數作了什麼。簡單來講這個函數將組件的構造參數取出來,放置在元素的componentOptions上。供後續建立真實dom時。標記該元素是組件。遞歸初始化。
跳過這些沉重的。咱們直接看看咱們的這個html生成的最終的虛擬dom長什麼樣。以下:
咱們在來看看咱們的my-component組件長什麼樣子:
componentOptios上存着初始化組件須要的參數。
構建好虛擬dom後。vue進入update階段:
這個階段vue會判斷先前有無該元素。是否爲第一次渲染。假如是第一次。那麼直接建立。若是不是有先前的ovnode,則比較差別。最小化更新。看看具體函數:
nction lifecycleMixin (Vue) { Vue.prototype._update = function (vnode, hydrating) { var vm = this; if (vm._isMounted) { callHook(vm, 'beforeUpdate'); } var prevEl = vm.$el; var prevVnode = vm._vnode;//取出緩存的久的虛擬dom var prevActiveInstance = activeInstance; activeInstance = vm; vm._vnode = 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 */, vm.$options._parentElm, vm.$options._refElm ); } 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. };
__patch__函數咱們就不細看了。算了看一下:
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { if (isUndef(vnode)) { if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); } return } var isInitialPatch = false; var insertedVnodeQueue = []; if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true;//假如第一次渲染。直接建立 createElm(vnode, insertedVnodeQueue, parentElm, refElm); } else { var isRealElement = isDef(oldVnode.nodeType); if (!isRealElement && sameVnode(oldVnode, vnode)) {//假如更新而且先後虛擬dom類似,這裏類似有本身的一個算法。好比tag,key必需一致。纔會去diff比較 // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly); } else { if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR); hydrating = true; } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true); return oldVnode } else { warn( 'The client-side rendered virtual DOM tree is not matching ' + 'server-rendered content. This is likely caused by incorrect ' + 'HTML markup, for example nesting block-level elements inside ' + '<p>, or missing <tbody>. Bailing hydration and performing ' + 'full client-side render.' ); } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode); } // replacing existing element var oldElm = oldVnode.elm; var parentElm$1 = nodeOps.parentNode(oldElm); createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm$1, nodeOps.nextSibling(oldElm) ); if (isDef(vnode.parent)) { // component root element replaced. // update parent placeholder node element, recursively var ancestor = vnode.parent; while (ancestor) { ancestor.elm = vnode.elm; ancestor = ancestor.parent; } if (isPatchable(vnode)) { for (var i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode.parent); } } } if (isDef(parentElm$1)) { removeVnodes(parentElm$1, [oldVnode], 0, 0); } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode); } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm }
patch方法中核心的是createElm:看懂這個函數很是重要代碼以下
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) { vnode.isRootInsert = !nested; // for transition enter check if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {//根據以前保存的componentoptions來識別是否爲組件。如果。則進這個邏輯 return } var data = vnode.data; var children = vnode.children; var tag = vnode.tag; if (isDef(tag)) { { if (data && data.pre) { inPre++; } if ( !inPre && !vnode.ns && !(config.ignoredElements.length && config.ignoredElements.indexOf(tag) > -1) && config.isUnknownElement(tag) ) { 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 */ { createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue); } insert(parentElm, vnode.elm, refElm); } if ("development" !== 'production' && data && data.pre) { inPre--; } } 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); } }
咱們這邊仍是先關注本身的組件部分。當children是組件元素時,很顯然調用了createComponent(vnode, insertedVnodeQueue, parentElm, refElm);
var componentVNodeHooks = { init: function init ( vnode, hydrating, parentElm, refElm ) { if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) { var child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance, parentElm,//調用了組件內部的_init方法遞歸建立子組件。正式進入子組件的生命週期 refElm ); child.$mount(hydrating ? vnode.elm : undefined, hydrating);觸發子組件的掛載。出發子組件的編譯和render。又從新來一遍/直到子組件徹底渲染好。再開始creelem下一個child } else if (vnode.data.keepAlive) { // kept-alive components, treat as a patch var mountedNode = vnode; // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode); } },
這裏就是遞歸建立子組件的核心部分.
總結: 第一次寫這個vue。失敗了。切模塊切的不夠細。組件機制感受用了好多東西。這個面太大了。本身講的時候也不知道該細講仍是。。。
總的來講:vue在建立虛擬dom的時候,若是元素是組件。則準備好組件的構造參數。包括模版和數據等等。組件中的元素如slot,和child放在組件元素的children下。供後面的內容分發用組件中的元素也是在父組件的做用域內編譯的。看—_render()函數就知道。而後在vue須要將虛擬dom變爲真實dom時。遇到組件元素時。開始遞歸初始化。直到把組件compile,render構建完後。開始構建下一個元素。最後添加到真實id=app上。而且把舊的刪了。哈哈。隨便寫了