本文最后更新于 2024-03-22T23:32:43+00:00
                  
                  
                
              
            
            
              
                
                结构更新 Vue3 的源码采用 TS + monorepo为什么越来越多的项目选择 Monorepo? - 掘金 
Vue3 的重大更新(breaking changes) [科普文] Vue3 到底更新了什么?-腾讯云开发者社区-腾讯云 
⭐️组合式 API 将 Vue2 的选项式更新为组合式
选项式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script>   export  default  {     data ( ) {       return  {         count : 1        }     },     mounted ( ) {       this .count  = 0      },     methods : {       addCount ( ) {         this .count ++       }     }   } </script>
 
缺点 
遵循语法写在特定区域:data、methods、computed、watch 等都是有固定语法的 
当项目的负责度增加后,这些逻辑就会散落在代码的各处,不利于后期维护 
 
组合式 1 2 3 4 5 6 7 8 9 10 <script setup>   import  { ref, onMounted } from  "vue"    const  count = ref (0 )   const  addCount  = ( ) => {     count.value ++   }   onMounted (() =>  {     count.value  = 0    }) </script>
 
优点 
不需要遵循在特点区域写,可以按照逻辑一行行书写,就跟传统的 JS 代码写法一致,可以将相同的逻辑放在一起 
 
⭐️响应式原理 Vue2:全部基于Object.defineProperty() 的get set实现。通过对data里面的数据递归处理,才能为每个属性增加getter setter,这样会有更高的性能开销,并且对于运行时动态新增/删除的属性无法自动处理为响应式 Vue3:基础类型基于对象的get|set实现,复杂类型则基于Proxy 实现。Proxy是 ES6 新增的 API,可以直接拦截对象上的所有操作,所以解决了vue2 中的运行时动态新增/删除的属性无法自动处理为响应式问题,并且减少了不必要的性能开销
其他新增功能 
Fragment允许组件返回多个根元素,减少层级
 
slot插槽的增强与语法简化
 
Suspense 组件异步内容加载组件,可以展示备用 UI
 
Teleport 组件允许将元素渲染到 DOM 的任意位置
 
编译优化:优化了 VDOM 的对比算法
 
TS 的支持
 
tree-shaking 的支持
 
生命周期优化
 
等等…
 
 
初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Vue .createApp ({   template : `     <div>       <h1>你好呀</h1>       <p>{{ msg }}</p>       <p v-if="array.length">{{ array.length }}</p>     </div>   ` ,   setup ( ) {     const  msg = Vue .ref ('hello, my children' )     const  array = Vue .reactive ([1 , 2 ])     setTimeout (() =>  {              msg.value  = 'hello, my children~~~~~~'      }, 2000 )     return  { msg, array }   } }).mount ('#app' )
 
初始化入口为:createApp  函数
初始化流程图 
响应式原理 Ref 原理 完整源码:vue3-ref.ts  核心源码解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 export  function  isRef (r: any ): r is Ref  {   return  !!(r && r.__v_isRef  === true ) }export  function  ref (value?: unknown ) {   return  createRef (value, false ) }function  createRef (rawValue: unknown, shallow: boolean ) {      if  (isRef (rawValue)) {     return  rawValue   }      return  new  RefImpl (rawValue, shallow) }class  RefImpl <T> {   private _value : T   private _rawValue : T   public dep?: Dep  = undefined    public readonly __v_isRef = true    constructor (    value: T,     public readonly __v_isShallow: boolean,    ) {               this ._rawValue  = __v_isShallow ? value : toRaw (value)               this ._value  = __v_isShallow ? value : toReactive (value)   }   get  value () {               trackRefValue (this )          return  this ._value    }   set  value (newVal ) {          const  useDirectValue =       this .__v_isShallow  || isShallow (newVal) || isReadonly (newVal)     newVal = useDirectValue ? newVal : toRaw (newVal)     if  (hasChanged (newVal, this ._rawValue )) {              this ._rawValue  = newVal              this ._value  = useDirectValue ? newVal : toReactive (newVal)              triggerRefValue (this , DirtyLevels .Dirty , newVal)     }   } }
 
