MVVM介绍---转自知乎(写的很好,留作备份)


作者:宗宇

链接:https://www.zhihu.com/question/42865607/answer/94985895

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


一、什么是mvvm

MVVM框架的核心不是双向绑定,也不是依赖收集或者脏检测,其核心思想可以表示为:

View = F(...States) 即视图是状态的函数,如果我们把所有的状态都记录到同一个对象里面(比如叫Model),然后给这个F函数起个名字叫ViewModel,那么公式就变成了View = ViewModel(Model),就是所谓的MVVM了。

通俗来讲,只要我们有一个js的对象Model,修改它页面就跟着改(Model -> View的单向绑定),并且满足Model相同页面就相同,这个框架就是一个MVVM的框架了,所以说这一年来一直很流行的react也是一个MVVM框架。

>

或者我们用代码来表达一下:

var tpl = '<p>这是一个任何模板引擎都可以用的模板:姓名:${name}</p>';
var model = {
 
        name: '张三'
 
    };
 
    var container = document.getElementById('container');
 
    model = new ViewModel(container, model, tpl);
 
    function ViewModel(elem, model, tpl) {
 
        var engine = new Engine(tpl);
 
        var vm = observer(model).on('change', function () {
 
            upDate(elem, engine.render(model));
 
        });
 
        return vm;
 
    }


大家可以仔细看看不同MVVM框架的API定义,是不是都需要给出容器(container)、模型数据(model )和模板(tpl)三个核心元素呢。它们之间的不同之处在于两点:

1、update不同:

最早的一些框架中,大家使用的是字符串模板,好处在于模板书写起来简单,缺点嘛很明显,字符串是不能进行对比的,因此只能全量更新elem元素了,性能差而且会有明显的抖动。

后来MVVM框架倾向于使用基于DOM的模板,这种模板的好处在于可以准确的定位到树上的任何一个元素,因此如果你能准确的知道Model中的哪个数据变化了,就能准确的知道我要修改哪个元素,可以以最小代价更新elem元素。问题也比较明显,我们需要把原来写在字符串模板中的各种指令,写到真实的DOM元素上去,于是种种奇葩的写法如:注释、自定义属性、插值、自定义元素都出来了,学习一个MVVM完全就是学习一门语言

React使用的是基于AST的模板引擎,算是一种兼顾前两者优点的解决方案,所以才会很留下,然而这也不是没有问题的,手写AST是相当反人类的,如果不手写你就要引入一个超过15000行的字符串模板 -> AST模板的翻译软件,这比DOM模板使用元素innerhtml就能翻译可大多了,好在我们可以用nodejs翻译了在网页中跑。

2、observer不同:


我们都知道MVVM的核心是当Model发生改变时,修改View,刚才我们分析了如何修改View,另外一个不同之处在于如果监控Model的变化。

最简单的办法是使用函数,比如我们可以写下边这样的代码:

function observer(model) {
        var obj = {};
 
        obj.on = function (type, fn) {
 
            obj[type] = fn;
 
            return model;
 
        };
 
 
        obj.emit = function (type) {
 
            obj[type]();
 
        };
 
        model.$set = function (value) {
 
            merge(model, value);
 
            obj.emit('change');
 
        }
 
        return obj;
< 
    }
 
    model.$set({
 
        name: '李四'
 
    });

不要觉得意外,最早的MVVM框架就是这么实现的,可能现在已经没有人这么用了吧。


好的框架总是懒人发明的,当某个人写$set写到烦躁的时候,他会憧憬下边的写法:

model.name = '李四';

这样看起来又清新,又自然还舒服。但是这有一个问题,在JS中简单的变量赋值是不会引发任何事件的,不过解决起来并不困难,比如这样:

function observer(model){
 
        var obj = {};
 
        obj.on = function (type, fn) {
 
            obj[type] = fn;
 
            return proxy;
 
        };
 
        obj.emit = function (type) {
 
            obj[type]();
 
        };
 
 
        var proxy = new Proxy(model, {
 
            get: function (target, key, receiver) {
 
                return Reflect.get(target, key, receiver);
 
            },
 
            set: function (target, key, value, receiver) {
 
                Reflect.set(target, key, value, receiver);
 
                obj.emit('change');
 
            }
 
        });
 
 
        return obj;
 
    }

以上方法好像没什么浏览器支持,但这确实是未来可以用的方法,类似的还有人想到了下边的办法:

function observer(model){
 
        var obj = {};
 
        obj.on = function (type, fn) {
 
            obj[type] = fn;
 
            return proxy;
 
        };
 
        obj.emit = function (type) {
 
            obj[type]();
 
        };
 
        var properties = {};
 
        for(var x in model) {
 
            properties[x] = {
 
                get: function () {
 
                    return model[x];
 
                },
 
                set:function (value) {
 
                    model[x] = value;
 
                    obj.emit('change');
 
                },
 
            }
 
        }
        var proxy = Object.create({}, properties);
 
        return obj;
 
    }

