关于前端:vue30-响应式原理超详细

7次阅读

共计 19114 个字符,预计需要花费 48 分钟才能阅读完成。

一 基于 proxy 的 Observer

1 什么是 proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

proxy 是 es6 新个性,为了对指标的作用次要是通过 handler 对象中的拦挡办法拦挡指标对象 target 的某些行为(如属性查找、赋值、枚举、函数调用等)。

/* target: 指标对象,待要应用 Proxy 包装的指标对象(能够是任何类型的对象,包含原生数组,函数,甚至另一个代理)。*/
/* handler: 一个通常以函数作为属性的对象,各属性中的函数别离定义了在执行各种操作时代理 proxy 的行为。*/ 
const proxy = new Proxy(target, handler);

2 为什么要用 proxy,改用 proxy 之后的利与弊

** 3.0 将带来一个基于 Proxy 的 observer 实现,它能够提供笼罩语言 (JavaScript——译注) 全范畴的响应式能力,打消了以后 Vue 2 系列中基于 Object.defineProperty 所存在的一些局限,这些局限包含:1 对属性的增加、删除动作的监测;2 对数组基于下标的批改、对于 .length 批改的监测;3 对 Map、Set、WeakMap 和 WeakSet 的反对;;

vue2.0 用 Object.defineProperty 作为响应式原理的实现,然而会有它的局限性,比方 无奈监听数组基于下标的批改,不反对 Map、Set、WeakMap 和 WeakSet 等缺点,所以改用了 proxy 解决了这些问题,这也意味着 vue3.0 将放弃对低版本浏览器的兼容(兼容版本 ie11 以上)。

3 proxy 中 hander 对象的根本用法

vue3.0 响应式用到的捕捉器(接下来会重点介绍)

handler.has() -> in 操作符 的捕获器。(vue3.0 用到)
handler.get() -> 属性读取 操作的捕获器。(vue3.0 用到)
handler.set() -> 属性设置 * 操作的捕获器。(vue3.0 用到)
handler.deleteProperty() -> delete 操作符 的捕获器。(vue3.0 用到)
handler.ownKeys() -> Object.getOwnPropertyNames 办法和 Object.getOwnPropertySymbols 办法 的捕获器。(vue3.0 用到)

vue3.0 响应式没用到的捕捉器(有趣味的同学能够钻研一下

handler.getPrototypeOf() -> Object.getPrototypeOf 办法的捕获器。
handler.setPrototypeOf() -> Object.setPrototypeOf 办法的捕获器。
handler.isExtensible() -> Object.isExtensible 办法的捕获器。
handler.preventExtensions() -> Object.preventExtensions 办法的捕获器。
handler.getOwnPropertyDescriptor() -> Object.getOwnPropertyDescriptor 办法的捕获器。
handler.defineProperty() -> Object.defineProperty 办法的捕获器。
handler.apply() -> 函数调用操作 的捕获器。
handler.construct() -> new 操作符 的捕获器。

① has 捕捉器

has(target, propKey)

target: 指标对象

propKey: 待拦挡属性名

作用: 拦挡判断 target 对象是否含有属性 propKey 的操作

拦挡操作:propKey in proxy; 不蕴含 for…in 循环

对应 Reflect: Reflect.has(target, propKey)

???? 例子:

const handler = {has(target, propKey){
        /*
        * 做你的操作
        */
        return propKey in target
    }
}
const proxy = new Proxy(target, handler)

② get 捕捉器

get(target, propKey, receiver)

target: 指标对象

propKey: 待拦挡属性名

receiver: proxy 实例

返回:返回读取的属性

作用:拦挡对象属性的读取

拦挡操作:proxy[propKey]或者点运算符

对应 Reflect:Reflect.get(target, propertyKey[, receiver])

???? 例子:

const handler = {get: function(obj, prop) {return prop in obj ? obj[prop] : '没有此水果';
    }
}

const foot = new Proxy({}, handler)
foot.apple = '苹果'
foot.banana = '香蕉';

console.log(foot.apple, foot.banana);    /* 苹果 香蕉 */
console.log('pig' in foot, foot.pig);    /* false 没有此水果 */

非凡状况

const person = {};
Object.defineProperty(person, 'age', {
  value: 18, 
  writable: false,
  configurable: false
})
const proxPerson = new Proxy(person, {get(target,propKey) {
    return 20
    // 应该 return 18; 不能返回其余值,否则报错
  }
})
console.log(proxPerson.age) /* 会报错 */

③ set 捕捉器

set(target,propKey, value,receiver)

target: 指标对象

propKey: 待拦挡属性名

value: 新设置的属性值

receiver: proxy 实例

返回:严格模式下返回 true 操作胜利;否则失败,报错

作用:拦挡对象的属性赋值操作

拦挡操作:proxy[propkey] = value

对应 Reflect:Reflect.set(obj, prop, value, receiver)

let validator = {set: function(obj, prop, value) {if (prop === 'age') {if (!Number.isInteger(value)) { /* 如果年龄不是整数 */
        throw new TypeError('The age is not an integer')
      }
      if (value > 200) {  /* 超出失常的年龄范畴 */
        throw new RangeError('The age seems invalid')
      }
    }
    obj[prop] = value
    // 示意胜利
    return true
  }
}
let person = new Proxy({}, validator)
person.age = 100
console.log(person.age)  // 100
person.age = 'young'     // 抛出异样: Uncaught TypeError: The age is not an integer
person.age = 300         // 抛出异样: Uncaught RangeError: The age seems invalid

当对象的属性 writable 为 false 时,该属性不能在拦截器中被批改

const person = {};
Object.defineProperty(person, 'age', {
    value: 18,
    writable: false,
    configurable: true,
});

const handler = {set: function(obj, prop, value, receiver) {return Reflect.set(...arguments);
    },
};
const proxy = new Proxy(person, handler);
proxy.age = 20;
console.log(person) // {age: 18} 阐明批改失败

④ deleteProperty 捕捉器

deleteProperty(target, propKey)

target: 指标对象

propKey: 待拦挡属性名

返回:严格模式下只有返回 true, 否则报错

作用:拦挡删除 target 对象的 propKey 属性的操作

拦挡操作:delete proxy[propKey]

对应 Reflect:Reflect.delete(obj, prop)


var foot = {apple: '苹果' , banana:'香蕉'}
var proxy = new Proxy(foot, {deleteProperty(target, prop) {console.log('以后删除水果 :',target[prop])
    return delete target[prop]
  }
});
delete proxy.apple
console.log(foot)

/*
运行后果:'以后删除水果 : 苹果'
{banana:'香蕉'}
*/

非凡状况:属性是不可配置属性时,不能删除

var foot = {apple: '苹果'}
Object.defineProperty(foot, 'banana', {
   value: '香蕉', 
   configurable: false
})
var proxy = new Proxy(foot, {deleteProperty(target, prop) {return delete target[prop];
  }
})
delete proxy.banana /* 没有成果 */
console.log(foot)

⑤ownKeys 捕捉器

ownKeys(target)

target:指标对象

返回:数组(数组元素必须是字符或者 Symbol, 其余类型报错)

作用:拦挡获取键值的操作

拦挡操作:

1 Object.getOwnPropertyNames(proxy)

2 Object.getOwnPropertySymbols(proxy)

3 Object.keys(proxy)

4 for…in… 循环

对应 Reflect:Reflect.ownKeys()

var obj = {a: 10, [Symbol.for('foo')]: 2 };
Object.defineProperty(obj, 'c', {
   value: 3, 
   enumerable: false
})
var p = new Proxy(obj, {ownKeys(target) {return [...Reflect.ownKeys(target), 'b', Symbol.for('bar')]
 }
})
const keys = Object.keys(p)  // ['a']
// 主动过滤掉 Symbol/ 非本身 / 不可遍历的属性

/* 和 Object.keys()过滤性质一样,只返回 target 自身的可遍历属性 */
for(let prop in p) {console.log('prop-',prop) /* prop-a */
}

/* 只返回拦截器返回的非 Symbol 的属性,不论是不是 target 上的属性 */
const ownNames = Object.getOwnPropertyNames(p)  /* ['a', 'c', 'b'] */

/* 只返回拦截器返回的 Symbol 的属性,不论是不是 target 上的属性 */
const ownSymbols = Object.getOwnPropertySymbols(p)// [Symbol(foo), Symbol(bar)]

/* 返回拦截器返回的所有值 */
const ownKeys = Reflect.ownKeys(p)
// ['a','c',Symbol(foo),'b',Symbol(bar)]

二 vue3.0 如何建设响应式

vue3.0 建设响应式的办法有两种:
第一个就是使用 composition-api 中的 reactive 间接构建响应式,composition-api 的呈现咱们能够在.vue 文件中,间接用 setup()函数来解决之前的大部分逻辑,也就是说咱们没有必要在 export default{} 中在申明生命周期,data(){} 函数,watch{} , computed{} 等,取而代之的是咱们在 setup 函数中,用 vue3.0 reactive watch 生命周期 api 来达到同样的成果,这样就像 react-hooks 一样晋升代码的复用率,逻辑性更强。

第二个就是用传统的 data(){ return{} } 模式 ,vue3.0 没有放弃对 vue2.0 写法的反对,而是对 vue2.0 的写法是齐全兼容的,提供了applyOptions 来解决 options 模式的 vue 组件。然而 options 外面的 data , watch , computed 等解决逻辑,还是用了 composition-api 中的 API 对应解决。

1 composition-api reactive

Reactive 相当于以后的 Vue.observable () API,通过 reactive 解决后的函数能变成响应式的数据,相似于 option api 外面的 vue 解决 data 函数的返回值。

咱们用一个 todoList 的 demo 试着尝尝鲜。


const {reactive , onMounted} = Vue
setup(){
    const state = reactive({
        count:0,
        todoList:[]})
    /* 生命周期 mounted */
    onMounted(() => {console.log('mounted')
    })
    /* 减少 count 数量 */
    function add(){state.count++} 
    /* 缩小 count 数量 */
    function del(){state.count--}
    /* 增加代办事项 */
    function addTodo(id,title,content){
        state.todoList.push({
            id,
            title,
            content,
            done:false
        })
    }
    /* 实现代办事项 */
    function complete(id){for(let i = 0; i< state.todoList.length; i++){const currentTodo = state.todoList[i] 
            if(id === currentTodo.id){state.todoList[i] = {
                    ...currentTodo,
                    done:true
                } 
                break
            }
        }
    }
    return {
        state,
        add,
        del,
        addTodo,
        complete
    }
}

2 options data

options 模式的和 vue2.0 并没有什么区别

export default {data(){
        return{
            count:0,
            todoList:[]}
    },
    mounted(){console.log('mounted')
    }
    methods:{add(){this.count++},
        del(){this.count--},
        addTodo(id,title,content){
           this.todoList.push({
               id,
               title,
               content,
               done:false
           })
        },
        complete(id){for(let i = 0; i< this.todoList.length; i++){const currentTodo = this.todoList[i] 
                if(id === currentTodo.id){this.todoList[i] = {
                        ...currentTodo,
                        done:true
                    } 
                    break
                }
            }
        }
    }
}

三 响应式原理初探

不同类型的 Reactive

vue3.0 能够依据业务需要引进不同的 API 办法。这里须要

① reactive

建设响应式 reactive,返回 proxy 对象,这个 reactive 能够深层次递归,也就是如果发现开展的属性值是 援用类型 的而且被 援用 ,还会用 reactive 递归解决。而且属性是能够被批改的。

② shallowReactive

建设响应式 shallowReactive,返回 proxy 对象。和 reactive 的区别是只建设一层的响应式,也就是说如果发现开展属性是 援用类型 也不会 递归

③ readonly

返回的 proxy 解决的对象,能够开展递归解决,然而属性是只读的,不能批改。能够做 props 传递给子组件应用。

④ shallowReadonly

返回通过解决的 proxy 对象,然而建设响应式属性是只读的,不开展援用也不递归转换,能够这用于为有状态组件创立 props 代理对象。

贮存对象与 proxy

上文中咱们提及到。用 Reactive 解决过并返回的对象是一个 proxy 对象,假如存在很多组件,或者在一个组件中被屡次 reactive,就会有很多对 proxy 对象和它代理的原对象。为了能把 proxy 对象和原对象建设关系,vue3.0 采纳了 WeakMap 去贮存这些对象关系。WeakMaps 放弃了对键名所援用的对象的弱援用,即垃圾回收机制不将该援用思考在内。只有所援用的对象的其余援用都被革除,垃圾回收机制就会开释该对象所占用的内存。也就是说,一旦不再须要,WeakMap 外面的键名对象和所对应的键值对会主动隐没,不必手动删除援用。

const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>() /* 只读的 */
const readonlyToRaw = new WeakMap<any, any>() /* 只读的 */

vue3.0 用 readonly 来设置被拦截器拦挡的对象是否被批改,能够满足之前的 props 不能被批改的单向数据流场景。
咱们接下来重点讲一下接下来的四个 weakMap 的贮存关系。

rawToReactive

键值对:{[targetObject] : obseved }

target(键): 指标对象值 (这里能够了解为reactive 的第一个参数。)
obsered(值): 通过 proxy 代理之后的 proxy 对象。

reactiveToRaw
reactiveToRaw 贮存的刚好与 rawToReactive 的键值对是相同的。
键值对 {[obseved] : targetObject }

rawToReadonly

键值对:{[target] : obseved }

target(键):指标对象。
obsered(值): 通过 proxy 代理之后的只读属性的 proxy 对象。

readonlyToRaw
贮存状态与 rawToReadonly 刚好相同。

reactive 入口解析

接下来咱们重点从 reactive 开始讲。

reactive({…object}) 入口

/* TODO: */
export function reactive(target: object) {if (readonlyToRaw.has(target)) {return target}
  return createReactiveObject(
    target,                   /* 指标对象 */
    rawToReactive,            /* {[targetObject] : obseved  }   */
    reactiveToRaw,            /* {[obseved] : targetObject }  */
    mutableHandlers,          /* 解决 根本数据类型 和 援用数据类型 */
    mutableCollectionHandlers /* 用于解决 Set, Map, WeakMap, WeakSet 类型 */
  )
}

reactive函数的作用就是通过 createReactiveObject 办法产生一个 proxy, 而且针对不同的数据类型给定了不同的解决办法。

createReactiveObject

之前说到的 createReactiveObject,咱们接下来看看 createReactiveObject 产生了什么。

const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  /* 判断指标对象是否被 effect */
  /* observed 为通过 new Proxy 代理的函数 */
  let observed = toProxy.get(target) /* {[target] : obseved  } */
  if (observed !== void 0) { /* 如果指标对象曾经被响应式解决,那么间接返回 proxy 的 observed 对象 */
    return observed
  }
  if (toRaw.has(target)) {/* { [observed] : target  } */
    return target
  }
  /* 如果指标对象是 Set, Map, WeakMap, WeakSet 类型,那么 hander 函数是 collectionHandlers 否侧指标函数是 baseHandlers */
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
   /* TODO: 创立响应式对象  */
  observed = new Proxy(target, handlers)
  /* target 和 observed 建设关联 */
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  /* 返回 observed 对象 */
  return observed
}