总结:通过核心代码的解析,可以发现调用ref(0)后,最终返回的是个对象,传入的值是放在.value上的,并且通过get|set 函数实现响应式 所以这也是为什么const count = ref(0)后,使用/设置时要用count.value = 1 但在<template>里面可以省略.value,因为Vue框架在<template>解析编译时,自动加上了value
Reactive 原理 完整源码:vue3-reactive.ts  核心源码解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 const  user = reactive ({ name : 'xx'  })export  function  reactive (target: object ) {      if  (isReadonly (target)) {     return  target   }   return  createReactiveObject (     target,     false ,     mutableHandlers,     mutableCollectionHandlers,     reactiveMap,   ) }function  createReactiveObject (  target: Target,   isReadonly: boolean,   baseHandlers: ProxyHandler<any>,   collectionHandlers: ProxyHandler<any>,   proxyMap: WeakMap <Target, any>,  ) {         if  (!isObject (target)) {     if  (__DEV__) {       console .warn (`value cannot be made reactive: ${String (target)} ` )     }     return  target   }         if  (     target[ReactiveFlags .RAW ] &&     !(isReadonly && target[ReactiveFlags .IS_REACTIVE ])   ) {     return  target   }      const  existingProxy = proxyMap.get (target)   if  (existingProxy) {     return  existingProxy   }      const  targetType = getTargetType (target)    if  (targetType === TargetType .INVALID ) {     return  target   }         const  proxy = new  Proxy (     target,     targetType === TargetType .COLLECTION  ? collectionHandlers : baseHandlers,    )   proxyMap.set (target, proxy)   return  proxy }
 
总结:通过核心代码的解析,可以发现核心在于new Proxy,针对不同的复杂类型,使用不同的handler函数,针对性的处理get|set方法
依赖收集、触发流程与原理 当明白了数据能被改为响应式后,则需要研究下数据变化后为什么对应的页面/函数会执行呢? 这就涉及到依赖的收集与触发
关键词 
副作用函数
会产生副作用的函数:使用/更改了函数外的变量的函数 
 
 
 
1 2 3 4 5 const  userInfo = { name : 'lisi'  }function  getUserInfo ( ) {    return  userInfo.name   }
 
响应式数据
数据发生变化时,能触发其他使用该数据的同步变化,这种数据就被称为响应式数据 
 
 
 
1 2 3 4 5 6 7 8 conts obj = { text : 'hello!'  }function  effect ( ) {   document .body .innerHTML  = obj.text  } obj.text  = '你好' 
 
实现思路(简易代码) 1 2 3 4 5 6 7 conts obj = { text : 'hello!'  }function  effect ( ) {   document .body .innerHTML  = obj.text  } obj.text  = '你好' 
 
通过上述例子代码观察可知:
当副作用函数执行时,可以发现会触发obj.text的读取操作 
当修改obj.text时,会触发obj.text的设置操作 
 
那我们是不是可以在读取与设置时进行拦截呢?ES6 的 Proxy可以做代理 当读取时,把对应的副作用函数收集存起来 当设置时,把收集的副作用函数拿出来执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const  bucket = new  Set (); const  obj = { text : "hello!"  };const  data = new  Proxy (obj, {      get (target, key ) {          bucket.add (effect);     console .log ("[ bucket ] >" , bucket);          return  target[key];   },      set (target, key, newValue ) {          target[key] = newValue;          bucket.forEach ((fn ) =>  fn ());   }, });function  effect ( ) {   document .body .innerHTML  = data.text ; }effect (); setTimeout (() =>  {   data.text  = "你好" ;  }, 3000 );
 
上面的代码就是一个简易的可运行的响应式原理(还存在很多设计问题)
完善的响应式 问题 1 副作用函数的命名被我们固定为effect了,真实情况可能是其他名字或匿名 解决:设计一个专门注册副作用函数的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let  activeEffect = undefined  function  effect (fn ) {      activeEffect = fn      activeEffect () }effect (      () =>  {     document .body .innerHTML  = data.text ;   } )
 
问题 2 当给响应式数据设置一个新值时,也会触发副作用函数的执行 解决:将副作用的存储与响应式数据的属性关联起来,存储就不能再使用Set了
1 2 3 4 5 6 7 8 9 10 11 effect (      () =>  {     document .body .innerHTML  = data.text ;   } ) 可以得到一个关系: data   -- text      -- effect
 
