二百行代码实现Vue.js数据双向绑定

前言

1、数据挂载

 <div id="app">
     <input type="text" v-model="text">
     <input type="text" v-model="man.name">
     <div v-html="text"></div>
     <div>{{man.name}}{{man.age}}{{text}}</div>
</div>
var app = new Vue({
    el: "#app",
    data: {
        man: {
            name: '小白',
            age: 20
        },
        text: 'hello world',
    }
})

上面代码,也许对于你再熟悉不过了

基于这样的形式,我们需要对数据进行挂载,将data的数据挂载到对应的DOM上

首先创建一个类来接收对象参数options

class Vue {
    constructor(options) {
        this.$el = options.el;
        this.$data  = options.data;
        if(this.$el) {
            new Compile(this.$el,this) ////模板解析
        }
    }
}

Compile类,用于模板解析,它的工作内容主要为以下几点

class Compile{
    constructor(el,vm) {
        // 创建文档碎片,接收el的里面所有子元素
        // 解析子元素中存在v-开头的属性及文本节点中存在{{}}标识
        // 将vm中$data对应的数据挂载上去
    }
}

基础代码:

class Compile {
    constructor(el,vm) {
        this.el = this.isElementNode(el)?el:document.querySelector(el);
        this.vm = vm;
        let fragment = this.node2fragment(this.el);
        this.compile(fragment)
    }
    isDirective(attrName) {
        return attrName.startsWith('v-'); //判断属性中是否存在v-字段 返回 布尔值
    }
    compileElement(node) {
        let attributes = node.attributes;
        [...attributes].forEach(attr => {
            let {name,value} = attr
            if(this.isDirective(name)) {
                let [,directive] = name.split('-')
                CompileUtil[directive](node,value,this.vm);
            }
        })
    }
    compileText(node) {
        let content = node.textContent;
        let reg = /\{\{(.+?)}\}/;
        if(reg.test(content)) {
            CompileUtil['text'](node,content,this.vm);
        }
    }
    compile(fragment) {
       let childNodes = fragment.childNodes;
       [...childNodes].forEach(child => {
           if(this.isElementNode(child)) {
            this.compileElement(child);
            this.compile(child);
           }else{
            this.compileText(child);
           }
       })
       document.body.appendChild(fragment);
    }
    node2fragment(nodes) { 
        let fragment = document.createDocumentFragment(),firstChild;
        while(firstChild = nodes.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment
    }
    isElementNode(node) {
        return node.nodeType === 1;
    }
}

CompileUtil = {
    getValue(vm,expr) {
        // 解析表达式值 获取vm.$data内对应的数据
        let value = expr.split('.').reduce((data,current) => {
            return data[current]
        },vm.$data)
        return value
    },
    model(node,expr,vm) { 
       let data = this.getValue(vm,expr);
       this.updater['modeUpdater'](node,data);
    },
    html(node,expr,vm){
        let data = this.getValue(vm,expr);
        this.updater['htmlUpdater'](node,data);
    },
    text(node,expr,vm){ 
        let content = expr.replace(/\{\{(.+?)}\}/g, (...args) => {
            return this.getValue(vm,args[1]);
        })
        console.log(content)
        this.updater['textUpdater'](node,content);
    },
    updater:{
        modeUpdater(node,value){
            node.value = value;
        },
        textUpdater(node,value){
            node.textContent = value;
        },
        htmlUpdater(node,value){
            node.innerHTML = value;
        }
    }
}

2、数据劫持

上面已经完成了对模板的数据解析,接下来再对数据的变更进行监听,实现双向数据绑定

class Vue {
    constructor(options) {
        this.$el = options.el;
        this.$data  = options.data;
        if(this.$el) {
            new Compile(this.$el,this);
            new Observer(this.$data); //新增 数据劫持
        }
    }
}

Observer类,用于监听数据,它的工作内容主要为以下几点

class Observer{
    constructor(el,vm) {
         // 利用Object.defineProperty监听所有属性
        // 递归循环监听所有传入的对象
    }
}

基础代码:

class Observer {
    constructor(data) {
      this.observer(data);
    }
    observer(data) {
        if(!data||typeof data !== 'object') return
        for(let key in data) {
            this.defineReactive(data,key,data[key]);
        }
    }
    defineReactive (obj,key,value) {
        this.observer(value);
        Object.defineProperty(obj,key,{
            get: () => {
                return value;
            },
            set: (newValue) => {
                if(newValue !== value) {
                    this.observer(newValue);
                    value = newValue;
                }
            }
        })

    }
}

3、发布订阅

将监听到的数据变更,实时的更替上去

首先我们需要一个Watcher类,它的工作内容如下

class Watcher {
   // 存储当前观察属性对象的数据
   // 当前观察属性对象数据变更时,更新数据
}

基础代码:

class Watcher {
    /*
        vm 对象实例
        expr 需要监听的对象表达式
        cb 更新数据的回调函数
    */
    constructor(vm,expr,cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        this.oldValue = this.get();
    }
    get() {
        let value = CompileUtil.getValue(this.vm,this.expr);
        return value;
    }
    update() {
        let newValue = CompileUtil.getValue(this.vm,this.expr);
        if(this.oldValue !== newValue) {
            this.cb(newValue)
        }
    }
}

再来一个发布订阅Dep的类

class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    notify() {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}

接下来,让我们把WatcherDep类关联起来

CompileUtilmodeltext方法中分别新建Watcher实例

Watcher在接收到Dep的广播时,需要一个对应的回调函数,更新数据

CompileUtil = {
    getValue(vm,expr) {
        // 解析表达式值 获取vm.$data内对应的数据
        let value = expr.split('.').reduce((data,current) => {
            return data[current]
        },vm.$data)
        return value
    },
    ...
    model(node,expr,vm) { 
       let data = this.getValue(vm,expr);
       //新增 观察者
       new Watcher(vm,expr,(newValue) => {
            this.updater['modeUpdater'](node,newValue);
       })
       this.updater['modeUpdater'](node,data);
    },
    html(node,expr,vm){
        let data = this.getValue(vm,expr);
         //新增 观察者
        new Watcher(vm,expr,(newValue) => {
            this.updater['htmlUpdater'](node,newValue);
        })
        this.updater['htmlUpdater'](node,data);
    },
    ...
    text(node,expr,vm){ 
        let content = expr.replace(/\{\{(.+?)}\}/g, (...args) => {
             /*
                 新增 观察者
                匹配多个{{}}字段
             */
            new Watcher(vm,args[1],() => {
                this.updater['textUpdater'](node,this.getContentValue(vm,expr));
            })
            return this.getValue(vm,args[1]);
        })
        this.updater['textUpdater'](node,content);
    },
}

实例化一个Watcher的同时会调用this.get()方法,this.get()在取值时,会触发被监听对象的getter

class Watcher {
    ...
    get() {
        // 在Dep设置一个全局属性
        Dep.target = this;
        // 取值会触发被监听对象的getter函数
        let value = CompileUtil.getValue(this.vm,this.expr);
        Dep.target = null;
        return value;
    }
    ...
}

来到Observer中,此时在get函数中,我们就可以将Watcher实例放进Dep的容器subs

这里dep,利用了闭包的特性,每次广播不会通知所有用户,提高了性能

class Observer {
    ...
    defineReactive (obj,key,value) {
        this.observer(value);
        let dep = new Dep()
        Object.defineProperty(obj,key,{
            get: () => {
                //新增 订阅
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set: (newValue) => {
                if(newValue !== value) {
                    console.log('监听',newValue)
                    this.observer(newValue);
                    value = newValue;
                    //广播
                    dep.notify();
                }
            }
        })
    }
}

此时,WatcherDep已形成关联,一旦被监听的对象数据发生变更,就会触发Depnotify广播功能,进而触发Watcherupdate方法执行回调函数!

测试:

setTimeout(function(){
    app.$data.test = "123"
},3000)

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jomuavyc-1580357957524)(https://s2.ax1x.com/2019/08/26/mR44DH.gif)]

到这里,我们已经完成了最核心的部分,数据驱动视图,但是众所周知,v-model是可以视图驱动数据的,于是我们再增加一个监听事件

CompileUtil = {
    ...
    setValue(vm,expr,value) {
        //迭代属性赋值
        expr.split('.').reduce((data,current,index,arr) => {
            if(index == arr.length - 1){
                data[current] = value
            }
            return data[current]
        },vm.$data)
    },
    model(node,expr,vm) { 
       let data = this.getValue(vm,expr);
       new Watcher(vm,expr,(newValue) => {
            this.updater['modeUpdater'](node,newValue);
       })
        //事件监听
       node.addEventListener('input', el => {
          let value = el.target.value;
          console.log(value)
          this.setValue(vm,expr,value)
       })
       this.updater['modeUpdater'](node,data);
    },
        ...
}

效果如下:

最后为Vue实例添加一个属性代理的方法,使访问vm的属性代理为访问vm._data的属性


class Vue {
    constructor(options) {
        ...
        this.$data  = options.data;
        Object.keys(this.$data).forEach(key => {
            this.proxyKeys(key);
        })
          ...
    }
    proxyKeys(key) {
        console.log(key)
        Object.defineProperty(this,key,{
            enumerable: true,
            configurable: false,
            get: () => {
                return this.$data[key];
            },
            set: (newValue) => {
                console.log('newValue',newValue)
                this.$data[key] = newValue;
            }
        })
    }
}

大功告成!

结束

源码:https://github.com/luojinxu520/simple-mvvm/blob/master/src/mvvm.js

参考 :

1、 剖析Vue实现原理 - 如何实现双向绑定mvvm
————————————————
版权声明:本文为CSDN博主「取个名字真麻烦啊」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/luojinxu520/article/details/104113895

发布了5 篇原创文章 · 获赞 4 · 访问量 3990

猜你喜欢

转载自blog.csdn.net/luojinxu520/article/details/104113933