本文最后更新于 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