Vue双向数据绑定原理

vue2

defineProperty

第一个参数为需要定义属性的对象

第二参数为需要定义的属性名

第三个参数为属性描述符

使用definePropery定义的属性不能对其进行修改,删除,以及枚举。
怎么解决呢?
在属性描述符中设置
writable:true 可修改
enumerable:true 可枚举
configurable:true 可操作/删除

var obj={}
Object.defineProperty(obj,"name",{
            value:"lisa",
            writable:true,
            enumerable:true,
            configurable:true
        })

defineProperty的set和get属性

用于实现数据劫持

var obj={}
var a=1;
Object.defineProperties(obj,{
    a:{
        // 注意:value,writable属性与get,set互斥,如果同时存在则会报错
        //所以,设置初始值只能在外部设置
        //在a被访问时返回a的值
        get(){
            console.log("a被访问了,值为"+a)
            return a;
        },
        //在a被修改时,将a的值修改为传入的新值
        set(newVal){
            a=newVal;
            console.log("a被修改了,值为"+a)
        }
    }
})
obj.a //访问a,值为1
obj.a=5  //设置a的值,值为5
obj.a//再次访问a,值为5

将操作的元素放入数组

function DataArr(){
    var Arrobj={} //数组的属性
    var arr=[]
    //this指向new DataArr()
    Object.defineProperty(this,"val",{
        get(){
            return Arrobj;
        },
        set(newVal){
            Arrobj=newVal
            //向数组中添加新的属性值
            arr.push({val:Arrobj})
        }
    })
    this.getArr=function(){
        return arr
    }
}
var dataArr=new DataArr()
dataArr.val=123
dataArr.val=456
console.log(dataArr.getArr()) //[{val:123},{val:456}]

问题

关于对象: Vue 无法检测 property 的添加或移除。

关于数组:不能利用索引直接设置一个数组项,也不能修改数组的长度。

vue3

原理:Vue3.0中的响应式采用了ES6中的 Proxy 方法。Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)

据阮一峰文章介绍:Proxy可以理解成,在目标对象之前架设一层 "拦截",当外界对该对象访问的时候,都必须经过这层拦截,而Proxy就充当了这种机制,类似于代理的含义,它可以对外界访问对象之前进行过滤和改写该对象。

  1. 监听数组的方法不能触发Object.defineProperty方法中的set操作(如果要监听的到话,需要重新编写数组的方法)。
  2. 必须遍历每个对象的每个属性,如果对象嵌套很深的话,需要使用递归调用。
const obj = new Proxy(target, handler);

target: 被代理对象。
handler: 是一个对象,声明了代理target的一些操作。
obj: 是被代理完成之后返回的对象。

但是当外界每次对obj进行操作时,就会执行handler对象上的一些方法。handler中常用的对象方法如下:

1. get(target, propKey, receiver)
2. set(target, propKey, value, receiver)
3. has(target, propKey)
4. construct(target, args):
5. apply(target, object, args)

const target = {
  name: 'kongzhi'
};

const handler = {
  get: function(target, key) {
    console.log("${key} 被读取");
    return target[key];
  },
  set: function(target, key, value) {
    console.log("${key} 被设置为 ${value}");
    target[key] = value;
  }
};

const testObj = new Proxy(target, handler);

/*
  获取testObj中name属性值
  会自动执行 get函数后 打印信息:name 被读取 及输出名字 kongzhi
*/
console.log(testObj.name);

/*
 改变target中的name属性值
 打印信息如下: name 被设置为 111 
*/
testObj.name = 111;

console.log(target.name); // 输出 111

如上代码所示:也就是说 target是被代理的对象,handler是代理target的,那么handler上面有set和get方法,当每次打印target中的name属性值的时候会自动执行handler中get函数方法,当每次设置 target.name 属性值的时候,会自动调用 handler中的set方法,因此target对象对应的属性值会发生改变,同时改变后的 testObj对象也会发生改变。同理改变返回后 testObj对象中的属性也会改变原对象target的属性的,因为对象是引用类型的,是同一个引用的。如果这样还是不好理解的话,可以简单的看如下代码应该可以理解了:

const target = {
  name: 'kongzhi'
};
const testA = target;
testA.name = 'xxx';
console.log(testA.name); // 打印 xxx
console.log(target.name); // 打印 xxx

get

该方法的含义是:用于拦截某个属性的读取操作。它有三个参数,如下解析:
target: 目标对象。
propKey: 目标对象的属性。
receiver: (可选),该参数为上下文this对象

const obj = {
  name: 'kongzhi'
};