解决问题 1、问题 2 后的完善代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 let  activeEffect = undefined ; function  effect (fn ) {      activeEffect = fn;      activeEffect (); }const  bucket = new  WeakMap (); const  reactive  = (_obj ) => {      return  new  Proxy (_obj, {          get (target, key ) {       if  (activeEffect) {         let  depsMap = bucket.get (target);         if  (!depsMap) {           depsMap = new  Map ();           bucket.set (target, depsMap);         }         let  deps = depsMap.get (key);         if  (!deps) deps = new  Set ();         deps.add (activeEffect);         depsMap.set (key, deps);       }              return  target[key];     },          set (target, key, newValue ) {              target[key] = newValue;       let  depsMap = bucket.get (target);       if  (!depsMap) return ;       let  deps = depsMap.get (key);       if  (!deps) return ;       deps.forEach ((fn ) =>  fn ());     },   }); };const  data = reactive ({ text : "hello!" , name : "张三"  });function  myEffect1 ( ) {   console .log ("[ myEffect1() ] >" );   document .body .innerHTML  = data.text ; }function  myEffect2 ( ) {   console .log ("[ myEffect2() ] >" );   document .body .innerHTML  = data.text  + data.name ; }effect (myEffect1);effect (myEffect2);setTimeout (() =>  {   console .log ("[ setTimeout 3000 ] >" );   data.pp  = "你好!" ;  }, 3000 );setTimeout (() =>  {   console .log ("[ setTimeout 5000 ] >" );   data.text  = "你好!" ;  }, 5000 );
 
其中的bucket的数据结构如下: 在将上述完善后的代码的reactive函数再次完善下,可以得到越来越接近于 Vue3 源码的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56   const  bucket = new  Map (); const  track  = (target, key ) => {   if  (activeEffect) {     let  depsMap = bucket.get (target);     if  (!depsMap) {       depsMap = new  Map ();       bucket.set (target, depsMap);     }     let  deps = depsMap.get (key);     if  (!deps) deps = new  Set ();     deps.add (activeEffect);     depsMap.set (key, deps);   } };const  trigger  = (target, key ) => {   let  depsMap = bucket.get (target);   if  (!depsMap) return ;   let  deps = depsMap.get (key);   if  (!deps) return ;   deps.forEach ((fn ) =>  fn ()); };const  reactive  = (_obj ) => {      return  new  Proxy (_obj, {          get (target, key ) {              track (target, key);              return  target[key];     },          set (target, key, newValue ) {              target[key] = newValue;              trigger (target, key);     },   }); };
 
问题 3 当使用过的属性不再使用时,已绑定的依赖项还会触发
1 2 3 4 5 6 7 8 9 10 11 effect (() =>  {   document .body .innerHTML  = obj.success  ? obj.msg  : 'error'  })obj (target)   -- success (key)      -- effect   -- msg (key)      -- effect
 
解决:给副作用函数增加一个属性,用于存储相关联的依赖项,在读取副作用时先断开联系,等真正执行副作用时会重新建立联系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 let  activeEffect = undefined function  clearup (effectFn ) {   effectFn.deps .forEach ((deps ) =>  {     deps.delete (effectFn);   });   effectFn.deps  = []; }function  effect (fn ) {   const  effectFn  = ( ) => {     clearup (effectFn);           activeEffect = effectFn;          fn ();   };   effectFn.deps  = [];    effectFn (); }const  track  = (target, key ) => {   if  (activeEffect) {     let  depsMap = bucket.get (target);     if  (!depsMap) {       depsMap = new  Map ();       bucket.set (target, depsMap);     }     let  deps = depsMap.get (key);     if  (!deps) deps = new  Set ();     deps.add (activeEffect);     depsMap.set (key, deps);     activeEffect.deps .push (deps)    } };const  trigger  = (target, key ) => {   let  depsMap = bucket.get (target);   if  (!depsMap) return ;   let  deps = depsMap.get (key);   if  (!deps) return ;   const  newDeps = new  Set (deps);    newDeps.forEach ((fn ) =>  fn ()); };
 
