概述

之前的章节中,我们创建了一个没有任何逻辑的vue对象,仅仅只是保证了var app = new Vue({...})不报错而已,这一篇我们将构建一个真正的vue对象,实现真正的值绑定。

build(构建)

这是html中创建vue的代码

var app = new Vue({
            el: '#app',
            data: {
                newTodo: '',
                todos: []
            },
            methods: {
                addTodo: function () {
                    this.todos.push({ text: this.newTodo });
                    this.newTodo = '';
                },
                deleteTodo: function (index) {
                    this.todos.splice(index, 1);
                }
            }
        })

思路是

  • 创建vue对象
  • 将data数据直接挂载到对象上,这样可以实现vue.newTodo的访问效果
  • 将method直接挂载到桂香上,可以实现vue.addTodo的效果

当然,实际上vue并不是这么实现的,vue通过proxy方式实现直接访问的效果,我们的目的是能用就行(大家真正实现一个框架不要报这种想法,本系列是为了让所有人都能理解,都能入门才用该方式实现)。

var vue = {};
function build() {
		for (let k in options.data) {
			let v = options.data[k];
			defineProperty(k, v);
		}
		for (let key in options.methods) {
			vue[key] = options.methods[key];
		}
	}

defineProperty

这个是实现绑定的核心步骤,代码如下:

function defineProperty(name, value) {
		Object.defineProperty(vue, name, {
			get: function() {
				return value;
			},
			set: function(newValue) {
				value = newValue;
				let items = subscriber[name];
				if (items) {
					for (let i = 0; i < items.length; ++i) {
						items[i].change(name, newValue);
					}
				}
			}
		})
	}

当我们给一个对象新增一个属性或者修改属性值的时候可以直接通过user.name='张三'实现,也可以通过函数Object.defineProperty进行定义,明显后者更为麻烦,但是Object.defineProperty可以监听值的变化,获取值和设置值都可以监听到,这就是为实现值变化更新dom的功能打下了基础。我们在set中监听值变化,从订阅者里面根据变量名称取出订阅者的指令,依次调用指令change方法。defineProperty其他用法参考这里

我们的data除了newTodo这个字符串变量外还有todos这个变量,其中新增代办的代码如下:

addTodo: function () {
            this.todos.push({ text: this.newTodo });
            this.newTodo = '';
        }

todos是通过调用push方法进行插入,那这样会触发set事件吗,答案是不会的,只有当this.todos=[]这种todos赋予新值才会触发值改变,这个肯定是不行的,这个要求开发者必须构建一个push后的数组再赋值给todos,可以用原型解决这个问题,原型最大的好处就是可以重新定义js的标准方法。

function defineArrayProperty() {
        var method=['push','splice'];
        for(let i=0;i<method.length;++i) {
            var origin = Array.prototype[method[i]];
            var fn = function () {
                origin.apply(this, arguments);
                let names = Object.getOwnPropertyNames(vue);
                for (let i = 0; i < names.length; ++i) {
                    if (vue[names[i]] === this) {
                        let items = subscriber[names[i]];
                        if (items) {
                            for (let j = 0; j < items.length; ++j) {
                                items[j].change(names[i], this);
                            }
                        }
                    }
                }
            }
            Object.defineProperty(Array.prototype, method[i], {
                value: fn
            });
        }
    }

通过apply方法调用原来的方法,再找到订阅者通知订阅者,对于变量可以一个个定义属性,这样可以获取到名称,但是对于修改js基本对象的属性,这里无法获取变量名,只能通过遍历vue属性找到变量名。

valueTrigger

完成了以上工作,我们上一篇中的valueTrigger逻辑就可以实现

function valueTrigger(name, value) {
	if (vue[name] != undefined) {
		vue[name] = value;
	}
}

当值发生变化,由于变量都直接绑定到vue对象本身,因此可以直接通过属性名找到并赋值。

事件

我们将配置中methods的方法也挂载到了vue对象上,那么事件响应就可以做了,我们稍微修改下上一篇中compile函数,增加事件响应

function compile(node) {
		let element = node.cloneNode(false);
		for (let i = 0; i < node.childNodes.length; ++i) {
			element.appendChild(compile(node.childNodes[i]));
		}
		if (element.nodeType == 3) {
			//文本类型解析
			let vars = parseVariable(element.textContent);
			for (let i = 0; i < vars.length; ++i) {
				let directive = Directive(element, vars[i], element.textContent);
				addSubscriber(vars[i], directive);
			}
		} else if (element.nodeType == 1 && element.attributes) {
			//元素类型解析
			let attrs = element.attributes;
			for (let i = 0; i < attrs.length; ++i) {
				let name = attrs[i].name;
				if (name.startsWith("v-bind") || name.startsWith(":") || name.startsWith("v-model")) {
					let vars = parseVariable(attrs[i].value);
					if (vars.length == 0) {
						let directive = Directive(element, name, attrs[i].value);
						addSubscriber(attrs[i].value, directive);
					} else {
						for (let i = 0; i < vars.length; ++i) {
							let directive = Directive(element, name, attrs[i].value);
							addSubscriber(vars[i], directive);
						}
					}
				}
				//事件响应
				if (name.startsWith("v-on:")) {
					let event = name.substr(5);
					addEvent(element, event, parseMethod(attrs[i].value));
				}
			}
		}
		return element;
	}
  • parseMethod通过正则表达式进行解析
function parseMethod(exp) {
		var method = {};
		let m;
		let methodRegExp = /([^\(]+)\(([^\)]*)\)/g;
		if (m = methodRegExp.exec(exp)) {
			method.name = m[1];
			let params = m[2];
			params = params.replace(/\s+/g, '');
			if (params && params.length > 0) {
				method.params = params.split(",");
			} else {
				method.params = [];
			}
		}
		return method;
	}
  • addEvent代码:
function addEvent(element, event, method) {
		element.addEventListener(event, function(e) {
			let params = [];
			let paramNames = method.params;
			if (paramNames) {
				for (let i = 0; i < paramNames.length; ++i) {
					params.push(vue[paramNames[i]]);
				}
			}
			vue[method.name].apply(vue, params);
		})
	}

通过调用addEventListener给节点增加事件,比如v-on:click就会增加click事件,通过apply方法动态调用定义在vue中的方法,完成事件的响应,

效果

可以点击这里查看效果,为了用上style属性,修改了下html,增加了style属性,效果如下:

完整的js代码可以点击这里查看

参考

点击余下链接,查看该系列其他文章

Trackback

no comment untill now

Add your comment now