好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

ES6实战2-实现 Vue3 effect 源码

ES6实战2-实现 Vue3 effect 源码

1. 前言

上一节我们实现了 Vue3 的数据劫持 功能 ,并对一些边界值做了处理。但是,当数据改变了我们希望更新试图,这个时候虽然我们能劫持到数据的变化但是没有做任何处理,我们需要对数据的 获取 和 修改 增加 更新的逻辑,并提供 一个 API 给业务用来响应式的处理数据的变化。Vue3 中提供了 effect,当 effect 回调 函数 中引用的响应式数据变化时,会触发 effect 回调 函数 的执行,相当于 vue2 中的 watcher。我们来看下面的应用示例:

  // /vue-next/public/index.html 
 < script src =  "./vue.reactivity.js"  >  <  / script > 
 < script > 
   const   {  reactive ,  effect  }   =  VueReactivity ; 
   const  proxy  =   reactive  (  { 
    name :   'ES6 Wiki'  , 
   }  ) 

   effect  (  (  )   =>   { 
    document .  getElementById  (  'app'  )  . innerHTML  =  proxy . name ; 
   }  ) 

   setTimeout  (  (  )   =>   { 
    proxy . name  =   'imooc ES6 Wiki 实战' 
   }  ,   ) 
 <  / script > 
 

上面的 代码 中我们引入了 Vue3 的 reactivity 库,初始化网页 内容 后,在 1 秒以后更新网页中的 内容 。本节我们就来实现 effect 这个 API 的 功能 ,本节源码参考: ES6 Wiki 。

2. effect 实现

2.1 创建响应式 effect

effect 在 Vue3 的响应式系统中是 一个 非常关键的 函数 ,后面的 ref、co mpu ted 等 函数 都会用到 effect 中的 功能 。在 Vue3 中的 effect 会接受不了两个参数:

  effect  ( fn ,  options ) 
 

基于 Vue3 响应式 API 的 effect 特点,需要将 effect 变成 一个 响应式 函数 ,effect 的响应式就是当数据变化时 fn 会 自动 执行。实现 effect 这个 函数 的 一个 目标就是,将 effect 回调 函数 中所有引用了响应式数据的 属性 收集起来,并和 effect 的回调 函数 关联上,在数据变化时在执行 effect 的回调 函数 。也就是上面的测试案例中,proxy 对象的 name 属性 在 effect 的回调 函数 中。要想让 effect 成为响应式的,就需要将 name 和 effect 关联起来,当 name 的值变化了,就执行 effect 的回调 函数 。

在本节 options 没用到,但是在 co mpu ted 中会使用到,本节使用了 options.lazy 属性 ,用于判断是否在第一次的时候执行回调 函数 中的 内容 。effect 中是 默 认执行回调 函数 的。

如果要把 effect 变成响应式,需要定义 一个 创建响应式的 方法 (createReactiveEffect)用于创建 一个 effect 函数 。createReactiveEffect 执行后会返回 一个 effect 函数 ,在 createReactiveEffect 函数 中会 默 认执行 fn。

  export   function   effect  ( fn ,  options )  { 
   const  effect  =   createReactiveEffect  ( fn ,  options ) 
   if   (  ! options . lazy )   { 
     effect  (  ) 
   } 
   return  effect
 } 

 function   createReactiveEffect  ( fn ,  options )   { 
   const   effect   =   function   reactiveEffect  (  )   { 
     return   fn  (  )  ; 	 //  用户 创建的回调 函数 ,fn 函数 内部会对响应式数据进行取值操作 
   } 
   return  effect
 } 
 

我们定义 一个 @R_ 419 _5683@ activeEffect,这样做是为了把 effect 存起来,方便后面 调用 ,在取值的时候就可以拿到这个 activeEffect。

  let  activeEffect ; 
 function   createReactiveEffect  ( fn ,  options )   { 
   const   effect   =   function   reactiveEffect  (  )   { 
    activeEffect  =  effect ; 
     return   fn  (  )  ; 
   } 
   return  effect
 } 		
 

