vue2响应式原理+模拟实现v-model
效果
简述原理
配置对象传入vue实例
模板解析,遍历出所有文本节点,利用正则替换插值表达式为真实数据
data数据代理给vue实例,以后通过this.xxx访问
给每个dom节点增加观察者实例,由观察者群组管理,内部每一个键值含有多个对不同dom的观察者
data数据劫持,给data的每个属性增加get和set函数,当值改变时触发观察者的update方法,更新所有与当前属性值相关的dom元素
劫持数据,说的挺好听的,就是加工数据嘛,多了set变化触发了模板重新渲染,该渲染方式使用观察者模式,获取观察者收集的各个dom的所有属性 div,观察的属性,div的属性textContent,同时根据最新值渲染模板
div.textContent=vm[key]
html
Document{{ name }} {{age}}new Vue({ el: '#app', data: { name: 'Zwwwww', age: 18, }, methods: { cli() { console.log(this); console.log(this.age); } }, }){{age}}
按钮
js代码
class Vue { constructor(options) { // 获取配置对象的节点,存放在vm$el身上 this.$el = document.querySelector(options.el) // console.log(this.$el) // 将配置对象的data对象代理到$data this.$data = options.data // 获取配置对象的method值, // vue实例监听,当触发了方法执行对应函数 this.$methods = options.methods // 代理数据,后续通过this调用data对象的值 this.$allWatcher = {} this.proxyData() // 劫持数据,为其增加观察者监视数据变化引起视图渲染 this.observe() // 收集所有观察者,用对象的属性存放 this.compile(this.$el) } // 数据代理到vue实例身上,后续this调用方法和data值 proxyData() { // 遍历$data身上所有key for (let key in this.$data) { // 数据代理给vue实例,this Object.defineProperty(this, key, { // 使用get和set后续触发获取值和设置值做额外操作 get() { // 返回当前data对应的key属性值 return this.$data[key] }, set(value) { // 设置新值给当前属性 this.$data[key] = value }, }) } } // js数据替换{{name}},模板解析 compile(node) { // 遍历根节点下的所有节点 node.childNodes.forEach((item, index) => { //递归元素节点, //如果还没到文本节点,也就是说元素节点内还有元素节点 //则继续递归,直到元素节点没有子节点 //第二种可能,如果为元素元素节点,判断是否有@click属性,并获取值 //该值为绑定的methods方法 if (item.nodeType === 1) { if (item.childNodes.length > 0) { this.compile(item) } if (item.hasAttribute('@click')) { let domKey = item.getAttribute('@click') // console.log('我是dom标签的key', domKey) // 设置监听器,如果被点击了,触发配置对象中的method函数 item.addEventListener('click', () => { // 通过模板获取的属性值方法命,调用函数 // 由于$methods只是引用地址,this指向还是原来的methods // 我们这里使用call来绑定他的上下文this,也就是绑定他的调用者 // 在html部分我们就可以使用this.$data.age来获取vue实例上的数据 // 如果我们想直接this.age 就需要将data代理到vue实例身上 this.$methods[domKey.trim()].call(this) }) } if (item.hasAttribute('v-model')) { let vmodelKey = item.getAttribute('v-model').trim() // console.log('我是v-model的key', vmodelKey) // 设置监听器,如果被点击了,触发配置对象中的method函数 // 先单向给input框设置值 item.value = this.$data[vmodelKey] item.addEventListener('input', () => { console.log('用户正在输入') // 每次输入时将输入框的值重新赋给data对象属性值,完成双向绑定 this.$data[vmodelKey] = item.value console.log(this.$data[vmodelKey]) // 数据更新的同时重新解析模板 // 这里使用观察者类观察数据变化所作出的响应 }) } } // 判断是否为文本节点,nodeType == 3 // console.log(item.nodeType) // 如果是文本节点,进行数据替换 // 如果不是文本节点,为元素节点则往里递归遍历文本节点 if (item.nodeType === 3) { // 定义正则,替换{{xxx}}形式的字串为data下的属性值 let reg = /\{\{(.*?)\}\}/g // 获取原本标签里的值,后续进行替换 let text = item.textContent // console.log(text) item.textContent = text.replace(reg, (match, dataKey) => { // 先将dataKey去空格处理 dataKey = dataKey.trim() // match为匹配到的整体,datakey为捕获到的子内容(.*?) //我们这里只需获取dataKey对应的值并塞入即可 // console.log(match, dataKey) // 返回值作为替换内容 去除dataKey的前后空格 // 增加观察者,传vue实例对象,data属性,item标签,标签属性 // 相当于给每个文本节点都添加了一个观察者 // 将所有观察者收集到vue实例上,在数据发生变化时调用观察者的update方法 let watcher = new Watcher(this, dataKey, item, 'textContent') // 先进行判断观察者群组里是否有该节点的观察者 // 如果有,就push添加,因为一个dataKey可能有多个模板使用 // 举个例子,name属性可能在div1里使用也在div2里使用 // 也就是将多个文本节点与同个datakey绑定 if (this.$allWatcher[dataKey]) { this.$allWatcher[dataKey].push(watcher) } // 如果没有该属性的观察者存在,则新建空数组,push该观察者进入 else { this.$allWatcher[dataKey] = [] this.$allWatcher[dataKey].push(watcher) } return this.$data[dataKey] }) } }) } observe() { console.log('开始劫持') // 遍历所有的key,对其data数据劫持,值增加响应式功能 for (let key in this.$data) { // 先获取value,否则数据重新定义后值会丢失 // 此处的value变量不会随着observe方法的结束而销毁 // 与内部匿名函数get和set作为闭包永远绑定在一起 // 同时value值是对$data的一个引用,修改value值会引起$data变化 let value = this.$data[key] // 保存一份vue的引用_this=this, // 防止后续在组件外部,也就是input输入框 // 此时触发的set为一个闭包环境,上下文变成由defineproper定义的this.$data数据对象 // 此时找不到vue实例作为上下文,对key和其他数据的引用也会失效 let _this = this Object.defineProperty(this.$data, key, { get() { console.log('有人要获取劫持数据值', value) // 返回上面存储的value值 // 由于是响应式的,只有当观察到数据变化时所以才接触数据 // 其value值作用域也作用在劫持过程中 return value }, set(newValue) { console.log('劫持到数据,修改值为', newValue) console.log('劫持前的数据为', value) value = newValue // 更新值的同时进行模板更新 // 由于观察者队列含有观察者来观察不同属性管理的若干个模板 // 调用该属性值下所有模板观察者即可, // 只要属性值变化,该属性值下的所有观察者重新渲染模板 console.log(_this.$allWatcher) console.log(_this.$allWatcher[key]) _this.$allWatcher[key].forEach((watcher, index) => { watcher.update() }) }, }) } console.log('劫持成功') } } class Watcher { constructor(vm, key, node, attr) { this.vm = vm this.key = key this.node = node this.attr = attr } // item.textContent = this.$data[dataKey.trim()] update() { console.log('开始渲染') // 将原始dom标签内容值替换为 data里的属性值 this.node[this.attr] = this.vm[this.key] } }
代码参考
VUE双向绑定原理分析~实现视图和数据的双向绑定~_哔哩哔哩_bilibili
文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。