Vue 中的 MVVM思想

1.什么是MVVM

model-view-viewModel
model:数据模型
view:视图层
viewModel:可以理解为沟通view和model的桥梁

他的设计思想就是关注model的变化,通过viewModel自动去更新DOM的状态,也就是Vue的一大特点:数据驱动。
MVVM
图片来源:https://blog.csdn.net/CSDN_l512/article/details/90348847

2.Vue中用MVVM思想做什么
  1. 通过数据操作DOM
    我们知道Vue使用简洁的模板语法来将数据渲染进DOM的系统。
    例如: {{…}}来绑定数据,或者,使用v-html指令输出html代码等等。
    首先我们要做到把这些渲染成目标的内容,这里的实现思路在后面的mounted板块有详细讲解。
  2. 监听到数据改变,自动更新DOM
    当我们发现数据变化的时候,我们可以监听到数据的变化,然后再执行第一步,操作DOM节点更新内容。这就涉及到我们常说的发布订阅模式。这一部分内容,在文章后面beforeUpdate板块有讲解。
3.结合Vue生命周期谈MVVM思想在Vue的实现

Vue官网对Vue生命周期的解释:
Vue生命周期

  • beforeCreate
    Vue创建Vue实例对象,用这个对象来处理DOM元素,这时候这个Vue对象就可以访问了。
    使用beforeCreate这个钩子,我们能在对象初始化之前执行一些方法。
el:undefined  // 虚拟DOM的形式存在,还没有被挂载
data:undefined
  • created
    在这个阶段,所有的data和内置方法都初始化完成,但el依旧没有挂载,这时,data可以访问。
    当然内置方法的初始化顺序是props => methods => data => computed => watch
    如果有需要请求的动态数据,可以在这个阶段发起请求。
  el:undefined  // 虚拟DOM的形式存在,还没有被挂载
  data:[object Object] // 已被初始化
  • beforeMouted
    beforeMouted执行
    这一步做了很多事情:
  1. 首先判断是否有el对象,如果没有,停止编译,Vue实例的生命周期走到create结束
  2. 如果有挂载的DOM节点,再查找是否有任何模板(template)被用在了DOM层。
  3. 如果有,则把template放到render函数中(如果是单文件组件,这个模板的编译将提前进行)。
  4. 如果没有template,则将外部HTML作为模板编译(于是,我们发现template优先级大于外部HTML)。
    当然这个过程中,如果我们使用了模板语法,例如{{...}} v-html等,他们还是以虚拟DOM形式存在,并没有被编译
el:[object HTMLDivElement] // 已经挂载,但是模板语言还没有被编译
                           // 例如:<div id="app">{{name}}</div>
data:[object Object] // 已被初始化
  • mounted
    mounted

这一步就是用Compile模块编译模板语言,当然这一步因为内容的替换,会引起大量的回流和重绘,所以这一步,在内存中进行(document.createDocumentFragment())。

对于内容的替换我们大致有这样一个思路:
递归遍历所有的节点,分为文本节点和元素节点。

/*  param 所有的元素节点(this.el)
    把节点放到内存中 */
node2fragment(node){
    // 内存中创建一个文档碎片
    let fragment = document.createDocumentFragment();
    let firstChild;
    while(firstChild = node.firstChild){
        // 每拿到一个元素碎片,都放到内存里
        // 因为节点被放到内存里,可以想成一个类似于出栈的操作
        // 每一次拿到的都是新的节点,直到节点取完
        fragment.appendChild(firstChild);
    }
    return fragment
}

对于文本节点:找到是否含有{{}},如果有的话,获取{{}}内的表达式,获取表达式相应的内容,渲染内容;

对于元素节点:我们要寻找是否有v-相关属性,如果有的话,获取v-后面的指令,同时获得指令的表达式相应的值;

/*  param   内存节点
    编译内存中的DOM节点,用data替换{{}}内容等 */
compile(fragment){
    // 获得第一层的子节点
    let childNodes = fragment.childNodes;
    [...childNodes].forEach( item =>{
        if( this.isElementNode(item) ){
            // 如果是元素,找有没有v-model类似的指令
            this.CompileElement(item);
            // 如果是元素,要递归遍历元素的子节点
            this.compile(item);
        }else{
            // 如果是文本,看有没有{{}}
            this.CompileText(item);
        }
    })
}

举个例子:

<div id="app">
    {{name}}
    <input v-model="name"/>
</div>

<script>
    let vm = new Vue({
        el:"#app",
        data(){
            return{
                name:"Amy"
            }
        }
    })
</script>