通过下面源码创立 proxy 对象的大抵流程是这样的:
①首先判断指标对象有没有被 proxy 响应式代理过,如果是那么间接返回对象。
②而后通过判断指标对象是否是 [Set, Map, WeakMap, WeakSet] 数据类型来抉择是用 collectionHandlers,还是baseHandlers-> 就是 reactive 传进来的 mutableHandlers 作为 proxy 的 hander 对象。
③最初通过真正应用 new proxy 来创立一个 observed,而后通过 rawToReactive reactiveToRaw 保留 target 和 observed 键值对。

大抵流程图:

四 拦截器对象 baseHandlers -> mutableHandlers

之前咱们介绍过 baseHandlers 就是调用 reactive 办法 createReactiveObject 传进来的 mutableHandlers 对象。
咱们先来看一下 mutableHandlers 对象

mutableHandlers

拦截器的作用域

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

vue3.0 用到了以上几个拦截器,咱们在上节曾经介绍了这几个拦截器的根本用法, 首先咱们对几个根本用到的拦截器在做一下回顾。

①get, 对数据的读取属性进行拦挡,包含 target. 点语法 和 target[]

②set,对数据的存入属性进行拦挡。

③deleteProperty delete 操作符进行拦挡。

vue2.0不能对对象的 delete 操作符 进行属性拦挡。

