Vue之MVVM原理,带你深入了解它

714 阅读3分钟

作为一位前端开发者,不管是面试还是在开发过程中,总能耳熟能详的听到MVVM 

作为一个前端面试官,我通常都会对前端面试者说:谈谈你对MVVM的理解?  

那么,到底什么是MVVM呢?它是怎么实现的呢? 

 MVVM是什么 

  1.  M - 数据模型(Model),简单的JS对象 
  2. VM - 视图模型(ViewModel),连接Model与View  
  3. V - 视图层(View),呈现给用户的DOM渲染界面  


我们可以看出最核心的就是ViewModel,它主要的作用:对View中DOM元素的监听和对Model中的数据进行绑定,当View变化会引起Modal中数据的改动,Model中数据的改动会触发View视图重新渲染,从而达到数据双向绑定的效果,这也是Vue最为核心的特性。其中view改变modal,可以通过绑定事件更新数据,那model更新view,是怎么去更新呢?

 话不多说,我们直接上例子




这是vue内部实现的

那么如何自己实现一个MVVM呢

源码git地址

github.com/horry99/vue…

实现自己的MVVM

要实现mvvm,我们应该实现以下几点:
  1. 实现一个模板编译-Compile,对每个编译元素和文本节点进行扫描编译,以及绑定相应的更新函数
  2. 实现一个数据劫持-Observer,将对象的每个属性进行监听,如有变动可拿到最新值并通知订阅者
  3. 实现一个-Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

如图:



模板编译-Compile

compile是解析模板指令和文本,将模板中的变量替换成数据,然后初始化渲染整页面视图,并绑定相关的更新
函数,添加监听数据的订阅者,一旦数据变动,收到通知,更新视图

首先compile做了哪几件事情呢?

  • 将当前根节点的所有节点放入到内存中
  • 编译-提取文本节点和元素节点
  • 将编译好的节点放回到真实DOM里

// 模板编译
class Compile {
    constructor(el, vm) {
         /**
        * @param {*} el 元素 注意:el选项中有可能是‘#app’字符串也有可能是document.getElementById('#app')
        * @param {*} vm 实例
        */
        // 判断是不是元素
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        this.vm = vm

        if (this.el) {
            // 1.将当前根节点的所有节点放入到内存中
            let fragment = this.node2Fragment(this.el)
            // 2.编译-提取文本节点和元素节点
            this.compile(fragment)
            // 3.将编译好的节点放回到真实DOM里
            this.el.appendChild(fragment)
        }
    }

    // 判断编译文本的正则
    isTextReg() {
        return /\{\{([^}]+)\}\}/g
    }
    // 判断是不是元素
    isElementNode(node) {
        return node.nodeType === 1
    }
    // 判断是不是指令
    isDirective(name) {
        return name.includes('v-')
    }
    compileElement(node) {
        // 获取元素节点的所有属性
        let attrs = node.attributes
        Array.from(attrs).forEach(attr => {
            let attrName = attr.name // 属性名
            let expr = attr.value
            // 判断是否存在v-指令
            if (this.isDirective(attrName)) {
                // todo...
                let [, type] = attrName.split('-')
                // node  message.title  this.vm.$data.message.title
                compileUtil[type](node, expr, this.vm)
            }
        })
    }
    compileText(node) {
        // 获取文本节点,并将大括号中的内容替换成数据
        let expr = node.textContent
        if (this.isTextReg().test(expr)) {
            // todo...
            compileUtil['text'](node, expr, this.vm)
        }
    }
    compile(fragment) {
        // 从文档碎片中获取所有的节点[注意:这里的childNodes只是所有的第一层节点]
        let childNodes = fragment.childNodes
        Array.from(childNodes).forEach(node => {
            // 判断当前节点是文本还是元素
            if (this.isElementNode(node)) {
                // 元素节点,还需要深入编译
                this.compileElement(node)
                this.compile(node)
            } else {
                // 文本节点
                this.compileText(node)
            }
        })
    }
    node2Fragment(el) {
        // 创建一个文档碎片,将所有的孩子都写入到该碎片中
        let fragment = document.createDocumentFragment()
        let firstChild
        // dom节点一个一个的移入内存中
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild)
        }
        // 内存中的节点,页面上的节点都移除了
        return fragment
    }

}