Vue3 源码内的依赖收集与触发 以reactive为例,讲述下依赖收集、触发的完整流程 reactive 的核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 function  reactive ( ) {   const  proxy = new  Proxy (     target,     mutableHandlers,    ) }export  const  mutableHandlers : ProxyHandler <object> = {   get : createGetter (),   set : createSetter (),   deleteProperty,   has,   ownKeys }const  enum TrackOpTypes  {   GET  = 'get' ,   HAS  = 'has' ,   ITERATE  = 'iterate'  }const  enum TriggerOpTypes  {   SET  = 'set' ,   ADD  = 'add' ,   DELETE  = 'delete' ,   CLEAR  = 'clear'  }function  createGetter (isReadonly = false , shallow = false  ) {   return  function  get (target: Target, key: string | symbol, receiver: object ) {          const  res = Reflect .get (target, key, receiver)               track (target, TrackOpTypes .GET , key)          return  res   } }function  createSetter (shallow = false  ) {   return  function  set (    target: object,     key: string | symbol,     value: unknown,     receiver: object    ): boolean {          const  result = Reflect .set (target, key, value, receiver)               trigger (target, TriggerOpTypes .SET , key, value, oldValue)     return  result   } }
 
依赖收集 通过getter实现依赖的收集
1 2 track (target, TrackOpTypes .GET , key)
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 type KeyToDepMap  = Map <any, Dep >const  targetMap = new  WeakMap <any, KeyToDepMap >()let  activeEffect = null function  track (target, type, key ) {   let  depsMap = targetMap.get (target)   if (!depsMap) {     depsMap = new  Map ()     targetMap.set (target, depsMap)   }   let  dep = depsMap.get (key)   if (!dep) {     dep = new  Set ()     depsMap.set (key, dep)   }   trackEffects (dep) }function  trackEffects (dep ) {   dep.add (activeEffect)   activeEffect!.deps .push (dep) }
 
依赖触发 通过setter实现依赖的收集
1 2 trigger (target, TriggerOpTypes .SET , key, value, oldValue)
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function  trigger (target, type, key, value, oldValue ) {   let  depsMap = targetMap.get (target)   if (!depsMap) return    let  deps = depsMap.get (key)   triggerEffects (deps) }function  triggerEffects (deps ) {   for  (const  dep of  deps) {     dep ()   } }
 
渲染流程 模板 -编译-> 渲染函数 -> 虚拟 DOM -> 渲染器 -> 真实 DOM
流程 大致跟 Vue2 一样的:编译 -> 运行时
编译
<template>转为模板 AST 树(用来描述模板的) 
将模板 AST 树转换为JS AST 树(用来描述渲染函数的)
期间会打上patchFlag(值为 number),用于精确化标记每个节点,只要打上了patchFlag则一定是动态的节点;没有打上的就是静态节点 
并且还会额外使用dynamicChildren数组来储存打标的节点,直接用该数据进行 diff 
 
 
基于JS AST 树生成render字符串 
最后基于render字符串生成render函数 
 
 
渲染时
运行实例的render函数,生成vnode
vnode一种用来描述真实 DOM 的 JS 对象 
 
 
基于vnode进行渲染到页面
vnode是通过renderer渲染器转化为真实 DOM 
期间会经历 diff 算法,实现最优的方式转化为真实 DOM 
renderer渲染器就是一堆DOM 的操作:createElement/addEventListener/... 
 
 
 
 
 
源码 编译 编译的主入口:Compile.ts ,触发条件:.mount('#app')函数的调用,并完成首次页面的渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 export  function  baseCompile (  template: string | RootNode,   options: CompilerOptions = {}  ): CodegenResult  {        const  ast = isString (template) ? baseParse (template, options) : template         transform (     ast,     extend ({}, options, {       prefixIdentifiers,       nodeTransforms : [         ...nodeTransforms,         ...(options.nodeTransforms  || [])        ],       directiveTransforms : extend (         {},         directiveTransforms,         options.directiveTransforms  || {}        )     })   )      return  generate (     ast,     extend ({}, options, {       prefixIdentifiers     })   ) }const  render = (   __GLOBAL__ ? new  Function (code)() : new  Function ('Vue' , code)(runtimeDom) ) as  RenderFunction 
 
