Vue响应式数据原理及具体方法

457 阅读5分钟

阅读建议

原理想去理解折腾,挺费时间的,不想知道原理。

直接跳过原理看使用方法,有具体的demo的case可以直接使用html跑。

原理

1、文档背景

根据官方文档以及图例做的翻译:

  • Data:数据

  • getter 获得者

  • touch 触摸

  • setter 安排者

  • Collect as Dependency 收集的依赖

  • Notify 通报

  • watcher观察者

  • Trigger re-render : 触发更新渲染

  • Component Render Function 渲染组件

  • Virtual Dom Tree 虚拟组件

大概的意思是组件通过Render函数,使用render方法,渲染到这个叫虚拟DOM的东东,虚拟DOM执行完毕之后,我们就可以愉快的去操作DOM,那么可以tonch一下,在对应的节点可以触发相应的事件,底层是用到了 Object.defineProperty()的getter 和 setter方法进行读和写的操作(这些 getter/setter 对用户来说是不可见的),watch->getter->Collect as Dependency ,dep.notify()->通知观察者,, dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新,执行触发更新渲染

抄自官方文档图例,注释一图定乾坤,如下方图示,虽然我也看不怎么懂。 image.png

民间大神解读: image.png

对于文章手写Vue2.0源码(一)-响应式数据原理|技术点评的总结

标记: ★->难点

关键api

Object.defineProperty 参数

  • obj

    要定义属性的对象。

  • prop

    要定义或修改的属性的名称或 Symbol 。

  • descriptor

    要定义或修改的属性描述符。 返回值

被传递给函数的对象。

2、数据初化

  • 关键函数
    • initMixin() 全局Vue挂载 _init(options),初始化Vue示例的函数
    • initState() 初始化状态 注意这里的顺序 比如我经常面试会问到 是否能在data里面直接使用prop的值,这里初始化的顺序依次是: prop>methods>data>computed>watch
    • initData() 里面的 observe 是响应式数据核心 所以另建 observer 文件夹来专注响应式逻辑,配合所定义的proxy()下Object.defineProperty的get和set(newValue)

3、对象的数据劫持

  • 关键函数
    • walk() 对象上的所有属性依次进行观测
    • defineReactive()Object.defineProperty数据劫持核心 兼容性在ie9以及以上 关键代码:
     function defineReactive(data, key, value) {
       observe(value); // 递归关键
     // --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
     //   思考?如果Vue数据嵌套层级过深 >>性能会受影响
     Object.defineProperty(data, key, {
       get() {
         console.log("获取值");
         return value;
       },
       set(newValue) {
         if (newValue === value) return;
         console.log("设置值");
         value = newValue;
       },
     });
    }
    
    • observe(value)如果传过来的是对象或者数组 进行属性劫持

4、数组的观测

  • 关键思路
    • Object.defineProperty 下有一个属性enumerable: false 表示不可枚举型,数组
    • 重写数组方法

5、响应式数据的思维导图

图片

具体的使用的方式

参考下<Vue篇> Vue2对数组与对象的响应式处理

  • 对象

    • Vue.set(vm.someObject, 'b', 2)
    • this.$set(this.someObject,'b',2)
    • this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
  • 数组重写的方法,可以实现相应式数据

Object
// 具体的细节看下注释
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Document</title>
   <style>
       #demo p{
           margin: 30px;
           text-align: center;
       }
   </style>
</head>

<body>
   <div id="demo">
       <p>我是vm.a : {{a}}</p>
       <p>我是vm.someObject.b:{{someObject.a === undefined ? '我是someObject.a':someObject.a}}</p>
       <p>我是vm.someObject.a:{{someObject.b === undefined ? '我是someObject.b':someObject.b}}</p>
       <p>我是vm.b:{{b === undefined?'我是b':b}}</p>
   </div>

   <div id="demo2">
       {{items[0]}}
   </div>
</body>

<script src="vue.js"></script>
// 不会找vue.js在哪? 直接官网 https://cn.vuejs.org/js/vue.js 作为src引入也可

<script>
     var vm = new Vue({
       el: '#demo',
       data: {
           someObject: {}
       },
       mounted() {
           /*
           对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。
           但是,可以使用 Vue.set(object, propertyName, value) 
           方法向嵌套对象添加响应式 property。例如,对于:
            */
           this.$set(this.someObject, 'b', 5) //视图层会返回5

           /*
               有时你可能需要为已有对象赋值多个新 property,
               比如使用 Object.assign() 或 _.extend()。但是,这样添加到对象上的新 property 不会触发更新。
在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。
           */
           this.someObject = Object.assign({}, this.someObject, {
               a: 1,
               b: 2
           })
           // this.$set(this.someObject, "b", 2)
       }
   })
   
   vm.a //a是响应式的
   vm.b = 2 //这里没有定义会报一个错误
   console.log(vm)
   console.log(vm.b) 
   
   // 上面难的
