vue的源碼包含三大核心:
Compiler模塊:編譯模板系統
Runtime模塊:也可以稱之為Renderer模塊,真正的渲染的模塊
Reactivity模塊:響應式系統
三個系統之間如何協同工作呢?
實現一個Mini-Vue
包含三個模塊:
渲染系統模塊
可響應式系統模塊
應用程序入口模塊
渲染系統的實現
該模塊主要包含三個功能:
功能一:h函數,用於返回一個VNode對象;
功能二:mount函數,用於將VNode掛載到DOM
功能三:patch函數,用於對兩個VNode進行對比,決定如何處理新的VNode
第一步,創建一個renderer.js文件,定義一個h函數
const h = (tag, props, children) => { // vnode就是一個JavaScript對象 return { tag, props, children } }
在html文件中,引入文件,並創建一個虛擬節點,可以輸出打印一下這個vnode
<div id="app"></div> <script src="./renderer.js"></script> <script> // 1、 通過h函數來創建一個vnode const vnode = h('div', { class: 'vnode' }, [ h('h2', null, '當前計數:100'), h('button', null, "+1") ]) console.log(vnode) </script>
第二步,實現掛載功能
在renderer.js文件中定義mount方法
const mount = (vnode, container) => { //1、 將vnode變為elemnt,創建出真實的dom,並且在vnode上保存一份el const el = vnode.el = document.createElement(vnode.tag) // 2、處理props if (vnode.props) { for (const key in vnode.props) { const value = vnode.props[key] // 判斷傳遞過來的是否是方法,比如onClick if (key.startsWith("on")) { el.addEventListener(key.slice(2).toLowerCase(), value) } // 設置屬性 el.setAttribute(key, value) } } // 3、處理children if (vnode.children) { // 如果子節點存在並且子節點是字符串,說明是其中的內容 if (typeof vnode.children === 'string') { // 將內容放進去 el.textContent = vnode.children } else { // 說明子節點中是一個數組,其內部還有子節點 vnode.children.forEach((item) => { // 再次調用掛載到el上 mount(item,el) }) } } // 4、將el掛載到container上 container.appendChild(el) }
在html文件中調用該mount方法
// 2、通過mount函數,將vnode掛載到#app上 mount(vnode,document.getElementById('app'))
再次刷新頁面的時候就可以看到界面已經加載出來了vnode
第三步實現diff算法
第一種情況:節點不相同
新建一個vnode
// 3、創建一個新的vnode const vnode1 = h('h2', { class: 'vnode' }, 'jerry')
將新的vnode替換舊的vnode,兩個vnode之間進行一個diff算法,根據diff算法找到需要修改真實dom的那個地方,找到之后在進行修改
在renderer.js文件中定義一個patch方法
const patch=(n1,n2)=>{ // 判斷兩個vnode的類型是否一樣,比如說n1為div,n2為h2 if(n1.tag!==n2.tag){ // 拿到n1節點的父元素 const n1ElementParent=n1.el.parentElement; // 移除n1節點 n1ElementParent.removeChild(n1.el) // 將n2節點添加上去 mount(n2,n1ElementParent) }else{ } }
在html文件中使用patch方法
patch(vnode,vnode1)
再次刷新頁面可以看到已經替換
第二種情況:節點相同,類名不同
patch方法
const patch = (n1, n2) => { // 判斷兩個vnode的類型是否一樣,比如說n1為div,n2為h2 if (n1.tag !== n2.tag) { // 拿到n1節點的父元素 const n1ElementParent = n1.el.parentElement; // 移除n1節點 n1ElementParent.removeChild(n1.el) // 將n2節點添加上去 mount(n2, n1ElementParent) } else { // 1、拿出element對象,並在n2中保留一份 const el = n2.el = n1.el // 2、處理props const oldProps = n1.props || {} const newProps = n2.props || {} // 2、1獲取所有的newProps添加到el中 for (const key in newProps) { const oldValue = oldProps[key] const newValue = newProps[key] if (newValue !== oldValue) { // 判斷傳遞過來的是否是方法,比如onClick if (key.startsWith("on")) { el.addEventListener(key.slice(2).toLowerCase(), newValue) } else { el.setAttribute(key, newValue) } } } // 2、2刪除舊的props for(const key in oldProps){ if(!(key in newProps)){ if (key.startsWith("on")) { const value=oldProps[key] el.removeEventListener(key.slice(2).toLowerCase(), value) } else { el.removeAttribute(key) } } } // 3、處理children } }
在html中新建一個節點,調用patch方法
// 3、創建一個新的vnode const vnode1 = h('div', { class: 'jerry' }, 'jerry') patch(vnode,vnode1)
之前
更新之后
接下來處理子節點
// 3、處理children const oldChildren = n1.children || []; const newChildren = n2.children || []; // 情況一:newChildren是一個string類型 if (typeof newChildren === "string") { if (typeof oldChildren === "string") { if (newChildren !== oldChildren) { el.textContent = newChildren } } else { el.innerHTML = newChildren; } }else{ // 情況二:newChildren是一個數組 if(typeof oldChildren==='string'){ el.innerHTML="" newChildren.forEach(item=>{ mount(item,el) }) }else{ // oldChildren:[n1,n2,n3] // newChildren:[n1,n2,n3,n4,n5] // 前面有相同節點的元素進行patch操作 const commonLength=Math.min(oldChildren.length,newChildren.length) for(let i=0;i<commonLength;i++){ patch(oldChildren[i],newChildren[i]) } // 如果newChildren.length>oldChildren // oldChildren:[n1,n2,n3] // newChildren:[n1,n2,n3,n4,n5] if(newChildren.length>oldChildren.length){ newChildren.slice(oldChildren.length).forEach(item=>{ mount(item,el) }) } // 如果newChildren.length<oldChildren // oldChildren:[n1,n2,n3,n4,n5] // newChildren:[n1,n2,n3] if(newChildren.length<oldChildren.length){ oldChildren.slice(newChildren.length).forEach(item=>{ el.removeChild(item.el) }) } } } }
創建兩個不同的節點,在進行patch操作
// 1、 通過h函數來創建一個vnode const vnode = h('div', { class: 'vnode' }, [ h('h2', null, '當前計數:100'), h('button',{onClick:function(){}}, "+1") ]) // 2、通過mount函數,將vnode掛載到#app上 mount(vnode,document.getElementById('app')) // 3、創建一個新的vnode const vnode1 = h('div', { class: 'jerry' }, 'jerry') patch(vnode,vnode1)
此時頁面就成為: