typescript 中的 Mvvm 框架底层原理

文章目录

需要的基础知识

需要了解一些底层的知识才能往下走哦,请前往另一个博客

基本思想

对象 Object
对于 object ,就是改变这个对象的属性,通过重写这个属性的 get 和 set
就可以在 set 里面进行需要的操作
当一个属性被赋值时,我们在 set 的时候去做一些其他的操作
比如发送某一个事件,告诉外部这个变量发生了改变,这个就是 Mvvm 框架所使用的
数组 Array
对于数组,我们需要对数组里面的对象监听变化,即遍历数组,并使用上面的方法
而数组整个的变化,我们也需要监听
比如数组的 push() , pop() 这种会改变数组内容的重要方法
我们也必须通知到位,跟劫持对象的属性一样
劫持对象的属性用 get 和 set 
对于数组我们就重写他的方法即可

实现 JsonOb


/**
 * 实现动态绑定的核心部分,
 * 每次修改属性值,都会调用对应函数,并且获取值的路径
 */

const OP = Object.prototype;
const types = {
    obj: '[object Object]',
    array: '[object Array]'
}
const OAM = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice'];




/**
 * 实现属性拦截的类
 */
export class JsonOb<T> {
    constructor(obj:T, callback: (newVal: any, oldVal: any, pathArray: string[]) => void) {
        if (OP.toString.call(obj) !== types.obj && OP.toString.call(obj) !== types.array) {
            console.error('请传入一个对象或数组');
        }
        this._callback = callback;
        this.observe(obj);
    }

    private _callback;
    /**对象属性劫持 */
    private observe<T>(obj: T, path?) {

        if (OP.toString.call(obj) === types.array) {
            this.overrideArrayProto(obj, path);
        }

        Object.keys(obj).forEach((key) => {
            let self = this;
            let oldVal = obj[key];
            let pathArray = path && path.slice();
            if (pathArray) {
                pathArray.push(key);
            }
            else {
                pathArray = [key];
            }
            Object.defineProperty(obj, key, {
                get: function () {
                    return oldVal;
                },
                set: function (newVal) {
                    //cc.log(newVal);
                    if (oldVal !== newVal) {
                        if (OP.toString.call(newVal) === '[object Object]') {
                            self.observe(newVal, pathArray);
                        }
                        self._callback(newVal, oldVal, pathArray)
                        oldVal = newVal
                    }
                }
            })

            if (OP.toString.call(obj[key]) === types.obj || OP.toString.call(obj[key]) === types.array) {
                this.observe(obj[key], pathArray)
            }

        }, this)
    }

    /**
     * 对数组类型进行动态绑定
     * @param array 
     * @param path 
     */
    private overrideArrayProto(array: any, path) {
        // 保存原始 Array 原型  
        var originalProto = Array.prototype;
        // 通过 Object.create 方法创建一个对象,该对象的原型是Array.prototype  
        var overrideProto = Object.create(Array.prototype);
        var self = this;
        var result;

        // 遍历要重写的数组方法  
        OAM.forEach((method) => {
            Object.defineProperty(overrideProto, method, {
                value: function () {
                    var oldVal = this.slice();
                    //调用原始原型上的方法  
                    result = originalProto[method].apply(this, arguments);
                    //继续监听新数组  
                    self.observe(this, path);
                    self._callback(this, oldVal, path);
                    return result;
                }
            })
        });
        // 最后 让该数组实例的 __proto__ 属性指向 假的原型 overrideProto  
        array['__proto__'] = overrideProto;

    }



}


有了这个类,就可以监听各种对象和数组的变化了

使用 JsonOb

接下来就是 Mvvm 框架对于 JsonOb 的使用和管理, 用一个 vm 对象和一个 vmManager 管理器来实现
import { JsonOb } from './JsonOb';

const VM_EMIT_HEAD = 'VC:';
const DEBUG_SHOW_PATH = false;


//通过 .  路径 设置值
function setValueFromPath(obj:any,path: string, value: any, tag:string = '') {
    let props = path.split('.');
    for (let i = 0; i < props.length; i++) {
        const propName = props[i];
        if (propName in obj === false) { console.error('['+propName + '] not find in ' +tag+'.'+ path); break; }
        if (i == props.length - 1) {
            obj[propName] = value;
        } else {
            obj = obj[propName];
        }
    }

}

//通过 . 路径 获取值
function getValueFromPath(obj:any,path: string,def?:any, tag:string = ''):any {
    let props = path.split('.');
    for (let i = 0; i < props.length; i++) 
    {
        const propName = props[i];
        if ((propName in obj === false)) 
        { 
            console.error('['+propName + '] not find in '+tag+'.'+ path); 
            return def; 
        }
        obj = obj[propName];
    }
    if(obj === null||typeof obj === "undefined")obj = def;//如果g == null 则返回一个默认值
    return obj;

}



/**
 * ModelViewer 类
 */
class ViewModel<T>{
    constructor(data:T,tag:string) {
        new JsonOb(data,this._callback.bind(this));
        this.$data = data;
        this._tag = tag;
    }

    public $data:T;

    //索引值用的标签
    private _tag:string = null;

    /**激活状态, 将会通过 cc.director.emit 发送值变动的信号, 适合需要屏蔽的情况 */
    public active:boolean = true;

    /**是否激活根路径回调通知, 不激活的情况下 只能监听末端路径值来判断是否变化 */
    public emitToRootPath:boolean = false;

    //回调函数 请注意 回调的 path 数组是 引用类型,禁止修改
    private _callback(n: any, o: any, path:string[]):void{
            if(this.active == true){
                let name = VM_EMIT_HEAD + this._tag+'.'+ path.join('.')
                if(DEBUG_SHOW_PATH)cc.log('>>',n,o,path);
                cc.director.emit(name,n,o,[this._tag].concat(path)); //通知末端路径

                if(this.emitToRootPath)cc.director.emit(VM_EMIT_HEAD+this._tag,n,o,path);//通知主路径

                if(path.length>=2){
                    for (let i = 0; i < path.length-1; i++) {
                        const e = path[i];
                        //cc.log('中端路径');
                        
                    }
                }

            }
    }

    //通过路径设置数据的方法
    public setValue(path: string, value: any) {
        setValueFromPath(this.$data,path,value,this._tag);
    }
    //获取路径的值
    public getValue(path: string,def?:any):any {
        return getValueFromPath(this.$data,path,def,this._tag);
    }
}

/**
 * VM 对象管理器(工厂)
 */
class VMManager {
        /**静态数组,保存创建的 mv 组件 */
        private _mvs:Array<{tag:string,vm:ViewModel<any>}> = [];

        private EMIT_HEAD = VM_EMIT_HEAD;


        /**
         * 绑定一个数据,并且可以由VM所管理
         * @param data 需要绑定的数据
         * @param tag 对应该数据的标签(用于识别为哪个VM,不允许重复)
         * @param activeRootObject 激活主路径通知,可能会有性能影响,一般不使用
         */
        add<T>(data:T,tag:string = 'global',activeRootObject:boolean = false){
            let vm = new ViewModel<T>(data,tag);
            let has = this._mvs.find(v=>v.tag === tag);
            if(tag.includes('.')){
                console.error('cant write . in tag:',tag);
                return;
            }
            if(has){
                console.error('already set VM tag:'+ tag);
                return;
            }

            vm.emitToRootPath = activeRootObject;
            this._mvs.push({tag:tag,vm:vm});
            
        }

        /**
         * 移除并且销毁 VM 对象
         * @param tag 
         */
        remove(tag:string){
            let index = this._mvs.findIndex(v => v.tag === tag);
            if(index >=0 ) this._mvs.splice(index,1);
        }

        /**
         * 获取绑定的数据
         * @param tag 数据tag
         */
        get<T>(tag:string):ViewModel<T>{
            let res = this._mvs.find(v => v.tag === tag);
            if(res == null){
                console.error('cant find VM from:',tag);
            }else{
                return res.vm;
            }
        }

        /**
         * 通过全局路径,而不是 VM 对象来 设置值
         * @param path - 全局取值路径
         * @param value - 需要增加的值
         */
        addValue(path: string, value: any) {
            path =  path.trim();//防止空格,自动剔除
            let rs = path.split('.');
            if(rs.length<2){console.error('Cant find path:'+path)};
            let vm = this.get(rs[0]);
            if(!vm){console.error('Cant Set VM:'+rs[0]);return;};
            let resPath = rs.slice(1).join('.');
            vm.setValue(resPath,vm.getValue(resPath)+value);
        }

        /**
         * 通过全局路径,而不是 VM 对象来 获取值
         * @param path - 全局取值路径
         * @param def - 如果取不到值的返回的默认值
         */
        getValue(path: string,def?:any):any {
            path =  path.trim();//防止空格,自动剔除
            let rs = path.split('.');
            if(rs.length<2){console.error('Get Value Cant find path:'+path);return;};
            let vm = this.get(rs[0]);
            if(!vm){console.error('Cant Get VM:'+rs[0]);return;};
            return vm.getValue(rs.slice(1).join('.'),def);
        }

        /**
         * 通过全局路径,而不是 VM 对象来 设置值
         * @param path - 全局取值路径
         * @param value - 需要设置的值
         */
        setValue(path: string, value: any) {
            path =  path.trim();//防止空格,自动剔除
            let rs = path.split('.');
            if(rs.length<2){console.error('Set Value Cant find path:'+path);return;};
            let vm = this.get(rs[0]);
            if(!vm){console.error('Cant Set VM:'+rs[0]);return;};
            vm.setValue(rs.slice(1).join('.'),value);
  
        }

        setObjValue = setValueFromPath;
        getObjValue = getValueFromPath;

        /**等同于 cc.director.on */
        bindPath(path: string, callback: Function, target?: any, useCapture?: boolean):void{
            path =  path.trim();//防止空格,自动剔除
            if(path == ''){
                console.error(target.node.name,'节点绑定的路径为空');
                return;
            }
            if(path.split('.')[0] === '*'){
                console.error(path,'路径不合法,可能错误覆盖了 VMParent 的onLoad 方法, 或者父节点并未挂载 VMParent 相关的组件脚本');
                return;
            }
            cc.director.on(VM_EMIT_HEAD + path, callback, target, useCapture);
        }

        /**等同于 cc.director.off */
        unbindPath(path: string, callback: Function, target?: any):void{
            path =  path.trim();//防止空格,自动剔除
            if(path.split('.')[0] === '*'){
                console.error(path,'路径不合法,可能错误覆盖了 VMParent 的onLoad 方法, 或者父节点并未挂载 VMParent 相关的组件脚本');
                return;
            }
            cc.director.off(VM_EMIT_HEAD + path, callback, target);
        }


        /**冻结所有标签的 VM,视图将不会受到任何信息 */
        inactive():void{
            this._mvs.forEach(mv=>{
                mv.vm.active = false;
            })
        }

        /**激活所有标签的 VM*/
        active():void{
            this._mvs.forEach(mv=>{
                mv.vm.active = true;
            })
        }

 

}

//   整数、小数、时间、缩写

/**
 * VM管理对象,使用文档: 
 *  https://github.com/wsssheep/cocos_creator_mvvm_tools/blob/master/docs/ViewModelScript.md
 */
export let VM = new VMManager();
上一篇:object KVC\KVO 简介


下一篇:什么是Vue?Vue的工作原理是什么?