2.2 属性 和 effect 关联

怎么才能让 属性 和这个 函数 进行关联呢?首先我们要创建 一个 收集 函数 (track)用于收集 属性 key 和 effect 回调 函数 的关联,并且只有在 effect 中使用到的 key,更新时才会执行 effect 中的回调,所以我们在收集依赖时需要先判断。

  function   track  ( target ,  key )   { 
   if   ( activeEffect  ===  viod  )   { 
     return  ; 
   } 
 } 
 

什么时候进行收集呢?effect 回调 函数 会 默 认执行,在 获取 值的时候对响应式对象上的 key 进行依赖收集,也就是在 createGetter 函数 中进行收集。

  function   createGetter  (  )   { 
   return   function   get  ( target ,  key ,  receiver )   { 
     const  res  =  Reflect .  get  ( target ,  key ,  receiver )  ; 
     if   (  isSymbol  ( key )  )   { 
       return  res ; 
     } 

     // 依赖收集 
     track  ( target ,  key )  ; 
    
     if   (  isObject  ( res )  )   { 
       return   reactive  ( res )  ; 
     } 
     return  res ; 
   }  ; 
 } 
 

如何关联呢?就是需要在 target 上的 key 中存放若干个 effect,那这要怎么存放呢?这时我们想到了 WeakMap,创建 一个 WeakMap 来保持 target 上的需要关联 effect 的 属性 。同时,

下面的伪 代码 数据结构是我们希望存放在 WeakMap 中的映射,其中 target 是目标对象。

  { 
  target1 :   { 
    key :   [ effect ,  effect ] 
   }  , 
  target2 :   { 
    key :   [ effect ,  effect ] 
   } 
 } 
 

在存放 effect 时可能还需要给 effect 加上 一些标识,如:id、deps、options 等,后面会用到。

 Let uid  =   ; 
 function   createReactiveEffect  ( fn ,  options )   { 
   const   effect   =   function   reactiveEffect  (  )   { 
    activeEffect  =  effect ; 
     return   fn  (  )  ; 
   } 
  effect . id  =  uid ++  ; 
  effect . deps  =   [  ]  ; 
  effect . options  =  opntions ; 
   return  effect
 } 	

 const  targetMap  =   new   WeakMap  (  )  ; 
 function   track  ( target ,  key )   { 
   if   ( activeEffect  ===  undefined )   { 
     return  ; 
   } 
   // 目标是创建 一个 映射:{target1: {name: [effect, effect]},target2: {name: [effect, effect]}} 
   let  depsMap  =  targetMap .  get  ( target )  ; 	 // depsMap存放target的值,是 一个 Map对象 
   if  (  ! depsMap )   { 	 // 如果targetMap中没用target对象,则创建 一个 。 
    targetMap .  set  ( target ,   ( depsMap  =   new   Map  (  )  )  )  ; 
   } 
   let  dep  =  depsMap .  get  ( key )  ; 	 //  获取 depsMap对象中 属性 是target上的key值 
   if  (  ! dep )   { 
    depsMap .  set  ( key ,   ( dep  =   new   Set  (  )  )  )  ;   // 存放effect的集合 
   } 
   if  (  ! dep .  has  ( effect )  )   { 
    dep .  add  ( activeEffect )  ; 
    activeEffect . deps .  push  ( dep )  ; 
   } 
 } 
 

上面的 代码 中,收集目标对象上所有的依赖,在 effect 的回调 函数 中没有使用到的 属性 ,就不需要进行依赖收集。在执行完创建响应式 effec 函数 createReactiveEffect 后需要把 activeEffect 置为 null。

  function   createReactiveEffect  ( fn ,  options )   { 
   const   effect   =   function   reactiveEffect  (  )   { 
     try   { 
      activeEffect  =  effect ; 
    	 return   fn  (  )  ; 
     }   finally   { 
      activeEffect  =   null  ; 
     } 
   } 
   return  effect
 } 	
 

