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

2020-01-29  本文已影响0人  锦衣无涯

数据挂载


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

        }

    }

}

数据劫持

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


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;

                }

            }

        })

    }

}

发布订阅

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

首先我们需要一个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);

    },

        ...

}

效果如下:

image

最后为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

结束

目前,Vue 的反应系统是使用 Object.defineProperty 的 getter 和 setter。 但是,Vue 3 将使用 ES2015 Proxy 作为其观察者机制。 这消除了以前存在的警告,使速度加倍,并节省了一半的内存开销。

为了继续支持 IE11,Vue 3 将发布一个支持旧观察者机制和新 Proxy 版本的构建。

上一篇 下一篇

猜你喜欢

热点阅读