带你深入了解vue mvvm的实现原理

相信只要你去面试vue,都会被问到vue的双向数据绑定,你要是就说个mvvm就是视图模型模型视图,只要数据改变视图也会同时更新!那你离被pass就不远了!那么读完本文后,相信你就可以说出很多东西了!

vue的mvvm由两部分组成,中间通过watcher来连接

  1. 数据劫持:通过Object.defineProperty来给每个数据属性加上get和set方法,每次取值的时候都会调用get方法,每次赋值的时候都会触发set方法;并且有new Dep(); 在每次取值调用get方法的时候存放watcher,每次赋值的时候依次调用watcher的updata方法。
  2. 模版编译:通过模版的内容取出指令例如 v-model 取出数据例如{{message}},将值替换
  3. Watcher:在编译的时候遇到数据需要编译的就要new Watcher,这个watcher实例身上有该vm, expr, cb,还有value,并且存在一个数组中保存好,以便后面调用,在我们改动数据的时候就会触发上面被劫持数据的set方法,set方法中会看一下改没改值,如果改了 先接着劫持一下数据(万一是对象就必须地递归深度劫持),然后通知刚刚存的所有watcher,这里有个数据更新了快来看看啊,执行watcher中的updata方法,updata方法会执行每个watcher实例的cb(new Watcher的时候存下来的)cb是是啥呢?其实就是更新了节点
// 文本更新
textUpdater(node, value) {
    node.textContent = value
},
// 输入框更新
modelUpdater(node, value) {
    node.value = value;
}
复制代码

MVVM.js

class MVVM{
    constructor(options) {
        this.$el = options.el;
        this.$data = options.data;

        if(this.$el) {
            // 数据劫持 就是把对象的所有属性 改成get和set方法
            new Observer(this.$data);
            // 将对象的属性代理到this身上
            this.proxyData(this.$data);
            new Compile(this.$el,this)
        }
    }
    //代理 将this.$data代理到this身上,这样我们赋值的时候就可以直接写this.xx不需要再写this.$data.xx
    proxyData(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this,key,{
                get(){
                    return data[key]
                },
                set(newValue){
                    data[key] = newValue
                }
            })
        });
    }
}
复制代码

下面放observer.js的代码并说明:

class Observer{
    constructor(data){
        this.observe(data)          //数据加get和set方法 这样我们就可以监听数据变化了
    }
    observe(data) {
        // 要对这个data数据将原有的属性改成set和get的形式
        if(!data || typeof data !== 'object'){
            return;
        }
        // 要将数据 一一劫持 先获取取到data的key和value
        Object.keys(data).forEach(key=>{
            // 劫持
            this.defineReactive(data,key,data[key]);
            this.observe(data[key]);// 深度递归劫持
        });
    }
    defineReactive(data,key,val) {
        let that = this;
        let dep = new Dep(); // 每个变化的数据 都会对应一个数组,这个数组是存放所有更新的操作
        Object.defineProperty(data,key,{
            enumerable:true,
            configurable:true,
            get(){     //取值的时候会触发该方法
                Dep.target && dep.addSub(Dep.target);    //这里的dep其实是一个闭包,每个key都对应独自的dep //后面我们会看到这个Dep.target其实就是watcher实例 watcher实例身上有vm,绑定的数据,和回调(更新节点数据)
                return val
            },
            set(newVal){    //设置值的时候会触发
                if(val != newVal) {
                    that.observe(newVal)  //如果是对象继续劫持
                    val = newVal;
                    dep.notify(); // 通知所有相关人 数据更新了
                }
            }
        })
    }
}
//类似发布订阅
class Dep{
    constructor(){
        this.subs = []
    }
    addSub(watcher){
        this.subs.push(watcher)
    }
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
}
复制代码

下面放watcher.js并说明:

class Watcher{
    constructor(vm, expr, cb){
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        this.value = this.get();
    }
    getVal(vm, expr) { // 获取实例上对应的数据
        expr = expr.split('.'); // [message,a]
        return expr.reduce((prev, next) => { // vm.$data.a
            return prev[next];
        }, vm.$data);
    }
    get(){
        Dep.target = this;
        let value = this.getVal(this.vm,this.expr);//特别要说明的就是这个地方
        //取值就会触发observer.js中的get方法 在这个时候给所有的watcher都给存下来
        Dep.target = null;
        return value;
    }
    // 对外暴露的方法
    update(){
        let newValue = this.getVal(this.vm, this.expr);
        let oldValue = this.value;
        if(newValue != oldValue){
            this.cb(newValue); // 对应watch的callback
        }
    }
}
复制代码