例子????:

delete object.a

是无奈监测到的。

vue3.0proxy 中deleteProperty 能够拦挡 delete 操作符,这就表述 vue3.0 响应式能够监听到属性的删除操作。

④has,对 in 操作符进行属性拦挡。

vue2.0不能对对象的 in 操作符 进行属性拦挡。

例子

a in object

has 是为了解决如上问题。这就示意了 vue3.0 能够对 in 操作符 进行拦挡。

⑤ownKeys Object.keys(proxy) ,for…in… 循环 Object.getOwnPropertySymbols(proxy)Object.getOwnPropertyNames(proxy) 拦截器

例子

Object.keys(object)

阐明 vue3.0 能够对以上这些办法进行拦挡。

五 组件初始化阶段

如果咱们想要弄明确整个响应式原理。那么组件初始化,到初始化过程中 composition-api 的 reactive 解决 data,以及编译阶段对 data 属性进行依赖收集是分不开的。vue3.0 提供了一套从初始化,到 render 过程中依赖收集,到组件更新, 到组件销毁残缺响应式体系,咱们很难从一个角度把货色讲明确,所以在正式讲拦截器对象如何收集依赖,派发更新之前,咱们看看 effect 做了些什么操作。

1 effect -> 新的渲染 watcher

vue3.0 用 effect 副作用钩子来代替 vue2.0watcher。咱们都晓得在 vue2.0 中,有渲染 watcher 专门负责数据变动后的从新渲染视图。vue3.0 改用 effect 来代替 watcher 达到同样的成果。