渲染 渲染时的主入口:无固定,触发条件:某个响应的数据的改变
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 class  RefImpl <T> {   private _value : T   private _rawValue : T   public dep?: Dep  = undefined    public readonly __v_isRef = true    constructor (value: T, public readonly __v_isShallow: boolean ) {     this ._rawValue  = __v_isShallow ? value : toRaw (value)     this ._value  = __v_isShallow ? value : toReactive (value)   }   get  value () {     trackRefValue (this )     return  this ._value    }   set  value (newVal ) {     const  useDirectValue =       this .__v_isShallow  || isShallow (newVal) || isReadonly (newVal)     newVal = useDirectValue ? newVal : toRaw (newVal)     if  (hasChanged (newVal, this ._rawValue )) {       this ._rawValue  = newVal       this ._value  = useDirectValue ? newVal : toReactive (newVal)              triggerRefValue (this , newVal)     }   } }export  function  triggerRefValue (ref: RefBase<any>, newVal?: any ) {   ref = toRaw (ref)   if  (ref.dep ) {     if  (__DEV__) {       triggerEffects (ref.dep , {         target : ref,         type : TriggerOpTypes .SET ,         key : 'value' ,         newValue : newVal       })     } else  {       triggerEffects (ref.dep )     }   } }export  function  triggerEffects (  dep: Dep | ReactiveEffect[],   debuggerEventExtraInfo?: DebuggerEventExtraInfo  ) {      const  effects = isArray (dep) ? dep : [...dep]   for  (const  effect of  effects) {     if  (effect.computed ) {       triggerEffect (effect, debuggerEventExtraInfo)     }   }   for  (const  effect of  effects) {     if  (!effect.computed ) {       triggerEffect (effect, debuggerEventExtraInfo)     }   } }function  triggerEffect (  effect: ReactiveEffect,   debuggerEventExtraInfo?: DebuggerEventExtraInfo  ) {   if  (effect !== activeEffect || effect.allowRecurse ) {     if  (__DEV__ && effect.onTrigger ) {       effect.onTrigger (extend ({ effect }, debuggerEventExtraInfo))     }     if  (effect.scheduler ) {       effect.scheduler ()     } else  {       effect.run ()     }   } } effect.run ()patch (...)
 
完整的流程图
其他知识 虚拟 DOM 一种用来描述真实 DOM 的 JS 对象 Vue 的组件本质也是可以用虚拟 DOM 来描述的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const  vnode = {   tag : 'div' ,   props : {    onClick : () =>  alert ('hello' )   },   children : 'click me'  }const  MyComponent  = function ( ) {   return  {      tag : 'div' ,     props : {      onClick : () =>  alert ('hello' )     },     children : 'click me'    } }const  MyComponent  = {  render () () {      return  {        tag : 'div' ,       props : {        onClick : () =>  alert ('hello' )       },       children : 'click me'      }   } }const  vnode = {   tag : MyComponent  }
 
渲染函数、渲染器 渲染函数:用于生成虚拟 DOM 的函数,因为手动写虚拟 DOM 的结构太麻烦了,所以封装成一个函数 每个组件有自己的渲染函数,在渲染器里面会用到它
1 2 3 4 5 6 7 8 const  h = (tag, props) {  return  {     tag,     props,   } }h ('h1' , { onClick : handler }) 
 
渲染器:用于将虚拟 DOM 生成为真实 DOM 的函数。仅一个,要渲染的时候调用它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function  renderer (vnode, container ) {      const  el = document .createElement (vnode.tag );      for  (const  key in  vnode.props ) {     if  (/^on/ .test (key)) {              el.addEventListener (         key.substr (2 ).toLowerCase (),          vnode.props [key]        );     }   }      if  (typeof  vnode.children  === "string" ) {          el.appendChild (document .createTextNode (vnode.children ));   } else  if  (Array .isArray (vnode.children )) {          vnode.children .forEach ((child ) =>  renderer (child, el));   }      container.appendChild (el); }renderer (vnode, document .body ); 
 
编译器 作用:将模板(<template>)编译为渲染函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29  <template>   <div  @click ="handlerClick" >     click me   </div >   </template><script >   export  default  {     data ( ) {  },     methods : {       handlerClick ( ) {  }     }   } </script > <script >   export  default  {     data ( ) {  },     methods : {       handlerClick ( ) {  }     },     render ( ) {       return  h ('div' , { onClick : handlerClick }, 'click me' )     }   } </script > 
 
