Before explaining, let’s first understand what data responsiveness is? The so-called data responsiveness is to establish the relationship between responsive data
and dependency
(the operation that calls the responsive data). When the responsive data changes , can notify those dependent operations that use these responsive data to perform related update operations, which can be DOM updates or the execution of some callback functions. Responsiveness is used from Vue2 to Vue3, so what are the differences between them?
Object.defineProperty()
. Proxy
. [Related recommendations: vuejs video tutorial, web front-end development]So what are the differences between them? Why does Vue3 choose Proxy instead of defineProperty? Let's first look at the following two examples:
// 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);
As can be seen from the above figure, defineProperty is used to define the object obj containing the name attribute, and then add the age attribute, add hobby attribute (array), and add student The properties are accessed separately, but the get and set methods in the obj object are not triggered. In other words, the defineProperty definition object cannot monitor changes in adding additional properties or modifying additional properties
. Let’s look at another example:
let obj = {}; // 初始化就添加hobby this.defineReactive(obj,'hobby',['游戏', '原神']); // 改变数组下标0的值 obj.hobby[0] = '王者'; console.log('obj.hobby',obj.hobby);
If we add the hobby attribute to obj from the beginning, we find that modifying the value of array subscript 0 does not trigger the set method in obj, which means that the defineProperty definition object cannot monitor the modification of array elements based on its own array subscript. Variety
. Then let’s continue to look at the object example of Proxy agent:
// 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);
From the above picture, do you find the obvious difference between Proxy and defineProperty? Proxy can support object addition or modification to trigger get and set methods. , no matter what properties are inside the object. So
Object.defineProperty(): defineProperty definition object cannot listen Add additional properties or Modify additional properties Changes; defineProperty definition objects cannot monitor changes in modifying array elements based on their own array subscripts. Let’s take a look at usage examples in 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); }, }
It is indeed possible to modify the properties in the data from the picture, but it cannot be rendered in time, so Vue2 provides two property methods to solve this problem: Vue.$set
and Vue.$delete
.
Note that you cannot add the age attribute directly to this._ data.age, which is also not supported.
this.$delete(this.student, 'name');// 删除student对象属性name this.$set(this.student, 'age', '21');// 添加student对象属性age this.$set(this.student.hobby, 0, '王者');// 更新student对象属性hobby数组
Proxy: Solving the above two drawbacks, proxy can achieve:
You can directly monitor objects instead of object properties, and you can monitor changes in additional properties added to objects;
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!
##You can directly monitor changes in the array.
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!
Proxy returns a new object, while Object.defineProperty can only traverse the object properties and modify them directly.
Supports up to 13 interception methods, not limited to apply, ownKeys, deleteProperty, has, etc. Object.defineProperty does not have.
Dependencies Collect and dependency updates .
1 Vue2 Responsive PrincipleThis analysis is based on the Vue2.6.14 version##Vue2 Responsive
: Through Object. defineProperty() monitors each property. When the property is read, the getter will be triggered, and when the property is modified, the setter will be triggered. First of all, we all know that the data attribute in the Vue instance defines responsive data, which is an object. Let’s take a look at the data in the following example: 从上图我们可以看到,data中的每一个属性都会带 ❓ 给data属性创建Observer实例:通过初注册响应式函数initState中调用了initData函数实现为data创建Observer实例。 ❓ 通过Observer实例把data中所有属性转换成getter/setter形式来实现响应性:对data属性分为两种情况处理: 注意地,由于Vue实例的data永远都是一个对象,所以data里面包含的数组类型只有对象属性、数组属性。 ❓ 在getter收集依赖,在setter中触发依赖:当读取data中的数据时,会在get方法中收集依赖,当修改data中的数据时,会在set方法中通知依赖更新。defineReactive方法中主要是做四件事情: 从上面我们知道了dep.depend()实现了依赖收集,dep.notify()实现了通知依赖更新,那么Dep类究竟做了什么?我们先看看下面的图: 从图中我们得明确一点, 怎么监听多层嵌套的对象,其实就是通过.分割为对象,循环数组一层层去读数据,最后一后拿到的就是想要对的数据。 当嵌套对象a.b.c属性发生变化时,就会触发第二个参数中的函数。也就是说a.b.c就是变化的数据,当它的值发生变化时,通知Watcher,接着Watcher触发第二个参数执行回调函数。我们看看Watcher类源码,是不是发现了cb其实就与watch的第二参数有异曲同工之妙。 ❓ 实现对数组的监听:从最开始的例子,我们了解对象以及嵌套对象的监听,但是Object.defineProperty是用来监听对象指定属性的变化,不支持数组监听,那么数组又是怎么监听?我们上面说了data中的数据被赋予响应性都是在Observer中实现的,那么监听的实现也是在Observer对象中实现的,先对数组的特定方法做自定义处理,为了拦截数组元素通知依赖更新,然后才通过observeArray函数遍历创建Observer实例,主要分为两种情况: 当浏览器支持__ proto __ 对象:强制赋值当前arrayMethods给target的__ proto __ 对象,直接给当前target数组带上自定义封装的数组方法,从而实现监听数组变化。其实arrayMethods处理后就是下面这样一个对象: 当浏览器不支持__ proto __ 对象:遍历数组元素通过defineProperty定义为元素带上自定义封装的原生数组方法,由于自定义数组方法中做了拦截通知依赖更新,从而实现监听数组的变化。 对数组的Array原生方法做了自定义封装的源码如下,在自定义方法中拦截通知依赖更新。 这里基于Vue3.2.41版本进行分析 其实Vue3的响应原理与Vue2的响应原理都差不多,唯一不同的就是它们的实现方式,Vue3通过创建Proxy的实例对象而实现的,它们都是收集依赖、通知依赖更新。而Vue3中把依赖命名为副作用函数effect,也就是数据改变发生的副作用,我们先来看一下例子: 通过Proxy创建一个代理对象,把house代理给obj,obj是代理对象,house是被代理对象。house对象中数据改变,由于effect函数读取了对象属性,所以当数据改变,也需要及时更新副作用函数effect。但是问题来了,假如对象中多个属性的,依赖于数据变化的多个副作用函数,数据变化一次都需要执行一次,代码写起来就会很冗余,所以我们需要这样处理: 把副作用函数都存到Set实例中,Set可以过滤重复数据,然后在获取数据中收集副作用函数,在修改数据中遍历执行副作用函数,这样就简化了代码,不需要每次改变都要执行一次了,也就是修改一次数据及时更新effect。虽然上面已经实现了响应式的雏形了,但是还需要解决很多问题: ❓ 假如这个副作用函数是一个匿名函数,这时候需要怎么处理? 添加一个全局变量临时存储。 ❓ 假如读取不存在的属性的时候,副作用函数发生什么? 副作用函数会被重新执行,由于 分三种情况分析副作用函数存储数据唯一标识 所以为了解决这些不同情况的副作用保存问题,所以Vue3引入了Weak、Map、Set三个集合方法来保存对象属性的相关副作用函数: ❓ WeakMap与Map的区别是? 区别就是垃圾回收器是否回收的问题,WeakMap对象对key是弱引用,如果target对象没有任何引用,可以被垃圾回收器回收,这就需要它了。相对于WeakMap,不管target是否引用,Map都不会被垃圾回收,容易造成内存泄露。我们看一下下面例子: ❓ 假如在一个副作用函数中调用了对象的两个属性,但是有布尔值控制,按正常来说,副作用函数只能执行一次get获取值的,但是我们现有的实现方法还实现不了,我们看看下面例子。 通过这个例子,我们是不是需要解决这个问题,也就是当每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。我们看看源码例子: ❓ 嵌套副作用函数处理:由于副作用函数可能是嵌套,比如副作用函数中effectFn1中有还有一个副作用函数effectFn2,以上面的方法对于嵌套函数的处理用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。看了很多资料举例用effect栈存储,是的没错,当执行副作用函数的时候把它入栈,执行完毕后把它出栈。现在我们一起看一下源码怎么处理的: 按位跟踪标记递归深度方式(优化方案):通过用二进制位标记当前嵌套深度的副作用函数是否记录过,如果记录过就,如果已经超过最大深度,因为采用降级方案,是全部删除然后重新收集副作用函数的。 为什么需要设置30位,因为31位会溢出。 为什么是左移一位,是因为第一位也就是说当前深度只是1,所以保持不变,不用管,从第二位开始。 如何判断当前依赖是否已记录过,通过按位与判断是否有位已经标识,有就大于0: 清理依赖: 完全清理方式(降级方案):逐个清理掉当前依赖集合deps中每个依赖。 ❓ 响应式可调度性scheduler:trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。 Vue3响应式的6个细节我们都了解了,我们可以对副作用工作流做一个全面总结如图: Vue3响应式的关键在于两个函数: Vue3中的副作用函数其实就是Vue2的依赖 The above is the detailed content of An article explaining the responsiveness principle of Vue2/Vue3 in detail. For more information, please follow other related articles on the PHP Chinese website!data(){
return {
name: 'Sapper',
hobby: ['游戏', '原神'],
obj: {
name: '张三',
student: {
major: '软件工程',
class: '1班',
}
}
}
}
__ob__
属性,它是一个Observer对象,其实Vue2中响应式的关键就是这个对象,在data中的每一个属性都会带get、set方法,而Vue源码中其实把get、set分别定义为reactiveGetter、reactiveSetter,这些东西怎么添加进去的。Vue2又是怎么数据变化同时实时渲染页面?先看看下面的图: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++
}
对象属性处理
(defineReactive实现)和数组属性处理
。数组怎么处理(后面再详细说明)创建Dep实例
、给对象属性添加get/set方法
、收集依赖
、通知依赖更新
。谁使用了变化的数据
,也就是说哪个依赖使用了变化的数据,其实就是Dep.taget,它就是我们需要收集的依赖,是一个Watcher实例对象,其实Watcher对象有点类似watch监听器,我们先看一个例子:vm.$watch('a.b.c',function(newVal,oldVal)){....}
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()
}
}
...
}
// 源码Observer类中对数组处理的部分代码
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
console.log(arrayKeys);// ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
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])// 遍历数组元素通过为元素带上
}
}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
console.log(arrayKeys);// ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
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])// 遍历数组元素通过为元素带上
}
}
// 遍历target实现创建Observer实例
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
Vue2响应式原理小结:
Vue3响应式原理
const house = {status:'未出租',price:1200,type:'一房一厅'};
const obj = new Proxy(house, {
get (target, key) {
return target[key];
},
set (target, key, newVal) {
target[key] = newVal;
return true;
}
})
function effect () {
console.log('房子状态:'+obj.status);
}
effect () // 触发了proxy对象的get方法
obj.status = '已出租!';
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;
}
})
effect (()=>console.log('房子状态:'+obj.status)) // 上面的例子会直接报not define
// 添加一个全局变量activeEffect存储依赖函数,这样effect就不会依赖函数的名字了
let activeEffect;
function effect (fn) {
activeEffect = fn;
// 执行副作用函数
fn()
}
目标字段与副作用函数没有建立明确的函数联系
。所以这就需要引入唯一key辨识每一个数据的副作用函数,以target(目标数据)、key(字段名)、effectFn(依赖)。看下图:setTimeout(() => {
obj.notExit = '不存在的属性';
}, 1000)
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())
})
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)
const effectFn = (() => {
const str = obj.status ? '' : obj.type;
})
const obj = new Proxy(house, {
get(target, key) {
console.log('get run!');// 打印了两次
...
},
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
}
}
let effectTrackDepth = 0 // 当前副作用函数递归深度
export let trackOpBit = 1 // 在track函数中执行当前的嵌套副作用函数的标志位
const maxMarkerBits = 30 // 最大递归深度支持30位,
// 每次执行 effect 副作用函数前,全局变量嵌套深度会自增1
trackOpBit = 1 << ++effectTrackDepth
// 执行完副作用函数后会自减
trackOpBit = 1 << --effectTrackDepth;
if (effectTrackDepth <= maxMarkerBits) {
// 执行副作用函数之前,使用 `deps[i].w |= trackOpBit`对依赖dep[i]进行标记,追踪依赖
initDepMarkers(this)
} else {
// 降级方案:完全清理
cleanupEffect(this)
}
//代表副作用函数执行前被 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
}
}
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
}
}
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 === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || 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('length'))
}
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响应式原理小结: