zoukankan      html  css  js  c++  java
  • Vue 的数据劫持 + 发布订阅

    Vue 的双向绑定策略基础是数据劫持,在 Vue2.0 中使用了 ES5 语法 Object.defineProperty,来劫持各个属性的 setter/getter,在数据变动时发布消息给订阅者(Wacther), 触发相应的监听回调。先来看一下这个 ES5 特性,我们可以通过 Object.defineProperty 这个方法,直接在一个对象上定义一个新的属性,或者修改已存在的属性,最终这个方法会返回该对象,如下为简单说明,对该特性不了解的同学可以查看《JavaScript 高级程序设计》的第六章,或者在线访问 MDN Web 文档: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty 

    var o = {};
    var value = 1;
    Object.defineProperty(o, 'a', {
      get: function() { return value; },
      set: function(newValue) { value = newValue; },
      enumerable: true,
      configurable: true
    });
    o.a; // 1
    o.a = 2;
    o.a; // 2

    结合这一特定与发布订阅机制,可以实现完整的双向绑定。如下所示,Observer 数据监听器能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,内部采用 Object.defineProperty 的 getter 和 setter 来实现 [3]。

    Compile 指令解析器,它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。

    Watcher 订阅者, 作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数。Dep 消息订阅器,内部维护了一个数组,用来收集订阅者(Watcher),数据变动触发 notify 函数,再调用订阅者的 update 方法。

    当执行 new Vue() 时,Vue 就进入了初始化阶段,一方面会遍历 data 选项中的属性,用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能;另一方面,Vue 的指令编译器 Compile 对元素节点的指令进行扫描和解析,初始化视图,并订阅 Watcher 来更新视图, 此时 Wather 会将自己添加到消息订阅器中 (Dep), 初始化完毕。当数据发生变化时,Observer 中的 setter 方法被触发,setter 会立即调用 Dep.notify(),Dep 开始遍历所有的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新 

    使用 Object.defineProperty 这个特性存在一些明显的缺点,总结起来大概是下面两个:

    1. Object.defineProperty 无法监控到数组下标的变化,当监控数组数据对象的时候,实质上就是监控数组的地址,地址不变也就不会被监测到。为了解决这个问题,经过 Vue 内部处理后可以使用 push、pop、shift、unshift、splice、sort、reverse 来监听数组。
    2. Object.defineProperty 只能劫持对象的属性, 因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过递归 + 遍历 data 对象来实现对数据的监控的。如果属性值也是对象,那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

    由于只针对了以上八种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。Vue3.0 中使用了 ES6 语法 Proxy,用于取代 defineProperty,使用 Proxy 有以下两个优点:

    1. 可以劫持整个对象,并返回一个新对象。
    2. 有 13 种劫持操作。

    既然 Proxy 能解决以上两个问题,而且 Proxy 作为 es6 的新属性在 Vue2.x 之前就有了,为什么 Vue2.x 不使用 Proxy 呢?一个很重要的原因就是,Proxy 是 ES6 提供的新特性,兼容性不好,并且是这个属性无法用 polyfill 来兼容。

    Vue 的双向绑定策略成为当前考察前端人员技术功底的重点,我们以 Object.defineProperty 特性实现一个简单的双向绑定,实现最初的 hello everyone 效果。

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <title>双向绑定最最最初级 demo</title>
            <meta charset="UTF-8">
        </head>
        <body>
            <div id="app">
                <input type="text" id="txt">
                <id="show-txt"></p>
                <button onClick="changeData()">更新数据</button>
            </div>
        </body>
        <script>
            var obj={}
            Object.defineProperty(obj,'txt',{
                get:function(){
                    return obj
                },
                set:function(newValue){
                    document.getElementById('txt').value = newValue
                    document.getElementById('show-txt').innerHTML = newValue
                }
            })
            document.addEventListener('keyup',function(e){
                obj.txt = e.target.value
            })
            changeData = function() {
                obj.txt = 'hello world';
            }
        </script>
    </html>

    由于 Object.defineProperty 默认只能劫持值类型数据,对引用类型数据的内部修改无法劫持,需要重写覆盖原原型方法,以 Array 为例,如下可以支持到 7 种数组方法:

    let arr = [];
    let arrayMethod = Object.create(Array.prototype);
    ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
        Object.defineProperty(arrayMethod, method, {
            enumerable: true,
            configurable: true,
            value: function () {
                let args = [...arguments]
                Array.prototype[method].apply(this, args);
                console.log(`operation: ${method}`);
            }
        })
    });
    arr.__proto__ = arrayMethod;
    arr.push(1); // 劫持到了 push 方法

    相对完整的仿 Vue 双向绑定实现,来自双向绑定数组源码: https://gitee.com/merico/Blog/tree/master/Object.defineProperty_Array 

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <title>双向绑定支持数组监听</title>
            <meta charset="UTF-8">
        </head>
        <body>
            <div id="app">
                <div id='list'></div>
                <input type="button" value="添加" onclick="btnAdd()" />
                <input type="button" value="删除" onclick="btnDel()" />
            </div>
        </body>
        <script>
            // 数据源
            let vm = {
                list: [1, 2, 3, 4]
            }
            // 用于管理 watcher 的 Dep 对象
            let Dep = function () {
                this.list = [];
                this.add = function (watcher) {
                    this.list.push(watcher)
                };
                this.notify = function (newValue) {
                    this.list.forEach(function (fn) {
                        fn(newValue)
                    })
                }
            };
            // 模拟 compile, 通过对 Html 的解析生成一系列订阅者(watcher)
            function renderList() {
                let listContainer = document.querySelector('#list');
                let contentList = '';
                vm.list.forEach(function (item) {
                    contentList = contentList + `<div><h3>${item}</h3></div>`
                })
                listContainer.innerHTML = contentList;
            }
            // 将解析出来的 watcher 存入 Dep 中待用
            let dep = new Dep();
            dep.add(renderList)
            // 核心方法
            function initMVVM(vm) {
                Object.keys(vm).forEach(function (key) {
                    let value = vm[key];
                    if (Array.isArray(value)) {
                        observeArray(vm, key)
                    }
                })
            }
            function observeArray(vm, key) {
                let arrayMethod = bindWatcherToArray();
                vm[key].__proto__ = arrayMethod;
            }
            function bindWatcherToArray() {
                let arrayMethod = Object.create(Array.prototype);
                ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
                    Object.defineProperty(arrayMethod, method, {
                        enumerable: true,
                        configurable: true,
                        value: function () {
                            let args = [...arguments]
                            Array.prototype[method].apply(this, args);
                            console.log(`operation: ${method}`)
                            dep.notify();
                        }
                    })
                });
                return arrayMethod
            }
            // 页面引用的方法
            function btnAdd() {
                vm.list.push(Math.random())
            }
            function btnDel() {
                vm.list.pop()
            }
            // 初始化数据源
            initMVVM(vm)
            // 初始化页面
            dep.notify();
        </script>
    </html>
  • 相关阅读:
    cinder-volume报错vmdk2 is reporting problems, not sending heartbeat. Service will appear "down".
    Linux下截取指定时间段日志并输出到指定文件
    使用diff或者vimdiff比较远程文件(夹)与本地文件夹
    OpenStack视图
    awk 计算某一列的和
    opencontrail—VXLAN模式下数据包的传输过程
    shell-计算虚拟机创建时间
    gnocchi resource批量删除
    openflow控制器和交换机之间的消息
    openflow packet_out和packet_in分析
  • 原文地址:https://www.cnblogs.com/chuanzi/p/12452141.html
Copyright © 2011-2022 走看看