咱们先简略介绍一下 mountComponent 流程,前面的文章会具体介绍 mount 阶段的

1 mountComponent 初始化 mountComponent

  // 初始化组件
  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    /* 第一步: 创立 component 实例   */
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    /* 第二步:TODO: 初始化 初始化组件, 建设 proxy , 依据字符窜模版失去 */
    setupComponent(instance)
    /* 第三步:建设一个渲染 effect,执行 effect */
    setupRenderEffect(
      instance,     // 组件实例
      initialVNode, //vnode  
      container,    // 容器元素
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )   
  }

下面是整个 mountComponent 的次要分为了三步,咱们这里别离介绍一下每个步骤干了什么:
① 第一步: 创立 component 实例。
② 第二步:初始化组件, 建设 proxy , 依据字符窜模版失去 render 函数。生命周期钩子函数解决等等
③ 第三步:建设一个渲染 effect,执行 effect。

从如上办法中咱们能够看到,在 setupComponent 曾经构建了响应式对象,然而还没有 初始化收集依赖

2 setupRenderEffect 构建渲染 effect

 const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    /* 创立一个渲染 effect */
    instance.update = effect(function componentEffect() {//... 省去的内容前面会讲到},{scheduler: queueJob})
  }

为了让大家更分明的明确响应式原理,我这只保留了和响应式原理有关系的局部代码。

setupRenderEffect 的作用

① 创立一个 effect,并把它赋值给组件实例的 update 办法,作为渲染更新视图用。
② componentEffect 作为回调函数模式传递给 effect 作为第一个参数

3 effect 做了些什么

export function effect<T = any>(fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {const effect = createReactiveEffect(fn, options)
  /* 如果不是懒加载 立刻执行 effect 函数 */
  if (!options.lazy) {effect()
  }
  return effect
}

effect 作用如下

① 首先调用。createReactiveEffect
② 如果不是懒加载 立刻执行 由 createReactiveEffect 创立进去的 ReactiveEffect 函数

4 ReactiveEffect

function createReactiveEffect<T = any>(fn: (...args: any[]) => T, /** 回调函数 */
  options: ReactiveEffectOptions
): ReactiveEffect<T> {const effect = function reactiveEffect(...args: unknown[]): unknown {
    try {enableTracking()
        effectStack.push(effect) // 往 effect 数组中里放入以后 effect
        activeEffect = effect //TODO: effect 赋值给以后的 activeEffect
        return fn(...args) //TODO:    fn 为 effect 传进来 componentEffect
      } finally {effectStack.pop() // 实现依赖收集后从 effect 数组删掉这个 effect
        resetTracking() 
        /* 将 activeEffect 还原到之前的 effect */
        activeEffect = effectStack[effectStack.length - 1]
    }
  } as ReactiveEffect
  /* 配置一下初始化参数 */
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = [] /* TODO: 用于收集相干依赖 */
  effect.options = options
  return effect
}

createReactiveEffect

createReactiveEffect的作用次要是配置了一些初始化的参数,而后包装了之前传进来的 fn,重要的一点是把以后的 effect 赋值给了 activeEffect, 这一点十分重要,和收集依赖有着间接的关系

在这里留下了一个疑点,

①为什么要用 effectStack 数组来寄存这里 effect

总结

咱们这里个响应式初始化阶段进行总结

① setupComponent 创立组件,调用 composition-api, 解决 options(构建响应式)失去 Observer 对象。

② 创立一个渲染 effect,外面包装了真正的渲染办法 componentEffect,增加一些 effect 初始化属性。

③ 而后立刻执行 effect,而后将以后渲染 effect 赋值给 activeEffect

最初咱们用一张图来解释一下整个流程。

六 依赖收集,get 做了些什么?

1 回归 mutableHandlers 中的 get 办法

1 不同类型的 get

/* 深度 get */
const get = /*#__PURE__*/ createGetter()
/* 浅 get */
const shallowGet = /*#__PURE__*/ createGetter(false, true)
/* 只读的 get */
const readonlyGet = /*#__PURE__*/ createGetter(true)
/* 只读的浅 get */
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

下面咱们能够晓得,对于之前讲的四种不同的建设响应式办法,对应了四种不同的 get, 上面是一一对应关系。

reactive ———> get

shallowReactive ——–> shallowGet

readonly ———-> readonlyGet

shallowReadonly —————> shallowReadonlyGet

四种办法都是调用了 createGetter 办法,只不过是参数的配置不同,咱们这里那第一个 get 办法做参考,接下来摸索一下 createGetter。

createGetter

function createGetter(isReadonly = false, shallow = false) {return function get(target: object, key: string | symbol, receiver: object) {const res = Reflect.get(target, key, receiver)
    /* 浅逻辑 */
    if (shallow) {!isReadonly && track(target, TrackOpTypes.GET, key)
      return res
    }
    /* 数据绑定 */
    !isReadonly && track(target, TrackOpTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ?
          /* 只读属性 */
          readonly(res)
          /*  */
        : reactive(res)
      : res
  }
}

这就是 createGetter 次要流程,非凡的数据类型 ref咱们临时先不思考。
这里用了一些流程判断,咱们用流程图来阐明一下这个函数次要做了什么?

咱们能够得出结论:
在 vue2.0 的时候。响应式是在初始化的时候就深层次递归解决了
然而

与 vue2.0 不同的是, 即使是深度响应式咱们也只能在获取上一级 get 之后能力触发下一级的深度响应式。
比方

setup(){const state = reactive({ a:{ b:{} } })
 return {state}
}

在初始化的时候,只有 a 的一层级建设了响应式,b 并没有建设响应式,而当咱们用 state.a 的时候,才会真正的将 b 也做响应式解决,也就是说咱们拜访了上一级属性后,下一代属性才会真正意义上建设响应式

这样做益处是,
1 初始化的时候不必递归去解决对象,造成了不必要的性能开销。
*2 有一些没有用上的 state,这里就不须要在深层次响应式解决。

2 track-> 依赖收集器

咱们先来看看 track 源码:

track 做了些什么


/* target 对象自身,key 属性值  type 为 'GET' */
export function track(target: object, type: TrackOpTypes, key: unknown) {/* 当打印或者获取属性的时候 console.log(this.a) 是没有 activeEffect 的 以后返回值为 0  */
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    /*  target -map-> depsMap  */
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    /* key : dep dep 观察者 */
    depsMap.set(key, (dep = new Set()))
  }
   /* 以后 activeEffect */
  if (!dep.has(activeEffect)) {
    /* dep 增加 activeEffect */
    dep.add(activeEffect)
    /* 每个 activeEffect 的 deps 寄存以后的 dep */
    activeEffect.deps.push(dep)
  }
}

外面次要引入了两个概念 targetMapdepsMap

targetMap
键值对 proxy : depsMap
proxy:为 reactive 代理后的 Observer 对象。
depsMap:为寄存依赖 dep 的 map 映射。

depsMap
键值对:key : deps
key 为以后 get 拜访的属性名,
deps 寄存 effect 的 set 数据类型。

咱们晓得 track 作用大抵是,首先依据 proxy 对象,获取寄存 deps 的 depsMap,而后通过拜访的属性名 key 获取对应的 dep, 而后将以后激活的 effect 存入以后 dep 收集依赖。

次要作用
①找到与以后 proxy 和 key 对应的 dep。
②dep 与以后 activeEffect 建立联系,收集依赖。

为了不便了解,targetMapdepsMap的关系,上面咱们用一个例子来阐明:
例子:
父组件 A


<div id="app" >
  <span>{{state.a}}</span>
  <span>{{state.b}}</span>
<div>
<script>
const {createApp, reactive} = Vue

/* 子组件 */
const Children ={template="<div> <span>{{ state.c}}</span> </div>",
    setup(){
       const state = reactive({c:1})
       return {state}
    }
}
/* 父组件 */
createApp({
   component:{Children} 
   setup(){
       const state = reactive({
           a:1,
           b:2
       })
       return {state}
   }
})mount('#app')

</script>

咱们用一幅图示意如上关系:

渲染 effect 函数如何触发 get

咱们在后面说过,创立一个渲染 renderEffect,而后把赋值给 activeEffect,最初执行 renderEffect,在这个期间是怎么做依赖收集的呢,让咱们一起来看看,update 函数中做了什么,咱们回到之前讲的 componentEffect 逻辑上来

function componentEffect() {if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const {el, props} = initialVNode
        const {bm, m, a, parent} = instance
        /* TODO: 触发 instance.render 函数,造成树结构 */
        const subTree = (instance.subTree = renderComponentRoot(instance))
        if (bm) {
          // 触发 beforeMount 申明周期钩子
          invokeArrayFns(bm)
        }
        patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
        )
        /* 触发申明周期 mounted 钩子 */
        if (m) {queuePostRenderEffect(m, parentSuspense)
        }
        instance.isMounted = true
      } else {
        // 更新组件逻辑
        // ......
      }
}

