一 基于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 = 100console.log(person.age)  // 100person.age = 'young'     // 抛出异样: Uncaught TypeError: The age is not an integerperson.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.appleconsole.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 } = Vuesetup(){    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,而后顺次执行,如果须要调度的,间接放入调度。

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