首页 > 其他 > 详细

vue中响应式原理

时间:2020-08-01 21:56:50      阅读:108      评论:0      收藏:0      [点我收藏+]

  Vue是对MVVM框架的很好体现,那什么是MVVM呢?顾名思义它其实就是Model-View-ViewModel模式。它是一个软件架构设计模式,Model可以看成是代表数据模型,View代表视图,它负责将数据模型转化成ui进行展示。ViewMode用来连接Model和View,将view需要的数据暴露,处理view层的具体业务逻辑。

  MVVM它可以分离视图(View)和模型(Model),降低代码耦合,利用双向绑定,数据更新后视图自动更新,来自动更新DOM,可以更好的编写测试代码。凡事有利有弊,它的不足之处是jbug很难被调试,因为使用了双向数据绑定,从而出现bug时,有可能是view的代码有问题,也有可能是model上的代码出故障,对于大型的图形应用程序来说,视图较多,维护成本偏高。

  Vue是MVVM很好的体现。学习vue的过程中我们知道它有以下三要素,响应式,模板引擎及渲染。最独特的一点是响应式,即数据模型更新,视图更新。在这期间我们要了解它是如何知道数据变化,怎样在数据变化的同时在视图上进行体现。

  Vue的响应式就是data中的属性被代理到vm上。在js中,侦测数据变化有两种方式,一种是通过Object.defineProperty进行数据劫持,另外一种是通过Proxy进行数据代理,下面我们详细进行分析。

  Object.defineProperty与Proxy

  Object.defineProperty()方法会直接在一个对象上定义一个新属性或者是修改一个对象的现在属性并返回这个对象。主要是利用Object.defineProperty中的访问器属性get和set方法。当把一个普通对象传入Vue实例作为data选项时,Vue将遍历对象中所有的属性,将其添加上访问器属性。当读取data中的数据时自动调用get方法,当修改data中的数据自动调用set方法。利用的是对象属性中的get/set方法来监听数据的变化。

 <script>
        //这是要被劫持的对象
        let data = {
            like: ‘‘
        };
        let newData = eat;
        function say(like) {
            if (like === swam) {
                console.log(我最喜欢的活动是游泳);
            } else {
                console.log(我最喜欢的活动是远足);
            }
        }

        // 只要是调用了data的like属性,那就会触发get函数,调用时获取到的结果就是get函数的返回值
        //只是给data的like赋值那么就会触发set函数,形参对应的就是设置的那个值。
        Object.keys(data).forEach(key => {
            Object.defineProperty(data, key, {
                get: function () {
                    console.log(获取时触发了get);
                    return newData;
                },
                set: function (val) {
                    console.log(设置时触发了set,val是: + val);
                    say(val);
                    // 不能直接进行设置不然会引起死循环,所以要用到第三方变量
                    // data.like=‘eat‘;

                }
            })
        })
        console.log(data);
        data.like = gram;
        console.log(data);
 </script>

  使用Object.defineProperty时,它是将对象的key转换成get/set形式来跟踪变化的,get/set它只能跟踪一个数据是否被修改,不能跟踪属性的新增与删除。这时删除属性我们可以用到vm.$delete实现,新增的话可以使用Vue.set(location,a,1)这样的方法添加响应式属性或者是给这个对象重新赋值data.location={...data.location,a:1}。

  它还无法监听到数组和对象的变化,对于数组而言,有以下的八种方法push,pop,shift,unshift,splice,sort,reverse它们是经过了一些内部处理对数组进行重写来保证响应式的。还有不能通过索引来直接设置数据项。Object.defineProperty它只能劫持对象的属性,我们需要对对象进行遍历。如果属性值也是对象时,则需要进行深度的遍历。这样非常麻烦,所以才有了Proxy数据代理。

  Proxy它是在目标对象之前就回调了一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,它提供了一种机制,可以对外界的访问进行过滤和改写,我们可以这样认为,Proxy是Object.defineProperty的加强版。

  Proxy它可以实现直接的监听对象而不是属性,它还可以直接监听数组的变化。但它的兼容性不是特别好。

<script>
    let obj = {
        like: swam,
        age: { age: 20 },
        arr: [1, 2, 3, 4]
    };
    function render() {
        console.log(render)
    }
    let handler = {
        get(target, key) {
            //判断取的值是否为对象
            if (typeof target[key] == object && target[key] !== null) {
                return new Proxy(target[key], handler);
            }
            return Reflect.get(target, key);
        },
        set(target, key, value) {
            if (key === length) return true
            render();
            return Reflect.set(target, key, value)
        }
    }
    let proxy = new Proxy(obj, handler)
    proxy.age.name = davina // 支持新增属性
    console.log(proxy.age.name) // 模拟视图的更新 "davina"
    proxy.arr[0] = 100 //支持数组的内容发生变化
    console.log(proxy.arr)  //Proxy {0: "100", 1: 2, 2: 3, 3: 4}
