Vue 源码(一)

162 阅读1分钟

一、前言

解析vue源码之前,先写一个简单版的双向绑定原理代码,有利于之后后续源码的理解,涉及到的知识点有Object.defineproperty。

1 Object.defineproperty 用法

  • Object.defineProperty(obj, prop, descriptor)
    obj是要定义属性的对象,prop指要定义和修改的属性名称或Symbol ,descriptor指定义和修改的属性描述符。
    get是属性的getter函数,返回值会用作属性的值,如果没有getter,属性值为undefined。访问该属性的时候会调用这个函数
    set是属性的setter函数,参数为新的属性值,如果没有setter,属性值为undefined。修改该属性的时候会调用这个函数

二、思路

首先要使数据变得可以监测,利用Object.defineproperty的get和set可以实现这一点。那么怎样监测DOM中的数据呢?需要compile解析出双花括号中和v-bind等指令的属性名,从而获取改属性名对应的属性,添加到dom中的node或者attr的value中。
数据变化后,怎样更新到视图中呢?通过new watch(vm,key,cb)收集依赖,而Dep是订阅器,在get函数中添加订阅者。数据更新后,在set函数中,Dep遍历订阅者并更新函数。

三、具体实现

class Vue{
    constructor(options){
        this.$options = options;
        this.$data = options.data;
        this.observer(this.$data);
        //代理不需要递归
        Object.keys(this.$data).forEach(key=>{
            this.proxyData(key);
        })
        new Compile(this.$options.el,this)
    }
    observer(obj){
        if(!obj || typeof(obj) !=='object'){
            return;
        }
        Object.keys(obj).forEach(key=>{
            this.defineReactive(obj,key,obj[key]);
        })
    }
    defineReactive(obj,key,value){
        //递归嵌套object
        let dep = new Dep();
        // 一个key对应一个dep,但有可能对应多个watcher
        Object.defineProperty(obj,key, {
            get(){
                Dep.target && dep.addSub();
                return value;
            },
            set(newval){
                if(value==newval){
                    return;
                }
                value = newval;
                dep.notify();
            }
        })
        this.observer(value);
    }
    proxyData(key){
        Object.defineProperty(this,key,{
            get(){
                return this.$data[key];
            },
            set(newval){
                // this.key设置的newvalue赋值在data上,get返回代理到vm上
                this.$data[key] = newval;
            }
        })
    }
}
// 订阅器,收集和更新订阅者
class Dep{
    constructor(){
        this.subs = [];
    }
    addSub(){
        if(Dep.target){
            this.subs.push(Dep.target);
        }
    }
    notify(){
        this.subs.forEach(sub=>{
            sub.update();
        })
    }
}
class Watch{
    constructor(vm,key,cb){
        this.vm = vm;
        this.key = key;
        this.cb = cb;
        this.value = this.get();
    }
    get(){
        Dep.target = this;
        const value = this.vm.$data[this.key];
        Dep.target = null;
        return value;
    }
    update(){
        let newval = this.vm.$data[this.key];
        if(this.value !== newval){
            this.value = newval;
        }
        this.cb(this.value);
    }
}
class Compile{
    constructor(el,vm){
        this.el = document.querySelector(el);
        this.vm = vm;
        this.fragNodes = this.getFragmentNodes();
        this.compileFragment(this.fragNodes);
        this.el.appendChild(this.fragNodes);
    }
    // 获取文档碎片
    getFragmentNodes(){
        let fragNodes = document.createDocumentFragment();
        while(this.el.firstChild){
            fragNodes.appendChild(this.el.firstChild);
        }
        return fragNodes;
    }
    compileFragment(nodes){
        let childNodes = nodes.childNodes;
        const self = this;
        // es5转换类数组方法:[].slice.call(类数组) Array.prototype.slice.call(类数组)
        Array.from(childNodes).forEach(node=>{
            if(this.isTextNode(node)){
                // .+?一旦匹配到就不继续搜寻,惰性模式 exec可返回()里面匹配的值
                let reg = /\{{2}(.+?)\}{2}/g,
                    t = node.textContent,
                    r = reg.exec(t),
                    key;
                    if(r){
                        key = r[1].trim();
                        this.CompileText(node,key);
                    }
            }else if(this.isElement(node)){
                let nodeAttrs = node.attributes;
                if(nodeAttrs.length>0){
                    Array.from(nodeAttrs).forEach(nodeAttr=>{
                        const attrName = nodeAttr.name;
                        if(this.isBindDirective(attrName)){
                            this.CompileBind(nodeAttr,nodeAttr.value);
                        }
                    })
                }
                
            }
            if(node.childNodes && node.childNodes.length>0){
                self.compileFragment(node);
            }
        })
        
    }
    CompileText(node,key){
        let val = this.vm.$data[key];
        this.updateText(node,key,val)
    }
    updateText(node,key,val){
        // get 新data
        node.textContent = val;
        // set 新data
        new Watch(this.vm,key,function(newval){
            node.textContent = newval;
        })
    }
    isBindDirective(attrName){
        const reg =/v\-bind|^\:/g;
        return reg.exec(attrName);
    }
    CompileBind(nodeAttr,key){
        let val = this.vm.$data[key];
        nodeAttr.value = val;
        new Watch(this.vm,key,function(newval){
            nodeAttr.value = newval;
        })
    }
    isTextNode(node){
        return node.nodeType === 3;
    }
    isElement(node){
        return node.nodeType === 1;
    }
}
let vm = new Vue({
	el:'#app',
	data:{
	    name:'banana',price:'$10',place:'越南'
	}
})