3

零基础入门Vue之画龙点睛——再探监测数据 - StarVik

 7 months ago
source link: https://www.cnblogs.com/Star-Vik/p/18009891
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

追忆#

上一节:零基础入门Vue之影分身之术——列表渲染&渲染原理浅析

虽然我深知,大佬告诉我”先学应用层在了解底层,以应用层去理解底层“,但Vue的数据如何检测的我不得不去学

否则,在写代码的时候,可能会出现我难以解释的bug

对此,本篇文章,将记录我对Vue检测数据的理解

对于Vue检测数据的实现,我打算由浅入深的去记录

  1. JavaScript实现数据监控
  2. 实现简单的数据监测(浅浅的响应式)
  3. Vue对哪些数据做了监测,哪些没有?

JavaScript的数据检测#

Object.defineProperty() 静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。

熟悉JavaScript的人,应该在一个月黑风高的夜晚都了解过Object上的一个方法: Object.defineProperty()

而Vue大部分的数据监测都是依赖于这个方法来实现的

ps:本篇不会深度探讨这个静态方法的用法,仅仅对其get和set方法的用法讲解,为下一章节做铺垫

Object.defineProperty(obj, prop, descriptor)
  • obj:要添加新属性的对象
  • prop:要添加属性的名称(一般为字符串)
  • descriptor:这个属性的相关描述(配置)

假设现在有一个对象如下:

let person = {
  name:"张三"
};

现在,我试图这个person对象新增一个age属性,那么我可以这么干

Object.defineProperty(person, "age", {
  value:18 //设置默认的值
});

此时输出person时可以看到

{name: '张三', age: 18}

题外话:关于为什么是点开后为什么age属性与其他属性颜色不一样,可以将enumerable:true,使它可迭代

但这真的是一个普通属性了吗?并不会,当我试图

person.age = 20
console.log(person.age); //18

似乎修改不了数据,因为少了一个配置项

Object.defineProperty(person, "age", {
    value:18,
    writable:true //配置为可修改
});

此时上述代码差不多等同于

person.age = 18; //假设age没定义过,重新给person追加一个属性

get#

用作属性 getter 的函数,如果没有 getter 则为 undefined。当访问该属性时,将不带参地调用此函数,并将 this 设置为通过该属性访问的对象(因为可能存在继承关系,这可能不是定义该属性的对象)。返回值将被用作该属性的值。默认值为 undefined。

从上面MDN上的话来说,当我们要用到对象上的某个属性时会调用 getter,如果对象没有设置,则默认是undefined,当访问这个属性时,访问得到的结果将是getter返回的结果

我现在有一个需求:当age属性被读取时,就加1岁,并且输出变化后的值

在上述代码里面是完不成这个需求的,此时就需要用到get方法了,当读取时会自动调用get方法,我可以在那个里面进行数据的递增

注意:官网描述中,get不能与 value 或 writable 同时使用

  1. 定义实际年龄数据:_age
  2. 当读取person.age,get选择器返回_age,并且把_age递增

具体实现代码如下:

let age = 18; //定义一个实际的年龄数据
let person = { //准备好目标对象
  name:"张三"
};

Object.defineProperty(person,"age",{
  get(){
    console.log("年龄即将递增一岁后:",age + 1);
    return age,age++;
  }
});
> person.age
年龄即将递增一岁后: 19
18
> person.age
年龄即将递增一岁后: 20
19

综上所述,当有人要读取某个属性的时候,可以对这个属性值做了处理在返回,或者是调用函数通知其他的事件触发等等

set#

用作属性 setter 的函数,如果没有 setter 则为 undefined。当该属性被赋值时,将调用此函数,并带有一个参数(要赋给该属性的值),并将 this 设置为通过该属性分配的对象。默认值为 undefined。

set的用法正好和get方法对应,一个是读一个是写,set方法当要给属性赋值时会被调用,并且可以接收一个参数作为新的值

同样以这个对象为例,还是把get赋值写好,每次都返回当前age的值

let age = 18; //定义一个实际的年龄数据
let person = { //准备好目标对象
  name:"张三"
};

Object.defineProperty(person,"age",{
  get(){
    return age;
  }
});

