Vue响应式原理01-入口函数和编译类compile的实现

392 阅读3分钟

先来看下目录结构:

vue原理学习        
├─ MVue.js     
├─ index.html  
└─ vue.js      

1.index.html模板

这个是常见的模板哈,从这个常见的vue模板去思考🤔

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue原理实现</title>
</head>

<body>
    <div id="app">
      <h2>{{person.name}} -- {{person.age}}</h2><h3>{{person.fav}}</h3>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <h3>{{msg}}</h3>
        <div v-text='msg'></div>
        <div v-text='person.fav'></div>
        <div v-html='htmlStr'></div>
        <input type="text" v-model='msg'>
        <button v-on:click='handlerClick'>按钮on</button>
        <button @click='handlerClick'>按钮@</button>
    </div>

    <script src="./MVue.js"></script>
    <script>
        let vm = new MVue({
            el: '#app',
            data: {
                person: {
                    name: "吾乃常山赵子龙",
                    age: 18,
                    fav: '花姑娘'
                },
                msg: '学习MVVM实现原理',
                htmlStr: '<h3>搞他搞他搞他</h3>'
            },
            methods: {
                handlerClick() {
                    console.log(this.$data);
                    this.person.name = '学习MVVM';
                    // this.$data.person.name = '学习MVVM';
                }
            }
        })
    </script>
</body>

</html>

2.MVue.js

这篇主要是实现从 new MVVM()Compile初始化视图相关部分内容。

首先是入口类Mvue,然后是编译类Compile,下面来看下部分的实现,Compile类中关于节点的具体编译还有待进一步实现。

  • Mvue入口类拿到options后将elthis传递给Compile编译类
//入口类MVue
class MVue {
    constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        if (this.$el) {
            //实现一个指令解析器
            new Compile(this.$el, this)
        }
    }
}
  • 进入编译类compile后先对el节点类型进行判断,获取到对应的节点后赋值给this.el
  • 创建文档碎片document.createDocumentFragment,将this.el中的节点内容放入其中,从而减少回流重绘
    • 从文档中一个节点插入文档碎片后,这个节点会从原本的文档树中删除,所以将el循环插入文档碎片模型之后,就暂时不会显示到页面中。(下图为打印粗来的文档碎片内容)
    • blog.csdn.net/xzxlemontea…
    • www.cnblogs.com/suihang/p/9…
  • 文档碎片插入到this.el中后就可以看到页面中的内容,当然,这之前还需要对文档碎片进行编译
//Compile编译类
class Compile {
    constructor(el, vm) {
        // 是否是元素节点
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        console.log(this.el);
      
        //1.获取文档碎片对象  放入内存中,会减少页面的回流和重绘
        const fragment = this.getFragment(this.el); //获取当前的文档碎片
      
        console.log(fragment);
        //2.编译模板
        this.compile(fragment);
        //3.插入到根节点
        this.el.appendChild(fragment);
    }
    getFragment(el) {
        const f = document.createDocumentFragment(); //这里创建文档碎片
        // let firstChild;
        while (el.firstChild) { //如果el有子节点,那么就给给这个节点添加到新创建的的文档碎片中去
            f.appendChild(el.firstChild);
        }
        return f;
    }
    compile(fragment) {
        const childNodes = fragment.childNodes;
        [...childNodes].forEach(child => {
            if (this.isElementNode(child)) {
                console.log("元素节点", child)
                this.compileElement(child);
            } else {
                // console.log("文本节点",child)
                this.compileTxt(child);
            }
            //如果子节点还有子节点的处理方式--递归
            if (child.childNodes && child.childNodes) {
                this.compile(child);
            }
        })
    }
    //1.编译元素节点
    compileElement(node) {
 
    }
    //2.编译其他类型节点
    compileTxt(node) {

    }
    //监测是否是元素节点:nodeType等于1 那么就是元素节点
    isElementNode(node) {
        return node.nodeType === 1;
    }
}

3.编译类的完整实现:

1.以编译元素节点函数compileElement为开始:

