1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        2021年,讓我們手寫一個mini版本的vue2.x和vue3.x框架

        共 24577字,需瀏覽 50分鐘

         ·

        2021-07-10 13:02

        作者:夕水

        來源:SegmentFault 思否社區(qū)

        mini版本的vue.js2.X版本框架

        模板代碼

        首先我們看一下我們要實現(xiàn)的模板代碼:

        <div id="app">
            <h3>{{ msg }}</h3>
            <p>{{ count }}</p>
            <h1>v-text</h1>
            <p v-text="msg"></p>
            <input type="text" v-model="count">
            <button type="button" v-on:click="increase">add+</button>
            <button type="button" v-on:click="changeMessage">change message!</button>
            <button type="button" v-on:click="recoverMessage">recoverMessage!</button>
        </div>

        邏輯代碼

        然后就是我們要編寫的javascript代碼。

        const app = new miniVue({
            el:"#app",
            data:{
                msg:"hello,mini vue.js",
                count:666
            },
            methods:{
                increase(){
                    this.count++;
                },
                changeMessage(){
                    this.msg = "hello,eveningwater!";
                },
                recoverMessage(){
                    console.log(this)
                    this.msg = "hello,mini vue.js";
                }
            }
        });

        運行效果

        我們來看一下實際運行效果如下所示:

        思考一下,我們要實現(xiàn)如上的功能應(yīng)該怎么做呢?你也可以單獨打開以上示例:
        點擊此處。

        源碼實現(xiàn)-2.x

        miniVue類

        首先,不管三七二十一,既然是實例化一個mini-vue,那么我們先定義一個類,并且它的參數(shù)一定是一個屬性配置對象。如下:
         class miniVue {
             constructor(options = {}){
                 //后續(xù)要做的事情
             }
         }
        現(xiàn)在,讓我們先初始化一些屬性,比如data,methods,options等等。
        //在miniVue構(gòu)造函數(shù)的內(nèi)部
        //保存根元素,能簡便就盡量簡便,不考慮數(shù)組情況
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
        this.$methods = options.methods;
        this.$data = options.data;
        this.$options = options;
        初始化完了之后,我們再來思考一個問題,我們是不是可以通過在vue內(nèi)部使用this訪問到vue定義的數(shù)據(jù)對象呢?那么我們應(yīng)該如何實現(xiàn)這一個功能呢?這個功能有一個專業(yè)的名詞,叫做代理(proxy)。

        代理數(shù)據(jù)

        因此我們來實現(xiàn)一下這個功能,很明顯在這個miniVue類的內(nèi)部定義一個proxy方法。如下:
        //this.$data.xxx -> this.xxx;
        //proxy代理實例上的data對象
        proxy(data){
            //后續(xù)代碼
        }
        接下來,我們需要知道一個api,即Object.defineProperty,通過這個方法來完成這個代理方法。如下:
        //proxy方法內(nèi)部
        // 因為我們是代理每一個屬性,所以我們需要將所有屬性拿到
        Object.keys(data).forEach(key => {
            Object.defineProperty(this,key,{
                enumerable:true,
                configurable:true,
                get:() => {
                    return data[key];
                },
                set:(newValue){
                    //這里我們需要判斷一下如果值沒有做改變就不用賦值,需要排除NaN的情況
                    if(newValue === data[key] || _isNaN(newValue,data[key]))return;
                    data[key] = newValue;
                }
            })
        })
        接下來,我們來看一下這個_isNaN工具方法的實現(xiàn),如下:
        function _isNaN(a,b){
            return Number.isNaN(a) && Number.isNaN(b);
        }
        定義好了之后,我們只需要在miniVue類的構(gòu)造函數(shù)中調(diào)用一次即可。如下:
        // 構(gòu)造函數(shù)內(nèi)部
        this.proxy(this.$data);
        代理就這樣完成了,讓我們繼續(xù)下一步。

        數(shù)據(jù)響應(yīng)式觀察者observer類

        我們需要對數(shù)據(jù)的每一個屬性都定義一個響應(yīng)式對象,用來監(jiān)聽數(shù)據(jù)的改變,所以我們需要一個類來管理它,我們就給它取個名字叫Observer。如下:
        class Observer {
            constructor(data){
                //后續(xù)實現(xiàn)
            }
        }
        我們需要給每一個數(shù)據(jù)都添加響應(yīng)式對象,并且轉(zhuǎn)換成getter和setter函數(shù),這里我們又用到了Object.defineProperty方法,我們需要在getter函數(shù)中收集依賴,在setter函數(shù)中發(fā)送通知,用來通知依賴進(jìn)行更新。我們用一個方法來專門去執(zhí)行定義響應(yīng)式對象的方法,叫walk,如下:
        //再次申明,不考慮數(shù)組,只考慮對象
        walk(data){
            if(typeof data !== 'object' || !data)return;
            // 數(shù)據(jù)的每一個屬性都調(diào)用定義響應(yīng)式對象的方法
            Object.keys(data).forEach(key => this.defineReactive(data,key,data[key]));
        }
        接下來我們來看defineReactive方法的實現(xiàn),同樣也是使用Object.defineProperty方法來定義響應(yīng)式對象,如下所示:
        defineReactive(data,key,value){
            // 獲取當(dāng)前this,以避免后續(xù)用vm的時候,this指向不對
            const vm = this;
            // 遞歸調(diào)用walk方法,因為對象里面還有可能是對象
            this.walk(value);
            //實例化收集依賴的類
            let dep = new Dep();
            Object.defineProperty(data,key,{
                enumerable:true,
                configurable:true,
                get(){
                    // 收集依賴,依賴存在Dep類上
                    Dep.target && Dep.add(Dep.target);
                    return value;
                },
                set(newValue){
                    // 這里也判斷一下
                    if(newValue === value || __isNaN(value,newValue))return;
                    // 否則改變值
                    value = newValue;
                    // newValue也有可能是對象,所以遞歸
                    vm.walk(newValue);
                    // 通知Dep類
                    dep.notify();
                }
            })
        }
        Observer類完成了之后,我們需要在miniVue類的構(gòu)造函數(shù)中實例化一下它,如下:
        //在miniVue構(gòu)造函數(shù)內(nèi)部
        new Observer(this.$data);
        好的,讓我們繼續(xù)下一步。

        依賴類

        defineReactive方法內(nèi)部用到了Dep類,接下來,我們來定義這個類。如下:
        class Dep {
            constructor(){
                //后續(xù)代碼
            }
        }
        接下來,我們來思考一下,依賴類里面,我們需要做什么,首先根據(jù)defineReactive中,我們很明顯就知道會有add方法和notify方法,并且我們需要一種數(shù)據(jù)結(jié)構(gòu)來存儲依賴,vue源碼用的是隊列,而在這里為了簡單化,我們使用ES6的set數(shù)據(jù)結(jié)構(gòu)。如下:
        //構(gòu)造函數(shù)內(nèi)部
        this.deps = new Set();
        接下來,就需要實現(xiàn)add方法和notify方法,事實上這里還會有刪除依賴的方法,但是這里為了最簡便,我們只需要一個addnotify方法即可。如下:
        add(dep){
            //判斷dep是否存在并且是否存在update方法,然后添加到存儲的依賴數(shù)據(jù)結(jié)構(gòu)中
            if(dep && dep.update)this.deps.add(dep);
        }
        notify(){
            // 發(fā)布通知無非是遍歷一道dep,然后調(diào)用每一個dep的update方法,使得每一個依賴都會進(jìn)行更新
            this.deps.forEach(dep => dep.update())
        }
        Dep類算是完了,接下來我們就需要另一個類。

        Watcher類

        那就是為了管理每一個組件實例的類,確保每個組件實例可以由這個類來發(fā)送視圖更新以及狀態(tài)流轉(zhuǎn)的操作。這個類,我們把它叫做Watcher。
        class Watcher {
            //3個參數(shù),當(dāng)前組件實例vm,state也就是數(shù)據(jù)以及一個回調(diào)函數(shù),或者叫處理器
            constructor(vm,key,cb){
                //后續(xù)代碼
            }
        }
        再次思考一下,我們的Watcher類需要做哪些事情呢?我們先來思考一下Watcher的用法,我們是不是會像如下這樣來寫:
        //3個參數(shù),當(dāng)前組件實例vm,state也就是數(shù)據(jù)以及一個回調(diào)函數(shù),或者叫處理器
        new Watcher(vm,key,cb);
        ok,知道了使用方式之后,我們就可以在構(gòu)造函數(shù)內(nèi)部初始化一些東西了。如下:
        //構(gòu)造函數(shù)內(nèi)部
        this.vm = vm;
        this.key = key;
        this.cb = cb;
        //依賴類
        Dep.target = this;
        // 我們用一個變量來存儲舊值,也就是未變更之前的值
        this.__old = vm[key];
        Dep.target = null;
        然后Watcher類就多了一個update方法,接下來讓我們來看一下這個方法的實現(xiàn)吧。如下:
        update(){
            //獲取新的值
            let newValue = this.vm[this.key];
            //與舊值做比較,如果沒有改變就無需執(zhí)行下一步
            if(newValue === this.__old || __isNaN(newValue,this.__old))return;
            //把新的值回調(diào)出去
            this.cb(newValue);
            //執(zhí)行完之后,需要更新一下舊值的存儲
            this.__old = newValue;
        }

        編譯類compiler類

        初始化

        到了這一步,我們就算是完全脫離vue源碼了,因為vue源碼的編譯十分復(fù)雜,涉及到diff算法以及虛擬節(jié)點vNode,而我們這里致力于將其最簡化,所以單獨寫一個Compiler類來編譯。如下:
        class Compiler {
            constructor(vm){
                //后續(xù)代碼
            }
        }
        注意:這里的編譯是我們自己根據(jù)流程來實現(xiàn)的,與vue源碼并沒有任何關(guān)聯(lián),vue也有compiler,但是與我們實現(xiàn)的完全不同。
        定義好了之后,我們在miniVue類的構(gòu)造函數(shù)中實例化一下這個編譯類即可。如下:
        //在miniVue構(gòu)造函數(shù)內(nèi)部
        new Compiler(this);
        好的,我們也看到了使用方式,所以接下來我們來完善這個編譯類的構(gòu)造函數(shù)內(nèi)部的一些初始化操作。如下:
        //編譯類構(gòu)造函數(shù)內(nèi)部
        //根元素
        this.el = vm.$el;
        //事件方法
        this.methods = vm.$methods;
        //當(dāng)前組件實例
        this.vm = vm;
        //調(diào)用編譯函數(shù)開始編譯
        this.compile(vm.$el);

        compile方法

        初始化操作算是完成了,接下來我們來看compile方法的內(nèi)部。思考一下,在這個方法的內(nèi)部,我們是不是需要拿到所有的節(jié)點,然后對比是文本還是元素節(jié)點去分別進(jìn)行編譯呢?如下:
        compile(el){
            //拿到所有子節(jié)點(包含文本節(jié)點)
            let childNodes = el.childNodes;
            //轉(zhuǎn)成數(shù)組
            Array.from(childNodes).forEach(node => {
                //判斷是文本節(jié)點還是元素節(jié)點分別執(zhí)行不同的編譯方法
                if(this.isTextNode(node)){
                    this.compileText(node);
                }else if(this.isElementNode(node)){
                    this.compileElement(node);
                }
                //遞歸判斷node下是否還含有子節(jié)點,如果有的話繼續(xù)編譯
                if(node.childNodes && node.childNodes.length)this.compile(node);
            })
        }
        這里,我們需要2個輔助方法,判斷是文本節(jié)點還是元素節(jié)點,其實我們可以使用節(jié)點的nodeType屬性來進(jìn)行判斷,由于文本節(jié)點的nodeType值為3,而元素節(jié)點的nodeType值為1。所以這2個輔助方法我們就可以實現(xiàn)如下:
        isTextNode(node){
            return node.nodeType === 3;
        }
        isElementNode(node){
            return node.nodeType === 3;
        }

        編譯文本節(jié)點

        接下來,我們下來看compileText編譯文本節(jié)點的方法。如下:
        //{{ count }}數(shù)據(jù)結(jié)構(gòu)是類似如此的
        compileText(node){
            //后續(xù)代碼
        }
        接下來,讓我們思考一下,我們編譯文本節(jié)點,無非就是把文本節(jié)點中的{{ count }}映射成為0,而文本節(jié)點不就是node.textContent屬性嗎?所以此時我們可以想到根據(jù)正則來匹配{{}}中的count值,然后對應(yīng)替換成數(shù)據(jù)中的count值,然后我們再調(diào)用一次Watcher類,如果更新了,就再次更改這個node.textContent的值。如下:
        compileText(node){
            //定義正則,匹配{{}}中的count
            let reg = /\{\{(.+?)\}\}/g;
            let value = node.textContent;
            //判斷是否含有{{}}
            if(reg.test(value)){
                //拿到{{}}中的count,由于我們是匹配一個捕獲組,所以我們可以根據(jù)RegExp類的$1屬性來獲取這個count
                let key = RegExp.$1.trim();
                node.textContent = value.replace(reg,this.vm[key]);
                //如果更新了值,還要做更改
                new Watcher(this.vm,key,newValue => {
                    node.textContent = newValue;
                })
            }
        }
        編譯文本節(jié)點到此為止了,接下來我們來看編譯元素節(jié)點的方法。

        編譯元素節(jié)點

        指令

        首先,讓我們想一下,我們編譯元素節(jié)點無非是想要根據(jù)元素節(jié)點上的指令來分別執(zhí)行不同的操作,所以我們編譯元素節(jié)點就只需要判斷是否含有相關(guān)指令即可,這里我們只考慮了v-text,v-model,v-on:click這三個指令。讓我們來看看compileElement方法吧。
        compileElement(node){
            //指令不就是一堆屬性嗎,所以我們只需要獲取屬性即可
            const attrs = node.attributes;
            if(attrs.length){
                Array.from(attrs).forEach(attr => {
                    //這里由于我們拿到的attributes可能包含不是指令的屬性,所以我們需要先做一次判斷
                    if(this.isDirective(attr)){
                        //根據(jù)v-來截取一下后綴屬性名,例如v-on:click,subStr(5)即可截取到click,v-text與v-model則subStr(2)截取到text和model即可
                        let attrName = attr.indexOf(':') > -1 ? attr.subStr(5) : attr.subStr(2);
                        let key = attr.value;
                        //單獨定義一個update方法來區(qū)分這些
                        this.update(node,attrName,key,this.vm[key]);
                    }
                })
            }
        }
        這里又涉及到了一個isDirective輔助方法,我們可以使用startsWith方法,判斷是否含有v-值即可認(rèn)定這個屬性就是一個指令。如下:
        isDirective(dir){
            return dir.startsWith('v-');
        }
        接下來,我們來看最后的update方法。如下:
        update(node,attrName,key,value){
            //后續(xù)代碼
        }
        最后,讓我們來思考一下,我們update里面需要做什么。很顯然,我們是不是需要判斷是哪種指令來執(zhí)行不同的操作?如下:
        //update函數(shù)內(nèi)部
        if(attrName === 'text'){
            //執(zhí)行v-text的操作
        }else if(attrName === 'model'){
            //執(zhí)行v-model的操作
        }else if(attrName === 'click'){
            //執(zhí)行v-on:click的操作
        }

        v-text指令

        好的,我們知道,根據(jù)前面的編譯文本元素節(jié)點的方法,我們就知道這個指令的用法同前面編譯文本元素節(jié)點。所以這個判斷里面就好寫了,如下:
        //attrName === 'text'內(nèi)部
        node.textContent = value;
        new Watcher(this.vm,key,newValue => {
            node.textContent = newValue;
        })

        v-model指令

        v-model指令實現(xiàn)的是雙向綁定,我們都知道雙向綁定是更改輸入框的value值,并且通過監(jiān)聽input事件來實現(xiàn)。所以這個判斷,我們也很好寫了,如下:
        //attrName === 'model'內(nèi)部
        node.value = value;
        new Watcher(this.vm,key,newValue => {
            node.value = newValue;
        });
        node.addEventListener('input',(e) => {
            this.vm[key] = node.value;
        })

        v-on:click指令

        v-on:click指令就是將事件綁定到methods內(nèi)定義的函數(shù),為了確保this指向當(dāng)前組件實例,我們需要通過bind方法改變一下this指向。如下:
        //attrName === 'click'內(nèi)部
        node.addEventListener(attrName,this.methods[key].bind(this.vm));
        到此為止,我們一個mini版本的vue2.x就算是實現(xiàn)了。繼續(xù)下一節(jié),學(xué)習(xí)vue3.x版本的mini實現(xiàn)吧。

        mini版本的vue.js3.x框架

        模板代碼

        首先我們看一下我們要實現(xiàn)的模板代碼:
        <div id="app"></div>

        邏輯代碼

        然后就是我們要編寫的javascript代碼。
        const App = {
            $data:null,
            setup(){
                let count = ref(0);
                let time = reactive({ second:0 });
                let com = computed(() => `${ count.value + time.second }`);
                setInterval(() => {
                    time.second++;
                },1000);
                setInterval(() => {
                    count.value++;
                },2000);
                return {
                    count,
                    time,
                    com
                }
            },
            render(){
                return `
                    <h1>How reactive?</h1>
                    <p>this is reactive work:${ this.$data.time.second }</p>
                    <p>this is ref work:${ this.$data.count.value }</p>
                    <p>this is computed work:${ this.$data.com.value  }</p>
                `
            }
        }
        mount(App,document.querySelector("#app"));

        運行效果

        我們來看一下實際運行效果如下所示:

        思考一下,我們要實現(xiàn)如上的功能應(yīng)該怎么做呢?

        源碼實現(xiàn)-3.x

        與vue2.x做比較

        事實上,vue3.x的實現(xiàn)思想與vue2.x差不多,只不過vue3.x的實現(xiàn)方式有些不同,在vue3.x,把收集依賴的方法稱作是副作用effect。vue3.x更像是函數(shù)式編程了,每一個功能都是一個函數(shù),比如定義響應(yīng)式對象,那就是reactive方法,再比如computed,同樣的也是computed方法...廢話不多說,讓我們來看一下吧!

        reactive方法

        首先,我們來看一下vue3.x的響應(yīng)式方法,在這里,我們?nèi)匀恢豢紤]處理對象。如下:
        function reactive(data){
            if(!isObject(data))return;
            //后續(xù)代碼
        }
        接下來我們需要使用到es6的proxyAPI,我們需要熟悉這個API的用法,如果不熟悉,請點擊此處查看。
        我們還是在getter中收集依賴,setter中觸發(fā)依賴,收集依賴與觸發(fā)依賴,我們都分別定義為2個方法,即track和trigger方法。如下:
        function reactive(data){
            if(!isObject(data))return;
            return new Proxy(data,{
                get(target,key,receiver){
                    //反射api
                    const ret = Reflect.get(target,key,receiver);
                    //收集依賴
                    track(target,key);
                    return isObject(ret) ? reactive(ret) : ret;
                },
                set(target,key,val,receiver){
                    Reflect.set(target,key,val,receiver);
                    //觸發(fā)依賴方法
                    trigger(target,key);
                    return true;
                },
                deleteProperty(target,key,receiver){
                    const ret = Reflect.deleteProperty(target,key,receiver);
                    trigger(target,key);
                    return ret;
                }
            })
        }

        track方法

        track方法就是用來收集依賴的。我們用es6的weakMap數(shù)據(jù)結(jié)構(gòu)來存儲依賴,然后為了簡便化用一個全局變量來表示依賴。如下:
        //全局變量表示依賴
        let activeEffect;
        //存儲依賴的數(shù)據(jù)結(jié)構(gòu)
        let targetMap = new WeakMap();
        //每一個依賴又是一個map結(jié)構(gòu),每一個map存儲一個副作用函數(shù)即effect函數(shù)
        function track(target,key){
            //拿到依賴
            let depsMap = targetMap.get(target);
            // 如果依賴不存在則初始化
            if(!depsMap)targetMap.set(target,(depsMap = new Map()));
            //拿到具體的依賴,是一個set結(jié)構(gòu)
            let dep = depsMap.get(key);
            if(!dep)depsMap.set(key,(dep = new Set()));
            //如果沒有依賴,則存儲再set數(shù)據(jù)結(jié)構(gòu)中
            if(!dep.has(activeEffect))dep.add(activeEffect)
        }
        收集依賴就這么簡單,需要注意的是,這里涉及到了es6的三種數(shù)據(jù)結(jié)構(gòu)即WeakMap,Map,Set。下一步我們就來看如何觸發(fā)依賴。

        trigger方法

        trigger方法很明顯就是拿出所有依賴,每一個依賴就是一個副作用函數(shù),所以直接調(diào)用即可。
        function trigger(target,key){
            const depsMap = targetMap.get(target);
            //存儲依賴的數(shù)據(jù)結(jié)構(gòu)都拿不到,則代表沒有依賴,直接返回
            if(!depsMap)return;
            depsMap.get(key).forEach(effect => effect && effect());
        }
        接下來,我們來實現(xiàn)一下這個副作用函數(shù),也即effect。

        effect方法

        副作用函數(shù)的作用也很簡單,就是執(zhí)行每一個回調(diào)函數(shù)。所以該方法有2個參數(shù),第一個是回調(diào)函數(shù),第二個則是一個配置對象。如下:
        function effect(handler,options = {}){
            const __effect = function(...args){
                activeEffect = __effect;
                return handler(...args);
            }
            //配置對象有一個lazy屬性,用于computed計算屬性的實現(xiàn),因為計算屬性是懶加載的,也就是延遲執(zhí)行
            //也就是說如果不是一個計算屬性的回調(diào)函數(shù),則立即執(zhí)行副作用函數(shù)
            if(!options.lazy){
                __effect();
            }
            return __effect;
        }
        副作用函數(shù)就是如此簡單的實現(xiàn)了,接下來我們來看一下computed的實現(xiàn)。

        computed的實現(xiàn)

        既然談到了計算屬性,所以我們就定義了一個computed函數(shù)。我們來看一下:
        function computed(handler){
            // 只考慮函數(shù)的情況
            // 延遲計算 const c = computed(() => `${ count.value}!`)
            let _computed;
            //可以看到computed就是一個添加了lazy為true的配置對象的副作用函數(shù)
            const run = effect(handler,{ lazy:true });
            _computed = {
                //get 訪問器
                get value(){
                    return run();
                }
            }
            return _computed;
        }
        到此為止,vue3.x的響應(yīng)式算是基本實現(xiàn)了,接下來要實現(xiàn)vue3.x的mount以及compile。還有一點,我們以上只是處理了引用類型的響應(yīng)式,但實際上vue3.x還提供了一個ref方法用來處理基本類型的響應(yīng)式。因此,我們?nèi)匀豢梢詫崿F(xiàn)基本類型的響應(yīng)式。

        ref方法

        那么,我們應(yīng)該如何來實現(xiàn)基本類型的響應(yīng)式呢?試想一下,為什么vue3.x中定義基本類型,如果修改值,需要修改xxx.value來完成。如下:
        const count = ref(0);
        //修改
        count.value = 1;
        從以上代碼,我們不難得出基本類型的封裝原理,實際上就是將基本類型包裝成一個對象。因此,我們很快可以寫出如下代碼:
        function ref(target){
            let value = target;
            const obj = {
                get value(){
                    //收集依賴
                    track(obj,'value');
                    return value;
                },
                set value(newValue){
                    if(value === newValue)return;
                    value = newValue;
                    //觸發(fā)依賴
                    trigger(obj,'value');
                }
            }
            return obj;
        }
        這就是基本類型的響應(yīng)式實現(xiàn)原理,接下來我們來看一下mount方法的實現(xiàn)。

        mount方法

        mount方法實現(xiàn)掛載,而我們的副作用函數(shù)就是在這里執(zhí)行。它有2個參數(shù),第一個參數(shù)即一個vue組件,第二個參數(shù)則是掛載的DOM根元素。所以,我們可以很快寫出以下代碼:
        function mount(instance,el){
            effect(function(){
                instance.$data && update(instance,el);
            });
            //setup返回的數(shù)據(jù)就是實例上的數(shù)據(jù)
            instance.$data = instance.setup();
            //這里的update實際上就是編譯函數(shù)
            update(instance,el);
        }
        這樣就是實現(xiàn)了一個簡單的掛載,接下來我們來看一下編譯函數(shù)的實現(xiàn)。

        update編譯函數(shù)

        這里為了簡便化,我們實現(xiàn)的編譯函數(shù)就比較簡單,直接就將定義在組件上的render函數(shù)給賦值給根元素的innerHTML。如下:
        //這是最簡單的編譯函數(shù)
        function update(instance,el){
            el.innerHTML = instance.render();
        }
        如此一來,一個簡單的mini-vue3.x就這樣實現(xiàn)了,怎么樣,不到100行代碼就搞定了,還是比較簡單的。


        點擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

        - END -


        瀏覽 74
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            又黄又骚的网站 | 人伦影院A级毛片 | 国产孕妇孕交 | 超碰人人大香蕉 | 成视人A片产无码免费视频看A片 | 中文字幕一区二区久久人妻 | 天堂v视频 | 操逼18| 午夜精品久久久99热蜜桃的推荐系统 | 大香蕉五月丁香 |