做爲一個vue愛好學習者,也加入了源碼解讀的學習陣營,對一個vue和react框架都用過的前端妹子來講,仍是更喜歡寫vue的語法,如今也很主流,一直想研究一下vue框架背後的實現機制,對api掌握、數據驅動、數據更新、以及組件等有個更全面的認識、而不只僅侷限於會用它,如今就當作記錄一下本身的理解,會持續更新~html
其實就是一個用Function實現的Class,經過它的原型prototype以及它自己擴展的一系列的方法和屬性,因此通常咱們會在main.js中會先new Vue一個實例對象出來,不然會報錯warn('Vue is a constructor and should be called with the new
keyword')前端
import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index' function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue
所謂的數據驅動,是指視圖是由數據驅動生成的,對視圖的修改,再也不直接操做DOM,而是經過修改數據。咱們所關心的只是數據的修改,DOM變成了數據的映射。vue
合併配置,初始化生命週期,初始化事件中心,初始化渲染,初始化 data、props、computed、watchernode
作了一層initState()的方法,給設置了data屬性,會執行getData()方法,這裏會先對data進行判斷,是否是一個function,代碼以下react
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } } function initData (vm: Component) { let data = vm.$options.data // 這裏判斷data是否是一個function data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (!isPlainObject(data)) { data = {} // 會報錯給咱們咱們data未初始換成一個對象的錯誤 process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) }
注意:在循環遍歷對象屬性時,會對props和data進行一層判斷,兩者不能重名,由於最後都會掛載到vm對象上,而後對vm對象進行一層proxy代理,下面的代碼很重要api
// proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } if (props && hasOwn(props, key)) { //會報props和data重名同樣的警告 process.env.NODE_ENV !== 'production' && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(key)) { // 進行數據代理操做 proxy(vm, `_data`, key) } } // 將vm對象用_data進行代理,收集和觸發更新依賴 proxy(vm, `_data`, key) export function proxy (target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) }
這裏的proxy,經過Object.defineProtery能夠作到給原型去作代理,get()方法收集依賴、set()方法去觸發更新,因此好比在mounted()時,例如打印一個console.log(this.messags)和console.log(this._data.message)是同樣的結果,實際上訪問的就是vm._data.messageapp
接着el設置了以後,進行mount函數處理,即mount鉤子函數框架
if (vm.$options.el) { vm.$mount(vm.$options.el) }
Vue不能掛載到body或html這樣的根節點上,通常都用div嵌套包括起來,會被覆蓋,Vue2.0版本中,全部的vue組件渲染最終都須要rendr方法,不論寫的是el或者template屬性,最終都會轉換陳render方法,即"在線編譯的過程"dom
// 原型上添加$mount方法 const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) /* istanbul ignore if */ // 若el掛載到body或者html上會報以下警告 if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this } const options = this.$options // resolve template/el and convert to render function // 若是是已經render()的話,沒必要再compile() if (!options.render) { let template = options.template if (template) { ..... } } // 若是是template模板,須要進行compile解析 if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } } // 最後會建立DOM元素,在這裏內容進行覆蓋,這也是爲何外層通常要有一個父級div包裹它,而不是寫在body或html上,實際上template會走一個compileToFunctions的過程 function getOuterHTML (el: Element): string { if (el.outerHTML) { return el.outerHTML } else { const container = document.createElement('div') container.appendChild(el.cloneNode(true)) return container.innerHTML } } Vue.compile = compileToFunctions
_render():Vue實例的一個私有方法,它用來把實例渲染成一個虛擬Node,用一個原生的JS對象去描述一個DOM節點,會比建立一個DOM的代價要小不少,這裏和react的思想是同樣的async
onstructor ( tag?: string, // vNode的標籤,例如div、p等標籤 data?: VNodeData, // vNode上的的data值,包括其全部的class、attribute屬性、style屬性已經綁定的時間 children?: ?Array<VNode>, // vNode上的子節點 text?: string, // 文本 elm?: Node, // vNode上對應的真實dom元素 context?: Component, //vdom的上下文 componentOptions?: VNodeComponentOptions ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.functionalContext = 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 } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } }
上面是VNode的初始化,而後Vue它是經過createElement方法建立的VNode
export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> { // 注意:這裏會先進行一層判斷,進行屬性值前移,該方法能夠借鑑在實際項目中 if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } // _createElement()是它的私有方法,建立成一個VNode,每一個 VNode 有 children,children 每一個元素也是一個 VNode,這樣就造成了一個 VNode Tree return _createElement(context, tag, data, children, normalizationType) }
目的是爲了把vNode轉換爲真實的DOM,_update會再首次渲染和數據更新的時候去調用,核心方法實際上是其中的_patch()方法
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 // 建立一個新的vNode 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 // 和以前的vNode,進行diff,將須要更新的dom操做和已經patch的vNode大道須要更新的vNode,完成真實的dom操做 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裏面作了什麼
// 定義了生命週期,這些鉤子函數 const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] export function createPatchFunction (backend) { let i, j const cbs = {} const { modules, nodeOps } = backend for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } } // ... // oldVnode:舊的VNode節點or DOM對象 // vnode: 執行了_render()以後範湖的VNode的節點 // hydrating:是不是服務端渲染,由於patch是和平臺相關的,在Web和Weex環境下,把VNode映射到平臺DOM的方法也是不一樣(有它本身的nodeOps和modules) // removeOnly: 給transition-group用的 return function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true // 建立新的節點 createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node // oldVNode和vnode進行diff,並對oldVnode打patch 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 if (process.env.NODE_ENV !== 'production') { 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 const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node // createElm的做用:經過傳入的VNode去建立真是的DOM元素,並插圖到它的父節點中, 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, nodeOps.nextSibling(oldElm) ) // update parent placeholder node element, recursively if (isDef(vnode.parent)) { ... } // destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } //執行全部created的鉤子並把vnodepush到insertedVnodeQueue 中 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } }
其中對oldVNode和vnode類型判斷中有一個sameVnode方法,這個方法很重要,是oldVNode和vnode須要進行diff和patch的前提
function sameVnode (a, b) { return ( a.key === b.key && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error) ) ) ) }
注意:insert()方法把DOM插入到父節點中,進行了遞歸調用,子元素會優先調用 insert,因此整個 vnode 樹節點的插入順序是先子後父
insert(parentElm, vnode.elm, refElm) function insert (parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (ref.parentNode === parent) { nodeOps.insertBefore(parent, elm, ref) } } else { nodeOps.appendChild(parent, elm) } } } export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) { parentNode.insertBefore(newNode, referenceNode) } export function appendChild (node: Node, child: Node) { node.appendChild(child) }
因此在patch的過程當中,會有這個問題拋出來
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 ) } } ..... }
能夠看到最終返回的是一個patch()方法,賦值給vm.__patch__()方法
在createElm過程當中,能夠看到若是vnode節點不包含tag的話,它有多是一個註釋或者純文本節點,能夠直接插入到父元素中,遞歸建立一個完整的DOM並插入到body中。
對數據渲染的過程有了更深的一層理解,從new Vue()開始,建立了一個vue是對象,會先進行init初始化——>$mount()——>compile(若已是render則該過程不須要)——>render——>建立VNode——>patch過程——>生成真實的DOM