一个完整的编译流程
编译优化 为了让渲染器能够快速的找到要更新的点,所以在编译期间做了一些优化:
PatchFlag 与 Block
编译时,可进行打标:动态、静态 
然后再收集这些动态节点,被称为 Block 
后续就可以从 Block 里找节点更新 
 
 
静态提升
将静态的节点创建放到渲染函数之外,这样只需要调用一次静态节点的创建 
 
 
预字符串化
基于[静态提升],将大量静态节点的创建规律化,最终变成一个静态节点的创建 
 
 
缓存内联函数(@click=”a+b”) 
 
浅响应、深响应 reactive默认是深响应的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const  obj = reactive ({ foo : { bar : 1  } })effect (() =>  {  console .log (obj.foo .bar ) }) obj.foo .bar  = 2 function  reactive (_obj, shallow ) {   new  Proxy (_obj, {     get (target, key, receiver ) {       track (target, key)       const  res = Reflect .get (target, key, receiver)       if (shallow) return  res        if (typeof  res === 'object' ) return  reactive (res)      }   }) }
 
浅只读、深只读 只读的实现也是在 new Proxy 的 get、set 里面处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function  reactive (_obj, shallow, isReadonly ) {   new  Proxy (_obj, {     get (target, key, receiver ) {       if (!isReadonly) track (target, key)       const  res = Reflect .get (target, key, receiver)       if (shallow) return  res       if (typeof  res === 'object' ) {         return  reactive (res, shallow, isReadonly)       }            },     set (target, key ) {       if (isReadonly) return  true             }   }) }
 
代理数组、Set、 Map 代理数组解决以下响应式的问题 
arr[大于长度] = xx 或 arr.length = x
关键点:在Proxy 的 get函数里面判断数组的长度 
 
 
for…in
关键点:使用Proxy 的 ownKeys函数,判断是否为数组还是对象 
 
 
for…of
关键点:该循环的实现是与数组的长度、索引有关的,读取了数组的 Symbol.iterator 属性 
 
 
一些查找方法:includes、indexOf、lastIndexOf
 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const  arrayInstrumentations = {} ['includes' , 'indexOf' , 'lastIndexOf' ].forEach (method  =>  {   const  originMethod = Array .prototype  [method]   arrayInstrumentations[method] = function (...args ) {          let  res = originMethod.apply (this , args)     if  (res === false  || res === -1 ) {                     res = originMethod.apply (this .raw , args)     }          return  res   } })
 
代理 Set 解决以下响应式的问题 因为Set的操作方法跟普通对象操作方法不一致,所以会代理处理
服务端渲染 CSR client-side rendering,客户端渲染,在客户端完成[数据获取+HTML]的拼装,最终在客户端渲染 优点:进行页面跳转后,不会刷新,是通过前端路由的方式动态地渲染页面,用户交互体验友好 缺点:会产生白屏问题,对 SEO(搜索引擎优化)也不友好
SSR server-side rendering,服务端渲染,在服务端完成[数据获取+HTML]的拼装,最终在客户端渲染 优点:不会产生白屏问题,对 SEO(搜索引擎优化)友好 缺点:进行页面跳转,会重复上述 5 个步骤,用户体验非常差;缺少响应式
CSR vs SSR 
同构渲染 分为首次与非首次渲染。 “同构”指:同一套代码即可在服务端运行,也可以在客户端运行。 同构渲染中的首次渲染与 SSR 的工作流程是一致的。当首次访问或者刷新页面时,整个页面的内容是在服务端完成渲染的,浏览器最终得到的是渲染好的 HTML 页面。 但是该页面是静态的,这意味着用户还不能与页面进行任何交互,因为整个应用程序的脚本还没有加载和执行。另外,该静态的 HTML 页面中也会包含<link>、<script>等标签。 同构渲染中的非首次渲染与 CSR 的工作流程是一致的。当浏览器已经接收到初次渲染的静态 HTML 页面,接下来浏览器会解析并渲染该页面。在解析过程中,浏览器会发现 HTML 代码中存在<link>和<script>标签,于是会从 CDN 或服务器获取相应的资源(这一步与 CSR 一致)。当 JavaScript 资源加载完毕后,会进行激活操作。激活完成后,后续操作都会按照 CSR 应用程序的流程来执行。 一句话总结:代码会在服务端和客户端分别执行一次。在服务端会被渲染为静态的 HTML 字符串,然后发送给浏览器,浏览器再把这段纯静态的 HTML 渲染出来,并补齐响应式、事件绑定等(这也称为“激活”)
Vue 中的同构原理 服务端原理:基于虚拟 DOM 将其转为 HTML 字符串,使用的库为vue-server-renderer 因为服务端不存在真实 DOM,所以只能转为 HTLM 字符串,客户端获取后可直接进行渲染 所以本质就是一个“虚拟 DOM 转 HTML 字符串”的函数,主要功能:字符串的拼接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const  vnode = {   tag : 'div' ,   props : {    onClick : () =>  alert ('hello' )   },   children : 'click me'  }const  renderElementVNode  = (vnode ) => {   const  { tag, props, children } = vnode   let  html = `<${tag} `    if (props) {        }   html += '>'    if (children) {     if (typeof  children === 'string' ) {            } else  {            }   }   html += `<${tag} >`  }renderElementVNode (vnode) 
 
