天天看點

Vue的MVVM響應式原理(雙向資料綁定)源碼解析(附面試題)MVVM源碼面試題

MVVM

MVVM是Model-View-ViewModel的簡寫,将視圖 UI 和業務邏輯分開

ViewMode:也就是mvc 的控制器,取出 Model 的資料同時幫忙處理 View 中由于需要展示内容而涉及的業務邏輯

源碼

MVue.js

import {Observer, Watcher} from './Observer'

//編譯類
class Compile{

    constructor(el,vm){

        //判斷是否為節點
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        this.vm = vm

        //1.擷取文檔碎片對象 ,放入記憶體中減少頁面的回流與重繪
        const fragment =  this.node2Fragment(this.el)
        
        //2.編譯模闆
        this.compile(fragment)

        //3.追加子元素到根元素
        this.el.appendChild(fragment)
    }

    //編譯
    compile(fragment){
        //1.擷取子節點
        const childNodes = fragment.childNodes;
        [...childNodes].forEach( child => {
            // console.log(item)
            if(this.isElementNode(child)){
                //元素節點
                //編譯元素節點
                // console.log('元素節點'+child.tagName)
                this.compileElement(child)
            }else{
                //文本節點
                //編譯文本節點
                // console.log('文本節點'+child)
                this.compileText(child)
            }

            if(child.childNodes && child.childNodes.length){
                this.compile(child)
            }
        })
    }

    //編譯元素節點
    compileElement(node){
        //取得屬性
        const attrs = node.attributes
        //通過轉換成數組來周遊屬性
        let arr = [...attrs]
        arr.forEach(attr => {
            const {name, value} = attr
            //進行v-開頭判定 true就是指令
            if(this.isDirective(name)){
                //v-text->text
                const [,dirctive] = name.split('-')
                //對on:click繼續->click  而如果為text 就放在第一個參數
                //v-bind:src
                const [dirName,eventName] = dirctive.split(':')
                //對不同類型的屬性進行指派到節點上  更新資料  資料驅動視圖
                compileUtil[dirName](node,value,this.vm,eventName)
                //删除有指令的标簽的屬性
                node.removeAttribute('v-'+ dirctive)
                
            }
            //@click='handle'
            else if(this.isEventName(name)){
                let [,eventName] = name.split('@')
                compileUtil['on'](node, value, this.vm, eventName)
            }
            //:class='red'
            else if(this.isEllipsisName(name)){
                let [, propsName] = name.split(':')
                compileUtil['bind'](node, value, this.vm, propsName)
            }
        });
    }

    //編譯文本節點
    compileText(node){
        //獲得文本
        const content = node.textContent
        //通過正則隻取{{}}的文本
        const expr = /\{\{(.+?)\}\}/
        if(expr.test(content)){
            compileUtil['text'](node,content,this.vm)
        }
    }

    //@開頭
    isEventName(attrName){
        return attrName.startsWith('@')
    }

    //:開頭
    isEllipsisName(attrName){
        return attrName.startsWith(':')
    }

    //判斷是否是以v-開頭
    isDirective(attrName){
        return attrName.startsWith('v-')
    }

    //建立文檔碎片對象
    node2Fragment(el){
        const fragment = document.createDocumentFragment()
        let firstChild;
        while(firstChild = el.firstChild){
            fragment.appendChild(firstChild)
        }
        return fragment
    }

    //判斷是不是節點對象
    isElementNode(node){
        return node.nodeType === 1 ; 
    }
}

//編譯工具類
export const compileUtil = {
    //node 節點  expr 屬性指向的資料名  vm vue執行個體對象用于去的data資料  event  事件屬性
    //解決 person.name類型
    getVal(expr,vm){
        //[person, name] data 之前的值  curr 目前值
        //執行過程  先将curr取到 person 将person對象付給data  再将curr取到name  就獲得person.name
        return expr.split('.').reduce((data,currentVal) => {
            // console.log(currentVal)
            return data[currentVal]
        },vm.$options.data)
    },
    setVal(expr,vm,inputVal){
        return expr.split('.').reduce((data,currentVal) => {
            // console.log(currentVal)
            data[currentVal] = inputVal
        },vm.$options.data)
    },
    getContentVal(expr,vm){
        let value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {                
            return this.getVal(args[1],vm)
        })
        return value
    },
    //處理文本
    text(node, expr, vm){
        let value
        //處理{{}}文本
        if(expr.indexOf('{{')!== -1){
            //取得{{}}裡的内容 并調getval取得data的資料
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {                
                new Watcher(vm, args[1], () => {
                    this.updater.textUpdater(node,this.getContentVal(expr,vm))
                })
                return this.getVal(args[1],vm)
            })
        }
        //處理v-text
        else{
            value = this.getVal(expr, vm)
        }
        //資料渲染到視圖
        this.updater.textUpdater(node,value)
    },
    html(node, expr, vm){
        let value = this.getVal(expr,vm)
        //綁定watcher 對資料監聽 更改資料時,綁定對應更新的函數
        new Watcher(vm, expr, (newVal) => {
            this.updater.htmlUpdater(node,newVal)
        })
        this.updater.htmlUpdater(node,value)
    },
    model(node, expr, vm){
        const value = this.getVal(expr,vm)
        //資料=》視圖
        new Watcher(vm, expr, (newVal) => {
            this.updater.modelUpdater(node,newVal)
        })
        //視圖=》資料
        node.addEventListener('input', (e) => {
            this.setVal(expr, vm, e.target.value)
        })
        this.updater.modelUpdater(node,value)
    },
    //event 響應事件函數
    on(node, expr, vm, event){
        let fn = vm.$options.methods && vm.$options.methods[expr]
        node.addEventListener(event,fn.bind(vm),false)
    },
    //props 屬性src等
    bind(node, expr, vm, props){
        const value = this.getVal(expr,vm)
        this.updater.bindUpdater(node,props,value)
    },
    //更新函數類
    updater:{
        textUpdater(node,value){
            node.textContent = value
        },
        htmlUpdater(node,value){
            node.innerHTML = value
        },
        modelUpdater(node,value){
            node.value = value
        },
        bindUpdater(node,props,value){
            node.setAttribute(props,value)
        }
    }
}