const handler = {
  get: function(target, propKey) {
    // 使用 Reflect来判断该目标对象是否有该属性
    if (Reflect.has(target, propKey)) {
      // 使用Reflect 来读取该对象的属性
      return Reflect.get(target, propKey);
    } else {
      throw new ReferenceError('该目标对象没有该属性');
    }
  }
};

const testObj = new Proxy(obj, handler);

/* 
 Proxy中读取某个对象的属性值的话,
 就会使用get方法进行拦截,然后返回该值。
 */
console.log(testObj.name); // kongzhi

/*
 如果对象没有该属性的话,就会进入else语句,就会报错:
 Uncaught ReferenceError: 该目标对象没有该属性
*/
// console.log(testObj.name2);

/*
 其实Proxy中拦截的操作是在原型上的,因此我们也可以使用 Object.create(obj)
 来实现对象的继承的。
 如下代码演示:
*/
const testObj2 = Object.create(testObj);
console.log(testObj2.name);

// 看看他们的原型是否相等 
console.log(testObj2.__proto__ === testObj.__proto__);  // 返回true

该方法是用来拦截某个属性的赋值操作,它可以接受四个参数,参数解析分别如下:
target: 目标对象。
propKey: 目标对象的属性名
value: 属性值
receiver(可选): 一般情况下是Proxy实列

const obj = {
  'name': 'kongzhi'
};

const handler = {
  set: function(obj, prop, value) {
    return Reflect.set(obj, prop, value);
  }
};

const proxy = new Proxy(obj, handler);

proxy.name = '我是空智';

console.log(proxy.name); // 输出: 我是空智
console.log(obj); // 输出: {name: '我是空智'}

当然如果设置该对象的属性是不可写的,那么set方法就不起作用了,如下代码演示:

const obj = {
  'name': 'kongzhi'
};

Object.defineProperty(obj, 'name', {
  writable: false
});

const handler = {
  set: function(obj, prop, value, receiver) {
    Reflect.set(obj, prop, value);
  }
};

const proxy = new Proxy(obj, handler);
proxy.name = '我是空智';
console.log(proxy.name); // 打印的是 kongzhi

注意:proxy对数组也是可以监听的;如下代码演示,数组中的 push方法监听:

const obj = [{
  'name': 'kongzhi'
}];

const handler = {
  set: function(obj, prop, value) {
    return Reflect.set(obj, prop, value);
  }
};

const proxy = new Proxy(obj, handler);

proxy.push({'name': 'kongzhi222'});

proxy.forEach(function(item) {
  console.log(item.name); // 打印出 kongzhi kongzhi222
});

该方法是判断某个目标对象是否有该属性名。接收二个参数,分别为目标对象和属性名。返回的是一个布尔型。
如下代码演示:

const obj = {
  'name': 'kongzhi'
};

const handler = {
  has: function(target, key) {
    if (Reflect.has(target, key)) {
      return true;
    } else {
      return false;
    }
  }
};

const proxy = new Proxy(obj, handler);

console.log(Reflect.has(obj, 'name')); // true
console.log(Reflect.has(obj, 'age')); // false

construct(target, args, newTarget):

该方法是用来拦截new命令的,它接收三个参数,分别为 目标对象,构造函数的参数对象及创造实列的对象。
第三个参数是可选的。它的作用是拦截对象属性。

如下代码演示:

function A(name) {
  this.name = name;
}

const handler = {
  construct: function(target, args, newTarget) {
    /*
     输出: function A(name) {
              this.name = name;
           }
    */
    console.log(target); 
    // 输出: ['kongzhi', {age: 30}]
    console.log(args); 
    return args
  }
};

const Test = new Proxy(A, handler);

const obj = new Test('kongzhi', {age: 30});
console.log(obj);  // 输出: ['kongzhi', {age: 30}]

其他转载自

深入理解 ES6中的 Reflect - 龙恩0707 - 博客园 (cnblogs.com)

vue2 和vue3的区别

监听数据原理

vue2的双向数据绑定是利用了es5 的一个API Object.definepropert() 对数据进行劫持 结合发布订阅模式来实现的。vue3中使用了es6的proxyAPI对数据进行处理。
相比与vue2,使用proxy API 优势有:defineProperty只能监听某个属性,不能对全对象进行监听;可以省去for in 、闭包等内容来提升效率(直接绑定整个对象即可);可以监听数组,不用再去单独的对数组做特异性操作,vue3可以检测到数组内部数据的变化。

选项式API和组合式API

optionsAPI

vue2中组织代码:我们会在一个vue文件中data,methods,computed,watch中定义属性和方法,共同处理页面逻辑

