1. 程式人生 > 實用技巧 >建立一個簡單的迷你Vue3-2

建立一個簡單的迷你Vue3-2

經過前面兩章,我們已經建立了模板渲染系統和響應式資料系統。

那麼接下來我們就要將他們組合在一起,以及加上一些生命週期的處理等等。來合成一個真正的mini-vue3.

首先是要將之前兩塊內容整合到一起:

<html>
    <head>
        <title>Mini-Vue3</title>
    </head>
    <body>
        <div id="app"></div>
        <script>
            // compile & render system
// 渲染函式,將函式描述的模版轉換為物件表述的json物件 function h(tag, props, children) { return { tag, props, children, }; } // 將指定的vnode裝在到container上 function mount(vnode, container) { const el
= document.createElement(vnode.tag); // 儲存vnode的dom引用 vnode.el = el; // props if (vnode.props) { for (const key in vnode.props) { const value = vnode.props[key]; el.setAttribute(key, value); } }
// children if (vnode.children) { // 區分字串還是array,簡單處理 if (typeof vnode.children === 'string') { el.textContent = vnode.children; } else { vnode.children.forEach(child => { mount(child, el); }); } } container.appendChild(el); } // 比對已有vnode和新node,進行更新 function patch(oldNode, newNode) { if (oldNode.tag === newNode.tag) { const el = oldNode.el;
          newNode.el=oldNode.el;
// 如果tag相同,則需要進行下一步的各種判斷,props的判斷,更新,children的判斷,更新
                // 首先處理props
                const oldProps = oldNode.props || {};
                const newProps = newNode.props || {};

                // 首先判斷新props,如果舊的有,則更新,無,則新增
                for (const key in newProps) {
                    const oldValue = oldProps[key];
                    const newValue = newProps[key];

                    if (newValue !== oldValue) {
                    // 如果新的與舊的不同,則更新
                    el.setAttribute(key, newValue);
                    }
                }
                // 然後判斷舊props裡面,如果新的沒有,則刪除
                for (const key in oldProps) {
                    if (!(key in newProps)) {
                    el.removeAttribute(key);
                    }
                }

                // 然後將props設定為新
                oldNode.props = newProps;

                // 接著處理children, 這裡我們簡單處理,認為children要麼是string,要麼是array
                const oldChildren = oldNode.children;
                const newChildren = newNode.children;

                if (typeof oldChildren === 'string') {
                    // 對於舊子節點為string的情況
                    if (typeof newChildren === 'string') {
                    // 如果新子節點也是string,則直接替換即可
                    el.textContent = newChildren;
                    oldNode.children = newChildren;
                    } else {
                    // 新節點是array
                    // 清空原有子節點內容
                    el.innerHTML = '';
                    newChildren.forEach(child => {
                        mount(child, el);
                    });
                    oldNode.children = newChildren;
                    }
                } else {
                    // 舊節點是array
                    if (typeof newChildren === 'string') {
                    el.innerHTML = newChildren;
                    oldNode.children = newChildren;
                    } else {
                    // 兩個都是array,就簡單處理,只是比對同順序

                    // 先取兩個陣列同樣長度對比,直接patch
                    const sameLength = Math.min(oldChildren.length, newChildren.length);
                    for (let i = 0; i < sameLength; i++) {
                        // 替換新元素
                        patch(oldChildren[i], newChildren[i]);
                    }
                    // 然後如果舊陣列還有,則移除多出部分
                    if (oldChildren.length > sameLength) {
                        oldChildren.slice(sameLength).forEach(child => {
                        el.removeChild(child.el);
                        });
                    }

                    // 如果新陣列還有,則新增多出部分
                    if (newChildren.length > sameLength) {
                        newChildren.slice(sameLength).forEach(child => {
                        mount(child, el);
                        });
                    }
                    }
                }
                
                } else {
                // 如果tag都不同,則直接替換即可
                const parent = oldNode.el.parentNode;
                mount(newNode, parent);
                parent.removeChild(oldNode.el);
                oldNode.el = newNode.el;
                }
            }


            // reactive system
            let activeEffect = null;

            class Dep {
                subscribers = new Set();

                // 新增依賴
                depend() {
                    if (activeEffect) {
                        this.subscribers.add(activeEffect);
                    }
                }

                // 依賴更新,通知回撥
                notify() {
                    this.subscribers.forEach(effect => {
                        effect();
                    });
                }
            }

            /**
             * @decription 註冊事件,當依賴的物件發生改變時,觸發effect方法執行
             */
            function watchEffect(effect) {
                activeEffect = effect;
                effect();
                activeEffect = null;
            }

            const targetMap = new WeakMap();

            function getDep(target, key) {
                // 新增整個物件的依賴
                let currMap = targetMap.get(target);

                if (!currMap) {
                    currMap = new Map();
                    targetMap.set(target, currMap);
                }

                // 獲取具體key的依賴
                let dep = currMap.get(key);
                // 如果沒有新增過則新增依賴
                if (!dep) {
                    dep = new Dep();
                    currMap.set(key, dep);
                }

                return dep;
            }

            const reactiveHandlers = {
                get(target, key, receiver) {
                    const dep = getDep(target, key);

                    // 為什麼每次讀取都要新增依賴,因為有可能會增加新的依賴
                    dep.depend();
                    return Reflect.get(target, key, receiver);
                },
                set(target, key, value, receiver) {                    
                    const dep = getDep(target, key);

                    // 類似於之前的Object.set
                    const ret = Reflect.set(target, key, value, receiver);

                    // 通知執行依賴
                    dep.notify();

                    // 因為proxy的set必須要返回一個boolean的值告訴設定成功與否,因此這裡返回系統api的結果即可
                    return ret;
                }
            };

            /**
             * @description 給物件封裝並返回響應式代理
             */
            function reactive(oldObj) {
                return new Proxy(oldObj, reactiveHandlers);
            }

        </script>
    </body>