上面的 代码 中 finally 是一定会执行的。在 effect 回调 函数 中嵌套使用 effect,并且在嵌套的 effect 后还有响应式数据,如果是下面这种写法, state.c = 300 将不会收集。

  effect  (  (  )   =>   { 
  state . a  =   ; 
   effect  (  (  )   =>   { 
    state . b  =   ; 
   }  ) 
  state . c  =   ; 
 }  ) 
 

这个时候我们就需要创建 一个 存放栈的数组(effectStack)来存放 activeEffect,执行完毕后也不用赋值 null 了,通过出栈的形式把最后 一个 移除,让当前的 activeEffect 值等于 effectStack 最后 一个 值 effectStack[effectStack.length-1] 。这样我们在执行完创建响应式 effect 函数 时,控制权又会交到上一层的 activeEffect 上,这样上面 代码 中的 state.c=300 就会被收集到第一层的 effect 中去。具体执行 代码 如下:

  const  effectStack  =   [  ]  ; 
 function   createReactiveEffect  ( fn ,  options )   { 
   const   effect   =   function   reactiveEffect  (  )   { 
     try   { 
      activeEffect  =  effect ; 
      effectStack .  push  ( activeEffect )  ; 
    	 return   fn  (  )  ; 
     }   finally   { 
      effectStack .  pop  (  )  ; 
      activeEffect  =  effectStack [ effectStack . length  -   ]  ; 
     } 
   } 
   return  effect
 } 
 

使用栈的还有 一个 好处可以防止递归执行,在 effect 如果有数据持续变化是如: state.a++ 这样的逻辑就会形成递归。这时需要处理为只执行一次, 增加 一个 条件判断 ,如下 代码 :

  function   createReactiveEffect  ( fn ,  options )   { 
   const   effect   =   function   reactiveEffect  (  )   { 
     if   (  ! effectStack .  includes  ( effect )  )   { 	 // 防止死循环 
       try   { 
        activeEffect  =  effect ; 
        effectStack .  push  ( activeEffect )  ; 
         return   fn  (  )  ; 
       }   finally   { 
        effectStack .  pop  (  )  ; 
        activeEffect  =  effectStack [ effectStack . length  -   ]  ; 
       } 
     } 
   } 
   return  effect
 } 
 

2.3 执行收集的 函数

上面的 内容 是依赖收集的过程,主要在响应式数据 获取 时执行,也就是在 调用 createGetter 的时候执行,那么依赖收集完后,当数据发生变化的时候,需要让收集的回调 函数 依次执行。而执行这样收集 函数 的过程是在 createSetter 中完成,因为 在这里 是更新数据的过程。上节中我们在 createSetter 中预留了新增和更新 属性 的判断:

  function   createSetter  (  )   { 
   return   function   get  ( target ,  key ,  value ,  receiver )   { 
		 ... 
     if   (  ! hadKey )   { 
      console .  log  (  '新增 属性 '  )  ; 
       trigger  ( target ,   'ADD'  ,  key ,  value ) 
     }   else   if   (  hasChanged  ( value ,  oldValue )  )   { 
      console .  log  (  '更新 属性 '  )  ; 
       trigger  ( target ,   'SET'  ,  key ,  value ,  oldValue ) 
     } 

     return  result ; 
   }  ; 
 } 
 

Vue3 中执行依赖的 函数 是 trigger,这个 函数 一共接受五个参数,在执行 trigger 时会传入 修改 数据的类型:新增(ADD)和更新(SET),这是 Vue 为了处理不同场景而设置的 属性 。这里我们先创建 tigger 函数 ,首先需要判断在 targetMap 中是否有被依赖的对象,没有则直接返回。

  export   function   trigger  ( target ,  type ,  key ,  newValue ,  oldValue )   { 
   const  depsMap  =  targetMap .  get  ( target ) 
   if   (  ! depsMap )   { 
     return 
   } 
 } 
 

