簡單實(shí)現(xiàn)VUE雙向數(shù)據(jù)綁定

前言

VUE是當(dāng)下比較熱門的一個(gè)前端框架大溜,其顯著特點(diǎn)就是雙向數(shù)據(jù)綁定差油,即data更新view,view更新data,但其具體實(shí)現(xiàn)一直模模糊糊泳炉,這次就來搞個(gè)明明白白憾筏。

核心原理

其核心原理就是數(shù)據(jù)劫持,數(shù)據(jù)劫持是用Object.definePorperty()來實(shí)現(xiàn)的,看代碼

let data = {}
let num = 0;
Object.defineProperty(data, 'num', {
    enumerable: true,
    configurable: true,
    get() {
        return num;
    },
    set(v) {
        num = v;
    }
});

每當(dāng)獲取data['num']的值時(shí)就會(huì)觸發(fā)它的get方法花鹅,設(shè)置data['num']的值時(shí)就會(huì)觸發(fā)它的set方法踩叭,這樣就實(shí)現(xiàn)了數(shù)據(jù)劫持,一旦數(shù)據(jù)發(fā)生一些改變翠胰,就可以監(jiān)聽到容贝,然后去實(shí)現(xiàn)一些自定義的功能,例如:更新view

第一步之景,確定初始化方式

就模仿VUE的初始化好了斤富,首先我們寫HTML

<div id="app">
    <h1>{{info}}</h1>
    <input type="text" v-model="info">
    <button v-on:click="btnClick">點(diǎn)擊</button>
</div>

接著,對它進(jìn)行初始化

    let ymvue = new YMVue({
        el: '#app',
        data: {
            info: 'hello world'
        },
        methods: {
            mounted() {
                console.log(this)
            },
            btnClick() {
                this.hello = 'Hello Vue'
            }
        }
    });

第二步锻狗,生成觀察者

VUE用的是觀察者模式满力,觀察者的主要功能就是數(shù)據(jù)劫持焕参,那我們定義一個(gè)觀察者Guard,當(dāng)我們初始化一個(gè)YMVue對象之后油额,就把它交給觀察者Guard叠纷,然后遍歷它的data集合,并對里面的數(shù)據(jù)進(jìn)行劫持潦嘶。

    function Guard(obj) {
        this.obj = obj;   // YMVue對象
        this.start(obj.data);
    }

    Guard.prototype = {
        start(data) {
            if (data && typeof data === 'object') {
                Object.keys(data).forEach((key) => {
                    this.addGuard(data, key, data[key]);
                });
            }
        },
        addGuard(data, key, val) {
            let self = this;
            this.start(data[key]);
            // let dep = new Dep();
            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: true,
                get() {
                    //if (Dep.target) {
                    //    dep.addSub(Dep.target);
                    //}
                    return val;
                },
                set(v) {
                    if (val === v) {
                        return;
                    }
                    val = v;
                    //dep.update();
                }
            })
        }
    };

上面代碼中注釋代碼僅作暫時(shí)注釋涩嚣,待下面進(jìn)行說明

第三步,生成訂閱者

如果給觀察者傳入一個(gè)回調(diào)函數(shù)callback掂僵,那么在觸發(fā)set方法后執(zhí)行回調(diào)航厚,好像是實(shí)現(xiàn)了所有功能。
但是锰蓬,but,一個(gè)數(shù)據(jù)的更新可能會(huì)觸發(fā)無數(shù)個(gè)方法幔睬,如果通過給觀察者傳入回調(diào),那就必須對同一個(gè)數(shù)據(jù)進(jìn)行多次劫持芹扭,那自然是不得行了麻顶,所以需要個(gè)訂閱者,把數(shù)據(jù)更新后帶來的連鎖反應(yīng)訂閱到這個(gè)數(shù)據(jù)上去舱卡。

    function BindCallbackToGuard(obj, name, callback) {
        this.obj = obj;             // YMVue對象
        this.callback = callback;   // 回調(diào)函數(shù)
        this.name = name;           // 訂閱的數(shù)據(jù)
        this.value = this.get();
    }

    BindCallbackToGuard.prototype = {
        excute() {
            let val = this.obj.data[this.name];
            let oldVal = this.value;
            if (val !== oldVal) {
                this.value = val;
                this.callback.call(this.obj, val, oldVal);
            }
        },
        get() {
            Dep.target = this;                    // 把自己設(shè)為訂閱對象 
            let value = this.obj.data[this.name]; // 觸發(fā)觀察者的get函數(shù)
            Dep.target = null;                    
            return value;
        }
    };