</script>
 

  我们可以用Proxy实现一个极简版本的双向绑定。

<input type="text" id="input">
<div class="box"></div>
<script>
    const input = document.getElementById(input);
    const box = document.querySelector(div);
    const obj = {};
    const newObj = new Proxy(obj, {
        get: function (target, key, receiver) {

            return Reflect.get(target, key, receiver);
        },
        set: function (target, key, value, receiver) {
            if (key === text) {
                input.value = value;
                box.innerHTML = value;
            }
            return Reflect.set(target, key, value, receiver);
        },
    });
    input.addEventListener(keyup, function (e) {
        newObj.text = e.target.value;
    });
</script>

  当数据的属性发生变化时,可以通知那些曾经使用过这个数据的地方数据变化了,那么我们是怎么知道曾经使用过这个数据的地方是哪些地方?我们要怎么进行通知?

  这时我们就要收集相应的依赖才能知道哪此地方依赖我们的数据,以及数据更新时进行相应的更新。这时我们就要用到”事件发布订阅模式“。接下来先介绍两外重要的角色-Dep订阅者和Watcher观察者。

  订阅者 Dep

  Dep是存储依赖的地方,它可以用来收集依赖,删除依赖,向依赖发送消息等等。它的主要作用是用来存放watcher观察者对象的,我们可以把观察者看成是一个中介的角色,数据发生变化时会通知它,然后再由它通知到其它的地方。当需求收集依赖时,我们可以调用addSub方法,当需求派发更新时我们调用notify方法。

   //订阅者
    class Dep {
        constructor() {
            //提供一个事件池
            this.subs = [];
        }
        //增加事件的操作,向事件池里放事件
        addSub(sub) {
            this.subs.push(sub);
        }
        //通知更新 
        notify() {
            this.subs.forEach(item => {
                // 让对应的事件做更新操作
                item.update();
            })
        }
    }

  Observer监听者

  我们需求知道属性值的变化,用Observer来实现监听

   /* 监听者*/
  function observe(data) {
        //简单判断类型
        if (typeof data !== object) {
            return;
        }
        let keys = Object.keys(data);//key是所有属性名组成的数组
        keys.forEach(key => {
            defineReactive(data, key, data[key])

        })
    }

    //封装一个defineReactive函数专门调用defineProperty,实现数据劫持
    function defineReactive(obj, key, value) {
        observe(value);//实现深层劫持

        let dep = new Dep;//每一个key都有一个私有变量dep

        Object.defineProperty(obj, key, {
            get() {
                if (Dep.target) {
                    dep.addSub(Dep.target);//Dep.target就是watcher实例
                }
                return value;
            },
            set(newval) {
                if (value !== newval) {
                    value = newval;
                    observe(value);
                    dep.notify();
                }
            }
        })

    }
    

  watcher观察者

  当属性发生变化后,我们要通知所有用到这个数据的地方,在一个项目或者是文件中用到这个数据的地方有很多,而且类型不一定相同,这时就需求我们抽象出一个类中集中的处理这个情况。我们在收集依赖的阶段只是收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其它的地方,这样速度和效率会快很多。依赖收集的目的是将watcher观察者对象存放到当前的闭包中的Dep订阅者的subs下。

/*watcher的简单实现*/
class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
 // 最后将 Dep.target 置空
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
   // 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
    this.cb(this.value)
  }
}

  以上就是watcher的简单实现 ,在执行构造函数的时候将Dep.target指向自己,从而使收集到了对应的watcher,在派发更新的时候取出相应的watcher,然后然后执行update。

  总结一下就是:所谓的依赖其实就是watcher,收集依赖时,我们要做到在get中收集依赖,在set中触发依赖。先收集依赖,就把用到这个数据的地方先收集起来,放到一个地方,然后属性发生变化时,把之前收集好的依赖循环触发就可以了。当外界通过watcher读取数据时,这就发触发get,从而将watcher添加到依赖中,哪个watcher触发了get,就把哪个watcher放到到Dep中,当数据发生变化时,会循环依赖列表,把所有的watcher都执行一次。

 一个完整的双向绑定有以下几点:

  1、在new Vue()后利用Proxy或Object.defineProperty方法对对象/对象属性进行"劫持",Vue中的data会通过observe添加上get/set属性,来对数据进行追踪变化,当对象被读取时会执行get函数,而当被赋值时执行set函数。在属性发生变化后通知订阅者

  2、解析器Compile解析模板中的指令,收集方法和数据,等待数据变化然后渲染。

  3、Watcher接收到的Observe产生的数据变化,并根据Compile提供的指令进行视图渲染,使得数据变化促使视图变化