这边代码大抵首先会通过 renderComponentRoot 办法造成树结构,这里要留神的是,咱们在最后 mountComponent 的 setupComponent 办法中,曾经通过编译办法 compile 编译了 template 模版的内容,state.a state.b 等形象语法树,最终返回的 render 函数在这个阶段会被触发,在 render 函数中在模版中的表达式 state.a state.b 点语法会被替换成 data 中实在的属性,这时候就进行了真正的依赖收集,触发了 get 办法。接下来就是触发生命周期 beforeMount , 而后对整个树结构从新 patch,patch 结束后,调用 mounted 钩子

依赖收集流程总结

① 首先执行 renderEffect,赋值给 activeEffect,调用 renderComponentRoot 办法,而后触发 render 函数。

② 依据 render 函数,解析通过 compile,语法树解决过后的模版表达式,拜访实在的 data 属性,触发 get。

③ get 办法首先通过之前不同的 reactive,通过 track 办法进行依赖收集。

④ track 办法通过以后 proxy 对象 target, 和拜访的属性名 key 来找到对应的 dep。

⑤ 将 dep 与以后的 activeEffect 建设起分割。将 activeEffect 压入 dep 数组中,(此时的 dep 中曾经含有以后组件的渲染 effect, 这就是响应式的根本原因)如果咱们触发 set,就能在数组中找到对应的 effect,顺次执行。

