一步一步手撸一个miniVue(二)

217 阅读4分钟

其实编译这部分是应该首先写的,因为在执行$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一下那就更好了