Vue2.0雙向綁定核心實現(xiàn)

/**
 * author:Echonessy
 * des:
 * date:2020.07.24
 * target: Vue
 *  1.Vue
 *      把data中的成員注入到Vue實例中若债,并且把data中的成員轉成getter/setter
 *  2.Observer
 *      能夠對數(shù)據(jù)對象的所有屬性進行監(jiān)聽火焰,如果變動可拿到最新值并通知Dep(發(fā)布者-目標)
 *  3.Watcher
 *      定義觀察者,定義update()函數(shù)纱耻,當數(shù)據(jù)發(fā)生變動,更新視圖
 *  4.Dep
 *      添加觀察者险耀,當數(shù)據(jù)發(fā)生變化的時候弄喘,通知所有的觀察者,執(zhí)行觀察者的update()函數(shù)
 *  5.Compiler
 *      負責編譯模板甩牺,解析指令/差值表達式蘑志,負責頁面的首次渲染,當數(shù)據(jù)變化后更新視圖
 * */



/**
 *  1.Vue
 *      把data中的成員注入到Vue實例中贬派,并且把data中的成員轉成getter/setter
 *      功能:
 *          1.負責接受初始化的參數(shù)(選項)
 *          2.負責吧data中的屬性注入到Vue實例急但,轉換成getter/setter
 *          3.負責調(diào)用Observer監(jiān)聽data中所有屬性的變化
 *          4.負責調(diào)用compiler解析指令/差值表達式
 *      結構:
 *          +$options :記錄所有參數(shù)配置
 *          +$el :記錄綁定的DOM Element
 *          +$data :記錄響應式數(shù)據(jù)
 *          ---------------------
 *          -_proxyData()  私有成員,把data中的屬性搞乏,轉換成getter/setter注入到Vue實例中
 * */
class Vue {
    constructor(options) {
        // 1.通過屬性保存選項的數(shù)據(jù)
        this.$options = options || Object.create(null);
        // data 必須是一個函數(shù)波桩,為了防止與內(nèi)部變量沖突
        if(typeof options.data !== 'function'){
            throw ('data must be a function')
            return
        }
        this.$data = options.data() || Object.create(null);
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el):options.el;
        // 2.把data中的成員轉換成getter/setter注入到Vue實例中
        this._proxyData(this.$data);
        // 3.調(diào)用Observer對象,監(jiān)聽數(shù)據(jù)的變化
        new Observer(this.$data)
        // 4.調(diào)用Compiler對象请敦,解析指令和差值表達式
        new Compiler(this)
    }
    // 私有成員镐躲,把data中的屬性,轉換成getter/setter注入到Vue實例中
    _proxyData(data){
        // 1.遍歷data中的所有屬性侍筛,
        if(!data || typeof data !== 'object') return;
        Object.keys(data).forEach(key =>{
            Object.defineProperty(this,key,{
                configurable:true,
                enumerable:true,
                get() {
                    return data[key]
                },
                set(nv) {
                    if(data[key] == nv) return;
                    data[key] = nv;
                }
            })
        })
    }
}


/**
 *  2.Observer 核心
 *      數(shù)據(jù)響應式處理
 *      功能:
 *          1.負責編譯模板萤皂,解析指令/差值表達式,
 *          2.負責頁面的首次渲染
 *          3.當數(shù)據(jù)變化后更新視圖
 *      結構:
 *          +go(data)
 *              負責遍歷對象屬性匣椰,對象攔截裆熙,只針對對象數(shù)據(jù)進行響應式處理,
 *          +proxyData(data)
 *              數(shù)據(jù)代理
 *              負責通過Object.defineProperty進行對象劫持,通過遞歸進行深度對象監(jiān)聽禽笑,
 *              針對新賦值屬性值入录,如果是對象,同樣進行數(shù)據(jù)攔截
 * */

//監(jiān)聽data
class Observer {
    constructor(data) {
        this.go(data)
    }
    go(data){
        if(typeof data !== 'object'){
            return
        }
        Object.keys(data).forEach(key =>{
            this.proxyData(data,key,data[key])
        })
    }
    proxyData(data,key,value){
        this.go(value);
        let that = this;
        //收集依賴佳镜,發(fā)送通知
        let dep = new Dep();
        Object.defineProperty(data,key,{
            configurable:true,
            enumerable:true,
            get() {
                // console.log('getter -> ' + value)
                Dep.target && dep.addSub(Dep.target)
                return value;
            },
            set(nv) {
                if(value == nv) return;
                console.log('數(shù)據(jù)變化'+value+'-->'+nv+'僚稿,發(fā)送通知')
                value = nv;
                that.go(nv);
                dep.notify(key,nv)
                //    數(shù)據(jù)變化,發(fā)送通知
            }
        })
    }
}


