likes
comments
collection
share

Vue之 响应式原理-调用栈小结

作者站长头像
站长
· 阅读数 17

一 概念介绍

1 efn/依赖/watcher:本质上就是 副作用函数:执行后会 影响其他函数执行结果

2 响应式数据:当数据A 发生变化后,可以自动让读取它的函数被自动重新执行,那么A就称作是一个 响应式数据

即 t1.key1值发生变化==> 读取key1的efns自动执行

3 bucket/dep: 结构为一个WeakMap: t1 <--> depsMap key1 <--> Set(efn1/ efn2 ....),记做deps

它的作用是 记录了 t1和 efn1之间 多对多的映射关系 使用 WeakMap而非Map的原因,其实是为了更高效的进行垃圾回收,节约内存使用

4 efn.deps/watcher.deps属性 + cleanUp(): 用于清理过期的依赖关联 + 避免添加重复的efn

5 activeEffect/Dep.target + effectStack: 用于处理嵌套effect的情况(即 组件嵌套渲染)

二 Vue3的响应式数据的 实现原理

1 renderWatcher的执行原理

S1 收集依赖 + 触发依赖

1 effect(fn1)
  2 effectFn(),记做efn1
    3.1 cleanUp(efn1):
    3.2 activeEffect = efn1 + effectStack.push(efn1)
    3.3 Fn()
      4.1 读取p1.key1==> p1.key1.get()
        5.1 track(t1, key1)
          6.1 建立 t1- key1- deps.add(efn1)的关联关系
          6.2activeEfn(即 efn1).deps收集到 efn1所属的所有keyXDeps
          
        5.2 return t1[key1]
        
      4.2 更新p1.key1的值==> p1.key1.set()
        5.1 t1[key1] = newVal
        5.2 trigger(t1, key1):获取到 depsMap->effects==> 执行efnX()/efnX.options.scheduler(efnX)
        
    3.4 effectStack.pop() + activeEffect = effectStack[effectStack.length-1]

3.1 cleanUp(efn1)的作用:每次执行efn1时,先删除它之前所有的keyXDeps的依赖关系,从而【清理过期的key-efn】关联,新的关联关系会在 fn()==>track(t1, keyX)里重新构建

3.2 activeEffect + effectStack的作用:防止嵌套efn执行时,activeEfn一直指向内部efn的情况

4.2--> 5.2实现注意点: 要解决 efn1内 data.key的自增操作导致的 无限递归情况,解决方法是 在keyEffects中筛选出不是activeEffect的 efns来执行;

利用efnX.options.scheduler,可以实现对efn执行的精确控制,比如 执行时机、执行次数等

2 computed的实现原理

S1 收集依赖

1 effect(fn2)
  2 return efn2/ efn2()
    3.1 cleanUp(efn2)
    3.2 activeEfn = efn2
    3.3 res = fn2()==> 读取comp1.value
      4.1 comp1._value = copmEfn1()
        5.1 cleanUp(compEfn1) 
        5.2 active = compEfn1 
        5.3 res = getter()==> track(p1, key1, efn1): 建立p1.key1和compEfn1关联
        5.4 重置activeEfn = efn2

      4.2 dirty锁开启
      4.3 track(comp1, 'value'): 建立comp1.value和efn2的关系

    3.4 重置activeEfn
    3.5 返回res

S2 触发依赖

1 p1.key1的更新/setter()
  2 trigger(p1, key1)
    3 compEfn1.scheduler()
      4.1 关闭 compEfn1.dirty
      4.2 trigger(copm1, 'value'): 触发comp1.value的enfX==> efn2()执行... 

1 通过effect(options.lazy) + 内部resObj实现了 计算属性特点1: 懒计算

2 通过computed内部定义的dirty标志位,来实现 计算属性的特点2: 值缓存

3 通过effect(options.scheduler),来实现 计算属性【值缓存】的正确值更新

4 通过 track(comp1, 'value') + trigger(copm1, 'value') 来实现计算属性的 efn嵌套

3 watch的实现原理

S1 收集依赖

1 watch(source1, cb1)
  2.1 定义 getter = source1/ traverse
  2.2 定义 efn1 = effect(getter, options)
    3 return efn1(因为options.lazytrue)

  2.3 oldValue = enf1()
    3.1 cleanUp(efn1)
    3.2 active = efn1
    3.3 res = fn(),即 getter()
      4 读取p1/ p1.key1的值==> track(t1, key1, efn1)

