数据响应式是啥?

先来说说vue框架,它本质上是一个MVVM框架,而MVVM框架的三要素:数据响应式、模板引擎、渲染
数据响应式:监听数据变化并在视图中更新
Object.defifineProperty()
Proxy
模版引擎:提供描述视图的模版语法
插值:{{}}
指令:v-bind,v-on,v-model,v-for,v-if
渲染:如何将模板转换为html
模板 => vdom => dom

所以简单来说,数据变更能够响应在视图中,就是数据响应式。本文暂不涉及虚拟dom,先就vue中的数据响应式和模板引擎做一个简单的解析和实践。

原理分析

首先,我们先看一段vue实际使用的代码,从中分析双向绑定的实现方式和原理。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <div id="app">
  9. <p l-text="count"></p>
  10. <p l-html="desc"></p>
  11. </div>
  12. <script src="lCompile.js"></script>
  13. <script src="lvue.js"></script>
  14. <script>
  15. const app = new LVue({
  16. el:'#app',
  17. data:{
  18. count:1,
  19. desc:'<span style="color:red">这是lvue?</span>'
  20. }
  21. }
  22. })
  23. </script>
  24. </body>
  25. </html>

image.gif
1. new LVue() 首先执行初始化,对data执行响应化处理,这个过程发生在Observer中
2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在 Compile中
3. 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
4. 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个 Watcher
5. 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

好了我知道你们不想看文字,然后我花了大力气画的图,好好看!肯定能看懂!看不懂找我!

解析vue中的数据响应式 - 图2image.gif

好了,下面开始代码部分,代码部分这里先直接贴一个链接,大家可以直接去看。代码

具体实现

LVue

相当于入口文件吧,处理整体逻辑,把数据存起来,模板拿过来处理。

  1. class LVue{
  2. constructor(options){
  3. this.$options = options
  4. this.$data = options.data
  5. // 代理方法
  6. proxy(this,'$data')
  7. // 创建observe观察者
  8. observe(this.$data)
  9. // 编译模板,下面写
  10. new Compile(options.el, this)
  11. }
  12. }
  13. // 代理方法,目的是可以直接用this访问到$data中的内容
  14. function proxy(vm,str) {
  15. Object.keys(vm[str]).forEach(val=>{
  16. Object.defineProperty(vm,val,{
  17. get(){
  18. return vm[str][val]
  19. },
  20. set(newVal){
  21. vm[str][val] = newVal
  22. }
  23. })
  24. })
  25. }
  26. // 就简单的看一下是不是对象,因为defineReactive是对象的方法,
  27. // 至于数组则是通过重写数组操作方法实现数据劫持的,不过vue3中使用了ES6的Proxy
  28. function observe(obj) {
  29. if (typeof obj !== 'object' || obj == null) {
  30. // 希望传入的是obj
  31. return
  32. }
  33. // 创建Observer实例,进行数据劫持,下面写
  34. new Observer(obj)
  35. }

image.gif

Observer(数据劫持)

我们知道可以利用**Obeject.defineProperty()**来监听属性变动,但是你不能简单的对那个对象监听一下,万一对象内部属性还是个对象呢??所以需要将observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter,这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。

  1. class Observe {
  2. constructor(value){
  3. this.value = value
  4. this.walk(value)
  5. }
  6. // 对传入的参数进行劫持
  7. walk(obj){
  8. // 因为前面已经判断过是对象了,直接循环执行数据劫持方法就行了
  9. Object.keys(obj).forEach(key => {
  10. defineReactive(obj, key, obj[key])
  11. })
  12. }
  13. }
  14. // 对象的响应式
  15. function defineReactive(obj,key,val){
  16. observe(val)
  17. const dep = new Dep() // 这里是消息订阅器,用来建立数据更新与页面更新的对应关系。
  18. // 所有dep都先不看,下面会具体分析
  19. Object.defineProperty(obj,key,{
  20. get:function(){
  21. Dep.target&&dep.addDep(Dep.target)
  22. return val
  23. },
  24. set:function (newVal) {
  25. //当给data属性中设置值的时候, 更改获取的属性的值
  26. if (newVal !== val) {
  27. observe(newVal)
  28. val = newVal
  29. dep.notify() // 改变值时触发dep内部的循环更新
  30. }
  31. }
  32. })
  33. }

image.gif

监听到变化之后就是怎么通知订阅者Watcher了,有人说,那就直接通知Watcher不就好了?!那下面,说一下依赖收集。
视图中会用到vue的data中的某个值,这称为依赖。同一个值,可能会出现很多次,每次出现都需要将它收集出来,用一个Watcher进行维护,这就是依赖收集。 而某个值出现多次,则需要多个Watcher,这时候我们就需要一个Dep来管理,我们在修改数据时由Dep通知Watcher批量更新。
来个简单版解释: 代码中某个值,在很多地方使用,每个使用的地方对应一个更新操作。需要把这些操作放到一个盒子里,值改变的时候把盒子里所有更新操作触发一下。

watcher

好了,大概知道WatcherDep是什么了,那下面给大家先来个实现思路:

  1. 劫持时defifineReactive为每一个key创建一个Dep(就是上面说的那个管理Watcher的东西)实例。
  2. 初始化视图时每一次读取某个key,例如name1,创建一个watcherName1。
  3. 此时就会触发key(name1)的getter方法,所以就可以在getter方法中将watcherName1添加到name1对应的Dep中。
  4. 当key(name1)更新时,setter触发,此时便可通过对应Dep通知其管理所有Watcher更新,这样Dep中所有的watcher都触发一次更新,就实现了数据的响应。