//Vue類
export class MVue{
     constructor(options){
         this.$el = options.el
         this.$data = options.$data
         this.$options = options
         if(this.$el){
             //1.實作資料觀察者
             new Observer(this.$options.data)
             //2.實作指令解析器
             new Compile(this.$el,this)
             this.proxyData(this.$options.data)
         }
     }

     proxyData(data){
         for(const key in data){
             Object.defineProperty(this, key, {
                get(){
                    return data[key]
                 },
                 set(newVal){
                     data[key] = newVal
                 }
             })
         }
     }
 }

// export default MVue
           

Observer.js

import {compileUtil} from './MVue'

//資料觀察者類
export class Observer{
    constructor(data){
        this.observer(data)
    }

    observer(data){
        if(data && typeof data === 'object'){
            //周遊data中的屬性
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key])
            })
        }
    }

    defineReactive(obj, key, value){
        //遞歸下一層
        this.observer(value)
        const dep = new Dep()
        //劫持資料  對所有屬性進行劫持
        Object.defineProperty(obj,key,{
            //可周遊
            enumerable: true,
            //可更改
            configurable:false,
            get(){
                //訂閱資料發生變化時,往dep中添加觀察者
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            //當我修改資料時,會進到set  然後進行監聽 然後進行值的更新
            set: (newValue) => {
                //對于新值進行監聽
                this.observer(newValue)
                if(newValue !== value){
                    value = newValue
                }
                //告訴dep變化
                //進行通知
                dep.notify()
            },
        })
    }
        
}

//資料依賴器
class Dep{
    constructor(){
        this.subs = [] 
    }
    //收集觀察者
    addSub(watcher){
        this.subs.push(watcher)
    }
    //通知觀察者更新
    notify(){
        //拿到觀察者 然後更新
        this.subs.forEach(w => {
            w.update()
        })
    }
}


//觀察資料
export class Watcher{

    constructor(vm,expr,cb){
        this.vm = vm
        this.expr = expr
        this.cb = cb
        //先儲存舊值
        this.oldVla = this.getOldVal() 
    }

    getOldVal(){
        //未初始化  挂載在dep
        Dep.target = this
        let oldVal = compileUtil.getVal(this.expr, this.vm)
        Dep.target = null
        return oldVal
    }

    //在watcher中拿到新值  callback
    update(){
        let newVal = compileUtil.getVal(this.expr, this.vm)
        if(newVal !== this.oldVla){
            this.cb(newVal)
        }
    }
}

// module.exports = {
//     Observer, 
//     Watcher
// }
           

index.js

import './index.less';
import {MVue} from './MVue'

const vm = new MVue({
    el:'#app',
    data:{
        person:{
            name:'xa',
            age:18, 
        },
        msg:'MVVM',
        color:'red'
    },
    methods:{
        handel(){
            this.msg= '學習MVVM'
        }
    }
})
           

index.html

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<title>webpack-template</title>
</head>

<body>
	<div id="app">
		<h2>{{person.name}}</h2>
		<h3 v-html='person.age'></h3>
		<h3 v-html='msg'></h3>
		<div v-text='msg'></div>
		<div v-text='person.name'></div>
		<input type="text" v-model='msg'>
		<button v-on:click='handel'>2222</button>
		<button @click='handel'>@@@</button>
		<ul>
			<li v-bind:class='color'>{{msg}}</li>
			<li :class='color'>{{msg}}</li>
			<li>{{msg}}</li>
		</ul>
	</div>
</body>
</html>
           

上面檔案通過webpack進行打包

面試題

闡述一下你所了解vue的MVVM響應式原理

vue.js 則是采用資料劫持結合釋出者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在資料變動時釋出消息給訂閱者,觸發相應的監聽回調。

MVVM作為資料綁定的入口,整合Observer、Compile和Watcher三者,通過Observer來監聽自己的model資料變化,通過Compile來解析編譯模闆指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁,達到資料變化 -> 視圖更新;視圖互動變化(input) -> 資料model變更的雙向綁定效果

Vue的MVVM響應式原理(雙向資料綁定)源碼解析(附面試題)MVVM源碼面試題

視訊講解

繼續閱讀