vue数据驱动视图代码实现

一.Document.createDocumentFragment()

描述:

DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。

因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。

二.MVVM.js

描述:挂载options对象(有data,el等属性),如果el存在,则则执行observe和compile,设置代理(可直接通过vm的点语法直接访问到vm.data下所有的属性,通过挂载this为MVVM实例)

代码:

class MVVM{
    constructor(options){
        this.$el = options.el;
        this.$data = options.data;
        if(this.$el){
            new Observer(this.$data);
            // 将数据代理到实例上直接操作实例即可,不需要通过vm.$data来进行操作
            this.proxyData(this.$data);
            new Compile(this.$el, this);
        }
    }
    proxyData(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    return data[key]
                },
                set(newValue){
                    data[key] = newValue
                }
            })
        })
    }
}

三.compile.js

描述:构建一个class Compile,挂载属性el和vm,内部执行编译逻辑:

1.通过fragment获取el的所有子节点node,目的:减少对DOM Tree的直接操作

2.对frament对象的childnodes迭代,每次迭代判断类型是否为elementNode(元素节点),还是textNode(文本节点)

3.如果是elementNode执行compileElement,

描述:迭代该node的属性attrs,每次迭代做正则判断是否为v-形式的指令,如果为指令提取指令文本,对照CompileUtil类的属性执行对应的方法,如果是v-model则赋值node的value属性

4.如果是textNode执行compileText,

描述: 用正则表达式/\{\{([^}]+)\}\}/g去匹配node的textContent,获取([^}]+)内的匹配内容,并赋值node的textContent

5.将上述流程处理好的fragment appendTo该el下

6.将watcher对象new在每一个compileUtil的model或text(用于设置input的value,非input的textContent),以便将watcher和每一个node通过参数绑定一起,在watcher updata的时候可以更新每一个node的value或textContent

7.如果为input元素,监听input事件,深层迭代到指定key处设置vm.$data的数据

compile.js完整代码:

class Compile {
    constructor(el, vm) {
        //判断传递的元素是不是DOM,不是DOM则获取
        this.el = this.isElementNode(el) ? el: document.querySelector(el);
        this.vm = vm;
        if(this.el) {
            //1.将真实DOM存入内存中 fragment(性能优化,减少对dom的直接操作)
            let fragment = this.node2fragment(this.el);
            //2.编译,提取想要的元素节点v-model和文本节点{{}}
            this.compile(fragment);
            //将编译好的fragment append进页面dom中
            this.el.appendChild(fragment);
        }
    }
    isElementNode(node) {
        return node.nodeType === 1;
    }
    node2fragment(el) { // 需要将el中的内容全部放到内存中
        // 文档碎片 内存中的dom节点
        let fragment = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
            // appendChild具有移动性
        }
        return fragment; // 内存中的节点
    }
    compile(fragment) {
        // 需要递归 每次拿子元素
        let childNodes = fragment.childNodes;
        Array.from(childNodes).forEach(node => {
            if (this.isElementNode(node)) {
                // 是元素节点,还需要继续深入的检查
                // 这里需要编译元素
                this.compileElement(node);
                this.compile(node)
            } else {
                // 文本节点
                // 这里需要编译文本
                this.compileText(node);
            }
        });
    }
    /*辅助的方法*/
    // 是不是指令
    isDirective(name) {
        return name.includes('v-');
    }
    //用正则提取元素的指令,对应工具类CompileUtile找对应的方法执行
    compileElement(node) {
        // 带v-model v-text 
        let attrs = node.attributes; // 取出当前节点的属性
        Array.from(attrs).forEach(attr => {
            // 判断属性名字是不是包含v-model 
            let attrName = attr.name;
            if (this.isDirective(attrName)) {
                // 取到对应的值放到节点中
                let expr = attr.value;
                let [, type] = attrName.split('-'); // 
                // 调用对应的编译方法 编译哪个节点,用数据替换掉表达式
                CompileUtil[type](node, this.vm, expr);
            }
        })
    }
    //用正则提取元素的文本内容,对应工具类CompileUtil的text属性执行方法
    compileText(node) {
        let expr = node.textContent; // 取文本中的内容
        let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}}
        if (reg.test(expr)) { 
            // 调用编译文本的方法 编译哪个节点,用数据替换掉表达式
            CompileUtil['text'](node, this.vm, expr);
        }
    }
        
}
CompileUtil = {
    text(node, vm, expr) { // 文本处理
        let updateFn = this.updater['textUpdater'];
        // 文本比较特殊 expr可能是'{{message.a}} {{b}}'
        // 调用getTextVal方法去取到对应的结果
        let value = this.getTextVal(vm, expr);
        expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            new Watcher(vm, arguments[1],(newValue)=>{
                // 如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
                updateFn && updateFn(node,this.getTextVal(vm,expr));
            });
        })
        updateFn && updateFn(node, value)
    },
    getTextVal(vm, expr) { // 获取编译文本后的结果
        return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            // 依次去去数据对应的值
            return this.getVal(vm, arguments[1]);
        })
    },
    getVal(vm, expr) { // 获取实例上对应的数据
        expr = expr.split('.'); // {{message.a}} [message,a] 实现依次取值
        // vm.$data.message => vm.$data.message.a
        return expr.reduce((prev, next) => { 
            return prev[next];
        }, vm.$data);
    },
    setVal(vm,expr,value){ 
        expr = expr.split('.');
        return expr.reduce((prev,next,currentIndex)=>{
            if(currentIndex === expr.length-1){
                return prev[next] = value;
            }
            return prev[next];
        },vm.$data);
    },
    model(node, vm, expr) { // 输入框处理
        let updateFn = this.updater['modelUpdater'];
        // 这里应该加一个监控 数据变化了 应该调用这个watch的callback
        new Watcher(vm,expr,(newValue)=>{
            // 当值变化后会调用cb 将新的值传递过来 
            updateFn && updateFn(node, newValue);
        });
        node.addEventListener('input',(e)=>{
            let newValue = e.target.value;
            // 监听输入事件将输入的内容设置到对应数据上
            this.setVal(vm,expr,newValue)
        });
        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    updater: {
        // 文本更新
        textUpdater(node, value) {
            node.textContent = value
        },
        // 输入框更新
        modelUpdater(node, value) {
            node.value = value;
        } 
  }
}

