手动实现一个简单的MVVM双向数据绑定【未完待续...】

Hokori 3月19日

题记

好久没产出了
最近玩脚手架玩太久了,感觉应该逐级回归下基础
写这篇文章借鉴了两个帖子

现在来讲一讲自己对MVVM的理解

MVVM

MVVM是一种架构模式
全称Model-View-ViewModel
与其同级的概念有MVC、MVP(以后我会再去尝试接触的,不过可能性不高)
从名字可以看出来,MVVM架构模式是由ModelViewViewModel组成的

由图可以看到

  1. ViewModel与View绑定,通过View的响应事件去触发ViewModel
  2. ViewModel得到响应后去对Model进行操作
  3. 然后Model会把更新的数据传给ViewModel
  4. ViewModel把这个更新的数据更新到View上

期间ViewModel是View和Model的桥梁,实现了所谓的数据驱动
也是整个MVVM架构模式的核心

本文具体讨论Vue.js对MVVM的实现

在Vue.js官网可以看到这幅图

深入响应式原理

并且官网是这么解释的

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

这里多说几句
shim的意思跟polyfill是一样的,就是利用ES5规范已有的方法去实现新规范如ES6、ES7的一些方法

看完官网我们大概就能明白原理了

  • 遍历一遍data然后用Object.defineProperty对每个属性添加相应的getter和setter,也就是所谓的Watcher
  • 在Vue实例化时,添加的这些Watcher通过data下属性的getter,把组件渲染时调用的数据属性作为依赖项。
  • 当用户在组件上操作,即“Touch”,通过setter改变了data下的属性
  • setter会去通知Watcher:“我这里数据变啦,你要重新把新数据渲染到组件展现给用户”
  • 然后Watcher就会去更新相应的DOM数据

其思想正是MVVM架构模式的实现

同时因为ES7规范废除了Object.observe(),所以Vue只能对初始化实例时处于data对象上的属性进行监听,
并且对于已经创建的实例,Vue不允许动态添加根级别的响应式属性,
但是可以使用Vue.set(object, propertyName, value)向嵌套对象(也就是已存在于data对象下的属性)添加响应式属性,
同样效果的有Vue实例上的vm.$set(object, propertyName, value)

接下来给出代码的实现与思路

1、首先是构造函数

//构造函数
function vm(options = {}) {

    //*** 1.进行数据代理
    //把options代理到this.$options上
    this.$options = options;

    //*** 1.进行数据代理
        //把options.data代理到this.$data上
    let data = this.$data = options.data;

    //*** 2.调用观察者函数
    this.observe(data)
}

2、然后在对象原型上添加方法

//观察者函数
vm.prototype.observe = function(data) {
    
    /*
    @params data [object] 观察的对象
    */

    //*** 1.仅对引用类型进行观察
    if (!data || typeof data !== 'object') {
        return;
    }
    
    //*** 2.遍历对象修改get、set方法
    for (let key in data) {

        //*** 3.用一个变量来储存值,防止调用死循环
        let val = data[key]
        
        //*** 4.递归,实现深度的数据观察
        arguments.callee(data[key]);

        //*** 5.观察
        Object.defineProperty(data, key, {
            get() {
                //不能用return data[key]; 否则会形成调用死循环
                return val;
            },
            set(newValue) {
                //通过作用域链向上找到val变量,并与newValue进行比较
                if (val === newValue) { //若新值与旧值相同,则不做响应
                    return;
                }
                val = newValue;
                arguments.callee(newValue); //新值重新观察
            }
        })
    }
}

通过递归调用观察者函数
可以看到不管是data下的一级对象,还是更深层的对象属性,都设定了setter和getter
这里我一开始想在setter里直接调用data[key],然而造成了死循环,所以这里的val变量作为一个中间量是必须的

实现了getter和setter的注入

我们还想到Vue中其实是把vm.$data以下的所有不以"$"和"_"开头的属性代理到了vm下(为什么是不以"$"和"_"开头呢,因为Vue实例的方法和属性都是由这两个开头的,是为了防止开发者修改导致错误)

3、我们可以在观察者函数内加上代理的步骤

        //*** 6.代理
        if (key.charAt(0) !== '$' && key.charAt(0) !== '_') {
            Object.defineProperty(vm, key, {
                get() {
                    return vm.$data[key];
                },
                set(newValue) {
                    vm.$data[key] = newValue;
                }
            })
        }

但是这样并不完美,如果我有了嵌套的对象,并且属性重名
如:

let obj = {
    text: 'outer',
    innerObj: {
        text: 'inner'
    }
}

这时候会出错

出现错误的原因是这样的写法始终是在vm下的一层进行代理,而不会进行第二层、第三层以上的代理,遇到了同名的自然不给redefine

因为我太菜了,甚至想到了给每个深层对象加上parent属性,反向去找到vm实例并向下代理的蠢办法
蠢到了自己,所以没有继续实现,真正合理的实现方法等我学到了就更新这篇文章吧

未完待续...

icon_biggrin.pngicon_neutral.pngicon_twisted.pngicon_arrow.pngicon_eek.pngicon_smile.pngicon_sad.pngicon_cool.pngicon_evil.pngicon_mrgreen.pngicon_exclaim.pngicon_surprised.pngicon_razz.pngicon_rolleyes.pngicon_wink.pngicon_cry.pngicon_confused.pngicon_lol.pngicon_mad.pngicon_question.pngicon_idea.pngicon_redface.png
expand_less