如何让依赖的 effect 执行呢?

首先要判断 key 是不是 undefined; 获取 key 中的 effect 函数 ,并执行。

  export   function   trigger  ( target ,  type ,  key ,  newValue ,  oldValue )   { 
   const  depsMap  =  targetMap .  get  ( target ) 
   if   (  ! depsMap )   { 
     return 
   } 
   const   run   =   ( effects )   =>   { 
     if   ( effects )   { 
      effects .  forEarch  ( effect  =>   effect  (  )  ) 
     } 
   } 
   if   ( key  ==   void   )   { 
     run  ( depsMap .  get  ( key )  )  ; 
   } 
 } 
 

上面是对对象的处理,但是在处理数组的时候还会有问题,如下 代码 :

  const  state  =   reactive  (  [  ,  ,  ]  )  ; 
 effect  (  (  )   =>   { 
  document .  getElementById  (  'app'  )  . innerHTML  =  state [  ]  ; 
 }  ) 

 setTimeout  (  (  )   =>   { 
  state . length  =   ; 
 }  ,   ) 
 

上面的 代码 中,数据变化是直接更新数组的长度,而在 effect 中没有使用 length 属性 ,所以在更新 length 属性 时不会触发 run(depsMap.get(key)); 的依次执行,这样 length 改变 effect 回调 函数 不会执行,视图也不会被更新。这时就需要对 属性 是 length 的数组进行验证,如果直接更新的是数组的长度就需要单独处理:

  export   function   trigger  ( target ,  type ,  key ,  newValue ,  oldValue )   { 
   const  depsMap  =  targetMap .  get  ( target ) 
   if   (  ! depsMap )   { 
     return 
   } 
   const   run   =   ( effects )   =>   { 
     if   ( effects )   { 
      effects .  forEarch  ( effect  =>   effect  (  )  ) 
     } 
   } 
   if   ( key  ===   'length'   &&   isArray  ( target )  )   { 
    depsMap .  forEarch  (  ( deps ,  key )   =>   { 
       if  ( key  ===   'length'   ||  key  >=  newValue )   { 	 // newValue是更新后的值, 
         run  ( deps ) 
       } 
     }  ) 
   }   else   { 
     if   ( key  ==   void   )   { 
       run  ( depsMap .  get  ( key )  )  ; 
     } 
   } 
 } 
 

上面的 代码 是在 修改 数组 length 属性 时,让收集依赖的 函数 执行。还有一种情况,是在 effect 回调中没有直接取索引的值,而且在 修改 数组时,直接在超过数组长度的位置上新增 一个 元素。

  const  state  =   reactive  (  [  ,  ,  ]  )  ; 
 effect  (  (  )   =>   { 
  document .  getElementById  (  'app'  )  . innerHTML  =  state ; 
 }  ) 

 setTimeout  (  (  )   =>   { 
  state [  ]   =   ; 
 }  ,   ) 
 

