内容简介:数据劫持是双向绑定各种方案中比较流行的一种,最著名的实现就是Vue。基于数据劫持的双向绑定离不开Proxy与Object.defineProperty等方法对对象/对象属性的”劫持”,我们要实现一个完整的双向绑定需要以下几个要点。本文主要总结了VueJS利用
数据劫持是双向绑定各种方案中比较流行的一种,最著名的实现就是Vue。
基于数据劫持的双向绑定离不开Proxy与Object.defineProperty等方法对对象/对象属性的”劫持”,我们要实现一个完整的双向绑定需要以下几个要点。
- 利用 Proxy 或 Object.defineProperty 生成的 Observer 针对对象/对象的属性进行”劫持”,在属性发生变化后通知 订阅者 。
- 解析器 Compile 解析模板中的 Directive (指令),收集指令所依赖的方法和数据,等待数据变化然后进行渲染。
- 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存在两个缺陷:
- 无法监听数组变化。
然而Vue的文档提到了Vue是可以检测到数组变化的,但是只有以下八种方法, vm.items[indexOfItem] = newValue
这种是无法检测的。
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
其实作者在这里用了一些奇技淫巧,把无法监听数组的情况hack掉了。由于只针对了八种方法进行了hack,所以其他数组的属性也是检测不到的。
- 只能对对象的属性进行数据劫持,所以需要深度遍历整个对象。
所以利用 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重写
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 3分钟了解vue数据劫持的原理
- 深入浅出 Vue 系列 -- 数据劫持实现原理
- ES6-Proxy与数据劫持(12)
- 可导致数百万玩家帐户被劫持:EA游戏帐户劫持漏洞分析
- 浅谈jsonp劫持
- 内网会话劫持
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。