第四步辅肾,創(chuàng)建一個(gè)訂閱者容器

第二步和第三步的代碼中都有一個(gè)Dep,那Dep到底是什么呢
Dep其實(shí)是一個(gè)訂閱者容器灼狰,每當(dāng)有一個(gè)訂閱者訂閱了自己宛瞄,觀察者就把它放進(jìn)訂閱者容器里面,然后當(dāng)觀察者監(jiān)聽到數(shù)據(jù)有變的時(shí)候交胚,就去遍歷訂閱者容器份汗,然后執(zhí)行每個(gè)訂閱者訂閱的方法。

    function Dep() {
        this.subs = []
    }

    Dep.prototype = {
        addSub(sub) {
            this.subs.push(sub);
        },
        update() {
            this.subs.forEach((sub) => {   // sub是一個(gè)訂閱者
                sub.excute();
            })
        }
    };

這時(shí)候蝴簇,就可以取消第二步代碼中的注釋了

第五步杯活,生成DOM解析器

上面我們已經(jīng)把功能都搞定了,現(xiàn)在就需要把那些特殊的HTML跟這些代碼聯(lián)系到一起熬词,這時(shí)候就需要一個(gè)DOM解析器旁钧,其中對DOM元素的操作我們用到了文檔碎片Fragment

    function Analysis(containerId, obj) {
        this.obj = obj;    // YMVue對象
        this.dom = document.querySelector(containerId);
        this.fragment = null;
        this.init();
    }

    Analysis.prototype = {
        init() {
            if (this.dom) {
                this.fragment = this.switchToFragment(this.dom);     // 將NODE節(jié)點(diǎn)轉(zhuǎn)換為文檔碎片
                this.analysisElement(this.fragment);            // 開始解析
                this.dom.appendChild(this.fragment);            // 用文檔碎片替換node節(jié)點(diǎn)
            } else {
                console.log('Dom 元素不存在');
            }
        },
        switchToFragment() {
            let fragment = document.createDocumentFragment();
            let child = this.dom.firstChild;
            while (child) {
                fragment.append(child);
                child = this.dom.firstChild;
            }
            return fragment;
        },
        analysisElement(dom) {
            let childNodes = dom.childNodes;
            [].slice.call(childNodes).forEach((node) => {
                let reg = /\{\{(.*)\}\}/;
                let text = node.textContent;

                if (this.isElementNode(node)) {      // 元素節(jié)點(diǎn)
                    this.analysisAttr(node);
                } else if (this.isTextNode(node) && reg.test(text)) {  // 文本節(jié)點(diǎn)
                    this.bindText(node, reg.exec(text)[1]);
                }

                if (node.childNodes && node.childNodes.length) {
                    this.analysisElement(node);
                }
            });
        },
        analysisAttr(node) {
            let nodeAttrs = node.attributes;
            Array.prototype.forEach.call(nodeAttrs, (attr) => {
                let name = attr.name;          // 屬性名稱
                if (this.isCommand(name)) {    // 屬性名以'v-'開頭
                    let value = attr.value;    // 屬性值
                    let command = name.substring(2);
                    if (this.isEventCommand(command)) {  // 事件指令,比如v-on:click
                        this.bindEvent(node, value, command);
                    } else { // v-model 指令
                        this.bindModel(node, value)
                    }
                    node.removeAttribute(name);  // 移除后互拾,頁面上不顯示
                }
            })
        },

        bindText(node, name) {
            let text = this.obj[name];
            node.textContent = text || '';
            // 添加一個(gè)訂閱者
            new BindCallbackToGuard(this.obj, name, function (v) {
                node.textContent = v;
            });

        },
        bindEvent(node, name, command) {
            let eventType = command.split(':')[1];
            let method = this.obj.methods && this.obj.methods[name];
            if (eventType && method) {
                node.addEventListener(eventType, method.bind(this.obj), false);
            }
        },
        bindModel(node, name) {
            let self = this;
            let text = this.obj[name];
            node.value = text || '';
            
            // 添加一個(gè)訂閱者
            new BindCallbackToGuard(this.obj, name, function (v) {
                node.value = v;
            });
            // 監(jiān)聽input事件歪今,當(dāng)它的value改變時(shí),同時(shí)更新其綁定值
            node.addEventListener('input', function (e) {
                let newVal = e.target.value;
                if (text === newVal) {
                    return;
                }
                self.obj[name] = newVal;
                text = newVal;
            }, false);
        },
        isCommand(attr) {
            return attr.indexOf('v-') === 0;
        },
        isEventCommand(dir) {
            return dir.indexOf('on:') === 0;
        },
        isElementNode(node) {
            return node.nodeType === 1;
        },
        isTextNode(node) {
            return node.nodeType === 3;
        }
    };