</script>

截图:

  • 1)、vm.a 是响应式的

vm.a.gif

  • 2)、vm.b 未定义会报错

image.png

这时候就需要用到我们的无脑翻译:

属性或方法“b”没有在实例上定义,但在渲染期间被引用。 通过初始化该属性,确保该属性是被动的,无论是在data选项中,还是对于基于类的组件。

这句话什么意思呢,就是当我们渲染数据的时候,如果data没有定义该属性,那么不会渲染到对应的视图,但是vm.b 其实是有修改的,可以理解成,这个数据只是只读的,但是不能写到我们的渲染层

这时候就会让我们跳转到以下链接

vuejs.org/guide/extra…

看了下是实用了proxy,emm,大概率是vue3的东东(未完待续)

image.png

  • 3)、Object在vue中如何做到响应式?
使用以下的方法:
$setObject.assign
    
// this.$set(this.someObject, 'b', 5)

// this.someObject = Object.assign({}, this.someObject, {
//     a: 1,
//     b: 2
// })

控制台输入vm.$set(vm.someObject, 'b', 5),现在视图层有了数据,使用这两句代码可以操作obj,后续要可以做到响应式了,如果未加,我们一开始就使用this.someObject.a=1这样子去操作obj数据是不会响应式的。

image.png

image.png

image.png

Array

对于Array不熟悉的话,参考下菜鸟教程案例每个demo敲敲就理解了。 菜鸟教程:

JavaScript Array 对象

乱入:有感而发,和主题无关

