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

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

内容简介:数据劫持是双向绑定各种方案中比较流行的一种,最著名的实现就是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重写


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

查看所有标签

猜你喜欢:

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

Head First JavaScript Programming

Head First JavaScript Programming

Eric T. Freeman、Elisabeth Robson / O'Reilly Media / 2014-4-10 / USD 49.99

This brain-friendly guide teaches you everything from JavaScript language fundamentals to advanced topics, including objects, functions, and the browser’s document object model. You won’t just be read......一起来看看 《Head First JavaScript Programming》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具