最初咱们用一个流程图来表白一下依赖收集的流程。

七 set 派发更新

接下来咱们 set 局部逻辑。


const set = /*#__PURE__*/ createSetter()
/* 浅逻辑 */
const shallowSet = /*#__PURE__*/ createSetter(true)

set 也是分两个逻辑,set 和 shallowSet, 两种办法都是由 createSetter 产生,咱们这里次要以 set 进行分析。

createSetter 创立 set

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {const oldValue = (target as any)[key]
    /* shallowSet 逻辑 */

    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    /* 判断以后对象,和存在 reactiveToRaw 外面是否相等 */
    if (target === toRaw(receiver)) {if (!hadKey) { /* 新建属性 */
        /*  TriggerOpTypes.ADD -> add */
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        /* 扭转原有属性 */
        /*  TriggerOpTypes.SET -> set */
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

createSetter 的流程大抵是这样的

① 首先通过 toRaw 判断以后的 proxy 对象和建设响应式存入 reactiveToRaw 的 proxy 对象是否相等。
② 判断 target 有没有以后 key, 如果存在的话,扭转属性,执行 trigger(target, TriggerOpTypes.SET, key, value, oldValue)。
③ 如果以后 key 不存在,阐明是赋值新属性,执行 trigger(target, TriggerOpTypes.ADD, key, value)。

trigger

/* 依据 value 值的扭转,从 effect 和 computer 拿出对应的 callback,而后顺次执行 */
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  /* 获取 depssMap */
  const depsMap = targetMap.get(target)
  /* 没有通过依赖收集的,间接返回 */
  if (!depsMap) {return}
  const effects = new Set<ReactiveEffect>()        /* effect 钩子队列 */
  const computedRunners = new Set<ReactiveEffect>() /* 计算属性队列 */
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {if (effectsToAdd) {
      effectsToAdd.forEach(effect => {if (effect !== activeEffect || !shouldTrack) {if (effect.options.computed) { /* 解决 computed 逻辑 */
            computedRunners.add(effect)  /* 贮存对应的 dep */
          } else {effects.add(effect)  /* 贮存对应的 dep */
          }
        }
      })
    }
  }

  add(depsMap.get(key))

  const run = (effect: ReactiveEffect) => {if (effect.options.scheduler) { /* 放进 scheduler 调度 */
      effect.options.scheduler(effect)
    } else {effect() /* 不存在调度状况,间接执行 effect */
    }
  }

  //TODO: 必须首先运行计算属性的更新,以便计算的 getter
  // 在任何依赖于它们的失常更新 effect 运行之前,都可能生效。computedRunners.forEach(run) /* 顺次执行 computedRunners 回调 */
  effects.forEach(run) /* 顺次执行 effect 回调(TODO: 外面包含渲染 effect)*/
}

咱们这里保留了 trigger 的外围逻辑

① 首先从 targetMap 中,依据以后 proxy 找到与之对应的 depsMap。
② 依据 key 找到 depsMap 中对应的 deps,而后通过 add 办法拆散出对应的 effect 回调函数和 computed 回调函数。
③ 顺次执行 computedRunners 和 effects 队列外面的回调函数,如果发现须要调度解决, 放进 scheduler 事件调度

值得注意的的是:

此时的 effect 队列中有咱们上述负责渲染的 renderEffect,还有通过 effectAPI 建设的 effect,以及通过 watch 造成的 effect。咱们这里只思考到渲染 effect。至于前面的状况会在接下来的文章中和大家一起分享。

咱们用一幅流程图阐明一下 set 过程。

八 总结

咱们总结一下整个数据绑定建设响应式大抵分为三个阶段

1 初始化阶段:初始化阶段通过组件初始化办法造成对应的 proxy 对象,而后造成一个负责渲染的 effect。

2 get 依赖收集阶段:通过解析 template,替换实在 data 属性,来触发 get, 而后通过 stack 办法,通过 proxy 对象和 key 造成对应的 deps,将负责渲染的 effect 存入 deps。(这个过程还有其余的 effect,比方 watchEffect 存入 deps 中)。

3 set 派发更新阶段:当咱们 this[key] = value 扭转属性的时候,首先通过 trigger 办法,通过 proxy 对象和 key 找到对应的 deps,而后给 deps 分类分成 computedRunners 和 effect, 而后顺次执行,如果须要 调度 的,间接放入调度。

微信扫码关注公众号,定期分享技术文章

正文完
 0