用了Vue也有两年时间了,一直以来都是只知其然,不知其所以然,为了能更好的使用Vue不被Vue所奴役,学习一下Vue底层的基本原理。

Vue官网有一段这样的介绍:当你把一个普通的JavaScript对象传给Vue实例的data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setterObject.definePropertyES5中一个无法shim的特性,这也就是为什么Vue不支持 IE8 以及更低版本浏览器。

通过这一段的介绍不难可以得出,Vue是通过Object.defineProperty对实例中的data数据做了挟持并且使用Object.definePropertygetter/setter并对其进行处理之后完成了数据的与视图的同步。

这张图应该不会很陌生,熟悉Vue的同学如果仔细阅读过Vue文档的话应该都看到过。猜想一下Vue使用Object.defineProperty做为ViewModel,对数据进行挟持之后如果ViewModel发生变化的话,就会通知其相对应引用的地方进行更新处理,完成视图的与数据的双向绑定。

下面举个例子:

html:

<div id="name"></div>

javaScript:

var obj = {};
Object.defineProperty(obj,"name",{
    get() {
        return document.querySelector("#name").innerHTML;
    },
    set(val) {
        document.querySelector("#name").innerHTML = val;
    }
})
obj.name = "Aaron";

通过上面的代码使用Object.definePropertyObj对象中的name属性进行了挟持,一旦该属性发生了变化则会触发set函数执行,做出响应的操作。

扯了这么多,具体说一下Vue实现的原理。

  1. 需要数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者。
  2. 需要指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
  3. 一个Watcher,作为连接ObserverCompile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
  4. MVVM入口函数,整合以上三者,实现数据响应。

接下来的文章将沿着这个思路一步一步向下进行,以便完成一个简单的Vue类,完成数据与视图的实时更新。

<div id="app">
    <p>{{name}}</p>
    <p q-text="name"></p>
    <p>{{age}}</p>
    <p>{{doubleAge}}</p>
    <input type="text" q-model="name"/>
    <button @click="changeName">点击</button>
    <div q-html="html"></div>
</div>
<script>
new QVue({
    el:"#app",
    data:{
        name:"I am test",
        age:12,
        html:"<button>这是一个后插入的按钮</button>"
    },
    created(){
        console.log("开始吧,QVue");
        setTimeout(() => {
            this.name = "测试数据,更改了么";
        },2000)
    },
    methods:{
        changeName(){
            this.name = "点击啦,改变吧";
            this.age = 1000000;
        }
    }
})
</script>

以上代码则是需要完成的功能,保证所有功能全部都能实现。

首先我们要考虑的是,要创建一个Vue的类,该类接收的是一个options的对象,也就是我们在实例化Vue的时候需要传递的参数。

class QVue {
    constructor(options){
        //  缓存options对象数据
        this.$options = options;
        //  取出data数据,做数据响应
        this.$data = options.data || {};
    }
}

通过上面的代码可以看出了,为什么我们可以在Vue实例上通过this.$data拿到我们所写的data数据。

对数据已经进行了缓存之后,接下来要做的事情就是对数据进行观察,达到数据变化之后能够做出对虚拟Dom的操作。

class QVue {
    constructor(options){
        this.$options = options;
        //  数据响应
        this.$data = options.data || {};
        //  监听数据变化
        this.observe(this.$data);
        //  主要用来解析各种指令,比如v-modal,v-on:click等指令
        new Compile(options.el,this);
        //  执行生命周期
        if(options.created){
            options.created.call(this);
        }
    }
    // 观察数据变化
    observe(value){
        if(!value || typeof value !== "object"){
            return;
        }
        let keys = Object.keys(value);
        keys.forEach((key)=> {
            this.defineReactive(value,key,value[key]);
            //  代理data中的属性到vue实例上
            this.proxyData(key);
        })
    }
    //  代理Data
    proxyData(key){
        Object.defineProperty(this,key,{
            get(){
                return this.$data[key];
            },
            set(newVal){
                this.$data[key] = newVal;
            }
        })
    }
    //  数据响应
    defineReactive(obj,key,val){
        //  解决数据层次嵌套
        this.observe(val);
        const dep = new Dep();
        Object.defineProperty(obj, key,{
            get(){
                //  向管理watcher的对象追加watcher实例
                //  方便管理
                Dep.target && dep.appDep(Dep.target);
                return val;
            },
            set(newVal){
                if(newVal === val){
                    return;
                }
                val = newVal;
                // console.log(`${key}更新了:${newVal}`)
                dep.notify();
            }
        })
    }
}

我们对data数据中的每一项都进行了数据挟持,可是然而并没有什么卵用啊,我们并没有对相对应的虚拟dom进行数据改变,当然我们肯定是不能把我们的需要更改的虚拟dom操作写在这里,然而在Vue中对其Dom进行了特殊的处理,慢慢的向下看。

想要做数据响应要做一个做具体更新的类何以用来管理这些观察者的类

//  管理watcher
class Dep {
    constructor() {
        //  存储
        this.deps = [];
    }
    //  添加watcher
    appDep(dep){
        this.deps.push(dep);
    }
    //  通知所有的watcher进行更新
    notify(){
        this.deps.forEach((dep) => {
            dep.update();
        })
    }
}
//  观察者 做具体更新
class Watcher {
    constructor(vm,key,cb){
        //  Vue实例
        this.vm = vm;
        //  需要更新的key
        this.key = key;
        //  更新后执行的函数
        this.cb = cb;
        //  将当前watcher实例指定到Dep静态属性target
        //  用来在类间进行通信
        Dep.target = this;
        //  触发getter,添加依赖
        this.vm[this.key];
        Dep.target = null;
    }
    update(){
        this.cb.call(this.vm,this.vm[this.key]);
    }
}