/**
 *  3.Compiler 核心
 *      編譯邀杏、更新視圖
 *      功能:
 *          1.負責編譯模板贫奠,解析指令和差值表達式
 *          2.負責頁面的首次加載
 *          3.當數(shù)據(jù)變化時唬血,更新視圖
 *
 *      結構:
 *          +el
 *              Vue構造函數(shù)的options.el ,DOM對象
 *          +vm
 *              Vue實例
 *          ----------------------------------------------
 *          +compile(el)
 *              用于遍歷DOM對象所有節(jié)點唤崭,如果是文本節(jié)點拷恨,解析差值表達式。如果是元素節(jié)點谢肾,解析指令腕侄。
 *          +compileText(node)
 *              解析差值表達式
 *          +compileElement(node)
 *              解析元素指令
 *          +isDirective(node)
 *              判斷是否是指令
 *          +isTextNode(node)
 *              判斷是否是文本節(jié)點
 *          +isElementNode(node)
 *              判斷是否是元素節(jié)點
 *          +update(node,key,attrName)
 *              更新視圖,執(zhí)行指令芦疏,根據(jù) attrName+Update 執(zhí)行對應方法
 *          +textUpdate(node,key,attrName)
 *              更新文本冕杠,執(zhí)行指令v-text
 *          +modelUpdate(node,key,attrName)
 *              更新表單value,執(zhí)行指令v-model
 *
 *      nodeType:12種節(jié)點類型
 *      1   Element 代表元素
 *      2   Attr    代表屬性
 *      3   Text    代表元素或屬性中的文本內(nèi)容。
 *      4   CDATASection    代表文檔中的 CDATA 部分(不會由解析器解析的文本)酸茴。
 *      5   EntityReference 代表實體引用分预。
 *      6   Entity  代表實體。
 *      7   ProcessingInstruction   代表處理指令薪捍。
 *      8   Comment 代表注釋笼痹。
 *      9   Document    代表整個文檔(DOM 樹的根節(jié)點)。
 *      10  DocumentType    向為文檔定義的實體提供接口
 *      11  DocumentFragment    代表輕量級的 Document 對象酪穿,能夠容納文檔的某個部分
 *      12  Notation    代表 DTD 中聲明的符號凳干。
 * */
class Compiler {
    constructor(vm) {
        this.vm = vm;
        this.el = vm.$el;
        this.compile(this.el)
    }
    //編譯模板,處理文本節(jié)點和元素節(jié)點
    compile(el){
        let childNodes = el.childNodes; // 所有節(jié)點被济,屬于偽數(shù)組需要通過Array.from()轉換成真實數(shù)組
        Array.from(childNodes).forEach(node =>{
            if(this.isTextNode(node)){
                // 處理文本節(jié)點
                this.compileText(node)
            } else if(this.isElementNode(node)){
                // 處理元素節(jié)點
                this.compileElement(node)
            }
            // 判斷node節(jié)點救赐,是否有子節(jié)點,如果有只磷,遞歸深度遍歷
            if(node.childNodes && node.childNodes.length) {
                this.compile(node)
            }
        })
    }
    //編譯元素節(jié)點经磅,處理指令
    compileElement(node){
        // v-text v-html
        // 1.遍歷所有的屬性節(jié)點
        // 2.判斷是否是指令
        Array.from(node.attributes).forEach(attr=>{
            let attrName = attr.name;
            if(this.isDirective(attrName)){
                attrName = attrName.substr(2);
                let key = attr.value;
                // 如果當前元素含有指令,則需要首次渲染指令對應的內(nèi)容
                this.update(node,key,attrName)
            }
        })
    }
    update(node,key,attrName){
        let updateFn = this[attrName+'Update'];
        updateFn && updateFn.call(this,node,this.vm[key],key);
    }
    // 處理v-for 指令
    forUpdate(node,value,key){
        let reg = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
        let list = this.vm[key.match(reg)[2]];
        list.forEach(item =>{
            // console.log(item)
        })
        // console.log(list)
    }
    // 處理v-text 指令
    textUpdate(node,value,key){
        node.textContent = value;
        new Watcher(this.vm,key,(k,nv) =>{
            console.log('創(chuàng)建Watcher 喳瓣,當數(shù)據(jù)改變更新視圖' + nv)
            node.textContent = nv;
        })
    }

    //編譯文本節(jié)點馋贤,處理差值表達式
    compileText(node){
        // {{name}}
        // .  匹配除換行符 \n 之外的任何單字符。要匹配 . 畏陕,請使用 \.
        // \  將下一個字符標記為或特殊字符、或原義字符仿滔、或向后引用惠毁、或八進制轉義符。例如崎页, 'n' 匹配字符 'n'鞠绰。'\n' 匹配換行符。序列 '\\' 匹配 "\"飒焦,而 '\(' 則匹配 "("蜈膨。
        // ?  匹配前面的子表達式零次或一次屿笼,或指明一個非貪婪限定符。要匹配 ? 字符翁巍,請使用 \?驴一。
        // +  匹配前面的子表達式一次或多次。要匹配 + 字符灶壶,請使用 \+肝断。
        let reg = /\{\{(.+?)\}\}/; // 匹配單個的{{key1}}
        let value = node.textContent;
        if(reg.test(value)){
            let key = RegExp.$1.trim();
            node.textContent = value.replace(reg,this.vm[key]);
            new Watcher(this.vm,key,(k,nv) =>{
                console.log('創(chuàng)建Watcher ,當數(shù)據(jù)改變更新視圖' + nv)
                node.textContent = this.vm[key];
            })
        }
    }
    // 處理v-model 指令
    modelUpdate(node,value,key){
        node.value = value;
        new Watcher(this.vm,key,(k,nv) =>{
            console.log('創(chuàng)建Watcher 驰凛,當數(shù)據(jù)改變更新視圖' + nv)
            node.value = nv;
        })
        //設置雙向綁定事件
        node.addEventListener('input',e => this.vm[key] = node.value)
    }
    // 判斷元素是否是指令
    isDirective(attrName){
        //判斷屬性是否是v-開頭
        return attrName.startsWith('v-');
    }
    //判斷是否是文本節(jié)點
    isTextNode(node){
        return node.nodeType === 3;
    }
    //判斷是否是元素節(jié)點
    isElementNode(node){
        return node.nodeType === 1;
    }
}


