手写Vue之MyCompile

这一节主要是如何对{{xxx}}表达式和指令进行编译

一.代码结构

 

二.主要流程

下面我们就按照上面的代码流程来解析。

 

三.dom元素转文档碎片

位于MyCompile的prototype中

 

四.编译文档碎片

不断递归所有的dom元素,每次到一个dom元素,判断是否是文本节点,如果是,进行表达式编译。

如果不是,则取出这个节点的所有属性,如果是事件绑定指令,进行事件板顶。如果是普通指令,则进行数据绑定。

这里的数据绑定还只是当向数据绑定。

 

 

下面看看对于文本标签怎么去做表达式解析:

用正则匹配出花括号里面对应的表达式,然后调用工具类的text方法。

分析一下调用联调:compileText -》 CompileUtril.text =》 bind =》 Updater.textUpdater

 

 

下面分析一下事件绑定指令

取出所有属性,对每个属性做处理,去掉属性前缀,判断是普通指令还是事件绑定指令。再做不同的处理。

事件绑定

 

普通指令绑定

调用链:compileAttrNoON  => CompileUtril.text => bind => Updater.textUpdater

 

下面我们把整个的思路捋一捋:

1.取出内存碎片

2.递归遍历所有的内存碎片

3.判断当前的dom节点

     3-1.如果是文本节点,取出表达式,解析,调用更新器进行更新。(CompileUtril.text =》 bind =》 Updater.textUpdater)

     3-2.如果是普通节点,遍历所有的属性。

          3-2-1.如果是事件绑定的指令,则调用事件绑定的方法。

          3-2-2.如果是普通的指令(目前只支持html、text、model、class)。

          (CompileUtril.text =》 bind =》 Updater.textUpdater)

4.循环第2步

 

 

 

 

 

代码:

/**
 * @param el 需要解析dom元素
 * @param vm Vue实例
 */
function MyCompile(el,vm){
    this.$vm = vm; // 把Vue实例挂载到Compile实例上
    // 获取到页面的dom元素
    let ParenthEle;
    el && document.querySelector(el) &&(ParenthEle = document.querySelector(el));
    /**
     *  1.取出目标元素下面的所有子元素,放入文档碎片
     *  2.对文档碎片中的所有dom节点进行编译(包括{{xxx}}表达式编译,v-on:click="函数名"编译(事件绑定),)
     *  3.取出文档碎片,渲染会原来的位置上
     */
    if(ParenthEle){
        this.$fragment = this.Node2Fragment(ParenthEle);
        this.CompileAllEleInFragment(this.$fragment);
        ParenthEle.appendChild(this.$fragment);
    }
}