菜鸟看着更新的也挺新的,新的数组方法都有引入。而且阅读的网站布局及样式风格比较符合我们的阅读感受,映入眼帘一片绿。看着太舒服了,再看了下掘金以蓝白为主色调的风格也是挺舒服的,阅读起来是比mdn舒服一些(强烈吐槽mdn左侧的导航栏,为什么不做一个移入的时候出现导航,而是左边出来一个大大的导航条,难受啊,light theme 亮瞎了我的狗眼(难怪我平时读不进去mdn,emm,然后发现了MDN开启暗黑模式才是最适合阅读的,emm,突然觉得它又香了,对眼睛伤害最小就是黑白颜色,但是不是亮色,亮色对眼睛伤害太大了,纯黑白,那种卓别林时代默剧的颜色demo就给人以很舒服的感觉)

贴下用户讨论:来自某乎用户的吐槽

mdn滚动条:(吐槽截图) image.png

色彩相关方面的可以看我写的另一篇文章使用电脑/手机,怎么样愉快的保护眼睛

阅读到这,兄弟们,别怪我跑题,突然有感而发,回到正题。

相关api的学习资料

菜鸟教程如何阅读Array相关api的,我觉得看下下面这几个应该就足够了,如果涉及浏览器兼容,建议到MDN去看看

定义和用法、语法、参数、返回值,比如 JavaScript concat() 方法:

image.png

具体案例

可分为三个部分:

  • a. 使用以下方法操作数组, 可以检测变动 push() pop() shift() unshift() splice() sort() reverse()
  • b. filter(), concat() 和 slice() ,map(),新数组替换旧数组
  • c. 不能检测以下变动的数组 vm.items[indexOfItem] = newValue 解决 (1)Vue.set(example1.items, indexOfItem, newValue) (2)splice

  
  <div id="box">
       <!-- input change事件区别 -->
       <input type="text" @input="handleInput" v-model="mytext" />
       <ul>
           <li v-for="data in datalist" :key="data">
               {{data}}
           </li>
       </ul>
   </div>
   
 <script src="lib/vue.js"></script>
 <script>
       var vm = new Vue({
           el: "#box",
           data: {
               mytext: "",
               datalist: ["aaa", "add", "bbb", "bbc", "ccc", "ddd", "eee", "ade"], // 要改的
               originList: ["aaa", "add", "bbb", "bbc", "ccc", "ddd", "eee", "ade"] // 原始数据
           },
           methods: {
               handleInput() {
                  /*
                     a. 使用以下方法操作数组,
                       可以检测变动 push() pop() shift() unshift() splice() sort() reverse()
                  */
                   // this.datalist.push("向后推送数据")
                   // this.datalist.pop();
                   // this.datalist.shift();
                   // this.datalist.unshift('向前插入数据');
                   // this.datalist.splice(0,0,'向第一个元素的删除0个 查润我我我我我'); //删除0个 插入
                   // this.datalist.sort()
                   // this.datalist.reverse()  //reverse() 方法用于颠倒数组中元素的顺序。


                   /*
                       b. filter(), concat() 和 slice() ,map(),新数组替换旧数组
                   */ 
                   // setTimeout(()=>{
                   //     this.datalist = this.originList.filter(item=> item.includes(this.mytext) )
                   // },2000)

                   // 遍历1 - 5
                   function calculateItem(mytext){
                       var arr = ['1','2','3','4','5']
                       for(let  i of arr){
                           if(i === mytext){
                               return true
                           }
                       }
                   }

                   //输入1 - 5 后返回原数组
                   if(calculateItem(this.mytext) ){
                       this.datalist = this.originList
                   }else{//否则执行数组的方法
                       // this.datalist = this.originList.concat([1,2,3])
                       // this.datalist = this.originList.slice(1,3)
                       this.datalist = this.originList.map(item=>{
                           return item = item +'a'
                       })
                   }

                   /*c. 不能检测以下变动的数组
                           vm.items[indexOfItem] = newValue
                           *解决* 
                               (1)Vue.set(example1.items, indexOfItem, newValue)
                               (2)splice
                    */

                   //(1)
                   this.$set(this.datalist,4,'我是改变的数据')
                   // 控制台可以输入
                    vm.$set(vm.datalist,4,'我是改变的数据')
                    
                   // (2)
                   // this.datalist.splice(0,0,'向第一个元素的删除0个 查润我我我我我')
                   // console.log(newlist)

               }
           },
           mounted(){
               console.log('mounted',this)
           }
       })

       var arr = ["aaa", "add", "bbb", "bbc", "ccc", "ddd", "eee", "ade"]

       var newlist = arr.filter(item => item.includes("a"))

       console.log(newlist, arr)
   </script>

以上可以把数组相关的数组方法解开后,自行debug,可以chrome调试器下的console 配合 vscode(或者读者的其他的编译器进行其他的操作)

截图 vm.$set修改

image.png

Array 总结
数组方法名 Or vue实例方法定义和用法语法返回值Vue中是否可以响应式
push()数组的末尾添加一个或多个元素,并返回新的长度array.push(item1item2, ..., itemX) )Number可以
pop()删除数组的最后一个元素并返回删除的元素array.pop()返回删除的元素(数组元素可以是一个字符串,数字,数组,布尔,或者其他对象类型。可以
shift()数组的第一个元素从其中删除,并返回第一个元素的值array.shift()数组原来的第一个元素的值( 数组元素可以是一个字符串,数字,数组,布尔,或者其他对象类型。可以
unshift()数组的开头添加一个或更多元素,并返回新的长度。array.unshift(item1,item2, ..., itemX)Number 数组新长度可以
unshift()数组的开头添加一个或更多元素,并返回新的长度。array.splice(index,howmany,item1,.....,itemX)Array(如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组)可以
sort()数组的元素进行排序array.sort(sortfunction)Array(对数组的引用。请注意,数组在原数组上进行排序,不生成副本)可以
reverse()颠倒数组中元素的顺序array.reverse()Array(颠倒顺序后的数组)可以
filter()创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素array.filter(function(currentValue,index,arr), thisValue)Array,包含了符合条件的所有元素。如果没有符合条件的元素则返回空数组不可以,要添加多一个备份的数组
concat()连接两个或多个数组array1.concat(array2,array3,...,arrayX)Array对象,返回一个新的数组。该数组是通过把所有 arrayX 参数添加到 arrayObject 中生成的。如果要进行 concat() 操作的参数是数组,那么添加的是数组中的元素,而不是数组。不可以,要添加多一个备份的数组
slice()slice() 方法可从已有的数组中返回选定的元素。slice() 方法可提取字符串的某个部分,并以新的字符串返回被提取的部array.slice(startend)Array,返回一个新的数组,包含从 start(包括该元素) 到 end (不包括该元素)的 arrayObject 中的元素。不可以,要添加多一个备份的数组
map()map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。map() 方法按照原始数组元素顺序依次处理元素array.map(function(currentValue,index,arr), thisValue)Array,数组中的元素为原始数组元素调用函数处理后的值。不可以,要添加多一个备份的数组
Vue.set向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi')vm.$set( target, propertyName/index, value设置的值可以

参考的其他大佬的文章

VUE===(1)

【Vue源码学习】依赖收集