1.compileElement函数:
  • 接收到node节点,获取节点上的属性
  • 通过isDirective来判断是不是v-开头的指令
  • 进行两次分割:
    • 1次分割:分割出v-后面的部分(html,model,on)
    • 2次分割:分割出:后面的eventName事件名(click等),如果没有:则保留原来的(html,model,on)。
  • 然后是根据dirName来执行compileUtil中对应的处理方法
//...省略
    compileElement(node){
        // 例如:<div v-text='msg'></div>
        const attrs = node.attributes;
        [...attrs].forEach(attr=>{
            const {name,value} = attr;
            //属性名字是否是一个指令:类似于v-html,v-model,v-on:click
            if(this.isDirective(name)){
                //这里值取directive的值(html,model,on),not不要
                const [not,directive] = name.split("-");
                //这里还需要在分割,主要是针对类似v-on:click的
                const [dirName,eventName] = directive.split(":");
                //根据dirName执行对应的函数,传入this.vm主要是为了获取data上定义的值
                compileUtil[dirName](node,value,this.vm,eventName)
            }
        })
    }
    //判断是否是“v-”开头的
    isDirective(attrName){
        return attrName.startsWith("v-")
    }
2.compileUtil的实现:
1.text方法:

compileUtil对象中,封装了各种处理函数,这里先以text为例。

  • 为了处理v-text='person.name'这种情况,通过getValue方法获取attrValuevm.$data中对应的值。
    • getValue方法中,先将person.name通过.分割,然后通过reduce方法获取到person.name对应的value,返回回去。
  • 将获得的value和以及之前的node传递给this.updater.textUpdater(node,value)
  • textUpdater中通过node.textContent更新节点内容。
  • html以及model方法的实现上面的基本类似,这里不做说明了。
const compileUtil = {
    text(node,attrValue,vm){
        // 这里需要更新值,以<div v-text='msg'></div>,首先得拿到msg在data中对应的值value;
        // 但是当<div v-text='person.name'></div>这种形式的时候会失效,所以下面这种形式不可取
        // const value = vm.$data[attrValue];
        //进而需要一个getValue的方法,当调用的时候得到的就是最终的值
        const value = this.getValue(attrValue,vm);
        this.updater.textUpdater(node,value)
    },
    html(node,attrValue,vm){
				const value = this.getValue(attrValue,vm);
        this.updater.htmlUpdater(node,value);
    },
    model(node,attrValue,vm){

    },
    on(node,attrValue,vm,eventName){

    },
    // 更新函数对象
    updater:{
        textUpdater(node, value) {
            node.textContent = value;
        },
        htmlUpdater(node, value) {
            node.innerHtml = value;
        },
        modelUpdater(node, value) {
            node.value = value;
        }
    },
    // 获取<div v-text='person.name'></div>中person.name以及其他形式的值
     // 这里将vm.$data传入,第一次的随后prevValue的值就是vm.$data
     //然后return 回去的prevValue将作为新的prevValue,之道获取到person.name的值为止。
    getValue(attrValue,vm){
        return attrValue.split(".").reduce((prevValue,currValue)=>{
                console.log("我是currentValue",currValue)
                return prevValue[currValue]
        },vm.$data)
    }
}
3.删除以v-开头的指令属性:

v-开头的指令属性渲染到页面后需要删除掉。所以需要在compileElement函数中添加如下处理:

  • node.removeAttribute("v-"+directive);
//compileElement函数
    compileElement(node){
        // 例如:<div v-text='msg'></div>
        const attrs = node.attributes;
        [...attrs].forEach(attr=>{
            const {name,value} = attr;
            //属性名字是否是一个指令:类似于v-html,v-model,v-on:click
            if(this.isDirective(name)){
                //这里值取directive的值(html,model,on),not不要
                const [not,directive] = name.split("-");
                //这里还需要在分割,主要是针对类似v-on:click的
                const [dirName,eventName] = directive.split(":");
                //根据dirName执行对应的函数,传入this.vm主要是为了获取data上定义的值
                compileUtil[dirName](node,value,this.vm,eventName);
                //需要删除指令的属性
                node.removeAttribute("v-"+directive);
            }
        })
    }