上图解释了optionsAPI的缺点,一个功能往往需要在不同的vue配置项中定义属性和方法,比较分散,项目小还好,清晰明了,但项目大了之后,一个methods中可能包含多个方法,往往分不清哪个方法对应着哪个功能,而且当你想要新增一个功能的时候,你可能需要在data,methods,computed,watch中都要写一些东西。但是这个时候选项里面的内容很多你需要上下来回的翻滚,特别影响效率

Composition API

compositionAPI是根据逻辑相关组织代码的,提高可读性和可维护性,基于函数组合的API更好的重用代码逻辑(在vue2 Options API中通过Mixins重用逻辑代码,容易发生命名冲突且关系不清)Composition API 最大的优点通俗的讲就是把跟一个功能相关的东西放在一个地方,它是目前最合理也最容易维护的。你可以随时将功能的一部分拆分出去。可以将每一个功能相关所有的东西比如methods、computed都放在如上图的function中,这个function可以独立存在,可以放在一个TS文件中,也可以在NPM独立发布,最终CompositionAPI把他们组合起来

代码

下面的例子,将处理count属性相关的代码放在同一个函数了

function useCount() {
    let count = ref(10);
    let double = computed(() => {
        return count.value * 2;
    });

    const handleConut = () => {
        count.value = count.value * 2;
    };

    console.log(count);

    return {
        count,
        double,
        handleConut,
    };
}

组件上使用count

export default defineComponent({
    setup() {
        const { count, double, handleConut } = useCount();
        return {
            count,
            double,
            handleConut
        }
    },
});

可以很清楚感受到CompositionAPI在逻辑组织方面的优势,以后修改属性功能的时候,只需要调到控制该属性的方法中即可

vue2中,我们是用过mixin去复用相同的逻辑,

export const MoveMixin = {
  data() {
    return {
      x: 0,
      y: 0,
    };
  },

  methods: {
    handleKeyup(e) {
      console.log(e.code);
      // 上下左右 x y
      switch (e.code) {
        case "ArrowUp":
          this.y--;
          break;
        case "ArrowDown":
          this.y++;
          break;
        case "ArrowLeft":
          this.x--;
          break;
        case "ArrowRight":
          this.x++;
          break;
      }
    },
  },

  mounted() {
    window.addEventListener("keyup", this.handleKeyup);
  },

  unmounted() {
    window.removeEventListener("keyup", this.handleKeyup);
  },
};

然后在组建中使用

<template>
  <div>
    Mouse position: x {{ x }} / y {{ y }}
  </div>
</template>
<script>
import mousePositionMixin from './mouse'
export default {
  mixins: [mousePositionMixin]
}
</script>

使用单个mixin似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时候

mixins: [mousePositionMixin, fooMixin, barMixin, otherMixin]

会存在两个非常明显的问题:

  • 命名冲突
  • 数据来源不清楚 现在通过Compositon API这种方式改写上面的代码
import { onMounted, onUnmounted, reactive } from "vue";
export function useMove() {
  const position = reactive({
    x: 0,
    y: 0,
  });

  const handleKeyup = (e) => {
    console.log(e.code);
    // 上下左右 x y
    switch (e.code) {
      case "ArrowUp":
        // y.value--;
        position.y--;
        break;
      case "ArrowDown":
        // y.value++;
        position.y++;
        break;
      case "ArrowLeft":
        // x.value--;
        position.x--;
        break;
      case "ArrowRight":
        // x.value++;
        position.x++;
        break;
    }
  };

  onMounted(() => {
    window.addEventListener("keyup", handleKeyup);
  });

  onUnmounted(() => {
    window.removeEventListener("keyup", handleKeyup);
  });

  return { position };
}

在组件中使用

<template>
  <div>
    Mouse position: x {{ x }} / y {{ y }}
  </div>
</template>

<script>
import { useMove } from "./useMove";
import { toRefs } from "vue";
export default {
  setup() {
    const { position } = useMove();
    const { x, y } = toRefs(position);
    return {
      x,
      y,
    };

  },
};
</script>

可以看到,整个数据来源清晰了,即使去编写更多的 hook 函数,也不会出现命名冲突的问题

在逻辑组织和逻辑复用方面,Composition API是优于Options API

因为Composition API几乎是函数,会有更好的类型推断。

Composition APItree-shaking 友好,代码也更容易压缩

Composition API中见不到this的使用,减少了this指向不明的情况

如果是小型组件,可以继续使用Options API,也是十分友好的

Last modification:January 29, 2023
如果觉得我的文章对你有用,请随意赞赏