//父組件 <template> <div> <h1>{{title}}</h1> <child :name="name" :age="age" :hobby="hobby" @titleChanged="titleChanged"></child> </div> </template> import child from "./components/child" export default { name: 'App', data(){ return{ title: '父級內容', name: 'hello', age: 19, hobby: ['swim','run','walk'] } }, components:{ "child":child }, titleChanged(val){ this.title = val; } }
//子組件 <template> <div class="hello"> <h3>{{name}}</h3> <p>{{age}}</p> <ul> <li v-for="h in hobby">{{h}}</li> //遍歷傳遞過來的值,而後呈現到頁面 </ul> <button @click="changeTitle">向父級傳值</button> </div> </template> <script> export default { name: 'HelloWorld', props:{ users:{ //這個就是父組件中子標籤自定義名字 type:String, required:true }, age: { type: Number, default: 0, }, hobby: { type: Array, defautl: ()=>[] } }, methods: { changeTitle(){ this.$emit("titleChanged","子向父組件傳值"); } } } </script>
Vue經過設定對象屬性的 setter/getter 方法來監聽數據的變化,經過getter進行依賴收集,而每一個setter方法就是一個觀察者,在數據變動的時候通知訂閱者更新視圖。html
function observe (obj) { // 咱們來用它使對象變成可觀察的 // 判斷類型 if (!obj || typeof obj !== 'object') { return } Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) function defineReactive (obj, key, value) { // 遞歸子屬性 observe(value) Object.defineProperty(obj, key, { enumerable: true, //可枚舉(能夠遍歷) configurable: true, //可配置(好比能夠刪除) get: function reactiveGetter () { console.log('get', value) // 監聽 return value }, set: function reactiveSetter (newVal) { observe(newVal) //若是賦值是一個對象,也要遞歸子屬性 if (newVal !== value) { console.log('set', newVal) // 監聽 render() value = newVal } } }) } }
observe
這個函數傳入一個 obj
(須要被追蹤變化的對象),經過遍歷全部屬性的方式對該對象的每個屬性都經過 defineReactive
處理,以此來達到實現偵測對象變化。值得注意的是,observe
會進行遞歸調用。vue
由於 Vue 經過Object.defineProperty
來將對象的key轉換成getter/setter
的形式來追蹤變化,但getter/setter
只能追蹤一個數據是否被修改,沒法追蹤新增屬性和刪除屬性。若是是刪除屬性,咱們能夠用vm.$delete
實現,那若是是新增屬性,該怎麼辦呢?
1)可使用 Vue.set(location, a, 1)
方法向嵌套對象添加響應式屬性;
2)也能夠給這個對象從新賦值,好比data.location = {...data.location,a:1}
node
Object.defineProperty
不能監聽數組的變化,須要進行數組方法的重寫,具體代碼以下:let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']; // 先獲取到原來的原型上的方法 let arrayProto = Array.prototype; // 建立一個本身的原型 而且重寫methods這些方法 let proto = Object.create(arrayProto) methods.forEach(method => { proto[method] = function() { // AOP arrayProto[method].call(this, ...arguments) render() } }) function observer(obj) { // 把全部的屬性定義成set/get的方式 if (Array.isArray(obj)) { obj.__proto__ = proto return } if (typeof obj == 'object') { for (let key in obj) { defineReactive(obj, key, obj[key]) } } } function defineReactive(data, key, value) { observer(value) Object.defineProperty(data, key, { get() { return value }, set(newValue) { observer(newValue) if (newValue !== value) { render() value = newValue } } }) } observer(obj) function $set(data, key, value) { defineReactive(data, key, value) }
這種方法將數組的經常使用方法進行重寫,進而覆蓋掉原生的數組方法,重寫以後的數組方法須要可以被攔截。react
關於 Vue 編譯原理這塊的總體邏輯主要分三個部分,也能夠說是分三步,這三個部分是有先後關係的:算法
模板字符串
轉換成 element ASTs
(解析器)<div> <p>{{name}}</p> </div>
上面這樣一個簡單的 模板
轉換成 element AST
後是這樣的:express
{ tag: "div" type: 1, staticRoot: false, static: false, plain: true, parent: undefined, attrsList: [], attrsMap: {}, children: [ { tag: "p" type: 1, staticRoot: false, static: false, plain: true, parent: {tag: "div", ...}, attrsList: [], attrsMap: {}, children: [{ type: 2, text: "{{name}}", static: false, expression: "_s(name)" }] } ] }
這段模板字符串會扔到 while
中去循環,而後 一段一段 的截取,把截取到的 每一小段字符串 進行解析,直到最後截沒了,也就解析完了數組
AST
進行靜態節點標記,主要用來作虛擬DOM的渲染優化(優化器)優化器的目標是找出那些靜態節點並打上標記,而靜態節點指的是 DOM
不須要發生變化的節點。性能優化
每次從新渲染的時候不須要爲靜態節點建立新節點;在 Virtual DOM 中 patching 的過程能夠被跳過。dom
element ASTs
生成 render
函數代碼字符串(代碼生成器){ render: `with(this){return _c('div',[_c('p',[_v(_s(name))])])}` }
經過遞歸去拼一個函數執行代碼的字符串,遞歸的過程根據不一樣的節點類型調用不一樣的生成方法,若是發現是一顆元素節點就拼一個 _c(tagName, data, children)
的函數調用字符串,而後 data
和 children
也是使用 AST
中的屬性去拼字符串。異步
若是 children
中還有 children
則遞歸去拼;最後拼出一個完整的 render
函數代碼。
渲染真實DOM的開銷是很大的,好比有時候咱們修改了某個數據,若是直接渲染到真實dom上會引發整個dom樹的重繪和重排,有沒有可能咱們只更新咱們修改的那一小塊dom而不要更新整個dom呢?diff算法可以幫助咱們。
咱們先根據真實DOM生成一顆virtual DOM
,當virtual DOM
某個節點的數據改變後會生成一個新的Vnode
,而後Vnode
和oldVnode
做對比,發現有不同的地方就直接修改在真實的DOM上,而後使oldVnode
的值爲Vnode
。
diff的過程就是調用名爲patch
的函數,比較新舊節點,一邊比較一邊給真實的DOM打補丁。
使用雙指針形式,對虛擬節點進行比對,對相同的節點進行復用,對發生變化的節點進行patch。如下是vue源碼中對虛擬節點的比對方式:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 // 舊頭索引 let newStartIdx = 0 // 新頭索引 let oldEndIdx = oldCh.length - 1 // 舊尾索引 let newEndIdx = newCh.length - 1 // 新尾索引 let oldStartVnode = oldCh[0] // oldVnode的第一個child let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最後一個child let newStartVnode = newCh[0] // newVnode的第一個child let newEndVnode = newCh[newEndIdx] // newVnode的最後一個child let oldKeyToIdx, idxInOld, vnodeToMove, refElm // removeOnly is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly // 若是oldStartVnode和oldEndVnode重合,而且新的也都重合了,證實diff完了,循環結束 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 若是oldVnode的第一個child不存在 if (isUndef(oldStartVnode)) { // oldStart索引右移 oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left // 若是oldVnode的最後一個child不存在 } else if (isUndef(oldEndVnode)) { // oldEnd索引左移 oldEndVnode = oldCh[--oldEndIdx] // oldStartVnode和newStartVnode是同一個節點 } else if (sameVnode(oldStartVnode, newStartVnode)) { // patch oldStartVnode和newStartVnode, 索引左移,繼續循環 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] // oldEndVnode和newEndVnode是同一個節點 } else if (sameVnode(oldEndVnode, newEndVnode)) { // patch oldEndVnode和newEndVnode,索引右移,繼續循環 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] // oldStartVnode和newEndVnode是同一個節點 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // patch oldStartVnode和newEndVnode patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) // 若是removeOnly是false,則將oldStartVnode.eml移動到oldEndVnode.elm以後 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) // oldStart索引右移,newEnd索引左移 oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] // 若是oldEndVnode和newStartVnode是同一個節點 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // patch oldEndVnode和newStartVnode patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) // 若是removeOnly是false,則將oldEndVnode.elm移動到oldStartVnode.elm以前 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) // oldEnd索引左移,newStart索引右移 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] // 若是都不匹配 } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 嘗試在oldChildren中尋找和newStartVnode的具備相同的key的Vnode idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 若是未找到,說明newStartVnode是一個新的節點 if (isUndef(idxInOld)) { // New element // 建立一個新Vnode createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) // 若是找到了和newStartVnodej具備相同的key的Vnode,叫vnodeToMove } else { vnodeToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !vnodeToMove) { warn( 'It seems there are duplicate keys that is causing an update error. ' + 'Make sure each v-for item has a unique key.' ) } // 比較兩個具備相同的key的新節點是不是同一個節點 //不設key,newCh和oldCh只會進行頭尾兩端的相互比較,設key後,除了頭尾兩端的比較外,還會從用key生成的對象oldKeyToIdx中查找匹配的節點,因此爲節點設置key能夠更高效的利用dom。 if (sameVnode(vnodeToMove, newStartVnode)) { // patch vnodeToMove和newStartVnode patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) // 清除 oldCh[idxInOld] = undefined // 若是removeOnly是false,則將找到的和newStartVnodej具備相同的key的Vnode,叫vnodeToMove.elm // 移動到oldStartVnode.elm以前 canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) // 若是key相同,可是節點不相同,則建立一個新的節點 } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) } } // 右移 newStartVnode = newCh[++newStartIdx] } }
從模板到真實dom節點還須要通過一些步驟
把模板編譯爲render函數;
實例進行掛載, 根據根節點render函數的調用,遞歸的生成虛擬dom;
對比虛擬dom,渲染到真實dom;
組件內部data發生變化,組件和子組件引用data做爲props從新調用render函數,生成虛擬dom, 返回到步驟3。
若是不採起異步更新,那麼每次更新數據都會對當前組件進行從新渲染,爲了性能考慮,Vue 會在本輪數據更新後,再去異步更新數據。
原理:
主要包括:上線代碼包打包、源碼編寫優化、用戶體驗優化。