# 前言
上篇文章简单实现了数据驱动,没看的小伙伴可以看下,我们今天主要来实现{{ counter }}的编译,以及简单指令的源码实现,包括指令类型的判断,依赖收集的逻辑等。
# 初步实现编译
上篇文章中我们的MyVue类主要实现了对options.data的响应式处理以及proxy代理两件事,今天我要实现第三件事:编译节点,如下:
class MyVue {
constructor(options) {
// 保存选项
this.$options = options;
this.$data = options.data;
// 对 data 实现数据响应式
observe(options.data);
// 做代理
proxy(this)
// 编译
new Compile(options.el, this)
}
}
实现 Compile 类
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
// 如果节点存在,则开始编译
if (this.$el) {
this.compile(this.$el)
}
}
// 遍历node,判断节点类型,做不同处理
compile(node) {
const childNodes = node.childNodes;
// 遍历所有子节点
Array.from(childNodes).forEach(n => {
// 判断节点类型
if (this.isElement(n)) {
console.log('编译元素')
// 递归遍历是否有子元素
if (n.childNodes.length > 0) {
this.compile(n)
}
} else {
console.log('编译文本')
}
})
};
// 判断节点是否为 元素
isElement(n) {
return n.nodeType === 1
}
}
如上:创建编译实例的时候接收两个参数:节点el和实例vm,接着在拿到节点后开始编译节点。编译节点时就需要递归遍历所有子节点,并判断每个节点的类型,这里我们简单实现了一个元素类型和非元素类型的判断,html代码和myVue.js代码如下:
html代码
<div id="app">
<p> {{counter}}</p>
<p k-text="counter"></p>
</div>
<script src="./src/myVue.js"></script>
<script>
const appVue = new MyVue({
el: "#app",
data: {
counter: 1
}
});
</script>
myVue.js 代码:
// 定义响应式属性
function defineReactive(obj, key, val) {
// 递归监测
observe(val);
// 定义响应式
Object.defineProperty(obj, key, {
get() {
console.log('get', key, obj);
return val;
},
set(newVal) {
if (newVal != val) {
console.log('set', newVal)
val = newVal;
}
}
})
}
// 遍历响应式处理
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return obj;
};
// 创建观测实例
new Observer(obj);
}
// 将传入的对象中的所有key都代理到指定的对象上
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(v) {
vm.$data[key] = v;
}
})
})
}
class Observer {
constructor(obj) {
if (Array.isArray(obj)) {
// 数组类型的响应式观测
} else {
// 非数组类型的响应式观测
this.walk(obj);
}
}
walk(obj) {
Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]));
}
}
class MyVue {
constructor(options) {
// 保存选项
this.$options = options;
this.$data = options.data;
// 对 data 实现数据响应式
observe(options.data);
// 做代理
proxy(this)
// 编译
new Compile(options.el, this)
}
}
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
// 如果节点存在,则开始编译
if (this.$el) {
this.compile(this.$el)
}
}
// 遍历node,判断节点类型,做不同处理
compile(node) {
const childNodes = node.childNodes;
// 遍历所有子节点
Array.from(childNodes).forEach(n => {
// 判断节点类型
if (this.isElement(n)) {
console.log('编译元素')
// 递归遍历是否有子元素
if (n.childNodes.length > 0) {
this.compile(n)
}
} else {
console.log('编译文本')
}
})
};
// 判断节点是否为 元素
isElement(n) {
return n.nodeType === 1
}
}
这时我们看下控制台的打印:
接下来我们继续完善Comile类的内容,如下:
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
this.$interRegExp = /\{\{(.*)\}\}/;
// 如果节点存在,则开始编译
if (this.$el) {
this.compile(this.$el)
}
}
// 遍历node,判断节点类型,做不同处理
compile(node) {
const childNodes = node.childNodes;
// 遍历所有子节点
Array.from(childNodes).forEach(n => {
// 判断节点类型为元素类型
if (this.isElement(n)) {
// 递归遍历是否有子元素
if (n.childNodes.length > 0) {
this.compile(n)
}
}
// 判断节点类型为文本节点且是插值表达式
else if (this.isInter(n)) {
// 编译插值文本
this.compileText(n);
}
})
};
// 判断节点是否为 元素
isElement(n) {
return n.nodeType === 1
};
// 判断是否是插值表达式形如:{{xxx}}
isInter(n) {
return n.nodeType === 3 && this.$interRegExp.test(n.textContent);
};
// 编译插值文本
compileText(n) {
// 拿到表达式
const interExp = n.textContent.match(this.$interRegExp)[1];
// 替换表达式内容
n.textContent = this.$vm[interExp]
}
}
这时html代码中的{{counter}}已经被被编译成1了,如下图:
实现了{{counter}}表达式后,我们继续实现k-text指令的编译。指令的编译肯定是在元素上的,所以当我们判断节点为元素的时候,就要遍历它所有的属性并做处理。这里我们增加一个compileElement方法去处理,如下:
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
this.$interRegExp = /\{\{(.*)\}\}/;
// 如果节点存在,则开始编译
if (this.$el) {
this.compile(this.$el)
}
}
// 遍历node,判断节点类型,做不同处理
compile(node) {
const childNodes = node.childNodes;
// 遍历所有子节点
Array.from(childNodes).forEach(n => {
// 判断节点类型为元素类型
if (this.isElement(n)) {
//编译指令
this.compileElement(n);
// 递归遍历是否有子元素
if (n.childNodes.length > 0) {
this.compile(n)
}
}
// 判断节点类型为文本节点且是插值表达式
else if (this.isInter(n)) {
// 编译插值文本
this.compileText(n);
}
})
};
// 判断节点是否为 元素
isElement(n) {
return n.nodeType === 1
};
// 判断是否是插值表达式形如:{{xxx}}
isInter(n) {
return n.nodeType === 3 && this.$interRegExp.test(n.textContent);
};
// 编译插值文本
compileText(n) {
// 拿到表达式
const interExp = n.textContent.match(this.$interRegExp)[1];
// 替换表达式内容
n.textContent = this.$vm[interExp]
};
// 编译元素:遍历所有属性拿到`k-`开头的指令做处理
compileElement(n) {
// 拿到所有属性
const attrs = n.attributes;
// 遍历属性,并判断是否为指令
Array.from(attrs).forEach(attr => {
const attrName = attr.name;
const attrValue = attr.value;
// 如当前属性是指令
if (this.isDir(attrName)) {
const dir = attrName.substring(2);
this[dir] && this[dir](n, attrValue)
}
})
};
// 指令 k-text 的处理函数
text(node, exp) {
node.textContent = this.$vm[exp];
};
// 判断是否为指令
isDir(attrName) {
return attrName.startsWith('k-');
}
}
这时再看我们的页面,如下图:
可以看到{{counter}和k-text="counter"都已经被编译了,nice ~~
问题:如果我们的
html代码加上个定时器每次累加counter,页面会实时更新吗?
<div id="app">
<p> {{counter}}</p>
<p k-text="counter"></p>
</div>
<script src="./src/myVue.js"></script>
<script>
const appVue = new MyVue({
el: "#app",
data: {
counter: 1
}
});
setInterval(_ => {
appVue.counter++
}, 1000);
</script>
结果是没有实时更新,如下图:
这是因为我们还没有实现依赖收集,接下来我们就看下如何实现依赖收集。
# 实现依赖收集 Watcher
在实现Watcher之前,我们先优化下我们的代码方便以后Watcher的调用。现在我们要把所有的更新操作全都提到update函数里,代码如下:
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
this.$interRegExp = /\{\{(.*)\}\}/;
// 如果节点存在,则开始编译
if (this.$el) {
this.compile(this.$el)
}
}
// 遍历node,判断节点类型,做不同处理
compile(node) {
const childNodes = node.childNodes;
// 遍历所有子节点
Array.from(childNodes).forEach(n => {
// 判断节点类型为元素类型
if (this.isElement(n)) {
//编译指令
this.compileElement(n);
// 递归遍历是否有子元素
if (n.childNodes.length > 0) {
this.compile(n)
}
}
// 判断节点类型为文本节点且是插值表达式
else if (this.isInter(n)) {
// 编译插值文本
this.compileText(n);
}
})
};
// 判断节点是否为 元素
isElement(n) {
return n.nodeType === 1
};
// 判断是否是插值表达式形如:{{xxx}}
isInter(n) {
return n.nodeType === 3 && this.$interRegExp.test(n.textContent);
};
// 编译插值文本
compileText(n) {
// 拿到表达式
const interExp = n.textContent.match(this.$interRegExp)[1];
// 替换表达式内容
this.update(n, interExp, 'text')
};
// 编译元素:遍历所有属性拿到`k-`开头的指令做处理
compileElement(n) {
// 拿到所有属性
const attrs = n.attributes;
// 遍历属性,并判断是否为指令
Array.from(attrs).forEach(attr => {
const attrName = attr.name;
const attrValue = attr.value;
// 如当前属性是指令
if (this.isDir(attrName)) {
const dir = attrName.substring(2);
this[dir] && this[dir](n, attrValue)
}
})
};
// 更新操作
update(node, exp, dir) {
// 初始化时
const fn = this[dir + 'Updater'];
fn && fn(node, this.$vm[exp]);
};
// 指令 k-text 的处理函数
text(node, exp) {
this.update(node, exp, 'text')
};
textUpdater(node, val) {
node.textContent = val;
};
// 指令 k-html 的处理函数
html(node, exp) {
this.update(node, exp, 'html')
};
htmlUpdater(node, val) {
node.innerHTML = val;
};
// 判断是否为指令
isDir(attrName) {
return attrName.startsWith('k-');
}
}
如上:所有动态的更新造作都没有直接更新,而是调用了update方法。updata方法主要干两件事:初始化时和数据更新时,现在我们只实现了初始化的操作。当初始化的时候,我通过拼接拿到了真正更新操作的函数xxUpdate,然后执行并更新。
准备操作做好了,我们开始实现依赖收集,这里主要是两个类:watcher 和 Dep,如下:
// 负责Dom更新
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.this.vm = updateFn;
}
// 更新Dom
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
// 保存Watcher实例的依赖类
class Dep {
constructor() {
this.deps = []
};
// dep: Watcher 的实例
addDep(dep) {
this.deps.push(dep)
};
// 执行更新
notify() {
this.deps.forEach(dep => dep.update())
}
}
到这里有个问题:依赖收集在哪开始呢?
答案就是在defineReactive函数中创建的,在此之前我们还得保存下Watcher实例,并出发一下get函数,如下:
// 负责Dom更新
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.this.vm = updateFn;
//触发get
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
// 定义响应式属性
function defineReactive(obj, key, val) {
// 递归监测
observe(val);
// 创建Dep实例
const dep = new Dep();
// 定义响应式
Object.defineProperty(obj, key, {
get() {
console.log('get', key, obj);
Dep.target && dep.addDep(Dep.target)
return val;
},
set(newVal) {
if (newVal != val) {
console.log('set', newVal)
val = newVal;
//执行更新
dep.notify();
}
}
})
}
上面我们提到,Compile 类里边的update方法主要干了来两件事:初始化和更新,所以我们要再实现下更新操作,如下:
// 更新操作
update(node, exp, dir) {
// 初始化时
const fn = this[dir + 'Updater'];
fn && fn(node, this.$vm[exp]);
// 数据更新时
new Watcher(this.$vm, exp, val => {
fn && fn(node, val);
})
};
至此已经创建Watcher、收集依赖以及依赖和Watcher怎么对应起来,页面执行结如下图:
完成代码如下:
// 定义响应式属性
function defineReactive(obj, key, val) {
// 递归监测
observe(val);
// 创建Dep实例
const dep = new Dep();
// 定义响应式
Object.defineProperty(obj, key, {
get() {
console.log('get', key, obj);
// 依赖关系的收集
Dep.target && dep.addDep(Dep.target)
return val;
},
set(newVal) {
if (newVal != val) {
console.log('set', newVal)
val = newVal;
//执行更新
dep.notify();
}
}
})
}
// 遍历响应式处理
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return obj;
};
// 创建观测实例
new Observer(obj);
}
// 将传入的对象中的所有key都代理到指定的对象上
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(v) {
vm.$data[key] = v;
}
})
})
}
class Observer {
constructor(obj) {
if (Array.isArray(obj)) {
// 数组类型的响应式观测
} else {
// 非数组类型的响应式观测
this.walk(obj);
}
}
walk(obj) {
Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]));
}
}
class MyVue {
constructor(options) {
// 保存选项
this.$options = options;
this.$data = options.data;
// 对 data 实现数据响应式
observe(options.data);
// 做代理
proxy(this)
// 编译
new Compile(options.el, this)
}
}
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
this.$interRegExp = /\{\{(.*)\}\}/;
// 如果节点存在,则开始编译
if (this.$el) {
this.compile(this.$el)
}
}
// 遍历node,判断节点类型,做不同处理
compile(node) {
const childNodes = node.childNodes;
// 遍历所有子节点
Array.from(childNodes).forEach(n => {
// 判断节点类型为元素类型
if (this.isElement(n)) {
//编译指令
this.compileElement(n);
// 递归遍历是否有子元素
if (n.childNodes.length > 0) {
this.compile(n)
}
}
// 判断节点类型为文本节点且是插值表达式
else if (this.isInter(n)) {
// 编译插值文本
this.compileText(n);
}
})
};
// 判断节点是否为 元素
isElement(n) {
return n.nodeType === 1
};
// 判断是否是插值表达式形如:{{xxx}}
isInter(n) {
return n.nodeType === 3 && this.$interRegExp.test(n.textContent);
};
// 编译插值文本
compileText(n) {
// 拿到表达式
const interExp = n.textContent.match(this.$interRegExp)[1];
// 替换表达式内容
this.update(n, interExp, 'text')
};
// 编译元素:遍历所有属性拿到`k-`开头的指令做处理
compileElement(n) {
// 拿到所有属性
const attrs = n.attributes;
// 遍历属性,并判断是否为指令
Array.from(attrs).forEach(attr => {
const attrName = attr.name;
const attrValue = attr.value;
// 如当前属性是指令
if (this.isDir(attrName)) {
const dir = attrName.substring(2);
this[dir] && this[dir](n, attrValue)
}
})
};
// 更新操作
update(node, exp, dir) {
// 初始化时
const fn = this[dir + 'Updater'];
fn && fn(node, this.$vm[exp]);
// 数据更新时
new Watcher(this.$vm, exp, val => {
fn && fn(node, val);
})
};
// 指令 k-text 的处理函数
text(node, exp) {
this.update(node, exp, 'text')
};
textUpdater(node, val) {
node.textContent = val;
};
// 指令 k-html 的处理函数
html(node, exp) {
this.update(node, exp, 'html')
};
htmlUpdater(node, val) {
node.innerHTML = val;
};
// 判断是否为指令
isDir(attrName) {
return attrName.startsWith('k-');
}
}
// 负责Dom更新
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
//触发get
Dep.target = this;
this.vm[this.key];
Dep.target = null;
};
// 通知视图进行更新
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
// 保存Watcher实例的依赖类
class Dep {
constructor() {
this.deps = []
};
// dep: Watcher 的实例,创建依赖关系时调用
addDep(dep) {
this.deps.push(dep)
};
// 执行更新
notify() {
this.deps.forEach(dep => dep.update())
}
}
到这里两篇文章主要模拟了数据驱动和指令的变编译,其中涉及到响应式处理、依赖收集和Watcher的创建等操作,虽是模拟造作,但也有源码的核心思想,希望对你理解Vue有所帮助,欢迎评论留言,nice ~~