删除后如下图:看到元素的属性v-被删除了

2.编译文本节点的函数compileText方法:

这里主要是针对<h2>{{person.name}} -- {{person.age}}</h2>如何赋值进行操作。

  • 通过/\{\{(.+?)\}\}/.test(value)正则表达式获取带有{{}}的内容,然后将这个value传递出去,调用的依然是 compileUtil.text()的找个方法---这意味着compileUtil.text方法中又得加一层对于{{}}的处理
//compileText函数
    compileText(node){
        //主要是针对{{}}
        const value = node.textContent;
        if(/\{\{(.+?)\}\}/.test(value)){
            compileUtil["text"](node,value,this.vm)
        }
    }
  • 通过正则表达式获取到value,然后传递给this.updater.textUpdater(node,value)进行渲染更新。

    attrValue.replace(/\{\{(.+?)\}\}/g,(...args)=>{
                    console.log("我是args",args)
                    return this.getValue(args[1],vm)
                })
    
//compileUtil
const compileUtil = {
    text(node,attrValue,vm){
        let value ;
        //这里需要对{{这种形式进行处理 <h2>{{person.name}} -- {{person.age}}</h2>
        if(attrValue.indexOf('{{') !== -1)
        {
            value = attrValue.replace(/\{\{(.+?)\}\}/g,(...args)=>{
                console.log("我是args",args)
                return this.getValue(args[1],vm)
            })
        }else
        {
            value = this.getValue(attrValue,vm);
        }
        this.updater.textUpdater(node,value)
    },
    //...略
    // 更新函数对象
    updater:{
        textUpdater(node, value) {
            node.textContent = value;
        },
        htmlUpdater(node, value) {
            node.innerHTML = value;
        },
        modelUpdater(node, value) {
            node.value = value;
        }
    },
    // 获取<div v-text='person.name'></div>中person.name以及其他形式的值
     // 这里将vm.$data传入,第一次的随后prevValue的值就是vm.$data
     //然后return 回去的prevValue将作为新的prevValue,之道获取到person.name的值为止。
    getValue(attrValue,vm){
        return attrValue.split(".").reduce((prevValue,currValue)=>{
                console.log("我是currentValue",currValue)
                return prevValue[currValue]
        },vm.$data)
    }
}

3.on方法的事件绑定处理函数:

  • 通过const handler = vm.$options.methods && vm.$options.methods[attrValue];找到方法
  • 将找到的方法handler绑定到node上,并将this指针方向归正
//on方法
    on(node,attrValue,vm,eventName){
        //获取到options中data的方法
        const handler = vm.$options.methods && vm.$options.methods[attrValue];
        node.addEventListener(eventName,handler.bind(vm),false);
    },

4.关于“@”的处理方法:

  • 通过this.isEventName(name)来判断是否是以@开头的。
  • 然后是一样的思路,split@符分割,取后面的作为eventName
  • 调用compileUtil["on"](node,value,this.vm,eventName)
// compileElement方法

compileElement(node){
        // 例如:<div v-text='msg'></div>
        const attrs = node.attributes;
        [...attrs].forEach(attr=>{
            const {name,value} = attr;
            //属性名字是否是一个指令:类似于v-html,v-model,v-on:click
            if(this.isDirective(name)){
                //这里值取directive的值(html,model,on),not不要
                const [not,directive] = name.split("-");
                //这里还需要在分割,主要是针对类似v-on:click的
                const [dirName,eventName] = directive.split(":");
                //根据dirName执行对应的函数,传入this.vm主要是为了获取data上定义的值
                compileUtil[dirName](node,value,this.vm,eventName);
                //需要删除指令的属性
                node.removeAttribute("v-"+directive);
            }else if(this.isEventName(name)){
                let [not,eventName]= name.split('@');
                compileUtil["on"](node,value,this.vm,eventName)
            }
        })
    }