下面放compile.js并说明:

class Compile{
    constructor(el,vm) {
        this.el = this.isElementNode(el) ? el: document.querySelector(el);
        this.vm = vm;

        if(this.el) {
            // 如果这个元素能获取到 我们才开始编译
            // 1.先把这些真实的DOM移入到内存中 fragment
            let fragment = this.node2fragment(this.el);
            // 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
            this.compile(fragment);
            // 3.把编译号的fragment在塞回到页面里去
            this.el.appendChild(fragment);
        }
    }
    // 辅助方法
    // 是不是元素
    isElementNode(node) {
        return node.nodeType === 1;
    }
    // 是不是指令
    isDirective(name) {
        return name.includes('v-');
    }
    // 核心方法
    compile(fragment){
        let childNodes = fragment.childNodes;
        Array.from(childNodes).forEach(node => {
            if (this.isElementNode(node)) {
                // 元素  
                this.compileElement(node);
                this.compile(node)
            } else {
                // 文本
                this.compileText(node);
            }
        })
    }
    compileElement(node){
        // 取出元素的所有属性 看看需不需要编译
        let attrs = node.attributes;
        for(var attr of attrs) {
            let attrName = attr.name;
            if(this.isDirective(attrName)) {
                let expr = attr.value;
                let [,type] = attrName.split('-')
                CompileUtil[type](node, this.vm, expr);
            }

        }
    }
    compileText(node){
        let expr = node.textContent;
        let reg = /\{\{([^}]+)\}\}/g
        if (reg.test(expr)) {
            // node this.vm.$data text
            CompileUtil['text'](node, this.vm, expr);
        }
    }
    node2fragment(el){
        let fragment = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild)
        }

        return fragment;
    }
}

CompileUtil = {
    getVal(vm, expr) { // 获取实例上对应的数据
        expr = expr.split('.'); // [message,a]
        return expr.reduce((prev, next) => { // vm.$data.a
            return prev[next];
        }, vm.$data);
    },
    getTextVal(vm, expr) { // 获取编译文本后的结果
        return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            return this.getVal(vm, arguments[1]);
        })
    },
    // node节点,vm实例 expr(message.a) v-text="message.a"  
    text(node,vm,expr){
        let updateFn = this.updater['textUpdater'];
        let value = this.getTextVal(vm, expr);
        expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            new Watcher(vm, arguments[1],(newValue)=>{
                //将每一个绑定的数据形成一个watcher --> observer的取值操作 -get-> Dep.target && dep.addSub(Dep.target); 
                // 如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
                updateFn && updateFn(node,this.getTextVal(vm,expr));
            });
        })
        updateFn && updateFn(node, value)
    },
    setVal(vm,expr,value){ // [message,a]
        expr = expr.split('.');
        // 收敛
        return expr.reduce((prev,next,currentIndex)=>{
            if(currentIndex === expr.length-1){
                return prev[next] = value;   //设置值了会触发observer中的set方法
            }
            return prev[next];
        },vm.$data);
    },
    model(node,vm,expr) {
        let updateFn = this.updater['modelUpdater'];

        // 这里应该加一个监控 数据变化了 应该调用这个watch的callback 
        new Watcher(vm,expr,(newValue)=>{
            // 当值变化后会调用cb 将新的值传递过来 ()
            updateFn && updateFn(node, this.getVal(vm, expr));
        });
        node.addEventListener('input',(e)=>{
            let newValue = e.target.value;
            this.setVal(vm,expr,newValue)
        })
        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    updater: {
        // 文本更新
        textUpdater(node, value) {
            node.textContent = value
        },
        // 输入框更新
        modelUpdater(node, value) {
            node.value = value;
        }
    }
}
复制代码

至此,一个简单的mvvm就实现了!三个js各自分工明确,互相配合!当然还有很多不足之处,欢迎各位提出宝贵的意见或建议,也希望能帮助到你从中获得一些知识,谢谢大家的关注!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值