技术分享图片

   vue是通过虚拟DOM追踪自己要改变的真实DOM,这里用真实的dom来简单的进行模拟。

<body>
    <div id="app">
        <h1>我的名字是:{{name}}</h1>
        <h2>今年是:{{age}}</h2>
        <input type="text" v-model=name></br>
        <input type="text" v-model=age>
    </div>
</body>
<script>
    /* 用来数据劫持 */
    function observe(data) {
        if (typeof data !== object) {
            return;
        }
        let keys = Object.keys(data);
        keys.forEach(key => {
            defineReactive(data, key, data[key])

        })
    }
    //封装一个defineReactive函数专门调用defineProperty,实现数据劫持
    function defineReactive(obj, key, value) {
        observe(value);
        let dep = new Dep;
        Object.defineProperty(obj, key, {
            get() {
                if (Dep.target) {
                    dep.addSub(Dep.target);
                }
                return value;
            },
            set(newval) {
                if (value !== newval) {
                    value = newval;
                    observe(value);
                    dep.notify();
                }
            }
        })

    }
    /* 模板编译 */
    //把元素节点转移到文档碎片上,将节点进行编译后再还给节点
    function nodeToFragment(node, vm) {
        let child;
        let fragment = document.createDocumentFragment();
        //把node中的每一个子节点,转移到了fragment
        while (child = node.firstChild) {
            //在移到fragment上先进行编译
            compile(child, vm);
            fragment.appendChild(child);
        }
        //又把fragment上所有的节点放到node上
        node.appendChild(fragment);
    }
    function compile(node, vm) {
        //判断node的节点类型
        if (node.nodeType == 1) {
            let attrs = node.attributes;
            [...attrs].forEach(item => {
                if (/^v\-/.test(item.nodeName)) {
                    //证明它是v-开头的
                    let valName = item.nodeValue;//获取"name"这个单词

                    new Watcher(node, vm, valName);

                    let val = vm.$data[valName];
                    node.value = val;
                    node.addEventListener(input, (e) => {
                        vm.$data[valName] = e.target.value;
                    })
                }
            });
            //针对有子节点的元素接着进行编译
            [...node.childNodes].forEach(item => {
                compile(item, vm);
            })
        } else {
            let str = node.textContent;
            node.str = str; 
            if (/\{\{(.+?)\}\}/.test(str)) {
                str = str.replace(/\{\{(.+?)\}\}/g, (a, b) => {
                    b = b.replace(/^ +| +$/g, ‘‘);
                    new Watcher(node, vm, b);
                    return vm.$data[b]
                })
                node.textContent = str
            }

        }
    }
    //订阅者
    class Dep {
        constructor() {
            this.subs = [];
        }
        addSub(sub) {
            this.subs.push(sub);
        }
        notify() {
            this.subs.forEach(item => {
                item.update();
            })
        }
    }
       //观察者
    class Watcher {
        constructor(node, vm, key) {
            Dep.target = this; 
            this.node = node;
            this.vm = vm;
            this.key = key;
            this.get();
            Dep.target = null;
        }
        //把对应节点里的内容进行更新
        update() {
            //如果是input更新value值,如果是文本更新textContent
            this.get();
            if (this.node.nodeType == 1) { 
                this.node.value = this.value;
            } else {
                let str = this.node.str;//node.str = str; 
                str = str.replace(/\{\{(.+?)\}\}/g, (a, b) => {
                    b = b.trim();
                    //考虑到在一个文本里有多个小胡子
                    if (b == this.key) {
                        return this.value
                    } else {
                        return this.vm.$data[b];
                    }
                })
                this.node.textContent = str;
            }
        }
        get() {
            this.value = this.vm.$data[this.key]
        }
    }
       function Vue(options) {
        //$el 存储的是当前元素 
        this.$el = document.querySelector(options.el);
        // $data存储的是data中的属性
        this.$data = options.data
        observe(this.$data)
        nodeToFragment(this.$el, this)
    }
    let vm = new Vue({
        el: #app,
        data: {
            name: davina,
            age: 10,
        }
    })
</script>

  

 

  

 

 

vue中响应式原理

原文:https://www.cnblogs.com/davina123/p/13413256.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!