MyCompile.prototype = {
    /**
     * 1.创建一个文档碎片
     * 2.把目标dom元素的子元素全部加入到碎片中
     * 3.返回对应的碎片
     * @param ParenthEle
     * @returns {DocumentFragment}
     * @constructor
     */
    Node2Fragment:function(ParenthEle){
        let Fragment = document.createDocumentFragment();
        let ChildNode;
        while(ChildNode = ParenthEle.firstChild){
            Fragment.appendChild(ChildNode);
        }
        return Fragment;
    },

    /**
     * 递归编译文档碎片里面的所有标签,包括指令和{{xxx}}表达式
     * 编译文档碎片里面的所有文档
     * 1.循环文档碎片,取出某个文档标签
     * 2.判断是文本标签还是普通标签
     *      普通标签:继续递归下去
     *      文本标签:直接解析
     * @param Fragment
     * @constructor
     */
    CompileAllEleInFragment:function (Fragment) {
        let me = this;
        let AllNode = Fragment.childNodes;
        let data = this.$vm;
        let methods = this.$vm.$option.methods;
        // 利用slice返回一个全新的数组,并且循环它
        Array.prototype.slice.call(AllNode).forEach(function (CurrentNode) {
                // 如果是文本节点,如果文本有内容
                if (CompileUtril.IsTextNode(CurrentNode) && CurrentNode.textContent) {
                    me.CompileText(CurrentNode, data);
                }else if(CompileUtril.IsElementNode(CurrentNode)){
                    // 不是文本标签,那么它必然就是一个普通标签,普通标签的话,需要解析出该标签对应的所有cjf-属性
                    me.CompileEleAttr(CurrentNode,methods);// 当前dom元素 methods?
                }
                CurrentNode.childNodes && CurrentNode.childNodes.length && me.CompileAllEleInFragment(CurrentNode);
            }
        );
    },

    /**
     * 解析该节点含有{{xxx}}的文本内容
     * <span>{{xxx.xxx}}</sapn>
     * @param Node 真实节点
     * @param vm   mvvm实例
     * [目前只支持诸如:{{name}}的单标签,不支持{{name+name1}}这种相加的形式]
     */
    CompileText:function (Node, vm) {
        // 匹配出{{}}两个嵌套花括号中的数据
        let reg = /\{\{(.*)\}\}/;
        let CurrentText = Node.textContent;
        // 如果文本为{{xxx}},则匹配成功
        if(reg.test(CurrentText)){//如果当前的文本标签能够匹配上
            CompileUtril.text(vm,RegExp.$1.trim(),Node);// Vue实例 花括号中的表达式 dom元素
        }
    },

    /** 解析该节点的所有属性
     * <input cjf-model="name.b.sex.boy" type="text"/>
     * 1.遍历所有属性
     * 2.提取出cjf-开头的属性name
     * 3.判断属性的分类
     *      3-1.事件指令:cjf-on:xxx=""
     *      3-2.普通指令:cjf-text,cjf-html,cjf-class
     * @param Node 当前节点
     * @param methods mvvm实例中的methods集合
     * @constructor
     *   [!]methods没有对应的事件
     */
    CompileEleAttr:function (Node,methods) {
        let CurrentNodeAllAttr = Node.attributes;// 取出所有属性
        let me = this;// 保存Mycompile实例
        // 利用slice返回一个全新数组的特性,对该标签的所有属性进行循环
        Array.prototype.slice.call(CurrentNodeAllAttr).forEach(function (key) {
            // 假设现在整个属性为 =》 1.cjf-model="xxx" 2.cjf-on:click="函数名"
            let AttrName = key.name;
            let tempAttrName = AttrName;// 临时保存属性名
            // 1.cjf-model="xxx" 2.cjf-on:click="函数名" => 第一种能拿到为表达式,第二种为函数名
            let AttrVal = key.value;
            // 1.cjf-model="xxx" 2.cjf-on:click="函数名" => 该指令是否是以"cjf-"开头,只处理以 "cjf-"开头的指令
            if(CompileUtril.IsCJFAttr(AttrName)){
                /**
                 * 1.cjf-model="xxx" 2.cjf-on:click => 从第四个字符开始截取
                 * 第一种情况为:
                 *      model 、html 、class、text
                 * 第二种情况为:
                 *      on:事件名
                 * @type {string}
                 */
                AttrName = AttrName.substring(4);
                // 到这里 AttrName = "on:事件名"
                if(CompileUtril.IsEventAttr(AttrName)){
                    // 指令绑定方法
                    me.EvenHandler(Node,AttrName,AttrVal,methods);// 当前dom节点 on:事件名 函数名 methods
                }else{
                    //到这里 AttrName = "html/class/text/model"
                    me.CompileAttrNoOn(me.$vm,AttrName,AttrVal,Node);// Vue实例 "html/class/text/model" 表达式 对应的dom元素
                }
                /**
                 *  如果是事件绑定指令,则该事件已经被绑定到对应的dom节点上
                 *  如果是普通指令,则对应的值会被加到dom节点上
                 *  so,到这里解析完一个指令,则需要把这个指令从对应的dom元素上进行移除
                 */
                Node.removeAttribute(tempAttrName);
            }
        })
    },

    /**<input cjf-on:click="函数名" />
     * 根据 属性名里面带的事件类型 和 vm  和 方法名,给node绑定上对应的事件
     * @param Node          当前节点
     * @param AttrName      属性名: on:click
     * @param AttrVal       属性值: 方法名
     * @param methods       vm里面的methods对象
     * @constructor
     */
    EvenHandler:function (Node,AttrName,AttrVal,methods) {
        // on:click => 提取出click
        let EventType = AttrName.split(":")[1];// click
        let Fn = methods[AttrVal];// 提取出对应的方法引用
        let vm = this.$vm; // 取出Vue实例
        // 如果methods中有对应的方法 并且指令没有写错 则给对应的dom节点绑定事件
        // 这里有个细节,Fn.bind(vm) ,这样设置之后,运行Fn的this才能是当前的Vue实例
        Fn && EventType && Node.addEventListener(EventType, Fn.bind(vm), false);
    },

    /**
     * 做一个委托功能,
     * @param vm        Vue实例
     * @param dir       text、class、html、model
     * @param exp       表达式
     * @param Node      当前的dom节点
     * @constructor
     */
    CompileAttrNoOn:function (vm, dir, exp, Node) {
        /**
         * 不可以这样调用,这样调用的话,this是compile实例,而compile实例没有bind方法
         *  Fn = CompileUtril[dir];
            Fn && Fn(vm,exp,Node);
            1.先使用工具类找到对应的方法,用策略模式避免硬代码
            2.
         */
        CompileUtril[dir] && CompileUtril[dir](vm,exp,Node);
    },

};

