[Vue]二百行代码实现数据双向绑定

前言

vue

图片来源: 剖析Vue实现原理 - 如何实现双向绑定mvvmjavascript

数据挂载

<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',
    }
})
复制代码

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

基于这样的形式,咱们须要对数据进行挂载,将data的数据挂载到对应的DOM上前端

首先建立一个类来接收对象参数optionsvue

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

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

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

基础代码:node

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;
        }
    }
}
复制代码

数据劫持

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

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类,用于监听数据,它的工做内容主要为如下几点github

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;
                }
            }
        })

    }
}
复制代码

发布订阅

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

首先咱们须要一个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)
复制代码

结果:

测试

到这里,咱们已经完成了最核心的部分,数据驱动视图,可是众所周知,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;
            }
        })
    }
}
复制代码

大功告成! 源码:github.com/luojinxu520…

结束

目前,Vue 的反应系统是使用 Object.defineProperty 的 getter 和 setter。 可是,Vue 3 将使用 ES2015 Proxy 做为其观察者机制。 这消除了之前存在的警告,使速度加倍,并节省了一半的内存开销。 为了继续支持 IE11,Vue 3 将发布一个支持旧观察者机制和新 Proxy 版本的构建。

参考 :

一、 剖析Vue实现原理 - 如何实现双向绑定mvvm

二、珠峰架构前端技术公开课

相关文章
相关标签/搜索