/**
 *  4.Dep 核心 dependence
 *      目標(發(fā)布者)
 *      功能:
 *          1.收集依賴胸懈,添加觀察者
 *          2.通知所有觀察者
 *
 *      結構:
 *          +subs 數(shù)組:存儲所有的觀察者
 *          ---------------------------------
 *          +addSub():添加觀察者
 *          +notify():當事件發(fā)生時,調(diào)用所有的觀察者的update()方法
 * */

class Dep {
    constructor() {
        // 記錄所有的(觀察者/訂閱者)
        this.subs = new Array(0);
    }
    addSub(sub){
        // 每一個觀察者都必須包含一個update方法
        if(sub && sub.update) this.subs.push(sub);
    }
    notify(key,nv){
        this.subs.forEach(sub =>sub.update(key,nv))
    }
}



/**
 *  4.Watcher 核心
 *      觀察者 ->update():當事件發(fā)生時恰响,具體要做的事情
 *      功能:
 *          1.當數(shù)據(jù)變化觸發(fā)依賴趣钱,dep通知所有的Watcher實例更新視圖
 *          2.自身實例化的時候往dep對象中添加自己
 *
 *      結構:
 *          +vm Vue 實例
 *          +key data中的屬性名稱
 *          +cb 回調(diào)函數(shù) 負責更新視圖
 *          +oldValue 記錄數(shù)據(jù)變化之前的值
 *          ------------------------------------
 *          +update() 當數(shù)據(jù)發(fā)生變化的時候,更新視圖
 * */

// 訂閱者-觀察者
class Watcher {
    constructor(vm,key,cb) {
        this.vm = vm;
        this.key = key;
        this.cb = cb;
        // 把Watcher對象記錄到Dep類的靜態(tài)屬性target
        // 觸發(fā)get方法胚宦,在get中會調(diào)用addSub
        Dep.target = this;
        // 當獲取vm[key]的時候會執(zhí)行getter
        this.oldValue = vm[key];
        // 當Watcher 添加到subs之后羔挡,我們要對Dep進行靜態(tài)屬性的重置
        Dep.target = null;
    }
    update(key,nv){
        if(nv == this.oldValue) return;
        this.cb(key,nv)
        this.oldValue = nv;
    }
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市间唉,隨后出現(xiàn)的幾起案子绞灼,更是在濱河造成了極大的恐慌,老刑警劉巖呈野,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件低矮,死亡現(xiàn)場離奇詭異,居然都是意外死亡被冒,警方通過查閱死者的電腦和手機军掂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來昨悼,“玉大人蝗锥,你說我怎么就攤上這事÷蚀ィ” “怎么了终议?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長葱蝗。 經(jīng)常有香客問我穴张,道長,這世上最難降的妖魔是什么两曼? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任皂甘,我火速辦了婚禮,結果婚禮上悼凑,老公的妹妹穿的比我還像新娘偿枕。我一直安慰自己璧瞬,他們只是感情好,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布渐夸。 她就那樣靜靜地躺著嗤锉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪捺萌。 梳的紋絲不亂的頭發(fā)上档冬,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音桃纯,去河邊找鬼酷誓。 笑死,一個胖子當著我的面吹牛态坦,可吹牛的內(nèi)容都是我干的盐数。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼伞梯,長吁一口氣:“原來是場噩夢啊……” “哼玫氢!你這毒婦竟也來了?” 一聲冷哼從身側響起谜诫,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤漾峡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后喻旷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體生逸,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年且预,在試婚紗的時候發(fā)現(xiàn)自己被綠了槽袄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡锋谐,死狀恐怖遍尺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涮拗,我是刑警寧澤乾戏,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站多搀,受9級特大地震影響歧蕉,放射性物質發(fā)生泄漏。R本人自食惡果不足惜康铭,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赌髓。 院中可真熱鬧从藤,春花似錦催跪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至悯搔,卻和暖如春骑丸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妒貌。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工通危, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人灌曙。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓菊碟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親在刺。 傳聞我的和親對象是個殘疾皇子逆害,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344