我们遍历节点,一个div元素,一个input,四个文本元素:一个{{name}}和三个空的换行
然后我们对他们进行判断,有用的节点是{{name}}和一个input元素,于是我们分别对他们进行处理
对于文本节点{{}},我们希望的是把它替换成文本“Amy”,对于节点input我们希望属性name和value绑定。
编译结束,我们的DOM树就完成渲染到页面。

el:[object HTMLDivElement] // 已经挂载,模板语言编译完成
                           // <div id="app">Amy</div>
data:[object Object] // 已被初始化

补充浏览器的渲染机制:

  1. 解析HTML代码生成DOM树
  2. 解析CSS生成CSSOM
  3. 结合DOM树和CSSOM树生成渲染树(render tree)
  4. 采用深度优先遍历(diff算法)遍历渲染节点

当然,这里css的渲染顺序完全就是编写顺序,如果css编写顺序不规范,这样一步也可能引起大量回流和重绘

  • beforeUpdate、updated

beforeUpdate在监听到数据改变之前执行,虚拟DOM重新渲染,并应用更新,完成改变之后执行updated

这里有个问题就是Vue是怎么监听到数据发生改变的呢?又是如何通知视图层进行更新的呢?

这就是我们MVVM最核心的思想,通过view-Model连接视图层和数据模型,Vue里用到了我们常说的发布订阅模式,这里涉及到几个类。
MVVM

  1. Observer:
    进行数据劫持,实现数据的双向绑定。在这里,他就是一个发布者,发布数据变更的消息。
    我们使用Object.defineProperty()方法给需要observe的数据对象(data)进行递归遍历,包括子属性对象的属性,都绑定gettersetter方法,这样就可以监听到每次读取数据和修改数据的变化。
const data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // 哈哈哈,监听到值变化了 kindeng --> dmq

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // 取出所有属性遍历
    Object.keys(data).forEach(key => {
        defineReactive(data, key, data[key]);
    });
};

function defineReactive(data, key, val) {
    observe(val); // 监听子属性
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再define
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
        }
    });
}
  1. Dep:
    收集Watcher依赖,一个属性有一个Dep,同来通知watcher数据变更。
    构造器里有一个subs数组存放多个watcher,因为一个属性可能在多个节点使用,每个使用这个属性的节点都有一个watcher
    一般两个方法,一个订阅addSub(添加Watcher),一个发布notify(通知Watcher进行更新
// ... 省略
function defineReactive(data, key, val) {
    var dep = new Dep();
    observe(val); // 监听子属性

    Object.defineProperty(data, key, {
        // ... 省略
        set: function(newVal) {
            if (val === newVal) return;
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
            dep.notify(); // 通知所有订阅者
        }
    });
}

function Dep() {
    this.subs = [];		// 存放多个watcher
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
  1. Watcher:
    订阅者。编译器Compiler为每一个编译过的元素节点和文本节点添加watcher,一旦数据更新,触发watcher回调,通知视图层进行变更。var dep = new Dep();是在 defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在 getter里面动手脚:
// Observer.js
// ...省略
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addSub(Dep.target);
        return val;
    }
    // ... 省略
});

// Watcher.js
Watcher.prototype = {
    get: function(key) {
        Dep.target = this;
        this.value = data[key];    // 这里会触发属性的getter,从而添加订阅者
        Dep.target = null;
    }
}

Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:

  1. 在自身实例化时往属性订阅器(dep)里面添加自己
  2. 自身必须有一个update()方法
  3. 待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
// Watcher.js
function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run();    // 属性值变化收到通知
    },
    run: function() {
        var value = this.get(); // 取到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
        }
    },
    get: function() {
        Dep.target = this;    // 将当前订阅者指向自己
        var value = this.vm[exp];    // 触发getter,添加自己到属性订阅器中
        Dep.target = null;    // 添加完毕,重置
        return value;
    }
};
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // 调用订阅者的update方法,通知变化
        });
    }
};
  • beforeDestory
    Vue被破坏并从内存释放之前,这个时候所有的方法和实例都可以访问。
    比如,我们一般在这个阶段清空计时器。
  • destroyed
    Vue实例内存被释放,这时所有的子组件、实践监听器、watcher都被清除。
4.Vue2.0和Vue3.0的数据劫持

Vue2.0用Object.definePeoperty来劫持数据;而Vue3.0采用Proxy代理数据。
其中最大的区别是Object.definePeoperty只能代理某一个对象的某个属性,但Proxy可以直接代理对象和数组。

参考文章:
https://juejin.im/post/5e492663f265da5709701728
https://segmentfault.com/a/1190000006599500

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值