使用原生js编写Vue双向数据驱动(1)

11 篇文章 0 订阅
10 篇文章 0 订阅

观察者模式与数据劫持部分还没完成,有时间在继续更新

function Vue(options){
    this.$el = options.el;
    this.$data = options.data;
    if(this.$el){//若这个el节点存在不为空,则编译模板
        //将data中的数据 全部转化成Object.defineProperty来定义 这样我们可以做到数据劫持 在观察者模式下我们可以实时感受到数据的变化
        new Observer(this.$data);
        //用来编译模板 将模板中对应的数据渲染出来 比如{{school.name}} 渲染出真正的值
        new Compiler(this.$el,this);
    }
}





function Observer(data){
    this.observer(data);
}
Observer.prototype.observer = function(data){
    if(data && typeof data == "object"){
        for(let key in data){
            this.defineReactive(data,key,data[key]);
        }
    }
}
/**
 * Object.defineProperty 是以树蕨劫持的方式来定义值
 * 这里就是为obj(data)对象来定义一个名为key(school)的变量 这个变量的结果就是 return value
 * 我们传统的定义方式是 let a = 10 改成数据劫持的方式就是 Object.defineProperty(window,10,{get(){return 10}})
 * 为window对象定义名为a的变量 它的结果为10
 */
Observer.prototype.defineReactive = function(obj,key,value){
    this.observer(value);
    Object.defineProperty(obj,key,{
        get(){
            return value
        },
        set:(newValue)=>{
            if(newValue!=value){
                this.observer(newValue);
                newValue = value;
            }
        }
    })
}








/**  
 @el 初始的父节点
 @vm 指向主类 Vue函数 用来调用Vue函数中的成员
 @tip 这个函数主要用来编译模板
*/
function Compiler(el,vm){
    this.vm = vm;
    this.el = this.isElementNode(el)?el:document.querySelector(el);
    //把当前节点中的元素全部获取到 然后放到内存当中去
    let fragment = this.node2fragment(this.el);

    //把节点中的内容进行替换

    //编译模板 用数据编译
    this.compile(fragment);
    //把内存当中的节点片段重新添加到主节点el中
    this.el.appendChild(fragment);
    // console.log(fragment);
}
/**
 * @attrName 属性名
 * @tip startsWith 用来判断某个字符是以什么开头 若符合则返回true
 */
Compiler.prototype.isDirective=function(attrName){
    return attrName.startsWith("v-");
}

/**
 * @tip 用来编译dom节点 判断dom节点中是否有v-开头的元素 若有则进行相应的数据操作
 * @tip [,directive] = name.split("-"); 为es6新增语法 split是将字符串以某种形式分割开来
 * 然后以数组的形式存储  这里用横线分割 分割后会生成一个有两个值的数组第一个值是横线之前
 * 的字符 第二个值是横线之后的字符 这里[,directive] 逗号之前的位置存储第一个字符,后面的就
 * 存储第二个字符 这里我们不需要第一个字符 所以不用管,只写存储第二个字符的变量名
 * @tip CompilerUtil[directive](node,value,this.vm) 就等同于
 * CompilerUtil["model"](node,value,this.vm) 这种json对象格式的可以用这种方式调用
 */
Compiler.prototype.compileElement = function(node){
    //node.attributes 就是我们定义在标签上的所有属性
    let attributes = node.attributes;
    // console.log([...attributes]);
    [...attributes].forEach(attr=>{
        //因为获取到的伪数组转换后是以键值对的方式存在的 所以我们可以获取key 和value
        //{name,value} es6的语法 这句意思是将attr数组中的名字为name和value的0值赋给对应的变量
        let {name,value} = attr;
        if(this.isDirective(name)){
            let [,directive] = name.split("-");
            CompilerUtil[directive](node,value,this.vm);
            // console.log(directive,node);
        }
    })
    // console.log(attributes);//
}

/**
 * @tip 用来编译文本节点 即空白位置 判断是否有{{}}存在 若有则进行相应的操作
 * @tip textContent es6新增语法 用来获取节点中的文本内容
 * @tip /\{\{.+?\}\}/ 即匹配一组花括号 .+?表示只匹配括号之间的内容
 */
Compiler.prototype.compileText = function(node){
    let content = node.textContent;
    //若找到了一组花括号 并且里面有至少一个内容
    if(/\{\{.+?\}\}/.test(content)){
        // console.log(content);
        CompilerUtil["text"](node,content,this.vm);
    }
}

/**
 * 编译工具
 * 用来根据 指令进行是编译操作 例如model 就是v-model 进行双向绑定操作 用来编译数据到表单上
 * @model 进行双向绑定的数据编译工作
 * @text 为空白出的文本节点进行数据编译工作 如{{school.name}}
 */
