效果
简述原理
配置对象传入vue实例
模板解析,遍历出所有文本节点,利用正则替换插值表达式为真实数据
data数据代理给vue实例,以后通过this.xxx访问
给每个dom节点增加观察者实例,由观察者群组管理,内部每一个键值含有多个对不同dom的观察者
data数据劫持,给data的每个属性增加get和set函数,当值改变时触发观察者的update方法,更新所有与当前属性值相关的dom元素
劫持数据,说的挺好听的,就是加工数据嘛,多了set变化触发了模板重新渲染,该渲染方式使用观察者模式,获取观察者收集的各个dom的所有属性 div,观察的属性,div的属性textContent,同时根据最新值渲染模板
div.textContent=vm[key]
html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><!-- <script src="./vue.js"></script> -->
</head><body><div id="app">{{ name }} {{age}}<h1>{{age}}</h1><button @click="cli">按钮</button><input type="text" v-model="name"></div>
</body>
<script src="./vue.js">
</script>
<script>new Vue({el: '#app',data: {name: 'Zwwwww',age: 18,},methods: {cli() {console.log(this);console.log(this.age);}},})</script></html>
js代码
class Vue {constructor(options) {// 获取配置对象的节点,存放在vm$el身上this.$el = document.querySelector(options.el)// console.log(this.$el)// 将配置对象的data对象代理到$datathis.$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身上所有keyfor (let key in this.$data) {// 数据代理给vue实例,thisObject.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.valueconsole.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 = thisObject.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 = vmthis.key = keythis.node = nodethis.attr = attr}// item.textContent = this.$data[dataKey.trim()]update() {console.log('开始渲染')// 将原始dom标签内容值替换为 data里的属性值this.node[this.attr] = this.vm[this.key]}
}
代码参考
VUE双向绑定原理分析~实现视图和数据的双向绑定~_哔哩哔哩_bilibili