原生js 模拟Vue双向数据绑定原理(详细代码及注释)

2021/7/24 6:11:48

本文主要是介绍原生js 模拟Vue双向数据绑定原理(详细代码及注释),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

数据劫持

先对data中的数据进行劫持并挂载到vue实例上,这时每个数据对象都可以模拟是一个订阅者,当数据发生改变,发布者会通知(调用notify方法)每一个订阅者去调用update方法进行更新,然后通过编译器编译渲染到视图上,当视图发生改变了,每个订阅者会向发布者进行订阅,并返回到进行数据更新,进行数据同步(注释仅个人理解,如有不对,请指教)

class Vue {
    constructor(options) {
        this.$options = options;
        this._data = options.data;
        this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el

        // 数据劫持
        this._proxyData(this._data);
        // 观察者模式
        new Observer(this._data)
        // 编译
        new Complier(this);
    }

    _proxyData(data) {
        Object.keys(this._data).forEach(key => {
            Object.defineProperty(this, key, {
                get() {
                    return data[key]
                },
                set(nValue) {
                    if (data[key] === nValue) {
                        return
                    }
                    data[key] = nValue;
                }
            })
        })
    }
}

观察者模式

class Observer {
    constructor(data) {
        this.walk(data)
    }
    // 遍历
    walk(data) {
        // 判断data是否存在,data是不是对象  如果不是或不存在则不处理

        // 开始  传入data:{}  是对象
        if (!data || typeof data !== "object") {
            return
        }

        // 遍历data中的每一个属性名key  拿到key
        // object.keys(),将data中的属性名存在一个数组中   [msg,crq,...]
        Object.keys(data).forEach(key => {

            //  挂载到vue实例中,对拿到的key值进行代理 key:msg data[key]:"hello vue"
            this.defineReactive(data, key, data[key])
        })
    }
    // 定义响应式数据  将属性属性值挂载到vue实例上
    defineReactive(data, key, value) {
        // 对每一个key进行发布和监听,在这里实例化publisher
        let publisher = new Publisher();

        let that = this;
        // 调用walk判断传进来的 value 也就是walk中data遍历后的属性值data[key]是不是对象,如果是对象,继续遍历,不是则返回,例如遍历data后key:crq  data[key]:{agr:...,sex:...}
        this.walk(value);
        // 将属性值挂载到vue实例上  
        // 这里将key进行代理,为每一个key添加get和set方法
        Object.defineProperty(data, key, {

            get() {
                // 收集依赖   添加观察者
                // publisher.target 就相当于添加的观察者的形参
                Publisher.target && publisher.addSub(Publisher.target);

                return value
            },
            set(nValue) {
                if (value === nValue) {
                    return
                }
                value = nValue
                // 如果数据修改或新添加数据,重新调用walk判断是否是对象然后进行挂载
                that.walk(nValue)

                // 发布通知,监测到数据发生改变,通知观察者更新数据
                publisher.notify()
            }
        })
    }
}

发布者

class Publisher {
    constructor() {
        // 这里初始化subs空数组  准备用来储存观察者  
        this.subs = [];
    }
    // 将观察者添加到数组中
    addSub(sub) {
        // 判断 观察者是否存在,并且观察者上是否有更新方法
        // 如果有  添加到subs数组中
        if (sub && sub.update) {
            this.subs.push(sub);
        }
    }

    notify() {
        // 通知每一个观察者 调用update方法更新
        console.log("notify");
        this.subs.forEach(w => {
            w.update();
            console.log("w.up");
        });

    }
}

订阅者(观察者)

class Wathcher {

    constructor(data, key, cb) {
        this.data = data;
        this.key = key;
        this.cb = cb

        Publisher.target = this

        this.oldValue = data[key]
    }
    // 更新视图


    // 更新前提是数据一定要发生改变,数据要和之前对比,如果没有变化就不更新
    // 获取发布者通知的信息
    update() {
        console.log("watcher--update");
        let newValue = this.data[this.key];
        // console.log(newValue);

        if (newValue === this.oldValue) {
            return
        }


        // 如果数据改变,dom更新
        this.cb(newValue);

    }
}

