MVVM 响应式实现原理:
1. 模板编译
2. 数据劫持
3.watcher
MVVM------------------ 视图 ----- 模型 ---- 视图模型
三者与 vue 的对应: view 对应 template,vm 对应 newVue({...}),model 对应 data
文中应用到的数据名词:
nodeType 判断节点是否是元素节点
querySelector 创建一个元素节点
createDocumentFragment 文档碎片
attributes 获取元素属性集合
textContent 获取文本内容
reduce(prev,next,currentIndex){}一个可以用上一个元素和当前元素做处理的方法
defineProperty(obj,key,value){}数据拦截的主要方法
首先建立一个 vue 的实例, 建立 mvvm.JS, 构建 mvvm 类. 获取 el 的节点和 data 放入实例中, 在将 Observer.JS(数据劫持)和 Compile.JS(模板编译)放入 mvvm 的 JS, 全部在 index 页面运行.
第一步: 模板编译
我首先制作 Compile.JS, 也就是模板编译.
首先需要获取 el 这个属性的值 用 nodeType === 1 判断是不是元素节点. 如果不是则用 queryselector() 生成一个节点 . 这样做的目的是, 有些人 el:#App 有些人是 document.getElementById('app'). 不管俩者如何, 我们都要生成一个节点来供后续使用.
随后判断 el 节点是否存在, 如果存在. 则进行编译 , 这里编译最好不要在 dom 里进行遍历编译, 非常耗性能 . 我推荐的是用 createDocumentFragment() 方法. 建立一个虚拟节点对象, 在这个虚拟节点对象里进行遍历以及对应的操作.
那么说到虚拟节点, 我们需要将我们获取的 el 节点整个放入进去 , 进行遍历, 将 App 里的每一个子节点都搬到 fragement 变量中.
然后进行节点的编译. 这里的节点又分为元素节点和文本节点. 还是用刚刚的 nodeType 判断区分吗, 然后做对应的操作.
接下来我们先编译元素节点首先我们需要知道, 获取元素节点要做什么, 为什么获取元素节点. 我是希望通过获取元素节点上的关于 vue 的指令, 比如: v-model,v-html,v-for. 等等... 那么这些指令是放在元素节点上的属性里, 所以我们用 attributes 获取元素节点的属性名的集合, 也就是我们说的 v-model. 通过遍历这个 attr 属性名的集合, 获取每个属性名. 通过 isDirective 函数判断 attrName 包含 v - 的属性, 这里我做给假设, 好方便理解. 这里通过上面的过滤, 可以得出 attrName 是一个指令名的集合. 那我假设这个指令名为 v-model. 我首先获取 v-model 的值, 也就是 expr. 然后做一个解耦对象 CompileUtil, 方便后面制作其他的指令. 所以这里调用的是 CompileUtil[model]{node,this.v,,expr};
调用 model 的指令后, 在 model 这个函数里做相对应的处理. 这里的 watcher 构造函数先不用管, 后面的事情. 这里的 uptate['modelUptate']和 model 一样放在 CompileUtil 中, 方便管理. 如果 updateFn 存在的话, 则执行 updateFn(), 将 v-model 的值赋予 input 节点的 value. 下图中的 getVal 是防止 v-model='messge.a'这种嵌套对象的. 这个函数里, 首先利用 split 将 messge.a 拆分成 [messge,a] 数组. 然后利用 reduce 方法返回上一个元素[当前元素], 而最下面的 vm.$data 是 reduce 方法遍历的初始值. 也就是 data.
因为 data:{messge:{a:'hello.world'}}. 这样的编译, 元素节点就可以编译出来了, 可以将 data 的值编译到元素节点上了.
接下来编译文本节点, 那文本节点, 我们首先获取文本节点里的值, 然后利用正则的 test 找 {{a}}, 和之前的元素节点一样, 执行对应的函数., 执行对应的行数. 这里第 86-90 可以先不管, 不过这里的 textVal 和上面的 getVal 函数不一样, 首先是需要将符合条件的元素里的变量取出来也就是{{a}} 里的 a,argments[1]就是 a 变量. 在考虑到对象嵌套, 就执行上面的 getVal. 然后就可以将 data 里的值替换到文本里了.
这样元素节点和文本节点都编译完成了. 然后将整个虚拟节点丢回 dom 树里去 .MVVM 的编译就结束了
第二步: 数据劫持, 函数很少. 但比较绕. 这里执行 observe, 利用递归遍历, 将 data 里的键值对全部拿出来处理, 执行 defineReactive 函数, 这里 18 行可以先不看. 看下面的最重点的 Object.defineProperty(). 这里要传入劫持的对象, 劫持的键, 以及回调函数. 这里回调函数里俩个参数在下图.
然后, get 函数是取值是做对应的操作, set 函数是设置值做对应的操作. 至此数据劫持就完成了
第三步: watcher 监察者 , 一旦变化执行对应的操作. 也就是将模板编译和数据劫持俩个函数联系在一起. 有衔接.
这里创建 watcher 类, 将需要的参数获取. vm 是实例, expr 是值, cb 是回调函数 callback.watcher 实例里的 value = get 方法的返回值, value 执行一次嵌套处理返回. 这里监察者作用主要是 一 更新值, 二是执行 callback 回调函数 cb. 三将自己的实例, 放入 dep 的 target 里. 那么 watcher 监察者就制作好了.
最后的连接部分, 首先 data 里的每个属性值都被加上了 set 和 get
1. 获取值
在最开始编译的时候, 编译节点的文本节点处理和元素节点处理的时候执行 watcher 函数, 在 watcher 函数里的 get 函数中将 watcher 函数自己放入 de 问值的时候, 则会执行 get 函数, 将 每个 watcher 放入 dep 数组中 .
2. 修改值
在修改值的时候, 会触发 Observer.JS 的 defineProperty 的 set 函数, set 函数里比较新的值和旧的值, value 是编译时候的值, newValue 是 set 函数的第一个参数, 也就是修改后的新值 . 将俩者比较, 如果不同, 就执行 Dep 构造函数的 notify 函数. notify 则会遍历全部存在的 dep 数组里的 watcher 的 update 方法. 在 watcher 的 update 方法中, 比较值的不同, 如果不同就则执行回调函数, 将视图更新. 这个回调函数是嵌套在处理文本节点和元素节点的方法里.
v-model 的双向绑定
至于 v-model 的双向绑定, 其实是绑定输入框的输入事件. 将输入事件新的值赋值给 input 节点的 value 值, 然后值的改变, 执行 set 函数, 将视图改变. 视图的改变, 会执行 wacther 的回调函数, 文本节点也会重新赋值.
这就是 mvvm 响应式原理的实现, 如果有残缺讲不清楚的地方, 欢迎指出. 谢谢.
来源: https://www.cnblogs.com/At867604340/p/12366176.html