客户端原理:将虚拟 DOM 与已渲染的真实 DOM 进行关联,补齐响应式/事项等(这也称为“激活”) 所以本质也是通过一个函数来建立关联
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const  html = renderComponentVNode (compVNode)const  container = document .querySelector ('#app' ) container.innerHTML  = html renderer.hydrate (VNode , container) renderer.hydrate  = (VNode, container ) =>  {     hydrateNode (container.firstChild , vnode) }const  hydrateNode  = (node, vnode ) => {         const  { type } = vnode    vnode.el  = node        return  node.nextSibling  }
 
同构导致的编码问题 部分 API/库 使用的时候需要判断环境(服务端/客户端),否则会报错;或者使用双端支持的 API/库 (axios)
补充知识 如何获取复杂数据的具体类型? 比如:
{ a: 1 },期望返回类型为object 
[{ a: 1 }],期望返回类型为array 
const a = function () {},期望返回类型为function 
 
1 2 3 4 5 const  objectType = (obj : object): string  =>  {   const  fullTypeString = Object .prootype .toString .call (obj)    const  typeString = fullTypeString.slice (8 , -1 )    return  typeString.toLocaleLowerCase ()  }
 
Map、WeakMap、Set、WeakSet Map:类似于object的,采用键值对存储数据,键可以是任意类型的(基础/复杂类型都可以),可以使用forEach遍历,并且按照set顺序返回
WeakMap:虚弱版的Map,键必须为复杂类型 ,弱引用当复杂类型设为null 后,WeakMap 里面的值也会自动垃圾回收,变为undefined ,不支持forEach遍历
Set:类似于array的,里面的值不允许重复,值是任意类型的(基础/复杂类型都可以),无法通过索引取值,只能forof循环取值
WeakSet:虚弱版的Set,值必须为复杂类型,弱引用当复杂类型设为null 后,WeakSet 里面的值也会自动垃圾回收,变为undefined ,不支持forof遍历
Proxy 1 2 3 4 5 6 7 8 9 10 11 new  Proxy (target, handle);const  handle = {   get : function  (target, property, receiver ) {},   set (target, property, value, receiver ) {} }
 
面试题 手写一份 Vue3 的响应式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 let  activeEffect = undefined const  effect  = fn => {   const  effectFn  = ( ) => {     activeEffect = effectFn     fn ()   }   effectFn () }const  targetMap = new  WeakMap ()const  track  = (target, key, receiver ) => {   let  depsMap = targetMap.get (target)   if (!depsMap) {     depsMap = new  Map ()     targetMap.set (target, depsMap)   }   let  depMap = depsMap.get (key)   if (!deps) deps = new  Set ()   deps.add (activeEffect)   depsMap.set (key, deps) }const  trigger  = (target, key, receiver ) => {   let  depsMap = targetMap.get (target)   if (!depsMap) return    let  depMap = depsMap.get (key)  if (!depMap) return   depMap.forEach (fn  =>  fn ()) }const  reactive  = _obj => {   return  new  Proxy (_obj, {     get (target, key, receiver ) {       track (target, key, receiver)       return  Reflect .get (target, key, receiver)     },     set (target, key, newValue, receiver ) {       Reflect .set (target, key, newValue,receiver)       trigger (target, key, receiver)     }   }) }
 
相关资料 Vue.js 设计与实现.pdf