Dep.target = this上面这段代码一定要注意,是向Dep类中添加了一个静态属性。

主要用来解析各种指令,比如v-modalv-on:click等指令。然后将模版中的变量替换成数据,渲染view,将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据发生变动,收到通知,更新视图。

简单说下双向绑定,双向绑定原理,在编译的时候可以解析出v-model在做操作的时候,在使用v-model元素上添加了一个事件监听(input),把事件监听的回调函数作为事件监听的回调函数,如果input发生变化的时候把最新的值设置到vue的实例上,因为vue已经实现了数据的响应化,响应化的set函数会触发界面中所有依赖模块的更新,然后通知哪些model做依赖更新,所以界面中所有跟这个数据有管的东西就更新了。

class Compile {
    constructor(el,vm) {
        //  要遍历的宿主节点
        this.$el = document.querySelector(el);
        this.$vm = vm;

        //  编译
        if(this.$el){
            //  转换宿主节点内容为片段Fragment元素
            this.$fragment = this.node2Fragment(this.$el);
            //  执行编译过程
            this.compile(this.$fragment);
            //  将编译完的HTML结果追加至宿主节点中
            this.$el.appendChild(this.$fragment);
        }
    }

    //  将宿主元素中代码片段取出来,遍历,这样做比较高效
    node2Fragment(el){
        const frag = document.createDocumentFragment();
        //  将宿主元素中所有子元素**(搬家,搬家,搬家)**至frag中
        let child;
        //  如果 el.firstChild 为undefined或null则会停止循环
        while(child = el.firstChild){
            frag.appendChild(child);
        }
        return frag;
    }

    compile(el){
        //  宿主节点下的所有子元素
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach((node) => {
            if(this.isElement(node)){
                //  如果是元素
                console.log("编译元素"+node.nodeName)
                //  拿到元素上所有的执行,伪数组
                const nodeAttrs = node.attributes;
                Array.from(nodeAttrs).forEach((attr) => {
                    //  属性名
                    const attrName = attr.name; 
                    //  属性值
                    const exp = attr.value;     
                    //  如果是指令
                    if(this.isDirective(attrName)){
                        //  q-text
                        //  获取指令后面的内容
                        const dir = attrName.substring(2);
                        //  执行更新
                        this[dir] && this[dir](node,this.$vm,exp);
                    }
                    //  如果是事件
                    if(this.isEvent(attrName)){
                        //  事件处理
                        let dir = attrName.substring(1);    //  @
                        this.eventHandler(node,this.$vm,exp,dir);
                    }
                })
            }else if(this.isInterpolation(node)){
                //  如果是插值文本
                this.compileText(node);
                console.log("编译文本"+node.textContent)
            }
            //  递归子元素,解决元素嵌套问题
            if(node.childNodes && node.childNodes.length){
                this.compile(node);
            }
        })
    }
    //  是否为节点
    isElement(node){
        return node.nodeType === 1;
    }
    //  是否为插值文本
    isInterpolation(node){
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
    }
    //  是否为指令
    isDirective(attr){
        return attr.indexOf("q-") == 0;
    }
    // 是否为事件
    isEvent(attr){
        return attr.indexOf("@") == 0;
    }

    //  v-text
    text(node,vm,exp){
        this.update( node, vm, exp, "text");
    }
    textUpdater(node,value){
        node.textContent = value;
    }

    //  双向绑定
    //  v-model
    model(node,vm,exp){
        //  指定input的value属性,模型到视图的绑定
        this.update(node,vm,exp,"model");
        //  试图对模型的响应
        node.addEventListener('input',(e) => {
            vm[exp] = e.target.value;
        })
    }
    modelUpdater(node,value){
        node.value = value;
    }

    //  v-html
    html(node,vm,exp){
        this.update(node,vm,exp,"html")
    }
    htmlUpdater(node,value){
        node.innerHTML = value;
    }
    
    //  更新插值文本
    compileText(node){
        let key = RegExp.$1;
        this.update( node, this.$vm, key, "text");
    }
    //  事件处理器
    eventHandler(node,vm,exp,dir){
        let fn = vm.$options.methods && vm.$options.methods[exp];
        if(dir && fn){
            node.addEventListener(dir,fn.bind(vm));
        }
    }

    //  更新函数 - 桥接
    update(node,vm,exp,dir){
        const updateFn = this[`${dir}Updater`];
        //  初始化
        updateFn && updateFn(node,vm[exp]);
        //  依赖收集
        new Watcher(vm,exp,function(value){
            updateFn && updateFn(node,value);
        })
    }
}

其实Compile整个编译过程,就是在做一个依赖收集的工作,然Vue知道每一个指令是做什么的。并做出对应的更新处理。

Vue整体的编译过程,因为vue所编写的指令html无法进行识别,通过编译的过程可以进行依赖收集,依赖收集以后把data中的数据和视图进行了关联,产生了依赖关系,如果以后数据模型发生变化我们可以通过这些依赖通知这些视图进行更新,这是执行编译的目的,就可以做到数据模型驱动视图变化。