手把手教你写vue1.0简装版
首先我们得了解VUE的原理。
相信很多小伙伴在看VUE双向绑定的时候都有看过这个图:

其实想简单点就像我们上学那会,你的成绩就是data中的数据,班主任就是那个Dep,管理了我们这个班的所有学生,然后考试了以后你成绩就会有变化,考试成绩然后就会送到你父母那里,考试就是那个Updater函数,你的家长就是那个视图变化了,你考得好你父母就高兴。不知道这么比喻大家能不能懂(可能也不是很恰当的比喻哈!看看就好,也可以之直接看图)。
首先我们先来实现最上面的那条线,也就是劫持数据添加订阅者,创建订阅者管理容器;
我们先创建一个kvue.js文件内容如下(因为我不喜欢代码分开写,所以我全部写完,但是每一步我都做了详细的注释):
class Kvue {
constructor (options) {
/*
平时我们一般这样使用vue
new Vue({
el: '#app',
data: {
a: 'a'
}
})
这个options就是我们在使用VUE时传入的对象
* */
// 先把这个options保存下来还有data里面的数据保存下来
this.$options = options
this.$data = options.data
// 这个时候我们就需要把data中的数据劫持变成订阅者,
// observe函数就是处理数据让其变成订阅者
this.observe(this.$data)、
// 这里就是做下生命周期的函数调用
if (options.created) {
options.created.call(this);
}
//这里开始做模板解析了
new Compile(this, options.el)
}
observe (data) {
/*
因为要遍历出这个数据的所有数据,所以得递归,
这里是递归的结束条件,
也就是这个数据底下没有对象了
*/
if (!data || typeof data !== 'object') return
// 这里使用对象的循环劫持数据
Object.keys(data).forEach(key => {
// 这个劫持函数
this.DataHijacking(data, key, data[key])
/*
这个是数据代理函数,比如平时我们可以实例化vue以后直接vue.a='b'
这样子去更改data中的数据。就是因为这里让vue代理了data中的数据
这里有点瑕疵我没有把data里面的数据全部代理完,只代理了第一层
*/
this.proxyData(key)
})
}
DataHijacking (obj, key, value) {
// 这里开始递归
this.observe(value)
/*每循环一次数据就要调用一次Dep类,
目的就是把这个数据变成订阅者然后添加到Dep的容器里面,方便管理*/
let dep = new Dep()
// 数据劫持函数这里不懂得可以去谷歌下哈!
Object.defineProperty(obj, key, {
get () {
/*判断Dep的target是否存在,存在就是调用dep的addDep方法,
添加订阅者到容器里*/
Dep.target && dep.addDep(Dep.target)
return value
},
set (newVal) {
if (value !== newVal) {
value = newVal
// 这里就是因为属性设置了新值就会触发dep更新函数,
// 这样子就能更新到视图
dep.notify()
}
}
})
}
proxyData (key) {
// 这里也是数据劫持
Object.defineProperty(this, key, {
get () {
return this.$data[key]
},
set (newVal) {
this.$data[key] = newVal
}
})
}
}
//以上就是我们创建好的kvue类
/*接下来我们需要创建订阅者管理容器,这个类很简单,
就是保存和管理订阅者就可以了*/
class Dep {
constructor () {
//这个是保存所有订阅者的数组
this.deps = []
}
// 这函数是添加订阅者的函数,配合数据劫持使用
addDep (watcher) {
this.deps.push(watcher)
}
// 这个函数是通知订阅者的函数
notify () {
// 循环所有的订阅者并执行他们的更新函数
this.deps.forEach(watcher => {
watcher.update()
})
}
}
// 接下来就是如何把数据变成一个个订阅者的类
class Watcher {
constructor (vm, key, callback) {
// VM是kvue这个类,key是模板出入过来的数据,
// callback是模板传入的回调函数,然后把这些数据保存起来
this.vm = vm
this.key = key
this.cb = callback
// 把这个类赋值给Dep类的target属性
Dep.target = this
// 强行触发kvue类的数据劫持的get函数
this.value = this.vm[this.key];
// 然后再把Dep的target置空,防止内存泄露
Dep.target = null
}
// 更新函数
update () {
this.cb.call(this.vm, this.vm[this.key])
}
}
现在就完成了最上面的那条线
接下来我们来完成compile模板指令解析:
创建一个kcompile.js文件;内容如下:
class Compile {
constructor (vm, el) {
// 这里的VM代表了kve的类,el就是那个跟元素,然后把他们保存下来
this.$vm = vm;
this.$el = document.querySelector(el);
// 这里要先判断下是否传入跟元素
if (this.$el) {
// 把根元素里面的所有子元素变成documentFragment节点,
// 这样子方便我们操作元素,这里不懂怎么变成的可以去谷歌下哈!
this.$fragment = this.node2Fragment(this.$el);
// 把变成ocumentFragment节点开始模板解析
this.compiles(this.$fragment);
// 然后再把解析好的元素插入到跟元素里面
/*
这里解释下因为vue1.0是没有用diff算法的虚拟dom。
差不多就是这么来解析模板的,这也是为什么在vue1.0的时候你刷新时回看到{{nam}}
这些代码的原因,是因为模板还没有解析好。到了2.0就开始使用diff算法虚拟DOM了
这样子就不会出现这些{{nam}}代码了
*/
this.$el.appendChild(this.$fragment);
}
}
node2Fragment (el) {
const fragment = document.createDocumentFragment();
let child
while (child = el.firstChild) {
fragment.appendChild(child)
}
return fragment
}
// 这里是模板解析的关键函数
compiles (el) {
// 这个el是传入的documentFragment节点。先保存下来
const childNodes = el.childNodes
//下步就开始遍历这个节点集的所有子节点找到里面的文字节点和元素节点
//一般都会分为元素节点和文本节点,元素节点话可能上面会有一些指令要做特别的解析
//如果文本节点直接展示数据就行了,搞不懂的同学可以去看看script高级教程哈,
// 里面解释的非常清楚我记得在247页开始。
Array.from(childNodes).forEach(node => {
if (this.isElement(node)) {
// 如果是元素节点那么直接元素节点的解析
this.compileElement(node)
}else if (this.isInterpolation(node)) { //这里就是判断文本节点是否有{{}}
// 文本节点的解析
this.compileText(node)
}
// 这里做下递归把所有子元素节点的子元素节点全部遍历出来,直到没有
if (node.childNodes && node.childNodes.length > 0) {
this.compiles(node);
}
})
}
isElement (node) {
return node.nodeType === 1
}
isInterpolation(node) {
//判断是否是文本节点,并且有{{}}
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
// 文本解析函数
compileText (node) {
/* 这个exp就是我们做{{}}这个正则匹配的值。
不清楚的小伙伴可以看javascript高级教程的103页开始*/
const exp = RegExp.$1;
// 开始进行文本节点的文本指令
// 这里的参数就是当前的节点,kvue类,属性值(也就是data绑定的数据),接下的操作
command.update(node, this.$vm, exp, 'text')
}
// 元素节点的模板解析
compileElement (node) {
// node是传入的参数,也就是一个元素节点
//获取这个元素节点上面的所有属性比如:v-model
let nodeAttrs = node.attributes
// 拿到这个元素的所有属性以后开始遍历,然后做相应的指令处理
Array.from(nodeAttrs).forEach( attrs => {
//取得元素的上面的属性名称
const attrName = attrs.name
//取得元素属性名称的值
const exp = attrs.value
// 这里判断是否有我们设定好的属性,也就是那些指令
if (attrName.indexOf(':') === 0) {
const operation = attrName.substring(1)
// 这里是做指令的处理,要判断是否有这个指令,有的话开始处理
// 传入参数为当前的这个元素节点,vm也就是kvue类,属性名称
command[operation] && command[operation](node, this.$vm, exp);
}else if (attrName.indexOf('@') === 0) {
// 这里是元素上绑定的事件
const eventName = attrName.substring(1)
command.eventHandler.call(this, node, this.$vm, exp, eventName)
}
})
}
}
// 这里创建一些指令集,这里只有一些简单的后续你有时间的话可以多加写上去
// :modle,:text,:html,@fn = 'f()'等指令
command = {
text (node, vm, exp) {
this.update(node, vm, exp, 'text')
},
model (node, vm, exp) {
//这里就是实现双向绑定的函数了,
// 其实就是在更新值的时候要监听下input的input事件
this.update(node, vm, exp, 'model')
// 这里监听input的ipnut事件
node.addEventListener("input", e => {
// 这里会触发kvue类中的set
vm[exp] = e.target.value;
});
},
html (node, vm, exp) {
this.update(node, vm, exp, 'html')
},
update (node, vm, exp, operation) {
// 这里做一个更新指令的复用
let fn = this[operation + 'Updator']
// 执行相关的更新函数
fn && fn(node, vm[exp])
/*
这里就是把绑定在这个模板上的data中的值做一个订阅者,
这样子如何模板里面的值改变了就会通知到data,这就是双向数据流
*/
new Watcher(vm, exp, function () {
fn && fn(node, vm[exp])
})
},
textUpdator (node, val) {
node.textContent = val
},
modelUpdator (node, val) {
node.value = val;
},
htmlUpdator( node, val) {
node.innerHTML = val
},
// 这里是事件处理的函数
eventHandler (node, vm, exp, eventName) {
const fn = vm.$options.methods && vm.$options.methods[exp];
if(eventName && fn) {
node.addEventListener(eventName, fn.bind(vm))
}
}
}
这样子我们就写好vue1.0的简装版,下面开始使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<div>
{{name}}
</div>
<input type="text" :model="name">
</div>
</body>
</html>
<script src="book-vue/kcompile.js"></script>
<script src="book-vue/kvue.js"></script>
<script>
let V = new Kvue({
el: '#app',
data: {
name: 'bbbb'
}
})
</script>
备注:由于时间问题就写了粗糙版,的里面有些可以简化,也可以多增加了一些东西,快来一起试试看吧!!

浙公网安备 33010602011771号