S2 触发依赖

1 p1.key1的更新/setter()
  2 trigger(p1, key1)==> efn1.scheduler()
    3.1 newVal = enf1() ==> cleanUp() + .... res = fn()/ getter()
    3.2 cb(newVal, oldVal)
    3.3 oldVal = newVal(更新oldVal值)

三 Vue3的响应式数据的 语法细节【选读即可】

1 get/set拦截内引入Reflect的原因

S1 effect(fn)
  2 efn1()
    3 cleanUp()... res= fn()==> 读取了p1.key1
      4 track(t1, key1, efn1) + key1是1个访问器属性:return this.key2

S2 this.key2:
  1 this指向t1, 则不会触发proxy的getter拦截
  2 this指向p1, 才能触发proxy的getter拦截==> 因此要使用Reflect.* 替换 target[key]

2 Proxy代理 对象类型的注意点

1 针对3种读取对象的方法,设置getter

  • 属性访问: t1.key1 ==> get拦截 get(t1, key1, receiver)
  • in操作符: key1 in t1 ==> has拦截 has(t1, key1)
  • for...in循环: for (let key in t1)
    • 依赖收集: ownKeys拦截 + track(t1, ite_key, 'add/set')
    • 依赖触发: ite_keyEfns = depsmap.get(ite_key),执行前提是 add了【新】属性

2 删除对象属性==>

  • 依赖收集:deleteProperty(t1, key1)拦截 + tirgger(t1, key, 'delete')
  • 依赖触发:当类型为add/delete时,获取ite_keyEfns (因为删除操作,会影响 ite_keyEfns)

3 优化处理 读取/设置原型对象上的属性

S1 fn1()==> child.key1get(t1, k1)
  2. track(t1, k1, efn1) + return Reflect.get(t1, k1) 
    3. k1在t1上不存在,向上查找t1的原型
      4. parent.k1get(t2, k1): track(t2, k1, efn1)

S2 child.k1set(t1, k1, newVal)
  2.1 oldVal = t1.k1--> t2.k1
  2.2 res = t1.k1Reflect.set==> t2.k1的set
    3.1 oldVal
    3.2 receiver.raw读取==> p1.raw的get==> 返回t1
    3.3 判断t2是否为 receiver.raw,相等才触发trigger

  2.3 t1 === receiver.raw ==> trigger(t1, k1, type)   

4 浅响应和深响应

S1 reactive(t1)
  2. return createReactive(t1)
    3. res = fn1()==> p1.k1.k1_1==> get(t1, k1)
      4. track(t1, k1, efn1) + reactive(res1)
        5. 递归调用 reactive(res1, k1_1): track(res1, k1_1)

5 只读和浅只读,略

3 Proxy代理 数组类型的注意点

1 数组的索引和length

S1 effect(fn1, options1)
  2. retun efn1/ !options1.lazy && efn1()
    3.1 cleanUp(efn1)
    3.2 active = efn1
    3.3 res = fn1()
      4.1 读取p1.k1值/ p1.k1.k1_1值==> get(t1, k1)
        5.1 非只读时,track(t1, k1)
        5.2 Rres = Reflect.get(t1, k1)==> return Rres/ readonly(Rres)/ reactive(Rres)
      
      4.2 in操作符==> has(t1, k1)
        5.1 track(t1, k1)
        5.2 return Reflect.has(t1, k1)

      4.3 for...in==> ownKeys(t1)
        5.1 track(t1, ite_key): 普通对象
        5.2 track(t1, 'length'): 数组

    3.5 重置active

S2 p1.key1的set
  2.1 确定type值:set/ add/ delete
  2.2 Rres = Reflect.set(t1, k1)
  2.3 trigger(t1, k1, type, newVal1)
    3.1 get key's efns
    3.2 type为 add/del, 可能会影响for...in结果: get ite's efns
    3.3 type为 add + 数组:修改下标可能会影响数组的长度:  get length's efns
    3.4 k1为length + 数组:修改length,可能会影响arr[0]的读取


四 renderWatcher的执行流程

1 实现 数据观测/数据代理