这个办法的兼容性就好多了,IE9+都可以支持,不过仍然有人不满足于这个办法,因为Object.create函数支持程度有限嘛,不过这都不是事,IE678不是有VB嘛,我们可以用VB来模拟一个,比如这样:

const defineProperties = (function () {

    /**
     * 用于生成一个随机前缀,以防止重复
     *
     * @inner
     * @type {number}
     */
    let prefix = expando('vb');

    /**
     * 属性组定义函数
     *
     * @type {Function}
     */
    let defineProperties = null;

    try {

        // IE8只能给DOM元素定义属性
        Object.defineProperty({}, '_', {
            value: 'x'
        });

        defineProperties = Object.defineProperties;
    }
    catch (e) {
    }

    // IE6-8使用VB来模拟defineProperties
    if (!defineProperties && window.VBArray) {

        window.execScript([

            // 执行VB字符串的函数
            'Function parseVB(code)',
            '    ExecuteGlobal(code)',
            'End Function'

        ].join('
'), 'VBScript');

        /**
         * 用于存储定义过的类
         *
         * @inner
         * @type {Object}
         */
        let VBModelPool = {};

        /**
         * VB回调JS的函数代理
         *
         * @inner
         * @param {Object} instance this对象
         * @param {Object} accessors 回调的执行函数集合
         * @param {string} name 属性名
         * @param {*} [value] 属性值,不传递表示获取
         * @return {*} 返回执行结果
         */
        let VBMediator = function (instance, accessors, name, value) {

            var accessor = accessors[name];

            // 设置模式
            if (arguments.length === 4) {

                // 可写属性处理
                if (accessor.writable) {
                    accessor.value = value;
                }
                // setter处理
                else if (accessor.set) {
                    accessor.set.call(instance, value);
                }
            }
            // getter 处理
            else if (accessor.get) {
                return accessor.get.call(instance);
            }

            // 其它情况直接返回值
            return accessor.value;
        };

        defineProperties = function (_, accessors) {

            // 如果accessors不传,丢出一个错误
            if (accessors == null) {
                throw new TypeError('Cannot convert undefined or null to object');
            }

            // 生成类代码
            let buffer = [];

            buffer.push(
                // 添加私有变量(__data__用于保存访问器,__proxy__用于代理执行JS函数)
                '    Private [__data__], [__proxy__]',

                // 定义构造函数
                '    Public Default Function [__const__] (d, p)',
                '        Set [__data__] = d',
                '        Set [__proxy__] = p',
                '        Set [__const__] = Me',
                '    End Function'
            );

            // 添加访问器属性
            each(accessors, name => {

                buffer.push(
                    // 添加LET方法(IE67使用LET方法)
                    `    Public Property Let [${name}](val` + prefix + `)`,
                    `        Call [__proxy__](Me, [__data__], "${name}", val${prefix})`,
                    `    End Property`,

                    // 添加SET方法(IE8使用SET方法)
                    `    Public Property Set [${name}](val${prefix})`,
                    `        Call [__proxy__](Me, [__data__], "${name}", val${prefix})`,
                    `    End Property`,

                    // 添加获取方法
                    `    Public Property Get [${name}]`,
                    `    On Error Resume Next`,
                    `        Set [${name}] = [__proxy__](Me, [__data__],"${name}")`,
                    `    If Err.Number <> 0 Then`,
                    `        [${name}] = [__proxy__](Me, [__data__],"${name}")`,
                    `    End If`,
                    `    On Error Goto 0`,
                    `    End Property`
                );
            });

            let code = buffer.join('
');

            // 生成一个唯一的类名
            let className = VBModelPool[code];

            if (!className) {

                className = VBModelPool[code] = 'VBClass' + guid(prefix);

                // 定义类
                buffer.unshift('Class ' + className);

                // 添加类定义结束
                buffer.push('End Class');

                // 添加工厂类函数
                buffer.push(
                    'Function ' + className + 'Factory(a, b)',
                    '    Dim o',
                    '    Set o = (New ' + className + ')(a, b)',
                    '    Set ' + className + 'Factory = o',
                    'End Function'
                );

                window.parseVB(buffer.join('
'));

            }

            // 得到其产品
            return window[className + 'Factory'](accessors, VBMediator);
        };
    }

    return defineProperties;
})();

于是IE678的问题就解决了,或者你觉得VB太反人类了,那就兼容到IE9+就好了嘛。不过这种办法只能针对对象,如果你要操作的东西是一个数组,你就没办法了,我们不可能给数组中的每一个项目都定义成属性吧,所以这类解决方案通常是在数组上使用$set函数,在对象上使用依赖收集,同时把数组的splice、push、pop、reverse、shift、unshift函数重载一下,进行监控。

依赖收集虽好,但是数组对象区别对待太反人类了,于是就有了脏检测(这办法是先出来的),其原理就更简单了,起一个定时器,顺便diff一下model和model-backup就可以了。但是这种办法的性能是有问题的,所以更多的框架并没有选择这个对用户更友好的方法。

二、双向绑定



双向绑定不是MVVM所必须的,只是因为它很有用,所有大部分MVVM框架都实现了,那么什么是双向绑定呢,我们来考虑下边的情况:


现在我们为模型添加一个价格函数,当用户名为张三时,返回10,不然返回5,姓名通过一个输入框输入。



用代码实现起来并不复杂是吧,我们可以写成这样:

model.price = function () {
    if(model.name === '张三') {
        return 10;
    } else {
        return 5;
    }
};

但这实际上是行不通的,因为name是通过输入框输入的,除了我们的ViewModel可以修改它,用户也是可以修改的。而双向绑定就是为了实现这个的。即:将用户的输入反馈到Model上,并触发View的一系列其它改变。


为了实现这一效果,我们来更改一下我们的ViewModel函数:

   function ViewModel(elem, model, tpl) {

    var engine = new Engine(tpl);

    var vm = observer(model).on('change', function () {
        update(elem, engine.render(model));
    });

    delegate(elem, function (type, name, value){
        vm[name] = value;  
    });

    return vm;
}

几乎所有的MVVM框架都是这么实现的,就是通过委托或者直接绑定事件,把用户所有可能关注的事件都找出来,然后在这个事件中从DOM上获取数据,并添加回去,唯一的区别在于,没实现双向绑定的框架,只能监控事件,而不能监控属性,所以你通过要这么写:

   model.price = function () {
    if(model.name === '张三') {
        return 10;
    } else {
        return 5;
    }
};

// 在HTML上写<input on-click="onNameChanged" value="${name}">
model.onNameChanged = function (e) {
     model.setValue('name', e.value);
};
//喜欢用React的朋友是不是经常写这样的代码呢?当然大家都是很懒的,并不想每次都绑定这样的事件,我们喜欢写成这样:
// 在HTML中写:<input value="${name}">
model.price = function () {
    if(model.name === '张三') {
        return 10;
    } else {
        return 5;
    }
};

让框架去自动识别要绑定什么事件,然后获取数值好了,我只管写我的业务逻辑。


三、为什么选择表单


大家都知道MVVM是通用解决方案,所以我们要考虑的情况非常的多,比如:

如果在模板中实现for循环,如果在模板中书写if。

focus、submit、change等等事件都怎么绑定。

css里面的绑定怎么处理。

要不要处理单位。

NaN !== NaN。

因此,我觉得在出题的时候要把题目出的小一点,谁也不希望大家写5000多行 - 30000多行的小练习吧,我自己也没有尝试过这么多。


选择表单有以下几个好处:

1、我们可以认为表单是不变的,所以for/if啊什么的复杂指令通通可以不要。

2、所有有可能出问题的事件比如submit、focus、change等等都有涉及。

3、CSS之类的变化不明显,我们可以辅助一些额外的DOM操作来解决样式变化的问题,让MVVM部分只关注表单自身。

4、对双向绑定有强需求,大家不会偷懒。

5、可以不用自定义模板,用HTML默认的属性就可以了。

6、表单操作和表单验证是大家都会遇到的问题,只要把这个弄明白了,就算不用MVVM,你也学到东西了。

四、框架可用性和可扩展性以及两者之间的取舍

MVVM很好用,但不是万能的,任何一个MVVM框架都是注重数据 &gt; 交互 &gt; 效果的,在很多实际的问题中,我们不可能使用MVVM框架解决所有实际问题,这样要求我们的框架需要和其它框架或者DOM操作有协同能力。所以如何平衡框架的可用性和可扩展性,会成为框架实现到最后一个要纠结的问题,希望大家有自己的想法,我也没有什么标准。但是我一直在尝试,让DOM操作和MVVM结合起来,一个管数据一个管交互和效果,不过这就不是框架该解决的问题了,希望大家可以自行体会。


本文标题:MVVM介绍---转自知乎(写的很好,留作备份)
本文链接:https://56way.com/p/79.html
作者授权:除特别说明外,本文由 无路 原创编译并授权 小无路 刊载发布。
版权声明:本文不使用任何协议授权,您可以任何形式自由转载或使用。

发表评论

必填

选填

选填

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。