>  기사  >  웹 프론트엔드  >  Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사

Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사

青灯夜游
青灯夜游앞으로
2023-01-25 05:30:012545검색

Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사

설명에 앞서 먼저 데이터 응답성이 무엇인지부터 알아볼까요? 소위 데이터 응답성반응형 데이터종속성(반응형 데이터를 호출하는 작업) 간의 관계를 설정하는 것입니다. 이러한 응답 데이터를 사용하여 DOM 업데이트 또는 일부 콜백 함수 실행과 같은 관련 업데이트 작업을 수행하는 종속 작업에 알립니다. Reactivity는 Vue2부터 Vue3까지 사용되는데 차이점은 무엇인가요? 响应式数据依赖(调用了响应式数据的操作)之间的关系,当响应式数据发生变化时,可以通知那些使用了这些响应式数据的依赖操作进行相关更新操作,可以是DOM更新,也可以是执行一些回调函数。从Vue2到Vue3都使用了响应式,那么它们之间有什么区别?

那么它们之间有什么区别?为什么Vue3会选择Proxy替代defineProperty?我们先看看下面两个例子:

// 
defineReactive(data,key,val){
    Object.defineProperty(data,key,{
      enumerable:true,
      configurable:true,
      get:function(){
        console.log(`对象属性:${key}访问defineReactive的get!`)
        return val;
      },
      set:function(newVal){
        if(val===newVal){
          return;
        }
        val = newVal;
        console.log(`对象属性:${key}访问defineReactive的get!`)
      }
    })
}
let obj = {};
this.defineReactive(obj,'name','sapper');
// 修改obj的name属性
obj.name = '工兵';
console.log('obj',obj.name);
// 为obj添加age属性
obj.age = 12;
console.log('obj',obj);
console.log('obj.age',obj.age);
// 为obj添加数组属性
obj.hobby = ['游戏', '原神'];
obj.hobby[0] = '王者';
console.log('obj.hobby',obj.hobby);

// 为obj添加对象属性
obj.student = {school:'大学'};
obj.student.school = '学院';
console.log('obj.student.school',obj.student.school);

Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사从上图可以看出使用defineProperty定义了包含name属性的对象obj,然后添加age属性、添加hobby属性(数组)、添加student属性并分别访问,都没有触发obj对象中的get、set方法。也就是说defineProperty定义对象不能监听添加额外属性或修改额外添加的属性的变化,我们再看看这样一个例子:

let obj = {};
// 初始化就添加hobby
this.defineReactive(obj,'hobby',['游戏', '原神']);
// 改变数组下标0的值
obj.hobby[0] = '王者';
console.log('obj.hobby',obj.hobby);

Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사

假如我们一开始就为obj添加hobby属性,我们发现修改数组下标0的值,并没有触发obj里的set方法,也就是说defineProperty定义对象不能监听根据自身数组下标修改数组元素的变化。那么我们继续看一下Proxy代理的对象例子:

// proxy实现
let targetProxy = {name:'sapper'};
let objProxy = new Proxy(targetProxy,{
    get(target,key){
      console.log(`对象属性:${key}访问Proxy的get!`)
      return target[key];
    },
    set(target,key,newVal){
      if(target[key]===newVal){
        return;
      }
      console.log(`对象属性:${key}访问Proxy的set!`)
      target[key]=newVal;
      return target[key];
    }
})
// 修改objProxy的name属性
objProxy.name = '工兵';
console.log('objProxy.name',objProxy.name);
// 为objProxy添加age属性
objProxy.age = 12;
console.log('objProxy.age',objProxy.age);
// 为objProxy添加hobby属性
objProxy.hobby = ['游戏', '原神'];
objProxy.hobby[0] = '王者';
console.log('objProxy.hobby',objProxy.hobby);
// 为objProxy添加对象属性
objProxy.student = {school:'大学'};
objProxy.student.school = '学院';
console.log('objProxy.student.school',objProxy.student.school);

Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사从上图是不是发现了Proxy与defineProperty的明显区别之处了,Proxy能支持对象添加或修改触发get、set方法,不管对象内部有什么属性。所以

  • Object.defineProperty():defineProperty定义对象不能监听添加额外属性修改额外添加的属性的变化;defineProperty定义对象不能监听根据自身数组下标修改数组元素的变化。我们看看Vue里的用法例子:

     data() {
       return {
         name: 'sapper',
         student: {
           name: 'sapper',
           hobby: ['原神', '天涯明月刀'],
         },
       };
     },
     methods: {
       deleteName() {
         delete this.student.name;
         console.log('删除了name', this.student);
       },
       addItem() {
         this.student.age = 21;
         console.log('添加了this.student的属性', this.student);
       },
       updateArr() {
         this.student.hobby[0] = '王者';
         console.log('更新了this.student的hobby', this.student);
       },
    }

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사从图中确实可以修改data里的属性,但是不能及时渲染,所以Vue2提供了两个属性方法解决了这个问题:Vue.$setVue.$delete

  • 그럼 둘 사이의 차이점은 무엇인가요? Vue3가 DefineProperty 대신 Proxy를 선택하는 이유는 무엇입니까? 다음 두 가지 예를 살펴보겠습니다.

    this.$delete(this.student, 'name');// 删除student对象属性name
    this.$set(this.student, 'age', '21');// 添加student对象属性age
    this.$set(this.student.hobby, 0, '王者');// 更新student对象属性hobby数组
    const user = {name:'张三'}
    const obj = new Proxy(user,{
      get:function (target,key){
        console.log("get run");
        return target[key];
      },
      set:function (target,key,val){
        console.log("set run");
        target[key]=val;
        return true;
      }
    })
    obj.age = 22;
    console.log(obj); // 监听对象添加额外属性打印set run!
    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사 위 그림에서 볼 수 있듯이, defineProperty를 사용하여 name 속성을 포함하는 객체 obj를 정의한 다음 age 속성을 추가하고, Hobby 속성(배열)을 추가하고, Student 속성을 추가하여 접근합니다. 각각, 그러나 그 중 어느 것도 obj 객체의 객체를 트리거하지 않습니다. 즉, defineProperty 정의 개체는 추가 속성 추가 또는 추가 속성 수정 시 변경 사항을 모니터링할 수 없습니다. 또 다른 예를 살펴보겠습니다.
    const obj = new Proxy([2,1],{
      get:function (target,key){
        console.log("get run");
        return target[key];
      },
      set:function (target,key,val){
        console.log("set run");
        target[key]=val;
        return true;
      }
    })
    obj[0] = 3;
    console.log(obj); // 监听到了数组元素的变化打印set run!

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사
  • obj에 처음부터 취미 속성을 추가하면 배열을 수정하는 것을 알 수 있습니다. 아래 첨자 0의 값은 obj의 set 메소드를 트리거하지 않습니다. 이는 defineProperty 정의 객체가 자체 배열 아래 첨자에 따라 수정된 배열 요소의 변경 사항을 모니터링할 수 없음을 의미합니다. 계속해서 프록시의 개체 예를 살펴보겠습니다.

    data(){
     return {
      name: 'Sapper',
      hobby: ['游戏', '原神'],
      obj: {
        name: '张三',
        student: {
          major: '软件工程',
          class: '1班',
        }
      }
     }
    }
    image .png위 그림에서 Proxy와 DefineProperty의 명백한 차이점을 볼 수 있습니다. Proxy는 개체 내부 속성에 관계없이 get 및 set 메서드를 트리거하기 위해 개체 추가 또는 수정을 지원할 수 있습니다. 따라서

    • Object.defineProperty()
    • : DefineProperty 정의 객체는

      추가 속성 추가 또는 추가 속성 수정 변경 내용을 수신할 수 없습니다. 자체 배열 첨자에 따라 배열 요소의 변경 사항을 수정합니다. Vue의 사용 예를 살펴보겠습니다.

      function initData(vm: Component) {
        // 获取组件中声明的data属性
        let data: any = vm.$options.data
        // 对new Vue实例下声明、组件中声明两种情况的处理
        data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
        ...
        // observe data
        const ob = observe(data) // 为data属性创建Observer实例
        ob && ob.vmCount++
      }
    • image. png그림의 데이터 속성을 수정하는 것은 실제로 가능하지만 시간 내에 렌더링할 수 없으므로 Vue2는 이 문제를 해결하기 위해 두 가지 속성 메서드를 제공합니다: Vue.$set Vue.$delete.
    • 이렇게 age 속성을 직접 추가할 수는 없으니 주의하세요._data.age 역시 지원하지 않습니다.

      vm.$watch('a.b.c',function(newVal,oldVal)){....}

    • Proxy

      : 프록시는 위의 두 가지 단점을 해결하여 다음을 실현할 수 있습니다.

    객체 속성 대신 객체를 직접 모니터링할 수 있고 객체에 추가 속성을 추가할 때 변경 사항을 모니터링할 수 있습니다;

    export function parsePath (path){
     const segment = path.split('.');
     return function(obj){
     ...
       for(let i=0;i<segment.length;i++){
         if(!obj) return;
         obj = obj[segment[i]]
       }
       return obj
     }
    }

    어레이의 변경 사항을 직접 모니터링할 수 있습니다
    .

    export default class Watcher implements DepTarget {
      vm?: Component | null
      cb: Function
      deps: Array<Dep>
      ...
      constructor(vm: Component | null,expOrFn: string | (() => any),cb: Function,...) {
        ...
        this.getter = parsePath(expOrFn)// 解析嵌套对象
        ...
      }
      get() { // 读取数据
        ...
        return value
      }
    
      addDep(dep: Dep) {
        ...
        dep.addSub(this)//添加依赖
        ...
      }
      cleanupDeps() {// 删除依赖
        ...
        dep.removeSub(this)
        ...
      }
      update() {// 通知依赖更新
       this.run()
       ...
      }
    
      run() {
       ...
       this.cb.call(this.vm, value, oldValue)
      }
      ...
      depend() { // 收集依赖
        let i = this.deps.length
        while (i--) {
          this.deps[i].depend()
        }
      }
      ...
    }

    Proxy는 새 객체🎜를 반환하는 반면 Object.defineProperty는 객체 속성을 탐색하고 직접 수정할 수만 있습니다. 🎜🎜🎜🎜🎜🎜apply, ownKeys, deleteProperty, has 등에 국한되지 않고 최대 13가지 차단 방법을 지원합니다🎜. Object.defineProperty에는 없습니다. 🎜🎜🎜🎜일반적으로 Vue3 반응형은 Vue2 반응형에 대한 비판을 해결하기 위해 프록시를 사용합니다. 원칙적으로는 🎜종속성 수집🎜 및 🎜종속성 업데이트🎜와 같은 작업을 수행합니다. 🎜1 getter가 트리거되고, 속성이 수정되면 setter가 트리거됩니다🎜. 우선, 우리 모두는 Vue 인스턴스의 data 속성이 객체인 반응형 데이터를 정의한다는 것을 알고 있습니다. 아래 예의 데이터를 살펴보겠습니다. 🎜
    data(){
     return {
      name: &#39;Sapper&#39;,
      hobby: [&#39;游戏&#39;, &#39;原神&#39;],
      obj: {
        name: &#39;张三&#39;,
        student: {
          major: &#39;软件工程&#39;,
          class: &#39;1班&#39;,
        }
      }
     }
    }

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사从上图我们可以看到,data中的每一个属性都会带 __ob__ 属性,它是一个Observer对象,其实Vue2中响应式的关键就是这个对象,在data中的每一个属性都会带get、set方法,而Vue源码中其实把get、set分别定义为reactiveGetter、reactiveSetter,这些东西怎么添加进去的。Vue2又是怎么数据变化同时实时渲染页面?先看看下面的图:

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사给data属性创建Observer实例:通过初注册响应式函数initState中调用了initData函数实现为data创建Observer实例。

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사

    function initData(vm: Component) {
      // 获取组件中声明的data属性
      let data: any = vm.$options.data
      // 对new Vue实例下声明、组件中声明两种情况的处理
      data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
      ...
      // observe data
      const ob = observe(data) // 为data属性创建Observer实例
      ob && ob.vmCount++
    }

    通过Observer实例把data中所有属性转换成getter/setter形式来实现响应性:对data属性分为两种情况处理:对象属性处理(defineReactive实现)和数组属性处理。数组怎么处理(后面再详细说明)

    注意地,由于Vue实例的data永远都是一个对象,所以data里面包含的数组类型只有对象属性数组属性

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사在getter收集依赖,在setter中触发依赖:当读取data中的数据时,会在get方法中收集依赖,当修改data中的数据时,会在set方法中通知依赖更新。defineReactive方法中主要是做四件事情:创建Dep实例给对象属性添加get/set方法收集依赖通知依赖更新

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사

    从上面我们知道了dep.depend()实现了依赖收集,dep.notify()实现了通知依赖更新,那么Dep类究竟做了什么?我们先看看下面的图:

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사从图中我们得明确一点,谁使用了变化的数据,也就是说哪个依赖使用了变化的数据,其实就是Dep.taget,它就是我们需要收集的依赖,是一个Watcher实例对象,其实Watcher对象有点类似watch监听器,我们先看一个例子:

    vm.$watch(&#39;a.b.c&#39;,function(newVal,oldVal)){....}

    怎么监听多层嵌套的对象,其实就是通过.分割为对象,循环数组一层层去读数据,最后一后拿到的就是想要对的数据。

    export function parsePath (path){
     const segment = path.split(&#39;.&#39;);
     return function(obj){
     ...
       for(let i=0;i<segment.length;i++){
         if(!obj) return;
         obj = obj[segment[i]]
       }
       return obj
     }
    }

    当嵌套对象a.b.c属性发生变化时,就会触发第二个参数中的函数。也就是说a.b.c就是变化的数据,当它的值发生变化时,通知Watcher,接着Watcher触发第二个参数执行回调函数。我们看看Watcher类源码,是不是发现了cb其实就与watch的第二参数有异曲同工之妙。

    export default class Watcher implements DepTarget {
      vm?: Component | null
      cb: Function
      deps: Array<Dep>
      ...
      constructor(vm: Component | null,expOrFn: string | (() => any),cb: Function,...) {
        ...
        this.getter = parsePath(expOrFn)// 解析嵌套对象
        ...
      }
      get() { // 读取数据
        ...
        return value
      }
    
      addDep(dep: Dep) {
        ...
        dep.addSub(this)//添加依赖
        ...
      }
      cleanupDeps() {// 删除依赖
        ...
        dep.removeSub(this)
        ...
      }
      update() {// 通知依赖更新
       this.run()
       ...
      }
    
      run() {
       ...
       this.cb.call(this.vm, value, oldValue)
      }
      ...
      depend() { // 收集依赖
        let i = this.deps.length
        while (i--) {
          this.deps[i].depend()
        }
      }
      ...
    }

    实现对数组的监听:从最开始的例子,我们了解对象以及嵌套对象的监听,但是Object.defineProperty是用来监听对象指定属性的变化,不支持数组监听,那么数组又是怎么监听?我们上面说了data中的数据被赋予响应性都是在Observer中实现的,那么监听的实现也是在Observer对象中实现的,先对数组的特定方法做自定义处理,为了拦截数组元素通知依赖更新,然后才通过observeArray函数遍历创建Observer实例,主要分为两种情况:

    // 源码Observer类中对数组处理的部分代码
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    }
    • 当浏览器支持__ proto __ 对象:强制赋值当前arrayMethods给target的__ proto __ 对象,直接给当前target数组带上自定义封装的数组方法,从而实现监听数组变化。其实arrayMethods处理后就是下面这样一个对象:

      Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사

      const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
      console.log(arrayKeys);// [&#39;push&#39;, &#39;pop&#39;, &#39;shift&#39;, &#39;unshift&#39;, &#39;splice&#39;, &#39;sort&#39;, &#39;reverse&#39;]
      copyAugment(value, arrayMethods, arrayKeys)
      
      function copyAugment (target: Object, src: Object, keys: Array<string>) {
        for (let i = 0, l = keys.length; i < l; i++) {
          const key = keys[i]
          def(target, key, src[key])// 遍历数组元素通过为元素带上
        }
      }
    • 当浏览器不支持__ proto __ 对象:遍历数组元素通过defineProperty定义为元素带上自定义封装的原生数组方法,由于自定义数组方法中做了拦截通知依赖更新,从而实现监听数组的变化。

      const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
      console.log(arrayKeys);// [&#39;push&#39;, &#39;pop&#39;, &#39;shift&#39;, &#39;unshift&#39;, &#39;splice&#39;, &#39;sort&#39;, &#39;reverse&#39;]
      copyAugment(value, arrayMethods, arrayKeys)
      
      function copyAugment (target: Object, src: Object, keys: Array<string>) {
        for (let i = 0, l = keys.length; i < l; i++) {
          const key = keys[i]
          def(target, key, src[key])// 遍历数组元素通过为元素带上
        }
      }

      对数组的Array原生方法做了自定义封装的源码如下,在自定义方法中拦截通知依赖更新。Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사

      // 遍历target实现创建Observer实例
      observeArray (items: Array<any>) {
       for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i])
       }
      }

    Vue2响应式原理小结:

    • 给data创建Observer实例
    • Observer类实现对数据封装getter、setter的响应性
    • 针对数组类型数据,自定义封装Array原生方法,在封装过程中拦截执行通知依赖更新
    • 真正通过Watcher通知依赖更新,通过run方法中的cb回调函数,实现类似watch侦听器第二参数中监听变化后的操作

    Vue3响应式原理

    这里基于Vue3.2.41版本进行分析

    其实Vue3的响应原理与Vue2的响应原理都差不多,唯一不同的就是它们的实现方式,Vue3通过创建Proxy的实例对象而实现的,它们都是收集依赖、通知依赖更新。而Vue3中把依赖命名为副作用函数effect,也就是数据改变发生的副作用,我们先来看一下例子:

    const house = {status:&#39;未出租&#39;,price:1200,type:&#39;一房一厅&#39;};
    const obj = new Proxy(house, {
      get (target, key) {
        return target[key];
      },
      set (target, key, newVal) {
        target[key] = newVal;
        return true;
      }
    })
    function effect () {
      console.log(&#39;房子状态:&#39;+obj.status);
    }
    
    effect () // 触发了proxy对象的get方法
    obj.status = &#39;已出租!&#39;;
    effect ()

    通过Proxy创建一个代理对象,把house代理给obj,obj是代理对象,house是被代理对象。house对象中数据改变,由于effect函数读取了对象属性,所以当数据改变,也需要及时更新副作用函数effect。但是问题来了,假如对象中多个属性的,依赖于数据变化的多个副作用函数,数据变化一次都需要执行一次,代码写起来就会很冗余,所以我们需要这样处理:

    const objSet = new Set();
    const obj = new Proxy(house, {
      // 拦截读取操作
      get (target, key) {
        objSet.add(effect) // 收集effect
        return target[key];
      },
      set (target, key, newVal) {
        target[key] = newVal;
        objSet.forEach(fn=>fn()) // 遍历effect
        return true;
      }
    })

    把副作用函数都存到Set实例中,Set可以过滤重复数据,然后在获取数据中收集副作用函数,在修改数据中遍历执行副作用函数,这样就简化了代码,不需要每次改变都要执行一次了,也就是修改一次数据及时更新effect。虽然上面已经实现了响应式的雏形了,但是还需要解决很多问题:

    假如这个副作用函数是一个匿名函数,这时候需要怎么处理? 添加一个全局变量临时存储。

    effect (()=>console.log('房子状态:'+obj.status)) // 上面的例子会直接报not define
    // 添加一个全局变量activeEffect存储依赖函数,这样effect就不会依赖函数的名字了
    let activeEffect;
    function effect (fn) {
     activeEffect = fn;
     // 执行副作用函数
     fn()
    }

    假如读取不存在的属性的时候,副作用函数发生什么? 副作用函数会被重新执行,由于目标字段与副作用函数没有建立明确的函数联系。所以这就需要引入唯一key辨识每一个数据的副作用函数,以target(目标数据)、key(字段名)、effectFn(依赖)。看下图:

    setTimeout(() => {
      obj.notExit = &#39;不存在的属性&#39;;
    }, 1000)

    分三种情况分析副作用函数存储数据唯一标识

    • 两个副作用函数同时读取同一个对象的属性值

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사

    • 一个副作用函数中读取了同一个对象不同属性

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사

    • 不同副作用函数中读取两个不同对象的相同属性

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사所以为了解决这些不同情况的副作用保存问题,所以Vue3引入了Weak、Map、Set三个集合方法来保存对象属性的相关副作用函数:

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사

    const weakMap = new WeakMap();
    let activeEffect;
    const track = ((target,key)=>{
      if(!activeEffect){
          return;
        }
        // 从weakMap中获取当前target对象
        let depsMap = weakMap.get(target);
        if(!depsMap){
          weakMap.set(target,(depsMap=new Map()))
        }
        // 从Map中属性key获取当前对象指定属性
        let deps = depsMap.get(key)
        if(!deps){
          // 副作用函数存储
          depsMap.set(target,(deps=new Set()))
        }
        deps.add(activeEffect)  
    })
    const trigger = ((target,key)=>{
      // 从weakMap中获取当前target对象
      const depsMap = weakMap.get(target);
        if(!depsMap) return;
        // 从Map中获取指定key对象属性的副作用函数集合
        const effects = depsMap.get(key);
        effects&&effects.forEach(fn=>fn())
    })

    WeakMap与Map的区别是? 区别就是垃圾回收器是否回收的问题,WeakMap对象对key是弱引用,如果target对象没有任何引用,可以被垃圾回收器回收,这就需要它了。相对于WeakMap,不管target是否引用,Map都不会被垃圾回收,容易造成内存泄露。我们看一下下面例子:

    const map = new Map();
    const weakMap = new WeakMap();
    (function(){
      const foo = {foo:1};
      const bar = {bar:2};
      map.set(foo,1);
      weakMap.set(bar,2);
    })() // 函数执行完,weakMap内的所有属性都被垃圾回收器回收了
    setTimeout(() => {
     console.log(weakMap);// 刷新页面发现weakMap里面没有属性了
    }, 2000)

    假如在一个副作用函数中调用了对象的两个属性,但是有布尔值控制,按正常来说,副作用函数只能执行一次get获取值的,但是我们现有的实现方法还实现不了,我们看看下面例子。

    const effectFn = (() => {
      const str = obj.status ? &#39;&#39; : obj.type;
    })
    const obj = new Proxy(house, {
      get(target, key) {
        console.log(&#39;get run!&#39;);// 打印了两次
        ...
      },
      set(target, key, newVal) {
       ...
      }
    })

    通过这个例子,我们是不是需要解决这个问题,也就是当每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。我们看看源码例子:

    // 清空副作用函数依赖的集合
    function cleanupEffect(effect: ReactiveEffect) {
      const { deps } = effect
      if (deps.length) {
        for (let i = 0; i < deps.length; i++) {
          deps[i].delete(effect)
        }
        deps.length = 0
      }
    }

    嵌套副作用函数处理:由于副作用函数可能是嵌套,比如副作用函数中effectFn1中有还有一个副作用函数effectFn2,以上面的方法对于嵌套函数的处理用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。看了很多资料举例用effect栈存储,是的没错,当执行副作用函数的时候把它入栈,执行完毕后把它出栈。现在我们一起看一下源码怎么处理的:

    • 按位跟踪标记递归深度方式(优化方案):通过用二进制位标记当前嵌套深度的副作用函数是否记录过,如果记录过就,如果已经超过最大深度,因为采用降级方案,是全部删除然后重新收集副作用函数的。

      let effectTrackDepth = 0 // 当前副作用函数递归深度
      export let trackOpBit = 1 // 在track函数中执行当前的嵌套副作用函数的标志位
      const maxMarkerBits = 30 // 最大递归深度支持30位,

      为什么需要设置30位,因为31位会溢出。

      // 每次执行 effect 副作用函数前,全局变量嵌套深度会自增1
      trackOpBit = 1 << ++effectTrackDepth
      
      // 执行完副作用函数后会自减
      trackOpBit = 1 << --effectTrackDepth;

      为什么是左移一位,是因为第一位也就是说当前深度只是1,所以保持不变,不用管,从第二位开始。

        if (effectTrackDepth <= maxMarkerBits) {
          // 执行副作用函数之前,使用 `deps[i].w |= trackOpBit`对依赖dep[i]进行标记,追踪依赖
          initDepMarkers(this)
        } else {
          // 降级方案:完全清理
          cleanupEffect(this)
        }

      如何判断当前依赖是否已记录过,通过按位与判断是否有位已经标识,有就大于0:

      //代表副作用函数执行前被 track 过
      export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
      //代表副作用函数执行后被 track 过
      export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0

      清理依赖

      export const finalizeDepMarkers = (effect: ReactiveEffect) => {
        const { deps } = effect
        if (deps.length) {
          let ptr = 0
          for (let i = 0; i < deps.length; i++) {
            const dep = deps[i]
            // 有 was 标记但是没有 new 标记,应当删除
            if (wasTracked(dep) && !newTracked(dep)) {
              dep.delete(effect)
            } else {
              // 需要保留的依赖
              deps[ptr++] = dep
            }
            // 清空,把当前位值0,先按位非,再按位与
            dep.w &= ~trackOpBit
            dep.n &= ~trackOpBit
          }
          // 保留依赖的长度
          deps.length = ptr
        }
      }
    • 完全清理方式(降级方案):逐个清理掉当前依赖集合deps中每个依赖。

      function cleanupEffect(effect: ReactiveEffect) {
        const { deps } = effect
        if (deps.length) {
          for (let i = 0; i < deps.length; i++) {
            deps[i].delete(effect)
          }
          deps.length = 0
        }
      }

    响应式可调度性scheduler:trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。

    Vue3响应式的6个细节我们都了解了,我们可以对副作用工作流做一个全面总结如图:

    Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사Vue3响应式的关键在于两个函数:track(收集依赖)和trigger(触发依赖)。

    // target: 响应式代理对象, type: 订阅类型(get、hase、iterate), key: 要获取的target的键值
    export function track(target: object, type: TrackOpTypes, key: unknown) {
    // 如果允许追踪, 并且当前有正在运行的副作用
      if (shouldTrack && activeEffect) {
      // 获取当前target订阅的副作用集合, 如果不存在, 则新建一个
        let depsMap = targetMap.get(target)
        if (!depsMap) {
          // 获取对应属性key订阅的副作用, 如果不存在, 则新建一个
          targetMap.set(target, (depsMap = new Map()))
        }
        let dep = depsMap.get(key)
        if (!dep) {
          depsMap.set(key, (dep = createDep()))
        }
        ...
        // 处理订阅副作用
        trackEffects(dep, eventInfo)
      }
    }
    
    export function trackEffects(dep: Dep,debuggerEventExtraInfo?: DebuggerEventExtraInfo) {
      let shouldTrack = false
      if (effectTrackDepth <= maxMarkerBits) { // 如果当前追踪深度不超过最大深度(30), 则添加订阅
        if (!newTracked(dep)) { // 如果未订阅过, 则新建
          dep.n |= trackOpBit // 据当前的追踪标识位设置依赖的new值
          shouldTrack = !wasTracked(dep) // 开启订阅追踪
        }
      } else {
        shouldTrack = !dep.has(activeEffect!)
      }
    
      if (shouldTrack) {
        dep.add(activeEffect!) // 将当前正在运行副作用作为新订阅者添加到该依赖中
        activeEffect!.deps.push(dep) // 缓存依赖到当前正在运行的副作用依赖数组
        ...
      }
    }
    // 根据不同的type从depsMap取出,放入effects,随后通过run方法将当前的`effect`执行
    export function trigger(target: object,type: TriggerOpTypes,key?: unknown,newValue?: unknown,oldValue?: unknown,oldTarget?: Map<unknown, unknown> | Set<unknown>) {
      const depsMap = targetMap.get(target) // 获取响应式对象的副作用Map, 如果不存在说明未被追踪, 则不需要处理
      if (!depsMap) {
        return
      }
      let deps: (Dep | undefined)[] = []
      // 如果是清除操作,那就要执行依赖原始数据的所有监听方法。因为所有项都被清除了。
      if (type === TriggerOpTypes.CLEAR) { // clear
        // 如果是调用了集合的clear方法, 则要对其所有的副作用进行处理
        deps = [...depsMap.values()]
      } else if (key === &#39;length&#39; && isArray(target)) {
        const newLength = Number(newValue)
        depsMap.forEach((dep, key) => {
          if (key === &#39;length&#39; || key >= newLength) {
            deps.push(dep)
          }
        })
      } else { // set add delete
        // key不为void 0,则说明肯定是SET | ADD | DELETE这三种操作 
        // 然后将依赖这个key的所有监听函数推到相应队列中
        if (key !== void 0) {
          deps.push(depsMap.get(key))
        }
        switch (type) { // 根据不同type取出并存入deps
          case TriggerOpTypes.ADD:
             // 如果原始数据是数组,则key为length,否则为迭代行为标识符
            if (!isArray(target)) {
              deps.push(depsMap.get(ITERATE_KEY))
              if (isMap(target)) {
                deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
              }
            } else if (isIntegerKey(key)) {
              deps.push(depsMap.get(&#39;length&#39;))
            }
            break
          case TriggerOpTypes.DELETE:
           // 如果原始数据是数组,则key为length,否则为迭代行为标识符
            if (!isArray(target)) {
              deps.push(depsMap.get(ITERATE_KEY))
              if (isMap(target)) {
                deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
              }
            }
            break
          case TriggerOpTypes.SET:
            if (isMap(target)) {
              deps.push(depsMap.get(ITERATE_KEY))
            }
            break
        }
      }
      ...
        const effects: ReactiveEffect[] = []
        for (const dep of deps) {
          if (dep) {
            effects.push(...dep)
          }
        }
        // 遍历effects元素执行run函数
        triggerEffects(createDep(effects))
      }
    }

    Vue3响应式原理小结:

    Vue3中的副作用函数其实就是Vue2的依赖

    • activeEffect解决匿名函数问题
    • WeakMap、Map、Set存储对象属性的相关副作用函数
    • 处理副作用函数时,假如有多个响应式属性,控制只触发生效的属性或用到的属性
    • 嵌套副作用函数,使用二进制位记录嵌套副作用,通过控制二进制位是否清理嵌套副作用实现层级追踪
    • track()实现依赖收集、层级依赖追踪、依赖清理(解决嵌套副作用)
    • trigger()当某个依赖值发生变化时触发的, 根据依赖值的变化类型, 会收集与依赖相关的不同副作用处理对象, 然后逐个触发他们的 run 函数, 通过执行副作用函数获得与依赖变化后对应的最新值

    (学习视频分享:vuejs入门教程编程基础视频

    위 내용은 Vue2/Vue3의 반응성 원리를 자세히 설명하는 기사의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    성명:
    이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제