</html>

接下來則是要像真正在Vue裡面寫元件那樣來實現這個mini-vue3:

因為現在我們只是實現了基礎的API,但是我們實際使用Vue都是通過元件的形式來組織程式碼的,並且通過Vue的引數來將元件傳入並掛<html>

    <head>
        <title>Mini-Vue3</title>
    </head>
    <body>
        <div id="app"></div>
        <script>
            // compile & render system
            
            // 渲染函式,將函式描述的模版轉換為物件表述的json物件
            function h(tag, props, children) {
                return {
                tag,
                props,
                children,
                };
            }

            // 將指定的vnode裝在到container上
            function mount(vnode, container) {
                const el = document.createElement(vnode.tag);
                // 儲存vnode的dom引用
                vnode.el = el;

                // props
                if (vnode.props) {
                for (const key in vnode.props) {
                    const value = vnode.props[key];
                    el.setAttribute(key, value);
                }
                }

                // children
                if (vnode.children) {
                // 區分字串還是array,簡單處理
                if (typeof vnode.children === 'string') {
                    el.textContent = vnode.children;
                } else {
                    vnode.children.forEach(child => {
                    mount(child, el);
                    });
                }
                }

                container.appendChild(el);
            }

            // 比對已有vnode和新node,進行更新
            function patch(oldNode, newNode) {
                if (oldNode.tag === newNode.tag) {
                const el = oldNode.el;
          newNode.el = oldNode.el;
// 如果tag相同,則需要進行下一步的各種判斷,props的判斷,更新,children的判斷,更新 // 首先處理props const oldProps = oldNode.props || {}; const newProps = newNode.props || {}; // 首先判斷新props,如果舊的有,則更新,無,則新增 for (const key in newProps) { const oldValue = oldProps[key]; const newValue = newProps[key]; if (newValue !== oldValue) { // 如果新的與舊的不同,則更新 el.setAttribute(key, newValue); } } // 然後判斷舊props裡面,如果新的沒有,則刪除 for (const key in oldProps) { if (!(key in newProps)) { el.removeAttribute(key); } } // 然後將props設定為新 oldNode.props = newProps; // 接著處理children, 這裡我們簡單處理,認為children要麼是string,要麼是array const oldChildren = oldNode.children; const newChildren = newNode.children; if (typeof oldChildren === 'string') { // 對於舊子節點為string的情況 if (typeof newChildren === 'string') { // 如果新子節點也是string,則直接替換即可 el.textContent = newChildren; oldNode.children = newChildren; } else { // 新節點是array // 清空原有子節點內容 el.innerHTML = ''; newChildren.forEach(child => { mount(child, el); }); oldNode.children = newChildren; } } else { // 舊節點是array if (typeof newChildren === 'string') { el.innerHTML = newChildren; oldNode.children = newChildren; } else { // 兩個都是array,就簡單處理,只是比對同順序 // 先取兩個陣列同樣長度對比,直接patch const sameLength = Math.min(oldChildren.length, newChildren.length); for (let i = 0; i < sameLength; i++) { // 替換新元素 patch(oldChildren[i], newChildren[i]); } // 然後如果舊陣列還有,則移除多出部分 if (oldChildren.length > sameLength) { oldChildren.slice(sameLength).forEach(child => { el.removeChild(child.el); }); } // 如果新陣列還有,則新增多出部分 if (newChildren.length > sameLength) { newChildren.slice(sameLength).forEach(child => { mount(child, el); }); } } } } else { // 如果tag都不同,則直接替換即可 const parent = oldNode.el.parentNode; mount(newNode, parent); parent.removeChild(oldNode.el); oldNode.el = newNode.el; } } // reactive system let activeEffect = null; class Dep { subscribers = new Set(); // 新增依賴 depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } // 依賴更新,通知回撥 notify() { this.subscribers.forEach(effect => { effect(); }); } } /** * @decription 註冊事件,當依賴的物件發生改變時,觸發effect方法執行 */ function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null; } const targetMap = new WeakMap(); function getDep(target, key) { // 新增整個物件的依賴 let currMap = targetMap.get(target); if (!currMap) { currMap = new Map(); targetMap.set(target, currMap); } // 獲取具體key的依賴 let dep = currMap.get(key); // 如果沒有新增過則新增依賴 if (!dep) { dep = new Dep(); currMap.set(key, dep); } return dep; } const reactiveHandlers = { get(target, key, receiver) { const dep = getDep(target, key); // 為什麼每次讀取都要新增依賴,因為有可能會增加新的依賴 dep.depend(); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { const dep = getDep(target, key); // 類似於之前的Object.set const ret = Reflect.set(target, key, value, receiver); // 通知執行依賴 dep.notify(); // 因為proxy的set必須要返回一個boolean的值告訴設定成功與否,因此這裡返回系統api的結果即可 return ret; } }; /** * @description 給物件封裝並返回響應式代理 */ function reactive(oldObj) { return new Proxy(oldObj, reactiveHandlers); } // 我們現在來按照Vue元件的格式定義一個簡單的自定義元件 const App = { data: reactive({ count: 0, }), render() { return h('div', null, App.data.count); }, }; // 接著我們需要通過一個安裝的方法來將這個元件掛載並且需要 // 在依賴的資料發生變化後重新渲染元件 </script> </body> </html>

因此這裡我們需要建立一個安裝方法來掛載這個元件

<html>
    <head>
        <title>Mini-Vue3</title>
    </head>
    <body>
        <div id="app"></div>
        <script>
            // compile & render system
            
            // 渲染函式,將函式描述的模版轉換為物件表述的json物件
            function h(tag, props, children) {
                return {
                tag,
                props,
                children,
                };
            }

            // 將指定的vnode裝在到container上
            function mount(vnode, container) {
                const el = document.createElement(vnode.tag);
                // 儲存vnode的dom引用
                vnode.el = el;

                // props
                if (vnode.props) {
                for (const key in vnode.props) {
                    const value = vnode.props[key];
                    el.setAttribute(key, value);
                }
                }

                // children
                if (vnode.children) {
                // 區分字串還是array,簡單處理
                if (typeof vnode.children === 'string') {
                    el.textContent = vnode.children;
                } else {
                    vnode.children.forEach(child => {
                    mount(child, el);
                    });
                }
                }

                container.appendChild(el);
            }

            // 比對已有vnode和新node,進行更新
            function patch(oldNode, newNode) {
                if (oldNode.tag === newNode.tag) {
                const el = oldNode.el;
          newNode.el = oldNode.el;
// 如果tag相同,則需要進行下一步的各種判斷,props的判斷,更新,children的判斷,更新 // 首先處理props const oldProps = oldNode.props || {}; const newProps = newNode.props || {}; // 首先判斷新props,如果舊的有,則更新,無,則新增 for (const key in newProps) { const oldValue = oldProps[key]; const newValue = newProps[key]; if (newValue !== oldValue) { // 如果新的與舊的不同,則更新 el.setAttribute(key, newValue); } } // 然後判斷舊props裡面,如果新的沒有,則刪除 for (const key in oldProps) { if (!(key in newProps)) { el.removeAttribute(key); } } // 然後將props設定為新 oldNode.props = newProps; // 接著處理children, 這裡我們簡單處理,認為children要麼是string,要麼是array const oldChildren = oldNode.children; const newChildren = newNode.children; if (typeof oldChildren === 'string') { // 對於舊子節點為string的情況 if (typeof newChildren === 'string') { // 如果新子節點也是string,則直接替換即可 el.textContent = newChildren; oldNode.children = newChildren; } else { // 新節點是array // 清空原有子節點內容 el.innerHTML = ''; newChildren.forEach(child => { mount(child, el); }); oldNode.children = newChildren; } } else { // 舊節點是array if (typeof newChildren === 'string') { el.innerHTML = newChildren; oldNode.children = newChildren; } else { // 兩個都是array,就簡單處理,只是比對同順序 // 先取兩個陣列同樣長度對比,直接patch const sameLength = Math.min(oldChildren.length, newChildren.length); for (let i = 0; i < sameLength; i++) { // 替換新元素 patch(oldChildren[i], newChildren[i]); } // 然後如果舊陣列還有,則移除多出部分 if (oldChildren.length > sameLength) { oldChildren.slice(sameLength).forEach(child => { el.removeChild(child.el); }); } // 如果新陣列還有,則新增多出部分 if (newChildren.length > sameLength) { newChildren.slice(sameLength).forEach(child => { mount(child, el); }); } } } } else { // 如果tag都不同,則直接替換即可 const parent = oldNode.el.parentNode; mount(newNode, parent); parent.removeChild(oldNode.el); oldNode.el = newNode.el; } } // reactive system let activeEffect = null; class Dep { subscribers = new Set(); // 新增依賴 depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } // 依賴更新,通知回撥 notify() { this.subscribers.forEach(effect => { effect(); }); } } /** * @decription 註冊事件,當依賴的物件發生改變時,觸發effect方法執行 */ function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null; } const targetMap = new WeakMap(); function getDep(target, key) { // 新增整個物件的依賴 let currMap = targetMap.get(target); if (!currMap) { currMap = new Map(); targetMap.set(target, currMap); } // 獲取具體key的依賴 let dep = currMap.get(key); // 如果沒有新增過則新增依賴 if (!dep) { dep = new Dep(); currMap.set(key, dep); } return dep; } const reactiveHandlers = { get(target, key, receiver) { const dep = getDep(target, key); // 為什麼每次讀取都要新增依賴,因為有可能會增加新的依賴 dep.depend(); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { const dep = getDep(target, key); // 類似於之前的Object.set const ret = Reflect.set(target, key, value, receiver); // 通知執行依賴 dep.notify(); // 因為proxy的set必須要返回一個boolean的值告訴設定成功與否,因此這裡返回系統api的結果即可 return ret; } }; /** * @description 給物件封裝並返回響應式代理 */ function reactive(oldObj) { return new Proxy(oldObj, reactiveHandlers); } /** * @description 將元件掛載到dom上 */ function mountApp(component, container) { // 先拿到vdom const vdom = component.render(); // 掛載 mount(vdom, container); } // 我們現在來按照Vue元件的格式定義一個簡單的自定義元件 const App = { data: reactive({ count: 1, }), render() { return h('div', null, String(App.data.count)); // 因為前面vdom限定了string }, }; // 接著我們需要通過一個安裝的方法來將這個元件掛載並且需要 // 在依賴的資料發生變化後重新渲染元件 mountApp(App, document.getElementById('app')) </script> </body> </html>

此時可以看下目前的效果:

已經渲染成功了,但是我們還少了一些東西,那就是我們應當要觸發我們依賴的物件的資料變更,並且我們的Dom要能重新渲染,因此我們應當在watchEffect方法中去更新我們的dom,

新的程式碼如下:

<html>
    <head>
        <title>Mini-Vue3</title>
    </head>
    <body>
        <div id="app"></div>
        <script>
            // compile & render system
            
            // 渲染函式,將函式描述的模版轉換為物件表述的json物件
            function h(tag, props, children) {
                return {
                tag,
                props,
                children,
                };
            }

            // 將指定的vnode裝在到container上
            function mount(vnode, container) {
                const el = document.createElement(vnode.tag);
                // 儲存vnode的dom引用
                vnode.el = el;

                // props
                if (vnode.props) {
                for (const key in vnode.props) {
                    const value = vnode.props[key];
                    el.setAttribute(key, value);
                }
                }

                // children
                if (vnode.children) {
                // 區分字串還是array,簡單處理
                if (typeof vnode.children === 'string') {
                    el.textContent = vnode.children;
                } else {
                    vnode.children.forEach(child => {
                    mount(child, el);
                    });
                }
                }

                container.appendChild(el);
            }

            // 比對已有vnode和新node,進行更新
            function patch(oldNode, newNode) {
                if (oldNode.tag === newNode.tag) {
                const el = oldNode.el;
          newNode.el = oldNode.el;
// 如果tag相同,則需要進行下一步的各種判斷,props的判斷,更新,children的判斷,更新 // 首先處理props const oldProps = oldNode.props || {}; const newProps = newNode.props || {}; // 首先判斷新props,如果舊的有,則更新,無,則新增 for (const key in newProps) { const oldValue = oldProps[key]; const newValue = newProps[key]; if (newValue !== oldValue) { // 如果新的與舊的不同,則更新 el.setAttribute(key, newValue); } } // 然後判斷舊props裡面,如果新的沒有,則刪除 for (const key in oldProps) { if (!(key in newProps)) { el.removeAttribute(key); } } // 然後將props設定為新 oldNode.props = newProps; // 接著處理children, 這裡我們簡單處理,認為children要麼是string,要麼是array const oldChildren = oldNode.children; const newChildren = newNode.children; if (typeof oldChildren === 'string') { // 對於舊子節點為string的情況 if (typeof newChildren === 'string') { // 如果新子節點也是string,則直接替換即可 el.textContent = newChildren; oldNode.children = newChildren; } else { // 新節點是array // 清空原有子節點內容 el.innerHTML = ''; newChildren.forEach(child => { mount(child, el); }); oldNode.children = newChildren; } } else { // 舊節點是array if (typeof newChildren === 'string') { el.innerHTML = newChildren; oldNode.children = newChildren; } else { // 兩個都是array,就簡單處理,只是比對同順序 // 先取兩個陣列同樣長度對比,直接patch const sameLength = Math.min(oldChildren.length, newChildren.length); for (let i = 0; i < sameLength; i++) { // 替換新元素 patch(oldChildren[i], newChildren[i]); } // 然後如果舊陣列還有,則移除多出部分 if (oldChildren.length > sameLength) { oldChildren.slice(sameLength).forEach(child => { el.removeChild(child.el); }); } // 如果新陣列還有,則新增多出部分 if (newChildren.length > sameLength) { newChildren.slice(sameLength).forEach(child => { mount(child, el); }); } } } } else { // 如果tag都不同,則直接替換即可 const parent = oldNode.el.parentNode; mount(newNode, parent); parent.removeChild(oldNode.el); oldNode.el = newNode.el; } } // reactive system let activeEffect = null; class Dep { subscribers = new Set(); // 新增依賴 depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } // 依賴更新,通知回撥 notify() { this.subscribers.forEach(effect => { effect(); }); } } /** * @decription 註冊事件,當依賴的物件發生改變時,觸發effect方法執行 */ function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null; } const targetMap = new WeakMap(); function getDep(target, key) { // 新增整個物件的依賴 let currMap = targetMap.get(target); if (!currMap) { currMap = new Map(); targetMap.set(target, currMap); } // 獲取具體key的依賴 let dep = currMap.get(key); // 如果沒有新增過則新增依賴 if (!dep) { dep = new Dep(); currMap.set(key, dep); } return dep; } const reactiveHandlers = { get(target, key, receiver) { const dep = getDep(target, key); // 為什麼每次讀取都要新增依賴,因為有可能會增加新的依賴 dep.depend(); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { const dep = getDep(target, key); // 類似於之前的Object.set const ret = Reflect.set(target, key, value, receiver); // 通知執行依賴 dep.notify(); // 因為proxy的set必須要返回一個boolean的值告訴設定成功與否,因此這裡返回系統api的結果即可 return ret; } }; /** * @description 給物件封裝並返回響應式代理 */ function reactive(oldObj) { return new Proxy(oldObj, reactiveHandlers); } /** * @description 將元件掛載到dom上 */ function mountApp(component, container) { // 是否已經掛載過 let isMounted = false; let vdom; // 註冊watchEffect的回撥 // 如果沒有掛載過,則掛載,否則patch watchEffect(() => { if (!isMounted) { vdom = component.render(); mount(vdom, container); } else { const vom2 = component.render(); patch(vdom, vdom2); vdom = vdom2; } }); } // 我們現在來按照Vue元件的格式定義一個簡單的自定義元件 const App = { data: reactive({ count: 1, }), render() { return h('div', null, String(App.data.count)); // 因為前面vdom限定了string }, }; // 接著我們需要通過一個安裝的方法來將這個元件掛載並且需要 // 在依賴的資料發生變化後重新渲染元件 mountApp(App, document.getElementById('app')) </script> </body> </html>

但是因為我們還沒有更新資料觸發effect的機制,因此我們修改下mount方法,引入事件繫結:

<html>
    <head>
        <title>Mini-Vue3</title>
    </head>
    <body>
        <div id="app"></div>
        <script>
            // compile & render system
            
            // 渲染函式,將函式描述的模版轉換為物件表述的json物件
            function h(tag, props, children) {
                return {
                tag,
                props,
                children,
                };
            }

            // 將指定的vnode裝在到container上
            function mount(vnode, container) {
                const el = document.createElement(vnode.tag);
                // 儲存vnode的dom引用
                vnode.el = el;

                // props
                if (vnode.props) {
                    for (const key in vnode.props) {
                        const value = vnode.props[key];
                        if (key.indexOf('on') === 0) {
                            // 如果屬性以on開頭則說明是方法
                            const funName = key.substring(2).toLowerCase();
                            el.addEventListener(funName, value);
                        } else {
                            el.setAttribute(key, value);
                        }

                    }
                }

                // children
                if (vnode.children) {
                    // 區分字串還是array,簡單處理
                    if (typeof vnode.children === 'string') {
                        el.textContent = vnode.children;
                    } else {
                        vnode.children.forEach(child => {
                            mount(child, el);
                        });
                    }
                }

                container.appendChild(el);
            }

            // 比對已有vnode和新node,進行更新
            function patch(oldNode, newNode) {
                if (oldNode.tag === newNode.tag) {
                const el = oldNode.el;
                newNode.el = oldNode.el;
                // 如果tag相同,則需要進行下一步的各種判斷,props的判斷,更新,children的判斷,更新
                // 首先處理props
                const oldProps = oldNode.props || {};
                const newProps = newNode.props || {};

                // 首先判斷新props,如果舊的有,則更新,無,則新增
                for (const key in newProps) {
                    const oldValue = oldProps[key];
                    const newValue = newProps[key];

                    if (newValue !== oldValue) {
                    // 如果新的與舊的不同,則更新
                    el.setAttribute(key, newValue);
                    }
                }
                // 然後判斷舊props裡面,如果新的沒有,則刪除
                for (const key in oldProps) {
                    if (!(key in newProps)) {
                    el.removeAttribute(key);
                    }
                }

                // 然後將props設定為新
                oldNode.props = newProps;

                // 接著處理children, 這裡我們簡單處理,認為children要麼是string,要麼是array
                const oldChildren = oldNode.children;
                const newChildren = newNode.children;

                if (typeof oldChildren === 'string') {
                    // 對於舊子節點為string的情況
                    if (typeof newChildren === 'string') {
                    // 如果新子節點也是string,則直接替換即可
                    el.textContent = newChildren;
                    oldNode.children = newChildren;
                    } else {
                    // 新節點是array
                    // 清空原有子節點內容
                    el.innerHTML = '';
                    newChildren.forEach(child => {
                        mount(child, el);
                    });
                    oldNode.children = newChildren;
                    }
                } else {
                    // 舊節點是array
                    if (typeof newChildren === 'string') {
                    el.innerHTML = newChildren;
                    oldNode.children = newChildren;
                    } else {
                    // 兩個都是array,就簡單處理,只是比對同順序

                    // 先取兩個陣列同樣長度對比,直接patch
                    const sameLength = Math.min(oldChildren.length, newChildren.length);
                    for (let i = 0; i < sameLength; i++) {
                        // 替換新元素
                        patch(oldChildren[i], newChildren[i]);
                    }
                    // 然後如果舊陣列還有,則移除多出部分
                    if (oldChildren.length > sameLength) {
                        oldChildren.slice(sameLength).forEach(child => {
                        el.removeChild(child.el);
                        });
                    }

                    // 如果新陣列還有,則新增多出部分
                    if (newChildren.length > sameLength) {
                        newChildren.slice(sameLength).forEach(child => {
                        mount(child, el);
                        });
                    }
                    }
                }
                
                } else {
                // 如果tag都不同,則直接替換即可
                const parent = oldNode.el.parentNode;
                mount(newNode, parent);
                parent.removeChild(oldNode.el);
                oldNode.el = newNode.el;
                }
            }


            // reactive system
            let activeEffect = null;

            class Dep {
                subscribers = new Set();

                // 新增依賴
                depend() {
                    if (activeEffect) {
                        this.subscribers.add(activeEffect);
                    }
                }

                // 依賴更新,通知回撥
                notify() {
                    this.subscribers.forEach(effect => {
                        effect();
                    });
                }
            }

            /**
             * @decription 註冊事件,當依賴的物件發生改變時,觸發effect方法執行
             */
            function watchEffect(effect) {
                activeEffect = effect;
                effect();
                activeEffect = null;
            }

            const targetMap = new WeakMap();

            function getDep(target, key) {
                // 新增整個物件的依賴
                let currMap = targetMap.get(target);

                if (!currMap) {
                    currMap = new Map();
                    targetMap.set(target, currMap);
                }

                // 獲取具體key的依賴
                let dep = currMap.get(key);
                // 如果沒有新增過則新增依賴
                if (!dep) {
                    dep = new Dep();
                    currMap.set(key, dep);
                }

                return dep;
            }

            const reactiveHandlers = {
                get(target, key, receiver) {
                    const dep = getDep(target, key);

                    // 為什麼每次讀取都要新增依賴,因為有可能會增加新的依賴
                    dep.depend();
                    return Reflect.get(target, key, receiver);
                },
                set(target, key, value, receiver) {                    
                    const dep = getDep(target, key);

                    // 類似於之前的Object.set
                    const ret = Reflect.set(target, key, value, receiver);

                    // 通知執行依賴
                    dep.notify();

                    // 因為proxy的set必須要返回一個boolean的值告訴設定成功與否,因此這裡返回系統api的結果即可
                    return ret;
                }
            };

            /**
             * @description 給物件封裝並返回響應式代理
             */
            function reactive(oldObj) {
                return new Proxy(oldObj, reactiveHandlers);
            }

            /**
             * @description 將元件掛載到dom上
             */
            function mountApp(component, container) {
                // 是否已經掛載過
                let isMounted = false;   
                let vdom;

                // 註冊watchEffect的回撥
                // 如果沒有掛載過,則掛載,否則patch
                watchEffect(() => {
                    if (!isMounted) {
                        vdom = component.render();
                        mount(vdom, container);
                        isMounted = true;
                    } else {
                        const vdom2 = component.render();
                        patch(vdom, vdom2);
                        vdom = vdom2;
                    }
                });
            }

            // 我們現在來按照Vue元件的格式定義一個簡單的自定義元件
            const App = {
                data: reactive({
                    count: 1,
                }),
                render() {
                    return h('div', {
                        onClick: () => {
                            // 新增點選事件方法
                            this.data.count += 1;
                        }
                    }, String(this.data.count)); // 因為前面vdom限定了string
                },
            };

            // 接著我們需要通過一個安裝的方法來將這個元件掛載並且需要
            // 在依賴的資料發生變化後重新渲染元件
            mountApp(App, document.getElementById('app'))
        </script>
    </body>
</html>

然後效果ok,沒有問題:

至此我們的mini-vue3已經跑起來了,只不過我們模板是需要我們自己去寫render function,如果把編譯系統也加入,使得我們直接通過模板寫vue元件,那就幾乎是一個完整功能的vue了。

後續或許要來實現他,我們的這一個小系列已經完成了,接下來會基於這個mini-vue3來寫Vue3中的一些新特性的實現。