现在呢,如果我去修改他得值,他还是出现改不掉的情况

这是因为set没配置,我先配置个最基本的set看看,能否修改age的值

let age = 18; //定义一个实际的年龄数据
let person = { //准备好目标对象
  name:"张三"
};

Object.defineProperty(person,"age",{
  get(){
    return age; //返回实际年龄
  },
  set(newVal){
      age = newVal //修改实际年龄
  }
});
> person.age
18
> person.age = 19
19
> person.age
19

很显然是可以修改的

现在呢,我希望当我对年龄做出了修改,如果不是递增1的话就弹出警告

那么我可以这么干

let age = 18; //定义一个实际的年龄数据
let person = { //准备好目标对象
  name:"张三"
};

Object.defineProperty(person,"age",{
  get(){
    return age;
  },
  set(newVal){
      if(newVal !== age+1){
          console.warn("注意:你这个年龄增加的有点快啊!!!");
      }
      age = newVal
  }
});

此时这段代码,能完美的完成需求

画龙点睛#

上面的例子,还是不怎么完善,万一有人给年龄随意赋值呢?那我是不是要弹出报错?

所以,当调用set的时候,可以进行一系列的数据类型判断,这里仅需判断是否为数值即可,区别不能为负值不然就抛出错误

代码如下:

let age = 18; //定义一个实际的年龄数据
let person = { //准备好目标对象
  name:"张三"
};

Object.defineProperty(person,"age",{
  get(){
    return age;
  },
  set(newVal){
      if(typeof newVal !== 'number'){
          throw "你在想什么呢?";
      }else if(newVal < 0){
          throw "你跟阎王沟通过?";
      }else if(newVal !== age+1){
          console.warn("注意:你这个年龄增加的有点快啊!!!");
      }
      age = newVal;
  }
});

实现简单的数据检测#

在第一篇:零基础入门Vue之梦开始的地方——插值语法 中我提到如下的说明

"{{}}"在这个表达式里面可以写js的表达式,并且它里面的执行语句的this是vue实例,同时vue官方文档指出,在data中配置的东西最后都会通过数据代理的方式挂在到vue实例上。

data配置项里面的所有数据,都会以数据代理的方式挂在到vm实例上,并且Vue也会提供一个纯净版的Vue._data,此时这个Vue._data等同于我们配置的data
(即:vm._data === data is true)

而这个数据代理就是依赖于上一节说的 Object.defineProperty() 来实现

实现原理简单分析&实现#

在Vue中,Vue对实际传入的data并没有直接挂在到vm对象及vm._data上,而是重新通过get和set去做一系列的数据代理和数据监测

这个过程中有许多细节要处理,本篇不可能以这一千不到的字数去说明白Vue的数据检测和数据代理

仅仅只是做一个基本的样例,供我自己学习

首先,我得准备一个方法用来刷新dom意思意思一下

function flashVirtualDom(){
  //此处省略新老虚拟dom之间的比较算法
  console.log("检测到数据更改,准备刷新虚拟dom");
}

然后呢,我要准备好一个数据

let data = {
    name:"张三",
    age:18,
    friends:["李四","王五","赵高"],
    school:{
        name:"北京大学",
        local:"北京",
        totalYears:4
    }
};

目标:接下来我希望不直接操作data的数据,而是用另外的方法去操作data的数据,并且当data数据发送改变时能被我写的代码检测到

既然不是直接操作,那么用户和真实数据应该有一个 中间层,所以我把它明明为Middle

这个中间层呢,使用Object.defineProperty() 把data的数据挂载到自己实例上,可以操作它的实例间接更改data

(换个说法:在上一节age就是真实数据,而person.age实际上是真实数据的代理,我并没有直接操作age,只不过这次数据交给data对象,更加的密集,集中保管和内部维护)

function Middle(obj){
    let keys = Object.keys(obj); //拿到所有的key
    for(let key of keys){
        //如果是对象
        if(typeof obj[key] === "object" && !(obj[key] instanceof Array)){
            this[key] = new Middle(obj[key]); //如果是对象嵌套那么递归调用
            continue;
        }else{
            //过滤undefined
            if(!obj[key]){
                continue;
            }

            //设置数据代理
            Object.defineProperty(this,key,{
                get(){
                    //读取就返回原模原样的值
                    return obj[key];
                },
                set(newVal){
                    //赋值就修改原始数据
                    flashVirtualDom();
                    obj[key] = newVal;
                }
            })
        }
    }
}

