文章目录
需要的基础知识
需要了解一些底层的知识才能往下走哦,请前往另一个博客
基本思想
对象 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();