let CompilerUtil = {
    /** 
        这个函数用来获取对象指向的值 例如 school.name值是多少
        或者是school.id的值
    */
    getVal(vm,value){
        return value.split(".").reduce((data,current)=>{
            return data[current];
        },vm.$data)
    },
    //node是有v-属性指令的节点 value是属性指令的值例如(school.name) vm是当前Vue实例
    model(node,value,vm){
        let fn = this.updater["modelUpdater"];//首先获取到更新v-model表单的方法
        let values = this.getVal(vm,value);//然后获取到data中的确定的值 比如shcool.name的值
        fn(node,values);//然后将节点和获取到的值传过去
    },
    /**
     * 
     * @param {*} node 
     * @param {*} content 
     * @param {*} vm 
     * @tip 这里需要注意  replace在使用回调函数时会有四个参数
     * 但箭头函数没有实参列表,直接获取是获取不到这四个参数的, 所以我们可以使用es6的三点运算符,
     * 在函数括号中使用时,可以将函数多余的参数组合成一个数组来存储,数组名写在三个点的后面
     */
    text(node,content,vm){
        //首先获得更新文本节点的函数
        let fn = this.updater["textUpdater"];
        //用正则表达式来获得匹配到的每一个{{}}中的数据
        let contents = content.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            //然后将数据传到getVal中 用来获取数据的真实值 例如shcool.name,或是shcool.id
            return this.getVal(vm,args[1]);
        });
        //然后将获取到的真实结果和文本节点传递到文本节点更新函数中
        fn(node,contents);
    },
    updater:{
        /**
         * @param {*} node 
         * @param {*} value 
         * @tip 用来更新双向绑定的表单的值
         */
        modelUpdater(node,value){
            node.value = value;
        },
        /**
         * @param {*} node 
         * @param {*} content 
         * @tip 用来更新文本节点的内容
         */
        textUpdater(node,content){
            node.textContent = content;
        }
    }
}

/**
 * @node 获取到的主节点el中的所有的元素 
 * @tip 注意 空白的位置也属于一个节点 即文本节点
 */
Compiler.prototype.compile = function(node){
    //这可以获取到一个类数组 即伪数组
    //childNodes获取到的是当前子节点 但是只获取第一场 例如子节中还有子节点则不会获取
    var childNode = node.childNodes;
    //我们需要将类数组转换为真正的数组 可以使用es6新增的扩展运输符[...]
    //或者使用 Array.from(需要转换的数组) 也是es6语法
    [...childNode].forEach(child=>{
        if(this.isElementNode(child)){//判断是否是元素 若是元素进行元素的数据编译工作
            this.compileElement(child);
            //如果这个节点是一个dom元素的话 那么便把它传进去 看他里面是否还有子节点存在
            this.compile(child);
        }else{//否则进行文本的数据编译工作
            this.compileText(child);
        }
    })    
}


/** 
 @node 获取到节点,用来判断这个node是否是真实的dom 还是字符串
 @tip 这个方法主要用来判断传过来的节点是否是真实的dom
*/
Compiler.prototype.isElementNode=function(node){
    //nodeType是dom元素上的一个属性 它的值为1
    //这里如果这个node是一个dom元素的话那么它便会返回true
    //如果是一个字符串的话它便不会存在这个属性 便返回false
    return node.nodeType === 1;
}

/**
 * @node 获取到的真实父节点 #app
 * @tip 这个方法主要用来把节点中的片段移动到内存当中
 * @tip appendChild 添加节点 如果该节点已经存在,则从当前父节点中删除,然后添加到新的父节点。
 * @tip document.createDocumentFragment() 即文档碎片 我们可以先将所有的节点移动到文档
 * 碎片上 然后一次性添加到body中 否则一条一条的添加很影响性能
 */
Compiler.prototype.node2fragment = function(node){
    let fragment = document.createDocumentFragment();
    let firstChild;
    //没循环一次将主节点中的第一个片段赋值给 firstChild
    //然后将firstChild重新移动到内存当中 即fragment中
    while(firstChild = node.firstChild){
        //appendChild 不仅可以添加一个节点 还可以移动节点
        fragment.appendChild(firstChild);
    }
    return fragment;
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>mvvm</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="school.id"/>
        <input type="text" v-model="school.name"/>
        {{school.name}}{{school.id}}
        <div>{{school.name}}</div>
        <div>{{school.id}}</div>
        <ul>
            <li></li>
            <li></li>
            <li></li>
        </ul>
    </div>
    <script type="text/javascript" src="mvvm.js"></script>
    <script type="text/javascript">
        let vm = new Vue({
            el:"#app",
            data:{
                school:{
                    name:"财经院校"
                    ,id:"1号"
                }
            }
        })

    </script>
</body>
</html>

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值