此时,在console执行如下代码

> let m = new Middle(data);
undefined
> m.age = 30
检测到数据更改,准备刷新虚拟dom
30
> m.age
30

展开m,与Vue实例的_data进行对比,相差不大,当我修改其中一个数据时会调用flashVirtualDom();方法用于刷新dom,同样还可以写其他方法做其他操作

数据劫持#

在尚硅谷的课程中,有提到”数据劫持“这一概念,对此,本篇也相应的记录下

个人理解的数据劫持,实际上是当数据发生变动时,率先拦截变动,做出处理后,在决定是否要变动数据

或者更改变动的结果,其在我理解更类似于hook的操作,当某个函数或者变量被赋值,然后我对他进行一次拦截

拦截之后做出我想要的操作,操作做完再让他回归正常运行

Vue中的数据监测#

在做Vue的开发中,Vue并非对任何数据都做了监测,因此我认为我作为Vue的学习者,应当去了解具体有哪些情况不会被监测到,从而避免日后开发的各种奇奇怪怪的问题。

对于常见数据,有两种比较容易出错

对象相关的数据监测问题&解决方案#

假设,在初始的data里面仅有如下数据

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="./vue.js"></script>
    <title>Document</title>
</head>
<body>
    <div id="root">
        <div>姓名:{{person.name}}</div>
        <div>性别:{{person.sex}}</div>
        <div>年龄:{{person.age}}</div>
    </div>
</body>
<script>
    let vm = new Vue({
        el:"#root",
        data:{
            person:{
                name:"张三",
                sex:"男"
            }
        }
    })
</script>
</html>

这都很正常,但如果我想不修改这个代码,在项目上线后根据后端给的数据动态的增加

假设在某处代码上后端返回的数据有年龄,我想data的数据增加一条年龄并且展示到应该展示的位置

那么我试图这么做

vm.data.age = 19; //假设后端给出的数据是19

此时页面无任何变化

(实际操作,可以先让页面运行起来,然后再console上去追加一个年龄)

这个后期追加的数据在Vue中并没有被监测,导致他没有显示(简单说就是没有get和set方法)

那我该如何解决呢?

Vue非常人性化的提供另外的方法:Vue.set( target, propertyName/index, value )

注:用vm.$set也是一样的,详细区别可以去翻官方文档

在这个方法允许给某个对象添加属性并且监测

  • target:目标对象
  • propertyName/index:属性名称/索引值(可用于数组)
  • value:值

所以当我接收到后端数据时,我可以用这个方法追加数据并且被Vue所监测

Vue.set(vm.person,'age',19); //假设后端给出的数据是19

此时age即可直接显示到页面上了

数组相关的数据监测问题&解决办法#

假设,这次问题代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="./vue.js"></script>
    <title>Document</title>
</head>
<body>
    <div id="root">
        <ul>
            <li v-for="per in friends" :key="per.id">
                {{per}}
            </li>
        </ul>
    </div>
</body>
<script>
    let vm = new Vue({
        el:"#root",
        data:{
            friends:["张三","李四","王五","赵高"]
        }
    })
</script>
</html>

现在呢,我想要修改friends第一个元素,修改为”张四“

正常做法如下:

vm.friends[0] = "张四";

但页面数据无变化,实际数据已经更改了

这是为什么呢?展开friends数组,发现这并没有数组成员的get和set方法,Vue并未对数组成员做监测,因此改了之后,数据并未刷新

那么,我该如何做呢?官网其实早就给出了答案:变更方法。 官方对以下七个方法进行了包裹(我感觉hook更好理解)

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

依次,但我通过这些方法去做数组进行”增删改查“时,Vue会检测到,所以我想修改第一个元素为李四可以这么干

vm.friends.splice(0,1,"张四")

执行完后页面变了,说明变动被监测到了,除此之外还可以使用set方法

Vue.set(vm.friends,0,"张四");

The End#

唔~(一口浊气)

这一篇可真长啊

本篇完~~~~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK