其实编译这部分是应该首先写的,因为在执行$mount后首先进行的工作就是编译。不过实现响应式是核心,现在我们把编译这部分补上,实现之后我们就能看到实实在在的效果。
实现编译我们需要一个compile类,我们首先确定需要接受的参数,实现编译我们需要知道将要编译的点,以及new Vue()后的实例。我们在构造函数中将参数保存一下。然后先将需要编译的节点剪切到新创建的fragment,这样我们就不会操作dom节点。我们将这个fragment编译之后再追加到原来的节点上,这就是大体流程。
//new Compile(el,vm)
class Compile {
constructor(el, vm) {
this.$el = document.querySelector(el);
this.$vm = vm
this.$fragment = this.convertFragment(this.$el)
this.compile(this.$fragment)
this.$el.appendChild(this.$fragment)
}
}
接下来我们动手实现这两个方法。convertFragment()每次将传入节点的第一个子节点赋予我们创建的fragment上。
convertFragment(el) {
const fragment = document.createDocumentFragment()
let child;
while (child = el.firstChild) {
fragment.appendChild(child)
}
return fragment;
}
剪切完成之后我们对返回的结果进行编译,对每一个子节点进行类型判断,这里有几种情况,一种是节点类型为1,这是元素类型,另一种节点类型等于3,这是文本类型。一个是双大括号语法,一个是v-xxx类型的指令,还有@开头的事件处理。我们的judgeType()对此进行分类然后分别进行处理。另外我们要对子节点进行判断,如果向下还有子节点的话我们要做一个递归处理。
compile(fragment) {
//开始遍历子元素,将特殊定义的语法翻译
const childNode = fragment.childNodes;
Array.from(childNode).forEach((node) => {
//类型判断开始
this.judgeType(node)
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
接下来我们动手实现这个judgeType(),上面说了,当节点类型为1时我们要取出来里面的属性,是v-xxx类型的指令,还有@开头的事件处理我们都是要在元素类型这里面进行处理。另一个双大括号的语法我们在文本类型里面处理。 我们先实现文本类型的处理,完成后我们可以看到实实在在的效果,我们敲了这么多代码别再出错了。 下面的代码利用正则拿到的RegExp.$1,就是data里面的属性。
judgeType(node) {
if (node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)) {
node.textContent = this.$vm.$data[RegExp.$1]
}
}
然后我们回到LinkVue.js,在构造函数的最后添加compile的初始化。
constructor(option){
this.$el = option.el;
this.$data = option.data;
this.observe(this.$data)
new Compile(this.$el,this)
}
回到index.html,在app下添加
<div id="app">
<p>{{firstdata}}</p>
<p>{{seconddata}}</p>
<p>{{thirddata}}</p>
</div>
同样的我们在data中添加这三个属性
const app = new LinkVue({
el: "#app",
data: {
firstdata: "have a try",
seconddata: "why not",
thirddata: "It will pay off",
}
})
在浏览器中打开index.html,

虽然我们成功的让data中的属性显示了出来,但是这个并不是响应式的,我们还没有将watcher加到里面。此时我们需要一个update函数,这个函数可以说是我们这个compile.js里面最重要的函数。
//重点,update函数,使数据可以响应式
update(node, vm, dataName, dir) {
const updater = this[dir + "Updater"]
updater && updater(node, this.$vm.$data[dataName])
//依赖收集
new Watcher(vm, dataName, value => {
updater && updater(node, value)
})
}
对应的,我们会将judgeType()中的对文本节点的处理修改成如下方法
this.update(node, this.$vm, RegExp.$1, "text")
回到class Watcher,需要修改一下构造函数,接收更新所需的参数。
//Watcher
class Watcher{
constructor(vm,key,callback){
//将当前watcher的实例指定到Dep的静态属性target下
this.vm = vm;
this.key = key;
this.callback = callback;
Dep.target = this;
this.vm.$data[this.key]//读一下,触发getter
Dep.target = null;
}
update(){
this.callback.call(this.vm,this.vm.$data[this.key])
}
}
上面update方法有一个textUpdater方法我们需要补写一下
textUpdater(node, value) {
node.textContent = value
}
这样我们就将data中大括号的内容变成了响应式的。
然后我们处理元素节点。拿出元素节点的属性集合,遍历后拿到属性的name,对这个name进行判断,如果这个name包含l-(这个我们可以自己定义,与vue的v-进行对比),则进行指令的处理。如果包含@(暂时处理@的事件处理,原理是一样的)则进行相应的事件处理。
judgeType(node) {
if (node.nodeType === 1) {
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(attrs => {
const attrsName = attrs.name;
const attrsValue = attrs.value;
//指令
if (attrsName.indexOf('l-') == "0") {
const dir = attrsName.substring(2);
this[dir] && this[dir](node, attrsValue)
}
//事件
if (attrsName.indexOf('@') == "0") {
const dir = attrsName.substring(1);
this.eventHandler(node, dir, attrsValue)
}
})
}
if (node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)) {
node.textContent = this.$vm.$data[RegExp.$1]
}
}
上面的代码有几个方法需要我们补一下,一个是发现属性名是l-开头的之后的执行方法,一个是@开头的事件处理的方法。我们写一个最为典型的v-model,相应的,当我们进行编译时发现指令为l-model时,我们需要一个model方法。
model(node, dataName) {
this.update(node, this.$vm, dataName, "model")
node.addEventListener("input", e => {
this.$vm.$data[dataName] = e.target.value
})
}
与之对应需要一个modelUpdater()
modelUpdater(node, value) {
node.value = value
}
我们修改index.html,新增一个input,使用指令l-model,
<input type="text" l-model="firstdata">
输入后我们发现它会与上面双大括号里的firstdata同步变化,这代表我们的v-model也实现了。 最后一步,我们把之前的@事件处理再补充一下
eventHandler(node, eventtype, methodName) {
let func = this.$options.methods && this.$options.methods[methodName]
if (methodName && func) {
node.addEventListener(eventtype, func.bind(this.$vm))
}
}
到这里基本上迷你的vue就已经实现了,不过还有很多细节没有处理,例如data中对象的深度监听,data中的数据代理到实例上,数组的监听,自定义组件的处理等等,有兴趣的同学可以自己尝试写一下。当你跟着写完这份miniVue之后,相信再看vue的源码收获应该会更大。
每天进步一点,坚持下去,肯定会有变化的。
最后,代码放在了https://github.com/MrLinkang/miniVue。emm,要是能star一下那就更好了
