使用 Proxy 实现简易的 Vue 双向数据绑定
proxy 的基本使用
可以直接看这个链接: #8
使用 proxy 实现数据劫持
let data = { name: YoLinDeng, height: '176cm' } const p = new Proxy(data, { get(target, prop) { return Reflect.get(...arguments) }, set(target, prop, newValue) { return Reflect.set(...arguments) } })
关于 vue 中数据响应式的原理
对数据进行侦测
- 在 vue2.X 中,实现一个
observe
类,对于对象数据,通过Object.defineProperty
来劫持对象的属性,实现getter
和setter
方法,这样就可以在 getter 的时候知道谁(订阅者)读取了数据,即谁依赖了当前的数据,将它通过Dep 类
(订阅器)收集统一管理,在 setter 的时候调用 Dep 类中的notify
方法通知所以相关的订阅者进行更新视图。如果对象的属性也是一个对象的话,则需要递归调用observe
进行处理。 - 对于数组则需要另外处理,通过实现一个拦截器类,并将它挂载到数组数据的原型上,当调用
push/pop/shift/unshift/splice/sort/reverse
修改数组数据时候,相当于调用的是拦截器中重新定义的方法,这样在拦截器中就可以侦测到数据改变了,并通知订阅者更新视图。 - vue3 中使用 Proxy 替代了 Object.defineProperty,优点在于可以直接监听对象而非属性、可以直接监听数组的变化、多达 13 种拦截方法。缺点是兼容性还不够好。Proxy 作为新标准将受到浏览器厂商重点持续的性能优化。
对模板字符串进行编译
- 实现 Compile 解析器类,将
template
中的模板字符串通过正则等方式进行处理生成对应的 ast(抽象语法树),通过调用定义的不同钩子函数进行处理,包括开始标签(start
)并判断是否自闭和以及解析属性、结束标签(end
)、文本(chars
)、注释(comment
) - 将通过 html 解析与文本解析的 ast 进行优化处理,在静态节点上打标记,为后面
dom-diff
算法中性能优化使用,即在对比前后 vnode 的时候会跳过静态节点不作对比。 - 最后根据处理好的 ast 生产
render
函数,在组件挂载的时候调用render
函数就可以得到虚拟 dom。
虚拟 dom
- vnode 的类型包括注释节点、文本节点、元素节点、组件节点、函数式组件节点、克隆节点,
VNode
可以描述的多种节点类型,它们本质上都是VNode
类的实例,只是在实例化的时候传入的属性参数不同而已。 - 通过将模板字符串编译生成虚拟 dom 并缓存起来,当数据发生变化时,通过对比变化前后虚拟 dom,以变化后的虚拟 dom 为基准,更新旧的虚拟 dom,使它和新的一样。把 dom-diff 过程叫做
patch
的过程,其主要做了三件事,分别是创建/删除/更新节点。 - 对于子节点的更新策略,vue 中为了避免双重循环数据量大时候造成时间复杂度高带来的性能问题,而选择先从子节点数组中 4 个特殊位置进行对比,分别是:新前与旧前,新后与旧后,新后与旧前,新前与旧后。如果四种情况都没有找到相同的节点,则再通过循环方式查找。
实现简易的 vue 双向数据绑定
vue 的双向数据绑定主要是指,数据变化更新视图变化,视图变化更新数据。
实现代码如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width= , initial-scale=1.0"> <title>Document</title> <script src="myVue.js"></script> </head> <body> <div> {{name}} <div>{{message}}</div> <input type="text" v-model="test"> <span>{{test}}</span> </div> <script> let vm = new vue({ el: '#app', data: { name: 'YoLinDeng', message: '打篮球', test: '双向绑定数据' } }) // console.log(vm._data) </script> </body> </html>
class vue extends EventTarget { constructor(option) { super() this.option = option this._data = this.option.data this.el = document.querySelector(this.option.el) this.compileNode(this.el) this.observe(this._data) } // 实现监听器方法 observe(data) { const context = this // 使用 proxy 代理,劫持数据 this._data = new Proxy(data, { set(target, prop, newValue) { // 自定义事件 let event = new CustomEvent(prop, { detail: newValue }) // 发布自定义事件 context.dispatchEvent(event) return Reflect.set(...arguments) } }) } // 实现解析器方法,解析模板 compileNode(el) { let child = el.childNodes let childArr = [...child] childArr.forEach(node => { if (node.nodeType === 3) { let text = node.textContent let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g if (reg.test(text)) { let $1 = RegExp.$1 this._data[$1] && (node.textContent = text.replace(reg, this._data[$1])) // 监听数据更改事件 this.addEventListener($1, e => { node.textContent = text.replace(reg, e.detail) }) } } else if (node.nodeType === 1) { // 如果是元素节点 let attr = node.attributes // 判断属性中是否含有 v-model if (attr.hasOwnProperty('v-model')) { let keyName = attr['v-model'].nodeValue node.value = this._data[keyName] node.addEventListener('input', e => { this._data[keyName] = node.value }) } // 递归调用解析器方法 this.compileNode(node) } }) } }
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论