1 new Vue(options)
  2 Vue.pty._init()
    3 initData(vm)
      4 observe(data, true)
        5 new Observer(value)
          6.1 value和ob互相关联
          6.2 handleObject/ob.walk()==> defineReactive()
          6.3 handleArray==> 代理数组原型对象 + ob.observeArray()

6.2 handleObject/ob.walk()
  7.defineReactive(obj, keyX) 
    8.1 keyDep(即setterDep) + getChildOb
    8.2 Obj.definePty.get: keyDep.depend() + childObDep?.depend() + dependArray(value) + 返回value值
    8.3 observe(newVal) + dep.notify()

6.3 handleArray
  7.1 代理数组原型对象 + ob.observeArray(inserted) + ob.dep.notify()
  7.2 ob.observeArray(value)

childOb.dep的作用是:

收集无法被setter触发的依赖,从而响应 vm.$set/arr.push等修改方式

也就是说,是 为了在【添加/删除属性、调用数组变异方法】时,能有方法触发 依赖执行

dependArray的作用是:

递归让数组里的 对象/数组成员,也通过ob.dep收集依赖,从而支持vm.$set等方法触发

2 触发依赖收集

1 new Vue(options)
  2 Vue.pty._init()
    3 initXXX(vm) + vm.$mount(vm.$options.el)
      4 getTemplate==> RenderFn(即compileToFunctions函数)
        5 mountComponent()
          6 new Watcher(vm, updateComponent, noop, {}, true)

6 Watcher.constructor(vm, expOrFn, cb, options, isRenderWatcher)
  7.1 建立组件和当前watcher间的联系 + initWatcherGetter
  7.2 watcher.get()
    8 pushTarget(watcher) + watcher.getter() + popTarget() + watcher.cleanupDeps()
      
8.1 watcher.getter()==> updateComponent() ==> vm._render()==> $data.xxx的getter拦截 ==> dep1.depend() ==> 
  9 watcher.addDep(dep1): 
    - 利用watcher.newDepIds和watcher.depIds 避免重复添加watcher 
    10 dep1.addSub(watcher)

8.2 watcher.cleanupDeps(): 利用watcher.newDepIds和watcher.depIds,删除过期的依赖(比如之前在页面显示,后来不显示的data_key)==> 
  dep.removeSub(watcher1)

所以watcher.newDepIds && watcher.depIds的作用,就是用来让dep正确收集watcher的==> 不收集重复的依赖(efn) + 删除过期的依赖(efn)

3 触发依赖

1 修改响应数据的属性值==> setter/ob.dep
  2 dep.notify()
    3 subItem.update()/ watcherX.update()
      4.1 handleCoumputed
      4.2 handleSyncWatcher/ watcherX.run()
      4.3 queueWatcher(watcherX)

4.3 queueWatcher(watcherX)
  5 nextTick(flushSchedulerQueue)==> flushSchedulerQueue()
    6 watcherX.run(): watcherX.get() + watcherX.cb()

五 vm.computed流程的执行流程

1 new Vue(options)
  2 Vue.pty._init()
    3 initState==> initComputed(vm, opts.computed)
      4.1 创建compWatcher,并把它添加到 vm._computedWatchers对象里
      4.2 调用 defineComputed(vm, key, userDef)
        5 定义computedGetter==> compA-Watcher.depend() + compA-Watcher.evaluate()

计算属性响应式流程:模板中使用了计算属性compA==> 编译成渲染函数==> 触发compA的 getter拦截器,即sharedPropertyDefinition.get==> 触发computedGetter==> watcher.depend/ dep.depend/ watcher.addDep/ dep.addSub==> compA.subs = [renderWatcher]==>

调用compA-Watcher.evaluate/watcher.get() 手动求值==> compA内读取了data.key1,从而让data.key1,从而让data.key1,从而让data.key1收集了compA-Watcher

修改$data.key1的值==> 触发compA-Watcher更新,即compA-Watcher.update ==> watcherdep.notify() + compA.subs = [renderWatcher] ==> 页面重新进行渲染,完成视图更新

六 参考文档

01 Vue.js设计实现与原理

02 Vue技术内幕

03 图解Vue响应式原理

转载自:https://juejin.cn/post/7244408781575733285
评论
请登录