51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

浅析-Vue3-响应式原理

前端目前两个当家花旦框架 VUE React,它们能够流行开来,响应式原理做出了巨大贡献。毕竟,它通过数据的变更就能够更新相应的视图,极大的将我们从繁琐的DOM操作中解放出来。

所以掌握它们的响应式原理,对掌握前端框架的精髓就很重要了。

Vue2.x的响应式 {#vue2x的响应式}

Vue2中通过 Object.defineProperty 实现数据劫持,使得数据实现响应式更新。Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

Object.defineProperty(obj, prop, descriptor)

  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称或 Symbol 。
  • descriptor:要定义或修改的属性描述符。

返回值:被传递给函数的对象。

响应实现 {#响应实现}

对象类型 {#对象类型}

通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)。

数组类型 {#数组类型}

通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹)。

let person = {   // 模拟Vue2实现响应式

    <span class="hljs-attr">name</span>:<span class="hljs-string">'亮哥'</span>,

    <span class="hljs-attr">age</span>:<span class="hljs-number">18</span>   




}


Object.defineProperty(person, "age", {


// 当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除,解决新增/删除属性,数据无响应问题


configurable: true,


get: () => {


// 收集依赖代码...


return person.age;


},


set: (newVal) => { // 一个给属性提供 setter 的方法


// 当属性值发生变化时我们可以进行额外操作 如调用监听器


person.age = newVal;


// 通知更新视图代码...


},


});

`data.`age` = `25` `// 触发set方法`
`

存在问题

  • 递归遍历数据对象属性,消耗大
  • 新增/删除属性,数据无响应;需要额外方法实现(Vue.set/Vue.delete、this.set/set/get/$delete)
  • 数组修改需要额外方法实现(Vue.set),或者通过重写的push/pop/shift/unshift/splice/sort/reverse方法实现

Vue3的响应式 {#vue3的响应式}

  • Proxy和Reflect实现响应式原理
    • 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
    • 通过Reflect(反射): 对源对象的属性进行操作。

Proxy {#proxy}

Vue3 的响应式原理依赖了 Proxy 这个核心 API,通过 Proxy 可以劫持对象的某些操作。

const obj = { a: 1 };
const p = new Proxy(obj, {
	get(target, property, receiver) {
		console.log("get");
		return Reflect.get(target, property, receiver);
	},
	set(target, property, value, receiver) {
		console.log("set");
		return Reflect.set(target, property, receiver);
	},
	has(target, prop) {
		console.log("has");
		return Reflect.has(target, prop);
	},
	deleteProperty(target, prop) {
		console.log("deleteProperty");
		return Reflect.deleteProperty(target, prop);
	},
});
`p.`a`; `// 输出 --> get`
p.`a` = `2`; `// 输出 --> set`
`"a"` `in` p; `// 输出 --> has`
`delete` p.`a`; `// 输出 --> deleteProperty`
`

如上例子,用 Proxy 代理了 Obj 对象的属性访问、属性赋值、in 操作符、delete 的操作,并进行 console.log 输出。

Reflect {#reflect}

Reflect 是与 Proxy 搭配使用的一个 API,当劫持了某些操作时,如果需要再把这些操作反射回去,那么就需要 Reflect 这个 API。

由于拦截了对象的操作,所以这些操作该有的功能都丧失了,例如,访问属性 p.a 应该得到 a 属性的值,但此时却不会有任何结果,如果还想拥有拦截之前的功能,那就需要用 Reflect 反射回去。

const obj = { a: 1 };
const p = new Proxy(obj, {
	get(target, property, receiver) {
		console.log("get");
		return Reflect.get(target, property, receiver);
	},
	set(target, property, value, receiver) {
		console.log("set");
		return Reflect.set(target, property, receiver);
	},
	has(target, prop) {
		console.log("has");
		return Reflect.has(target, prop);
	},
	deleteProperty(target, prop) {
		console.log("deleteProperty");
		return Reflect.deleteProperty(target, prop);
	},
});

举个例子 {#举个例子}

以下全文都会通过这个例子来讲述 Vue3 响应式的原理。

<div id="app"></div>

\<script\>
// 创建一个响应式对象
const state = reactive({ counter: 1 });


    // 立即运行一个函数,当响应式对象的属性发生改变时重新执行。
    effect(() =&gt; {
    	document.querySelector("#app").innerHTML = state.counter;
    });

    // 2s 后视图更新
    setTimeout(() =&gt; {
    	state.counter += 1;
    }, 2000);



`</script>
`

用 reactive 创建了一个响应式对象 state,并调用了 effect 方法,该方法接受一个副作用函数,effect 的执行会立即调用副作用函数,并将 state.counter 赋值给 #app.innerHTML;两秒后,state.counter += 1,此时,effect 的副作用函数会重新执行,页面也会变成 2.

内部的执行过程大概如下图所示:

  1. 调用 reactive() 返回一个 Proxy 代理对象,并劫持对象的 get 与 set 操作
  2. 调用 effect() 方法时,会访问属性 state.counter,此时会触发 proxy 的 get 操作。
  3. get 方法会调用 track() 进行依赖收集;建立一个对象(state)、属性(counter)、effect 副作用函数的依赖关系;
  4. set 方法会调用 trigger() 进行依赖更新;通过对象(state)与属性(coutner)找到对应的 effect 副作用函数,然后重新执行。

reactive {#reactive}

reactive 会返回如下一个 Proxy 对象

const reactive = (target) => {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);

      <span class="hljs-title function_">track</span>(target, key); <span class="hljs-comment">// 收集依赖</span>

      <span class="hljs-keyword">if</span> (<span class="hljs-title function_">isObject</span>(res)) {
        <span class="hljs-comment">// 如果当前获取的属性值是一个对象,则继续将为此对象创建 Proxy 代理</span>
        <span class="hljs-keyword">return</span> <span class="hljs-title function_">reactive</span>(res);
      }

      <span class="hljs-keyword">return</span> res;
    },

    <span class="hljs-title function_">set</span>(<span class="hljs-params">target, key, value, receiver</span>) {
      <span class="hljs-title class_">Reflect</span>.<span class="hljs-title function_">set</span>(target, key, value, receiver);
      <span class="hljs-title function_">trigger</span>(target, key); <span class="hljs-comment">// 依赖更新</span>
    },



`});
};
`

effect {#effect}

let activeEffect;
function effect(fn) {
  const _effect = function reactiveEffect() {
    activeEffect = _effect;
    fn();
  };
_effect`();
}
`

首先定义全局的 activeEffect,它永远指向当前正在执行的 effect 副作用函数。effect 为 fn 创建一个内部的副作用函数,然后立即执行,此时会触发对象的 get 操作,调用 track() 方法。

effect(() => {
	// effect 的立即执行会访问 state.counter,触发了对象的 get 操作。
	document.querySelector("#app").innerHTML = state.counter;
});

track {#track}

track 会建立一个 对象(state) => 属性(counter) => effect 的一个依赖关系

const targetMap = new WeakMap();
function track(target, key) {
	if (!activeEffect) {
		return;
	}

    <span class="hljs-keyword">let</span> depsMap = targetMap.<span class="hljs-title function_">get</span>(target);
    <span class="hljs-keyword">if</span> (!depsMap) {
    	targetMap.<span class="hljs-title function_">set</span>(target, (depsMap = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Map</span>()));
    }

    <span class="hljs-keyword">let</span> dep = depsMap.<span class="hljs-title function_">get</span>(key);
    <span class="hljs-keyword">if</span> (!dep) {
    	depsMap.<span class="hljs-title function_">set</span>(key, (dep = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Set</span>()));
    }

    <span class="hljs-keyword">if</span> (!dep.<span class="hljs-title function_">has</span>(activeEffect)) {
    	dep.<span class="hljs-title function_">add</span>(activeEffect);
    }



`}
`

执行完成成后得到一个如下的数据结构:

[ // map 集合
  {
    key: {counter: 1} // state 对象,
    value: [ // map 集合
      {
        key: "counter",
        value: [ // set
          function reactiveEffect() {} // effect 副作用函数
        ],
      }
    ],
  },
];

注意:当调用 effect 时,会将当前的副作用函数赋值给全局的 activeEffect,所以此时可以正确关联其依赖。

trigger {#trigger}

当给 state.counter 赋值的时候就会触发代理对象的 set 操作,从而调用 trigger 方法

setTimeout(() => {
  // 给 counter 属性赋值会触发 set 操作
  state.counter += 1;
}, 2000);
function trigger(target, key) {
	const depsMap = targetMap.get(target);
	if (!depsMap) return;

    <span class="hljs-type">const</span> <span class="hljs-variable">effects</span> <span class="hljs-operator">=</span> depsMap.get(key);
    effects &amp;&amp; effects.forEach((effect) =&gt; effect());



`}
`

赞(1)
未经允许不得转载:工具盒子 » 浅析-Vue3-响应式原理