javascript基础修炼(9)——MVVM中双向数据绑定的基本原理 (2)

compiler.js——模板解析器类

/** * 模板编译器 */ class Compiler{ constructor(){ this.strategy = new Strategy();//封装的策略类,下一节描述 this.strategyKeys = Object.keys(this.strategy); } /** *编译方法 *@params vm Dash类的实例(即VisualModel实例) *@params node 待编译的DOM节点 */ compile(vm, node){ if (node.nodeType === 3) {//解析文本节点 this.compileTextNode(vm, node); }else{ this.compileNormalNode(vm, node); } } /** *编译文本节点,此处仅实现一个空方法,实际开发中可能是字符串转义过滤方法 */ compileTextNode(vm, node){} /** *编译DOM节点,遍历策略类中支持的自定义指令,如果发现某个指令dir *则以this.Strategy[str]的方式取得对应的处理函数并执行。 */ compileNormalNode(vm, node){ this.strategyKeys.forEach(key=>{ let expr = node.getAttribute(key); if (expr) { this.strategy[key].call(vm, node, expr); } }); //递归处理当前DOM标签的子节点 let childs = node.childNodes; if (childs.length > 0) { childs.forEach(subNode => this.compile(vm, subNode)); } } } //为方便理解,此处直接在全局生成一个编译器单例,实际开发中请挂载至适当的命名空间下。 window.Compiler = new Compiler(); 2.2.3 策略封装

我们使用策略模式实现一个单例的策略类Strategy,将所有指令所对应的解析方法封装起来并传入解析器,当解析器递归解析每一个标签时,如果遇到可以识别的指令,就从策略类中直接取出对应的处理方法对当前节点进行处理即可,这样Strategy类只需要实现一个Strategy.register( customDirective, options)方法就可以暴露出未来用以添加自定义指令的接口。(细节可参考附件中的代码)

strategy.js——指令解析策略类

//策略类的基本结构 class Strategy{ constructor(){ let strategy = { 'd-bind':function(){//...}, 'd-model':function(){//...}, 'd-click':function(){//...} } return strategy; } //注册新的指令 register(customDir,options){ ... } }

模板解析的工作就比较清晰了,相当于带着一本《解析指南》去遍历处理DOM树,不难看出,实际上绑定的工作就是在策略对应的方法里来实现的,在MVVM结构种,这一步被称为“依赖收集”

2.2.4 订阅数据模型变化

以最基本的d-bind指令为例,通过使用strategy['d-bind']方法处理节点后,被处理的节点应该具备感知数据模型变化的能力。以上面的模板为例,当this.data.myname发生变化时,就需要将被处理节点的内容改为对应的值。此处就需要用到发布-订阅模式。为了实现这个方法,需要一个观察者类Observer,它的功能是观察数据模型的变化(通过数据劫持实现),管理订阅者(维护一个回调队列管理订阅者添加的回调方法), 变化发生时通知订阅者(依次调用订阅者注册的回调方法),同时将提供回调方法并执行视图更新行为的逻辑抽象为一个订阅者类Subscriber,订阅者实例拥有一个update方法,当该方法被观察者(同时也是发布者)调用时,就会刷新对应节点的视图,很明显,subscriber实例需要被添加至指定的观察者类的回调队列中才能够生效。

//发布订阅模式的伪代码 //... 'd-bind':function(node, expr){ //实例化订阅者类 let sub = new Subscriber(node, 'myname',function(){ //更新视图 node.innerHTML = VM.data['myname']; }); //当观察者实例化时,需要将这个sub实例的update方法添加进 }, //...

subscriber.js——订阅者类

class Subscriber{ constructor(vm, exp, callback){ this.vm = vm; this.exp = exp; this.callback = callback; this.value = this.vm.data[this.exp]; } /** * 提供给发布者调用的方法 */ update(){ return this.run(); } /** * 更新视图时的实际执行函数 */ run(){ let currentVal = this.vm.data[this.exp]; if (this.value !== currentVal) { this.value = currentVal; this.callback.call(this.vm, this.value); } } } 2.2.5 数据劫持

在生成一个subscriber实例后,还要实现一个observer实例,然后才能够通过调用observer.addSub(sub)方法来将订阅者添加进观察者的回调队列中。先来看一下Observer这个类的定义:

observer.js——观察者类

/** * 发布者类,同时为一个观察者 * 功能包括: * 1.观察视图模型上数据的变化 * 2.变化出现时发布变化消息给订阅者 */ class Observer{ constructor(data){ this.data = data; this.subQueue = {};//订阅者Map this.traverse(); } //遍历数据集中各个属性并添加观察器具 traverse(){ Object.keys(this.data).forEach(key=>{ defineReactive(this.data, key, this.data[key], this); }); } notify(key){ this.subQueue[key].forEach(fn=>fn.update()); } } //修改对象属性的get/set方法实现数据劫持 function defineReactive(obj, key, val, observer) { //当键的值仍然是一个对象时,递归处理,observe方法定义在dash.js中 let childOb = observe(val); //数据劫持 Object.defineProperty(obj, key, { enumerable:true, configurable:true, get:()=>{ if (window.curSubscriber) { if (observer.subQueue[key] === undefined) {observer.subQueue[key] = []}; observer.subQueue[key].push(window.curSubscriber); } return val; }, set:(newVal)=>{ if (val === newVal) return; val = newVal; //监听新值 childOb = observe(newVal); //通知所有订阅者 observer.notify(key); } }) }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpxwsd.html