MVVM双向数据绑定(二):VueJS数据劫持

栏目: 编程工具 · 发布时间: 5年前

内容简介:数据劫持是双向绑定各种方案中比较流行的一种,最著名的实现就是Vue。基于数据劫持的双向绑定离不开Proxy与Object.defineProperty等方法对对象/对象属性的”劫持”,我们要实现一个完整的双向绑定需要以下几个要点。本文主要总结了VueJS利用

数据劫持是双向绑定各种方案中比较流行的一种,最著名的实现就是Vue。

基于数据劫持的双向绑定离不开Proxy与Object.defineProperty等方法对对象/对象属性的”劫持”,我们要实现一个完整的双向绑定需要以下几个要点。

  1. 利用 Proxy 或 Object.defineProperty 生成的 Observer 针对对象/对象的属性进行”劫持”,在属性发生变化后通知 订阅者
  2. 解析器 Compile 解析模板中的 Directive (指令),收集指令所依赖的方法和数据,等待数据变化然后进行渲染。
  3. Watcher 属于 Observer 和 Compile 桥梁,它将接收到的 Observer 产生的数据变化,并根据 Compile 提供的指令进行视图渲染,使得数据变化促使视图变化。

本文主要总结了VueJS利用 Object.defineProperty()Proxy ,结合发布者-订阅者模式实现双向数据绑定的基本原理。

Object.defineProperty()

Vue 内部使用了 Object.defineProperty() 来实现双向绑定,当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,监听到 set 和 get 的事件。

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

Object.defineProperty(obj, prop, descriptor)

该函数接受三个参数:

  • obj:要在其上定义属性的对象。
  • prop:要定义或修改的属性的名称。
  • descriptor:将被定义或修改的 属性描述符

详细的文档可以参阅 MDN

Observer对象

class Observer {
    constructor(data) {
        this._data = data;
        this.walk(this._data);
    }
    walk(data) {
        Object.keys(data).forEach((key) => { 
            this.defineRective(data, key, data[key]) 
        })
    };
    defineRective(vm, key, value) {
		 // 将这个属性的依赖表达式存储在闭包中。
        var self = this;
        if (value && typeof value === "object") {
            this.walk(value);
        }
        Object.defineProperty(vm, key, {
            get: function() {
                return value;
            },
            set: function(newVal) {
                if (value != newVal) {
                    if (newVal && typeof newVal === "object") {
                        self.walk(newVal);
                    }
                    value = newVal;
 					// 通知所有的 viewModel 更新
                	dep.notify();
                }
            }
        })
    }
}
module.exports = Observer;

为每个属性添加了 getter 和 setter ,当属性是一个对象,那么就递归添加。

一旦获取属性值或者为属性赋值就会触发 get 或 set ,当触发了 set ,即 model 变化,就可以发布消息, dep.notify(); 通知所有 viewModel 更新。

Dep 对象就是一个闭包。

class Dep {
    constructor() {
        // 依赖列表
        this.dependences = [];
    }
    // 添加依赖
    addDep(watcher) {
        if (watcher) {
            this.dependences.push(watcher);
        }
    }
	  depend() {
		// Dep.target是一个实例化的全局 watcher 对象
		if (Dep.target) {
			// 传入闭包中的 dep 对象
			Dep.target.addDep(this)
		}
	  }
    // 通知所有依赖更新
    notify() {
        this.dependences.forEach((watcher) => {
            watcher.update();
        })
    }
}
Dep.target = null
function update(value) {
  document.querySelector('div').innerText = value
}
module.exports = Dep;

这里的每个依赖就是一个 Watcher 。每一个 Watcher 都会有一个唯一的 id 号,它拥有一个表达式和一个回调函数 。

var uid = 0;
class Watcher {
    constructor(viewModel, exp, callback) {
		  // viewModel 就是 obj
        this.viewModel = viewModel;
        this.id = uid++;
        this.exp = exp;
        this.callback = callback;
        this.oldValue = "";
        this.update();
    }
    get() {
		 // 将 Dep.target 指向自己
        Dep.target = this;
        var res = this.compute(this.viewModel, this.exp);
        Dep.target = null;
        return res;
    }
    update() {
        var newValue = this.get();
        if (this.oldValue === newValue) {
            return;
        }
        // callback 里进行Dom 的更新操作
        this.callback(newValue, this.oldValue);
        this.oldValue = newValue;
    }
    compute(viewModel, exp) {
        var res = replaceWith(viewModel, exp);
        return res;
    }
}
module.exports = Watcher;

由于当前表达式需要在 当前的 model 下面执行,所以 采用 replaceWith 函数来代替 with。

通过 get 添加依赖, 修改Object.defineProperty为以下代码

Object.defineProperty(vm, key, {
    get: function() {
        var watcher = Dep.target;
        if (watcher && !dep.dependences[watcher.id]) {
            dep.addDep(watcher);
        }
        return value;
    },
    set: function(newVal) {
        if (value != newVal) {
            if (newVal && typeof newVal === "object") {
                self.walk(newVal);
            }
            value = newVal;
				// 执行 watcher 的 update 方法
            dep.notify();
        }
    }
})

以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加。

Proxy

Object.defineProperty存在两个缺陷:

  1. 无法监听数组变化。

然而Vue的文档提到了Vue是可以检测到数组变化的,但是只有以下八种方法, vm.items[indexOfItem] = newValue 这种是无法检测的。

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

其实作者在这里用了一些奇技淫巧,把无法监听数组的情况hack掉了。由于只针对了八种方法进行了hack,所以其他数组的属性也是检测不到的。

  1. 只能对对象的属性进行数据劫持,所以需要深度遍历整个对象。

所以利用 Proxy,就可以很好地避免上述两个缺陷。

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

可以看到,Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty。

Proxy的其他优势

Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。

Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。

当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

领域驱动设计

领域驱动设计

埃文斯 / 赵俐、盛海艳、刘霞 / 人民邮电出版社 / 2010-11 / 69.00元

《领域驱动设计:软件核心复杂性应对之道》是领域驱动设计方面的经典之作。全书围绕着设计和开发实践,结合若干真实的项目案例,向读者阐述如何在真实的软件开发中应用领域驱动设计。书中给出了领域驱动设计的系统化方法,并将人们普遍接受的一些最佳实践综合到一起,融入了作者的见解和经验,展现了一些可扩展的设计最佳实践、已验证过的技术以及便于应对复杂领域的软件项目开发的基本原则。《领域驱动设计:软件核心复杂性应对之......一起来看看 《领域驱动设计》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具