/**
 * 基础工具类,判断是否为元素节点,是否为文本节点,解析表达式等工具类
 */
CompileUtril = {
    /**
     * 判断是否cjf开头的指令
     * @param dir
     * @returns {boolean}
     * @constructor
     */
    IsCJFAttr:function(dir){
        return dir.indexOf('cjf-') == 0;
    },

    /**
     * 判断属性是否是on开头
     * @constructor
     */
    IsEventAttr:function(dir){
        return dir.indexOf('on') == 0;
    },

    IsElementNode: function(node) {
        return node.nodeType == 1;
    },

    IsTextNode: function(node) {
        return node.nodeType == 3;
    },

    /**
     * 根据表达式解析data域对应的值,如果表达式与data域对应不上,则返回undefined或者null
     * @param data  Vue实例的data域
     * @param exp   表达式 =》 假设为 =》 name.sex.a
     * @returns {*}
     * @constructor
     */
    GetValInObjectByReg:function (data,exp) {
        // name.sex.a 先把表达式切割 =》 name sex a
        let keys = exp.split(".");
        // 取出data域
        let oldV = data;
        // 遍历 name sex a
        keys.forEach(function (key) {
            if(!oldV)return oldV;// 如果是undefined或者null,则返回undefined
            // 不断取出oldV新引用,赋值给旧引用
            oldV = oldV[key];
        });
        return oldV;// 能遍历完并且不会中途被return,则说明这个值解析正确
    },

    /**
     * 根据表达式解析data,一直到最后一个属性
     * @constructor
     */
    GetValInObjectByRegLastAndSet:function(data,exp,NewVal){
        let keys = exp.split(".");
        let oldV = data;
        keys.forEach(function (key,index) {
            if(index < keys.length-1){
                oldV = oldV[key];
            }else{
                oldV[key] = NewVal;
            }
        });
    },


    /**
     * 1.先进行数据解析
     * 2.调用更新器进行更新
     * @param vm Vue实例
     * @param dir 对应的指令:html/model/text/class
     * @param exp 表达式
     * @param Node 当前的dom节点
     */
    bind:function (vm, dir, exp, Node) {
        UpdaterFn = Updater[dir + 'Updater'];// 获取对应的更新器
        // 根据Vue实例保存的参数,解析表达式结果
        let Result = CompileUtril.GetValInObjectByReg(vm.$option.data,exp);// Vue的data域 表达式
        // 如果解析出来的值正确,并且更新器也能正确拿到,则对相应的dom节点进行渲染
        Result && UpdaterFn && UpdaterFn(Node,Result);

        //为当前的表达解析式new出一个Watcher
        new MyWatcher(exp,vm, UpdaterFn, Node);

        return Result;// 最后返回对应的解析结果
    },

    /**
     * 下面4个方法 html、text、model、class,使用了策略模式来避免硬代码的产生
     * 每个不同的方法处理不同的指令
     * 这里的this指向CompileUtril,so,bind就是CompileUtril.bind
     * @param vm
     * @param exp
     * @param Node
     */
    text:function (vm, exp, Node) {
        let dir = 'text';
        this.bind(vm, dir, exp, Node);// Vue实例  ‘text’ 表达式 dom元素
    },
    html:function (vm, exp, Node) {
        let dir = 'html';
        this.bind(vm, dir, exp, Node);
    },
    class:function (vm, exp, Node) {
        let dir = 'class';
        this.bind(vm,dir, exp, Node);
    },

    // 在这里增加v-model事件
    model:function (vm, exp, Node) {
        let me = this;
        let dir = 'model';
        let val = this.bind(vm,dir, exp, Node);

        // 给当前Node绑定input事件
        Node.addEventListener('input',function (e) {
            let NewVal = e.target.value;
            if(NewVal === val)return;
            // 更新到data中
            me.GetValInObjectByRegLastAndSet(vm._MyData, exp, NewVal);

            // 调用对应的这个节点的Dep的notify

        });
    }
}

/**
 *  dom节点的更新器
 *  策略模式对应四种指令操作,做map映射,避免硬代码
 */
Updater = {
    textUpdater:function (Node,value) {
        Node.textContent = typeof value == 'undefined' ? '' : value;
    },

    htmlUpdater:function (Node,value) {
        Node.innerHTML = typeof value == 'undefined' ? '' : value;
    },
    classUpdater:function (Node,value) {
        let oldClass = Node.className;

        Node.className = (Node.className === '' ? '':Node.className+" ")+value;
    },
    modelUpdater:function (Node,value) {
        Node.value = typeof value == 'undefined' ? '' : value;
    }
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值