在这 种情况下也没有索引 key 进行收集,但是确实使用数组的索引 增加 了值。这时我们就需要借助 trigger 中的 type 类型来进行处理,当对数组索引进行 添加 操作时,需要触发数组的更新。

  export   function   trigger  ( target ,  type ,  key ,  newValue ,  oldValue )   { 
   const  depsMap  =  targetMap .  get  ( target ) 
   if   (  ! depsMap )   { 
     return 
   } 
   const   run   =   ( effects )   =>   { 
     if   ( effects )   { 
      effects .  forEarch  ( effect  =>   effect  (  )  ) 
     } 
   } 
   if   ( key  ===   'length'   &&   isArray  ( target )  )   { 
    depsMap .  forEarch  (  ( deps ,  key )   =>   { 
       if  ( key  ===   'length'   ||  key  >=  newValue )   { 	 // newValue是更新后的值, 
         run  ( deps ) 
       } 
     }  ) 
   }   else   { 
     if   ( key  ==   void   )   { 
       run  ( depsMap .  get  ( key )  )  ; 
     } 
     switch   ( type )   { 
       case   'ADD'  : 
         if  (  isArray  ( target )  )   { 
           if  ( isIntergerKey )   { 	 // 判断key是否是索引类型 
             run  ( depsMap .  get  (  'length'  )  )  ; 	 // 新增 属性 时直接触发length收集的依赖即可 
           } 
         } 
         break  ; 
     } 
   } 
 } 
 

这样我们就基本上实现了 effect 的响应式的源码。

小结

本节我们主要实现了 Vue3 中 effect 函数 ,它是 一个 响应式的 函数 ,在源码实现过程中需要注意几点:

使用 WeakMap 数据结构来存放 target 上的 key 和 effect 的关系; 对 effect 的嵌套处理时,引入了栈的方式来控制当前的 activeEffect 值; 在使用数组时,在对 length 直接 修改 等操作时进行特殊的处理。

查看更多关于ES6实战2-实现 Vue3 effect 源码的详细内容...

  阅读:42次

上一篇

下一篇

第1节:ES6+ 简介    第2节:ES6 环境配置    第3节:ES6+ let    第4节:ES6+ const    第5节:ES6+ 展开语法    第6节:ES6+ 剩余参数    第7节:ES6+ 解构赋值    第8节:ES6+ 模版字符串    第9节:ES6+ 箭头函数    第10节:ES6+ 数值扩展    第11节:ES6+ isFinite()&isNaN()    第12节:ES6+ Number 对象的方法    第13节:ES6+ Math 对象的扩展    第14节:ES6+ includes()    第15节:ES6+ 字符串的扩展    第16节:ES6+ startsWith()    第17节:ES6+ endsWith()    第18节:ES6+ repeat()    第19节:ES6+ padStart()    第20节:ES6+ padEnd()    第21节:ES6+ trim()    第22节:ES6+ Array.from()    第23节:ES6+ of()    第24节:ES6+ find()和findIndex()    第25节:ES6+ copyWithin()    第26节:ES6+ fill()    第27节:ES6+ isArray()    第28节:ES6+ 对象的扩展    第29节:ES6+ flat()    第30节:ES6+ 可选链操作符    第31节:ES6+ Object.is()    第32节:ES6+ Object.assign()    第33节:ES6+ Object.keys()    第34节:ES6+ Object.values()    第35节:ES6+ Object.entries()    第36节:ES6+ 数据结构扩展    第37节:ES6+ Set    第38节:ES6+ WeakSet    第39节:ES6+ Map    第40节:ES6+ WeakMap    第41节:ES6+ Symbol    第42节:ES6+ for...of    第43节:ES6+ 迭代协议    第44节:ES6+ 实现一个简版的 Promise    第45节:ES6+ Promise 基础    第46节:ES6+ Promise 进阶    第47节:ES6+ Generator 基础    第48节:ES6+ Generator 函数应用    第49节:ES6+ async/await    第50节:ES6+ Class 前置知识    第51节:ES6+ Class    第52节:ES6+ Proxy    第53节:ES6+ Reflect(一)    第54节:ES6+ Reflect(二)    第55节:ES6+ 模块化(一)    第56节:ES6+ 模块化(二)    第57节:ES6实战1-实现Vue3 reactive 源码    第58节:ES6实战2-实现 Vue3 effect 源码    第59节:ES6 实战2-封装请求    第60节:ES6+ 实战3-代码整洁之道    第61节:ES6 Map原理分析    第62节:ES6module语法加载importexport    第63节:ES6的循环与可迭代对象示例详解