「干货」实现一个简单的Vue
这周参考了一些博文,自己写了一个简单的vue,网上这类实现很多,我的实现也没什么新奇,权当一个自我练习吧
本文同时发在我的github博客上,欢迎star
具体实现
首先,得先有一个Vue类,当然,我写的一个很粗糙的Vue类,所以我把它叫做BabyVue:
function BabyVue(options) { const { data, root, template, methods } = options; this.data = data; this.root = root; this.template = template; this.methods = methods; this.observe(); this.resolveTemplate(); }
BabtVue构造函数接受一个options,options中包含data,root(即html中指定的根结点),template模版,methods四个option,我们把这些option挂载到this方法上,以便后续的函数能轻松地拿到他们。然后执行observe和resolveTemplate方法
observe方法:
BabyVue.prototype.observe = function() { Object.keys(this.data).forEach(key => { let val = this.data[key]; const observer = new Observer(); Object.defineProperty(this.data, key, { get: () => { if (Observer.target) { observer.subscribe(Observer.target); } return val; }, set: newValue => { if (val === newValue) { return; } val = newValue; observer.publish(newValue); } }); }); };
observe方法中先对this.data中的数据进行遍历,这里没有考虑更深层的结构,只对第一层数据进行遍历,利用闭包缓存它的当前值val和一个观察者observer,并用Object.defineProperty方法设置它的get和set属性,在获取值的时候判断Observer.target是否存在,若存在,则将Observer.target加入订阅者(后面再详述其作用),最后返回val;设置值的时候,将新值与val对比,若不同,则更新val值,并通知订阅者更新
下面是Observer的代码,实现了一个简单的观察者模式:
function Observer() { this.subscribers = []; } Observer.prototype.subscribe = function(subscriber) { !~this.subscribers.indexOf(subscriber) && this.subscribers.push(subscriber); }; Observer.prototype.publish = function(newVal) { this.subscribers.forEach(subscriber => { const ele = document.querySelector(`[${subscriber}]`); ele && (ele.innerHTML = newVal); }); };
订阅者用其特殊属性进行标识,在更新时,先通过属性选择器拿到目标dom再更新其值
下面是resolveTemplate的代码,其主要是渲染模版、增加元素标识和挂载事件,Vue中对模版解析使用的应当是更高级的方法,我这里只是对template字符串一些简单的解析
BabyVue.prototype.resolveTemplate = function() { const root = document.createElement("div"); root.innerHTML = this.template; const children = root.children; const nodes = [].slice.call(children); let index = 0; const events = []; while (nodes.length !== 0) { const node = nodes.shift(); const _index = index ; node.setAttribute(`v-${_index}`, ""); if (node.children.length > 0) { nodes.push(...node.children); } else { if (/\{\{(.*)\}\}/.test(node.innerHTML)) { const key = node.innerHTML.replace(/\{\{(.*)\}\}/, "$1"); Observer.target = `v-${_index}`; node.innerHTML = this.data[key]; Observer.target = null; } const method = node.getAttribute("v-on:click"); if (method) { events.push({ key: `v-${_index}`, type: "click", method }); } } } this.root.innerHTML = root.innerHTML; events.forEach(event => { const { key, type, method } = event; const ele = document.querySelector(`[${key}]`); ele.addEventListener(type, this.methods[method].bind(this)); }); };
我对模版中的每一个元素增加一个特殊标示,形似v-xxx,方便根据表示标示获取真实dom(为什么不直接保存node?可以试试使用了createElement创建的元素再设置innerHTML,会出现一些问题)。
先根据正则匹配{},若符合条件,获取了大括号的标识符后,先将Object.target设为元素的标识,在将元素的innerHTML置为data中的数据,要注意,在此时,我们获取了一次this.data[key],会触发之前设置的get属性,在其中判断Observer.target是否存在,因为我们刚刚设置过,Observer.target当前为元素的标识,所以,它被加到订阅者中。
再获取其事件属性,我们这里只简单地获取v-on:click属性,我们将它的属性值和元素标识保存到events中
最后等待模版挂载在root元素中后,我们遍历events数组,挂载事件
至此,我的BabyVue已基本实现了
Demo
实现的是一个简单的计数器:

有兴趣的小伙伴可以复制以下代码运行查看效果:
BabyVue