proxy的观察者模式探索
vueConf大会,尤小右实锤vue3.0将改definePrototype为proxy。
却之为何
1.在Vue中,Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组。(事实上,Object.defineProperty本身是可以监控到数组下标的变化的,参Vue为什么不能检测数组变动)
1 2 3 4 5 6 7 |
push() pop() shift() unshift() splice() sort() reverse() |
由于只针对了以上八种方法进行了hack处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue里,是通过递归以及遍历data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象,不管是对操作性还是性能都会有一个很大的提升。
而要取代它的Proxy有以下两个优点;
- 可以劫持整个对象,并返回一个新对象
- 有13种劫持操作
vue2.x之前之所以不用Proxy,主要Proxy是es6提供的新特性,兼容性不好,最主要的是这个属性无法用polyfill来兼容。
基于proxy的vue数据双向绑定实现
■ Observer: 基于proxy处理代理,当属性修改时通知Dep。
■ compile: 解析指令,订阅数据变化,绑定更新函数。 根据对应指令绑定相应watcher。
■ Dep: 为全局对象subscribe添加对应属性,当数据变化,通知watcher,调用相关update方法
■ Watcher: 主要update方法,修改相关node节点数据。
1. 页面结构
1 2 3 4 5 6 7 8 9 10 |
<div id="app"> <!-- input框, 包含v-model指令 --> <input type="text" v-model="num" /> <!-- input框, 包含v-model指令 --> <input id="btn" type="button" value="添加到Todolist" v-click="addList" /><br /> <span>您输入的是:</span> <!-- span, 包含v-bind指令 --> <span v-bind="num"></span> <ul id="list"></ul> </div> |
2. 参照vue调用
1 2 3 4 5 6 7 8 9 10 11 12 13 |
new proxyVue({ el: "#app", data: { num: 0, arr: [] }, methods: { addList() { this.arr.push(this.num); Render.addList(this.num); } } }); |
proxyVue实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
// 渲染todolist列表 const Render = { // 初始化 init: function (arr) { const fragment = document.createDocumentFragment(); for (let i = 0; i < arr.length; i++) { const li = document.createElement("li"); li.textContent = arr[i]; fragment.appendChild(li); } list.appendChild(fragment); }, addList: function (val) { const li = document.createElement("li"); li.textContent = val; list.appendChild(li); } }; class ProxyVue { constructor(options) { this.$options = options || {}, this.$methods = this.$options.methods; const data = (this._data = this.$options.data); // subscribe对象结构:{ // num: [watcher, watcher] // } this.subscribe = {}; this.observe(data); this.compile(options.el); } observe(data) { let handel = { get: (target, key) => Reflect.get(target, key), set: (target, key, value) => { // 需要将Reflect赋值并return let res = Reflect.set(target, key, value); // 当对象改变,遍历执行其属性对应的watcher数组,并调用自身update方法 this.subscribe[key].map(item => { item.update(); }) return res; } } this.$data = new Proxy(data, handel); } compile(el){ // 将nodes类数组转换成数组,以供遍历 let nodes = Array.from(document.querySelector(el).children); let data = this.$data; nodes.map(node => { if (node.hasAttribute('v-model')) { let property = node.getAttribute('v-model'); this.publish(new Watcher(node, "value", data, property)); node.addEventListener("input", () => { data[property] = node.value; }); } if (node.hasAttribute('v-bind')) { let property = node.getAttribute('v-bind'); this.publish(new Watcher(node, "innerHTML", data, property)); } if (node.hasAttribute('v-click')) { let methodName = node.getAttribute('v-click'); let method = this.$methods[methodName].bind(data); node.addEventListener("click", method); } }) } // 订阅消息 publish(watcher) { if (!this.subscribe[watcher.property]) { this.subscribe[watcher.property] = [] } this.subscribe[watcher.property].push(watcher); } } class Watcher{ constructor(node, attr, data, property) { this.node = node; this.attr = attr; this.data = data; this.property = property; } update(){ this.node[this.attr] = this.data[this.property]; } |