四.observer.js

描述:深度递归观察对象的每个key,劫持get和set属性,重新赋予新的逻辑

1.get:返回value值,如果dep的target有值,则添加wath进观察者对象的数组

2.set:调用observe观察新值newValue,设置当前value为newValue,调用dep.notify通知观察者对象调用update方法更新视图

代码:

class Observer{
    constructor(data){
       this.observe(data); 
    }
    observe(data){ 
        // 要对这个data数据将原有的属性改成set和get的形式
        // defineProperty针对的是对象
        if(!data || typeof data !== 'object'){
            return;
        }
        // 要将数据 一一劫持 先获取取到data的key和value
        Object.keys(data).forEach(key=>{
            // 定义响应式变化
            this.defineReactive(data,key,data[key]);
            this.observe(data[key]);// 深度递归劫持
        });
    }
    // 定义响应式
    defineReactive(obj,key,value){
        let that = this;
        let dep = new Dep(); // 每个变化的数据 都会对应一个数组,这个数组是存放所有更新的操作
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get(){ // 当取值时调用的方法
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newValue){
                if(newValue!=value){
                    that.observe(newValue);
                    value = newValue;
                    dep.notify(); // 通知所有人 数据更新了
                }
            }
        });
    }
}

五。watcher.js

描述:观察每一个node的value或textContent,需要更新的时候调用updata同步更新对应绑定的node的value或textContent(通过回调方式传入不同的更新视图的方式方法)

代码:

class Watcher{ // 因为要获取老值 所以需要 "数据" 和 "表达式"
    constructor(vm,expr,cb){
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 先获取一下老的值 保留起来
        this.value = this.get();
    }
    // 老套路获取值的方法,这里先不进行封装
    getVal(vm, expr) { 
        expr = expr.split('.'); 
        return expr.reduce((prev, next) => {
            return prev[next];
        }, vm.$data);
    }
    get(){
        let value = this.getVal(this.vm,this.expr);
        return value;
    }
    // 对外暴露的方法,如果值改变就可以调用这个方法来更新
    update(){
        let newValue = this.getVal(this.vm, this.expr);
        let oldValue = this.value;
        if(newValue != oldValue){
            this.cb(newValue); // 对应watch的callback
        }
    }
}

六。dep.js

描述:一个调度类,有add方法用于在watch对象初始化的时候添加每一个watch到watch数组中,有notify方法用于调用每一个watch对象的update方法(更新node的文本内容)

代码:

class Dep{
    constructor(){
        // 订阅的数组
        this.subs = []
    }
    addSub(watcher){
        this.subs.push(watcher);
    }
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
}

 

转载于:https://my.oschina.net/u/3937325/blog/3014230

你可能感兴趣的:(vue数据驱动视图代码实现)