最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 走进Vue3源码:响应式原理(超详细)

    正文概述 掘金(再来以后)   2021-01-28   683

    概述

    响应式原理主要由 3 个部分构成:1. 初始化;2. 收集依赖;3. 触发依赖
    Vue3.0 观测数据提供4个方法:reactive, shallowReactive, readonly, shallowReadonly
    下面从 reactive 入手分析响应式原理(Vue 3.0.5 版本)

    Demo

    <div id="demo">
      <!-- 2. 收集依赖 -->
      <h1 @click="handleAddCount"> Vue.js Count: {{state.count}}</h1>
    </div>
    
    <script>
    import { reactive } from 'vue'
    export default{
      setup() {
        // 1. 初始化
        const state = reactive({ count: 1 })
        
        const handleAddCount = () => {
          // 3. 触发依赖
          state.count ++
        }
        
        return {
          state,
          handleAddCount
        }
      }
    }
    </script>
    

    1. 初始化阶段

    调用代码
    const state = reactive({count: 1}) // 传入 Object,返回 Proxy
    
    源码位置: reactive.ts
    export function reactive(target: object) {
      // 如果观测一个 readonly proxy 直接返回
      if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
        return target
      }
      return createReactiveObject(
        target,
        false,
        mutableHandlers,
        mutableCollectionHandlers
      )
    }
    
    
    function createReactiveObject(
      target: Target,
      isReadonly: boolean,
      baseHandlers: ProxyHandler<any>,
      collectionHandlers: ProxyHandler<any>
    ) {
      // 1. 非对象不允许观测
      if (!isObject(target)) {
        if (__DEV__) {
          console.warn(`value cannot be made reactive: ${String(target)}`)
        }
        return target
      }
      // 2. target 已经是一个 Proxy,直接返回
      // target is already a Proxy, return it.
      // exception: calling readonly() on a reactive object
      if (
        target[ReactiveFlags.RAW] &&
        !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
      ) {
        return target
      }
      // 3.target 已经是一个观测过的对象,直接返回
      // 但是一个对象可以经 reactive, readonly 都观测一次
      const proxyMap = isReadonly ? readonlyMap : reactiveMap
      const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      // 4. 仅白名单类型的对象可以观测
      const targetType = getTargetType(target)
      // targetType === TargetType.INVALID 有如下情况
      // a. 经 markRaw 处理的对象(__v_skip属性为true)
      // b. 禁止扩展的对象(Object.preventExtensions, Object.seal, Object.freeze 处理过)
      // c. 非 Object, Array, Map, Set, WeakMap, WeakSet 的对象(比如 Date 对象)
      if (targetType === TargetType.INVALID) {
        return target
      }
      // 5. 进行观测,普通对象(Object, Array)和 COLLECTION 对象(Map, Set, WeakMap, WeakSet)会有不同的处理,下面着重分析普通对象
      const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
      )
      // 6. 保存 target 到 Map
      proxyMap.set(target, proxy)
      return proxy
    }
    

    2. 收集依赖阶段

    调用代码
    // 渲染节点时等同于执行下面的代码,renderTemplate 方法模拟渲染 dom 的 render 方法
    // 会对 state.count 进行取值,触发了 state 的 get 拦截
    watchEffect(() => {
      renderTemplate(
        `<h1 @click="handleAddCount"> Vue.js Count: {{state.count}}</h1>`,
        { state, handleAddCount }
      )
    })
    
    源码位置: baseHandlers.ts & effect.ts
    // baseHandlers.ts
    const get = /*#__PURE__*/ createGetter()
    
    function createGetter(isReadonly = false, shallow = false) {
      return function get(target: Target, key: string | symbol, receiver: object) {
        // 1. 处理标识属性的值
        // 1.1 ReactiveFlags.IS_REACTIVE => __v_isReactive 是否是响应式
        if (key === ReactiveFlags.IS_REACTIVE) {
          return !isReadonly
        // 1.2 ReactiveFlags.IS_READONLY => __v_isReadonly 是否是只读
        } else if (key === ReactiveFlags.IS_READONLY) {
          return isReadonly
        // 1.3 ReactiveFlags.RAW => __v_raw 原始对象
        } else if (
          key === ReactiveFlags.RAW &&
          receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
        ) {
          return target
        }
    
        const targetIsArray = isArray(target)
        // 2. 劫持数组方法(proxy 不能拦截的操作,vue 将进行如下 hack 处理)
        // 2.1 'includes', 'indexOf', 'lastIndexOf' 执行这 3 个方法时遍历收集数组所有元素的依赖
        // 2.2 'push', 'pop', 'shift', 'unshift', 'splice' 获取这些方法时会暂时他停止收集依赖
        if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
          return Reflect.get(arrayInstrumentations, key, receiver)
        }
    
        const res = Reflect.get(target, key, receiver)
    
        // 3. 返回 key 为 Symbol 类型的数据,不会收集依赖
        if (
          isSymbol(key)
            ? builtInSymbols.has(key as symbol)
            : key === `__proto__` || key === `__v_isRef`
        ) {
          return res
        }
        // 4. 收集依赖(只读属性不会触发依赖收集,因为永远不会触发 set,也就不用收集依赖)
        if (!isReadonly) {
          // track 方法详解见后面
          track(target, TrackOpTypes.GET, key)
        }
        // 5. 浅观测只收集第一层属性的依赖
        if (shallow) {
          return res
        }
        // 6. 对于属性值为 ref 时,对象会自动解包,数组则不会
        if (isRef(res)) {
          // ref unwrapping - does not apply for Array + integer key.
          const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
          return shouldUnwrap ? res.value : res
        }
        // 7. 对于深层的对象懒观测,即只有在 get 该值时才会进行观测,有利于提升性能
        if (isObject(res)) {
          // Convert returned value into a proxy as well. we do the isObject check
          // here to avoid invalid value warning. Also need to lazy access readonly
          // and reactive here to avoid circular dependency.
          return isReadonly ? readonly(res) : reactive(res)
        }
    
        return res
      }
    }
    
    export const mutableHandlers: ProxyHandler<object> = {
      get,
      set,
      deleteProperty,
      has,
      ownKeys
    }
    
    // effect.ts
    // targetMap 作为全局依赖,所有被观测的对象的依赖全部保存在这
    // 观测粒度为对象的 key,数据结构如下
    type Dep = Set<ReactiveEffect>
    type KeyToDepMap = Map<any, Dep>
    const targetMap = new WeakMap<any, KeyToDepMap>()
    
    // 2. 依赖收集方法
    export function track(target: object, type: TrackOpTypes, key: unknown) {
      // 1. 判断是否允许收集
      // shouldTrack:用于标识是否可执行 track,由内部方法 pauseTracking和 enableTracking 切换状态
      // activeEffect:表示当前执行的 effect,即要收集的依赖(本案例中当 watchEffect 传入的函数对应的 effect 运行时就会被标记为 activeEffect)
      if (!shouldTrack || activeEffect === undefined) {
        return
      }
      // 2. 获取 target 的所有依赖,如果不存在则进行初始化
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
      }
      // 3. 获取 target 对应 key 的所有依赖,如果不存在则进行初始化
      let dep = depsMap.get(key)
      if (dep === void 0) {
        depsMap.set(key, (dep = new Set()))
      }
      // 4. 如果当前依赖不存在则保存
      if (!dep.has(activeEffect)) {
        // 加入依赖
        dep.add(activeEffect)
        activeEffect.deps.push(dep)
        if (__DEV__ && activeEffect.options.onTrack) {
          activeEffect.options.onTrack({
            effect: activeEffect,
            target,
            type,
            key
          })
        }
      }
      // 依赖收集完毕
    }
    

    3. 触发依赖阶段

    调用代码
    const handleAddCount = () => {
      state.count ++ // 此时触发
    }
    
    源码位置: baseHandlers.ts & effect.ts
    // baseHandlers.ts
    const set = /*#__PURE__*/ createSetter()
    
    // 1. 设置 state.count 触发的 set 拦截
    function createSetter(shallow = false) {
      return function set(
        target: object,
        key: string | symbol,
        value: unknown,
        receiver: object
      ): boolean {
        // 1. 获取对象 key 对应的旧值
        const oldValue = (target as any)[key]
        if (!shallow) {
          value = toRaw(value)
          // 2. 自动解包 ref 对象,设置 value
          // 此时 oldValue.value 又会触发 ref 对象的 set,从而触发其依赖触发
          if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
            oldValue.value = value
            return true
          }
        } else {
          // in shallow mode, objects are set as-is regardless of reactive or not
        }
        
        const hadKey = hasOwn(target, key)
        const result = Reflect.set(target, key, value, receiver)
        // don't trigger if target is something up in the prototype chain of original
        if (target === toRaw(receiver)) {
          // 3. 判断当前 key 是否已存在,分别触发 add 和 set 类型的 trigger
          if (!hadKey) {
            // 新增属性也会触发依赖更新,不像 vue2.0 中必须使用 $set
            trigger(target, TriggerOpTypes.ADD, key, value)
          } else if (hasChanged(value, oldValue)) {
            // 当值有改变时才会触发依赖
            trigger(target, TriggerOpTypes.SET, key, value, oldValue)
          }
        }
        return result
      }
    }
    
    export const mutableHandlers: ProxyHandler<object> = {
      get,
      set,
      deleteProperty,
      has,
      ownKeys
    }
    
    // effect.ts
    // 触发依赖的执行
    
    export function trigger(
      target: object,
      type: TriggerOpTypes,
      key?: unknown,
      newValue?: unknown,
      oldValue?: unknown,
      oldTarget?: Map<unknown, unknown> | Set<unknown>
    ) {
      // 1. 获取 target 对应的依赖
      const depsMap = targetMap.get(target)
      if (!depsMap) {
        // never been tracked
        return
      }
      // 2. 获取要执行的 effect 队列
      const effects = new Set<ReactiveEffect>()
      const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
        if (effectsToAdd) {
          effectsToAdd.forEach(effect => {
            if (effect !== activeEffect || effect.allowRecurse) {
              effects.add(effect)
            }
          })
        }
      }
    
      // 3. collection 执行 clear 时,将所有 key 的依赖都添加到队列
      if (type === TriggerOpTypes.CLEAR) {
        // collection being cleared
        // trigger all effects for target
        depsMap.forEach(add)
      // 4. 当修改数组的 length 时,将数组 length 和 大于数组 length 下标的依赖都添加到队列 
      } else if (key === 'length' && isArray(target)) {
        depsMap.forEach((dep, key) => {
          if (key === 'length' || key >= (newValue as number)) {
            add(dep)
          }
        })
      // 5. 处理常见的 修改,新增,删除 操作
      } else {
        // 5.1 找到 key 对应的依赖添加到队列
        // schedule runs for SET | ADD | DELETE
        if (key !== void 0) {
          add(depsMap.get(key))
        }
    
        // 5.2 找到 ITERATE_KEY 的依赖添加到队列(在 ownKeys 拦截中收集)
        // also run for iteration key on ADD | DELETE | Map.SET
        switch (type) {
          case TriggerOpTypes.ADD:
            if (!isArray(target)) {
              add(depsMap.get(ITERATE_KEY))
              if (isMap(target)) {
                add(depsMap.get(MAP_KEY_ITERATE_KEY))
              }
            } else if (isIntegerKey(key)) {
              // new index added to array -> length changes
              add(depsMap.get('length'))
            }
            break
          case TriggerOpTypes.DELETE:
            if (!isArray(target)) {
              add(depsMap.get(ITERATE_KEY))
              if (isMap(target)) {
                add(depsMap.get(MAP_KEY_ITERATE_KEY))
              }
            }
            break
          case TriggerOpTypes.SET:
            if (isMap(target)) {
              add(depsMap.get(ITERATE_KEY))
            }
            break
        }
      }
    
      const run = (effect: ReactiveEffect) => {
        if (__DEV__ && effect.options.onTrigger) {
          effect.options.onTrigger({
            effect,
            target,
            key,
            type,
            newValue,
            oldValue,
            oldTarget
          })
        }
        if (effect.options.scheduler) {
          effect.options.scheduler(effect)
        } else {
          effect()
        }
      }
    
      // 6. 执行队列中所有依赖
      effects.forEach(run)
    }
    
    

    关于对 Map, Set, WeakMap, WeakSet 的观测

    源码位置: collectionHandlers.ts
    概述

    均只拦截了 get 方法,然后通过代理对象 重写 colection 的 get, set, has, add, set, delete, clear, forEach 来实现依赖收集和触发,原理和对象的拦截类型,有兴趣的同学可以自行了解


    起源地下载网 » 走进Vue3源码:响应式原理(超详细)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元