编译器

class Complier {
    constructor(vm) {
        this.el = vm.$el;
        this.data = vm._data;


        this.complie(this.el)

    }

    complie(el) {
        let childNodes = el.childNodes

        Array.from(childNodes).forEach(node => {
            // 代码分割
            if (this.isTextNode(node)) {
                // 文本处理
                this.complieText(node)
            } else if (this.isElementNode(node)) {
                // 元素处理
                this.complieElement(node)
            }
            // 当元素中包含文本和插值表达式时,需要再调用编译,编译出元素中嵌套的插值表达式和文本
            // 判断编译一遍后,里面是否还有节点,如果存在节点则再调用一遍编译  complie
            if (node.childNodes && node.childNodes.length > 0) {

                this.complie(node)
            }

        })
    }
    // 编译元素
    complieElement(node) {
        // 获取所有属性

        let attributes = node.attributes
        // 遍历所有属性
        Array.from(attributes).forEach(attr => {
            // 获取属性名
            // console.log(attr);
            let attrName = attr.name;
            // console.log(attrName);
            // 判断属性是否是  v-  开头 如果是  
            if (this.isDirective(attrName)) {


                // 获取v-后面部分   指令名  
                attrName = attrName.substring(2)
                console.log(attrName);

                // 属性值   data[key]
                let key = attr.value

                //  text - 映射方法,不同指令 不同函数
                this.update(node, attrName, key)
                console.log(this.data[key]);
            }

        })

    }

    // 更新   

    update(node, attrName, key) {

        // 判断  不同指令调用不同函数


        // if (attrName === "text") {
        //     this.textUpdate(node, attrName, key)
        // }

        // if (attrName === "model") {
        //     this.modelUpdate(node, attrName, key)
        // }

        //   fn 为声明的变量, 不能直接调用,要改变this指向  指向complier
        // 声明fn  代替各种判断

        let fn = this[attrName + 'Update'];
        fn && fn.call(this, node, attrName, key)
        console.log(key);
    }

    // 属性名为  text 时
    textUpdate(node, attrName, key) {

        console.log(key);
        // 更新节点文本内容    这里this.data[key]
        node.textContent = this.data[key];
        console.log(this.data[key]);

        //  将数据传入watcher在watcher里面判断,如果数据发生改变,则调用watcher中的update方法,更新视图中数据
        new Wathcher(this.data, key, (newValue) => {
            console.log("txtup");
            node.textContent = newValue
        })
    }


    // 属性名为  model 时   model 实现双向数据绑定
    modelUpdate(node, attrName, key) {
        console.log(key);
        // console.log(node);
        // input 里面文本是value
        node.value = this.data[key]
        console.log(this.data[key]);
        // key = node.value
        // console.log(node.value);

        new Wathcher(this.data, key, (newValue) => {
            node.value = newValue
        })

        node.addEventListener('input', () => {
            this.data[key] = node.value
        })
    }


    // 编译文本
    complieText(node) {
        // console.log(node);
        // console.log(node.textContent);
        let value = node.textContent  //内容
        let reg = /\{\{(.+?)\}\}/ // 正则规则

        // 判断 传入的文本内容中是否有正则表达式,如果有,则获取表达式中的值
        if (reg.test(value)) {

            // 获取:插值表达式的变量名
            let k = RegExp.$1.trim()
            console.log(k);

            //  替换
            node.textContent = value.replace(reg, this.data[k])


            // 数据变化 - 通知更新 - update - cb - dom
            new Wathcher(this.data, k, (newValue) => {
                node.textContent = newValue
            })

        }
    }




    // 节点判断



    // 判断属性名是否有v-
    isDirective(attrName) {
        // 返回值为boolen
        return attrName.startsWith("v-")
    }

    isTextNode(node) {
        return node.nodeType === 3
    }

    isElementNode(node) {
        return node.nodeType === 1
    }
    isAttrNode(node) {
        return node.nodeType === 2
    }

}


这篇关于原生js 模拟Vue双向数据绑定原理(详细代码及注释)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程