compileUtil = {
    // 获取值
    getVal(expr, vm) {
        // message.title
        expr = expr.split('.')
        return expr.reduce((prev, next) => {
            return prev[next]
        }, vm.$data)
    },
    setVal(expr, value) {
        expr = expr.split('.')
        return expr.reduce((prev, next, currentIndex) => {
            if (currentIndex == expr.length - 1) {
                return prev[next] = value
            }
            return prev[next]
        }, vm.$data)
    },
    getTextVal(expr, vm) {
        return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            return this.getVal(arguments[1], vm)
        })
    },
    text(node, expr, vm) {
        const updateFn = this.updater['textUpdater']
        expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            new Watcher(arguments[1], vm, (newValue) => {
                // 当数据更新了,文本节点需要重新获取依赖的属性更新文本中的内容(A的值变了,需要重新获取A的值再加上B的值,重新渲染)
                updateFn && updateFn(node, this.getTextVal(expr, vm))
            })
        })
        updateFn && updateFn(node, this.getTextVal(expr, vm))
    },
    /**
     * 处理v-model
     * @param {*} node 对应的节点
     * @param {*} expr 表达式
     * @param {*} vm 当前实例
     */
    model(node, expr, vm) {
        // 不同的指令调取不同的方法
        const updateFn = this.updater['modelUpdater']
        // 数据更新,会调用watcher的update方法,重新编译元素
        new Watcher(expr, vm, (newValue) => {
            updateFn && updateFn(node, this.getVal(expr, vm))
        })
        node.addEventListener('input', (e) => {
            let newValue = e.target.value
            this.setVal(expr, newValue)
        })
        updateFn && updateFn(node, this.getVal(expr, vm))
    },
    updater: {
        // 文本更新
        textUpdater(node, value) {
            node.textContent = value
        },
        // 元素更新
        modelUpdater(node, value) {
            node.value = value
        }
    }
}

数据劫持-Observer

// 数据劫持
class Observer {    
    constructor(data) {
        this.observer(data)
    }
    observer(data) {
      // 判断是否是对象,才监测
      if (data instanceof Object) {
            for (let key in data) {
                this.defineReactive(data, key, data[key])
            }
        }
    }
    defineReactive(obj, key, value) {
        const _this = this
        // 该数组存放所有更新的数据
        let dep = new Dep()
        // 如果value还是对象,还需要观察
        this.observer(value);
        Object.defineProperty(obj, key, {
            get() {
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set(newValue) {
                if (newValue != value) {
                    _this.observer(newValue); // 如果赋值的也是对象的话  还需要观察
                    value = newValue
                    // 通知所有的依赖,数据更新了
                    dep.notify()
                }
            }
        })
    }}

// 依赖收集器
class Dep {
    constructor() {
        this.subs = []
    }
    // 添加依赖
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // 通知依赖更新
    notify() {
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}


发布订阅-Watcher

class Watcher {
    constructor(expr, vm, cb) {
        this.vm = vm
        this.expr = expr
        this.cb = cb // 更新回调
        this.value = this.get() // 保存老数据
    }
    getVal(expr, vm) {
        expr = expr.split('.')
        return expr.reduce((prev, next) => {
            return prev[next]
        }, vm.$data)
    }
    get() {
        // 将当前实例赋值给依赖收集的容器
        Dep.target = this
        // this.expr会进入到getter/setter
        let value = this.getVal(this.expr, this.vm)
        Dep.target = null // 更新完再将依赖收集清空
        return value
    }
    // 更新函数
    update() {
        let newValue = this.get(this.expr, this.vm)
        let oldValue = this.value
        if (newValue != oldValue) {
            this.cb()
        }
    }
}

资源整合-MVVM

MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

class MVVM {
    constructor(options) {
        this.$el = options.el
        this.$data = options.data
        // 有根节点,再编译
        if (this.$el) {
            new Observer(this.$data)
            // 将this.$data进行代理
            this.proxyData(this.$data)
            new Compile(this.$el, this)
        }
    }
    proxyData(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                get() {
                    return data[key]
                },
                set(newValue) {
                    return data[key] = newValue
                }
            })
        })
    }
}

MVVM中有proxyData这样一个函数,为什么呢?

【注意】: 有这样一个问题,在开发中是通过实例+属性(vm.title)来获取数据,而我们自己写的MVVM是通过实例+$data+属性(vm.$data.title)来获取数据,所有为了方便,我们需要将vm.$data代理到vm上面

这样的话,我们实现了MVVM整个的一个实现过程

问题

  • Object.defineProperty() 有那些缺点?
  • 实现数组的一个监听?
  • Vue3 中是如何用 Proxy 实现的? 

这些问题,有时间的时候会慢慢补充的,大家可以持续关注噢~~~