这时候看完这些再回去理解前面劫持部分的代码有关dep的部分是不是就理解了呢。那下面我们来看一下实现:

  1. class Watcher {
  2. constructor(vm,key,updateFn){
  3. // vue实例
  4. this.vm = vm
  5. // 可触发/依赖 的key
  6. this.key = key
  7. // 更新函数
  8. this.updateFn = updateFn
  9. // 下面两行要回去对照数据劫持getter部分看一下
  10. Dep.target = this // 把Watcher存一下,get中直接dep.addDep(Dep.target)存进去
  11. this.vm[this.key]; // 这里的意思就行调用了一下对应的key,这样就能出发getter方法了
  12. Dep.target = null
  13. }
  14. update(){
  15. this.updateFn.call(this.vm,this.vm[this.key])
  16. }
  17. }

image.gif

Dep

Dep就相对简单多了,本质上就是维护了一组Watcher,有一个更新事假,执行的是循环出发Watcher中的更新方法。代码如下:

  1. class Dep{
  2. constructor(){
  3. this.deps = []
  4. }
  5. addDep(dep){
  6. this.deps.push(dep)
  7. }
  8. notify(){
  9. // deps里面是一个一个的 watch , 改变值后循环触发update方法
  10. this.deps.forEach(dep => dep.update());
  11. }
  12. }

image.gif

Compile

接下来是编译部分,说实话这一部分理解起来特别简单,就是以传进来的根节点为基础遍历整个dom,然后找出节点上属性中“v-”,”{{}}”等等这一类的标识属性,挨个创建Watcher来监听他们就好了。虽然理解简单,但是代码却很多,所以代码细节就不一行一行的解释了,大家可以仔细看看代码,我还是写了不少注释的~ 有疑问也欢迎骚扰~

  1. class Compile {
  2. constructor(el, vm){
  3. this.$el = document.querySelector(el)
  4. this.$vm = vm
  5. if (this.$el){
  6. this.compile(this.$el)
  7. }
  8. }
  9. // 编译,vue的语法
  10. compile(el){
  11. const childNodes = el.childNodes
  12. Array.from(childNodes).forEach(node=>{
  13. if (this.isElement(node)){
  14. // console.log("编译元素" + node.nodeName)
  15. this.compileElement(node)
  16. } else if(this.isInterpolation(node)){
  17. // console.log("编译差值文本" + node.textContent)
  18. this.compileText(node)
  19. }
  20. if (node.childNodes&&node.childNodes.length>0) {
  21. this.compile(node)
  22. }
  23. })
  24. }
  25. isElement(node){
  26. return node.nodeType === 1
  27. }
  28. isInterpolation(node){
  29. return node.nodeType === 3 &&/\{\{(.*)\}\}/.test(node.textContent)
  30. }
  31. // node为元素时编译方法
  32. compileElement(node){
  33. let nodeAttrs = node.attributes
  34. Array.from(nodeAttrs).forEach(attr=>{
  35. let attrName = attr.name
  36. let exp = attr.value
  37. console.log(exp)
  38. // 属性名以 l- 开头时处理
  39. if (attrName.indexOf("l-")===0){
  40. let dir = attrName.substring(2)
  41. // 拿出后面的html、text 等,html、text会被在内部定义方法
  42. this[dir]&&this[dir](node,exp)
  43. }
  44. // 时间处理
  45. if(this.isEvent(attrName)){
  46. // @click = onClick
  47. const dir = attrName.substring(1) // click
  48. // 事件监听
  49. this.eventHandler(node,exp,dir)
  50. }
  51. })
  52. }
  53. isEvent(dir){
  54. return dir.indexOf('@') == 0
  55. }
  56. eventHandler(node,exp,dir){
  57. const fn = this.$vm.$options.methods &&
  58. this.$vm.$options.methods[exp]
  59. node.addEventListener(dir,fn.bind(this.$vm))
  60. }
  61. // node为文本时处理
  62. compileText(node){
  63. this.update(node,RegExp.$1,'text')
  64. }
  65. // 初始化时执行 更新方法,并传入text
  66. text(node,exp){
  67. this.update(node,exp,'text')
  68. }
  69. // 初始化时执行 更新方法 目的是 update中创建了Watcher,可以传入改变方法,在数据监听时就可执行了
  70. html(node,exp){
  71. this.update(node,exp,'html')
  72. }
  73. model(node,exp){
  74. // update方法只完成赋值和更新
  75. this.update(node,exp,'model')
  76. // 所以还需要事件监听
  77. node.addEventListener('input',e=>{
  78. // 将新的值赋值给数据即可
  79. this.$vm[exp]=e.target.value
  80. })
  81. }
  82. // 创建更新函数,和watcher绑定
  83. update(node,exp,dir){
  84. const fn = this[dir+'Updater']
  85. fn && fn(node,this.$vm[exp])
  86. new Watcher(this.$vm,exp,function (val) {
  87. fn && fn(node,val)
  88. })
  89. }
  90. // v-text 绑定text方法
  91. textUpdater(node,val){
  92. node.textContent = val
  93. }
  94. // v-html 绑定html方法
  95. htmlUpdater(node,val){
  96. node.innerHTML = val
  97. }
  98. modelUpdater(node,val){
  99. // 多用在表单元素,暂时只考虑表单元素赋值
  100. node.value = val
  101. }
  102. }

image.gif

结语

这篇文章主要是介绍了一下Vue的数据响应式和他的原理以及实现。有疑问欢迎提问,当然发现问题也欢迎随之指正。