第六步颜矿,定義初始化類

現(xiàn)在所有的功能基本完成寄猩,就差一個(gè)初始類YMVue

    function YMVue(opts) {
        this.data = opts.data;
        this.methods = opts.methods || {};

        Object.keys(this.data).forEach((key) => {
            this.proxy(key);      // 設(shè)置代理
        });
        new Guard(this);           // 初始化觀察者
        new Analysis(opts.el, this);     // dom解析
        this.methods.mounted && this.methods.mounted.call(this);  // 初始化完成后,執(zhí)行mounted方法
    }

    YMVue.prototype = {
        proxy(key) {  
            let self = this;
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return self.data[key];
                },
                set(v) {
                    self.data[key] = v;
                }
            })
        }
    };

初始化類有一個(gè)代理proxy骑疆,它有什么用呢田篇?
因?yàn)閂UE改變的數(shù)據(jù)的操作是this.info='xxxx'替废,但是this指向的是VUE對象,info又在data集合里泊柬,所以應(yīng)該是this.data.info='xxx'椎镣,那取消中間的data就需要用到代理。

結(jié)束

OK兽赁,大功告成状答,雖然功能距離真正的VUE還差的遠(yuǎn),但是簡單的數(shù)據(jù)雙向綁定就這么實(shí)現(xiàn)了闸氮,寫成一個(gè)插件剪况,隨便在哪都可以用起來教沾。
完整代碼在這

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蒲跨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子授翻,更是在濱河造成了極大的恐慌或悲,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堪唐,死亡現(xiàn)場離奇詭異巡语,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)淮菠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進(jìn)店門男公,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人合陵,你說我怎么就攤上這事枢赔。” “怎么了拥知?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵踏拜,是天一觀的道長。 經(jīng)常有香客問我低剔,道長速梗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任襟齿,我火速辦了婚禮姻锁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘猜欺。我一直安慰自己位隶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布替梨。 她就那樣靜靜地躺著钓试,像睡著了一般装黑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上弓熏,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天恋谭,我揣著相機(jī)與錄音,去河邊找鬼挽鞠。 笑死疚颊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的信认。 我是一名探鬼主播材义,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼嫁赏!你這毒婦竟也來了其掂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤潦蝇,失蹤者是張志新(化名)和其女友劉穎款熬,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體攘乒,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贤牛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了则酝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片殉簸。...
    茶點(diǎn)故事閱讀 38,625評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖沽讹,靈堂內(nèi)的尸體忽然破棺而出般卑,到底是詐尸還是另有隱情,我是刑警寧澤妥泉,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布椭微,位于F島的核電站,受9級(jí)特大地震影響盲链,放射性物質(zhì)發(fā)生泄漏蝇率。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一刽沾、第九天 我趴在偏房一處隱蔽的房頂上張望本慕。 院中可真熱鬧,春花似錦侧漓、人聲如沸锅尘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽藤违。三九已至浪腐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間顿乒,已是汗流浹背议街。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留璧榄,地道東北人特漩。 一個(gè)月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像骨杂,于是被迫代替她去往敵國和親涂身。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 前言 使用vue也好有一段時(shí)間了搓蚪,雖然對其雙向綁定原理也有了解個(gè)大概蛤售,但也沒好好探究下其原理實(shí)現(xiàn),所以這次特意花了...
    指尖跳動(dòng)閱讀 7,981評論 0 16
  • 這方面的文章很多陕凹,但是我感覺很多寫的比較抽象悍抑,本文會(huì)通過舉例更詳細(xì)的解釋鳄炉。(此文面向的Vue新手們杜耙,如果你是個(gè)大牛...
    Ivy_2016閱讀 15,375評論 8 64
  • 本文是lhyt本人原創(chuàng),希望用通俗易懂的方法來理解一些細(xì)節(jié)和難點(diǎn)拂盯。轉(zhuǎn)載時(shí)請注明出處佑女。文章最早出現(xiàn)于本人github...
    lhyt閱讀 2,203評論 0 4
  • 從源碼分析雙向綁定 這部分代碼,是源碼的簡化版谈竿,相對比較容易理解团驱。 html代碼: 從html代碼,vue僅僅從初...
    zdxhxh閱讀 412評論 0 0
  • 第一步 編寫程序 以github上的N皇后問題為例 測試一下運(yùn)行 第二步 混淆 打開Oxyry網(wǎng)站空凸,將代碼復(fù)制到左...
    yiqingxu閱讀 3,131評論 1 3