前言
关于响应式原理想必大家都很清楚了,下面我将会根据响应式API来具体讲解Vue3.0中的实现原理, 另外我只会针对get,set进行深入分析,本文包含以下API实现,推荐大家顺序阅读
- effect
- reactive
- readonly
- computed
- ref
对了,大家一定要先知道怎么用哦~
引子
先来段代码,大家可以直接复制哦,注意引用的文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <script src="/UploadFiles/2021-04-02/vue.global.js">这段代码,想必大家都看得懂,点击后count增加,视图也随之更新,effect监听了count改变,那么为什么effect能观察到count变化呢,还有为什么reactive可以实现响应式?
effect
为什么要先说这个函数呢,因为它和其他函数都息息相关,只有先了解它才能更好的理解其他响应式API
上源码
export function effect( fn: Function, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect { if ((fn as ReactiveEffect).isEffect) { fn = (fn as ReactiveEffect).raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect }if判断,判断如果传入的fn函数,它已经是effect了,也就是一个标识,直接获取该函数上的raw属性,这个属性后面会讲到
调用createReactiveEffect
如果options中有lazy,就会立即调用effect,其实本质上调用的还是传入的fn函数
// 了解一下options有哪些 { lazy"color: #ff0000">createReactiveEffect
上面提到了createReactiveEffect函数,我们来看看它的实现
function createReactiveEffect( fn: Function, options: ReactiveEffectOptions ): ReactiveEffect { // 又包装了一层函数 const effect = function effect(...args): any { return run(effect as ReactiveEffect, fn, args) } as ReactiveEffect effect.isEffect = true // 标识effect effect.active = true // 如果active effect.raw = fn // 传入的回调 effect.scheduler = options.scheduler effect.onTrack = options.onTrack effect.onTrigger = options.onTrigger effect.onStop = options.onStop effect.computed = options.computed effect.deps = [] // 用于收集依赖 return effect }注意,敲黑板,这里有个run函数,很重要,因为它保存了依赖
function run(effect: ReactiveEffect, fn: Function, args: any[]): any { if (!effect.active) { return fn(...args) } if (activeReactiveEffectStack.indexOf(effect) === -1) { cleanup(effect) try { activeReactiveEffectStack.push(effect) return fn(...args) } finally { activeReactiveEffectStack.pop() } } }他把依赖存储在了一个全局的数组中activeReactiveEffectStack, 他以栈的形式存储,调用完依赖后,会弹出,大家要留意一下这里,后面会用到
怎么样,是不是很简单~
reactive
export function reactive(target: object) { // 如果target是已经被readonly对象,那么直接返回对应的proxy对象 if (readonlyToRaw.has(target)) { return target } // 如果target是已经被readonly对象,那么直接返回对应的真实对象 if (readonlyValues.has(target)) { return readonly(target) } return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers ) }前两个if是用来处理这种情况的
// 情况一 const state1 = readonly({ count: 0 }) const state2 = reactive(state1) // 情况二 const obj = { count: 0 } const state1 = readonly(obj) const state2 = reactive(obj) 可以看到reactive它的参数是被readonly的对象,reactive不会对它再次创建响应式,而是通过Map映射,拿到对应的对象,即Proxy <==> Object的相互转换。 createReactiveObject创建响应式对象,注意它的参数 createReactiveObject( target, rawToReactive, // Object ==> Proxy reactiveToRaw, // Proxy ==> Object mutableHandlers, // get set has ... mutableCollectionHandlers // 很少会用,不讲了~ )以上就是reative一开始所做的一些事情,下面继续分析createReactiveObject
createReactiveObject
function createReactiveObject( target: any, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { // 如果不是对象,在开发环境报出警告 if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } let observed = toProxy.get(target) // 如果目标对象已经有proxy对象,直接返回 if (observed !== void 0) { return observed } // 如果目标对象是proxy的对象,并且有对应的真实对象,那么也直接返回 if (toRaw.has(target)) { return target } // 如果它是vnode或者vue,则不能被观测 if (!canObserve(target)) { return target } // 判断被观测的对象是否是set,weakSet,map,weakMap,根据情况使用对应proxy的,配置对象 const handlers = collectionTypes.has(target.constructor) "htmlcode">// 这种情况 const obj = { count: 0 } const state1 = reative(obj) const state2 = reative(obj)toRaw拿到Proxy对象对应的真实对象,如果存在直接返回
// 这种情况 const obj = { count: 0 } const state1 = reative(obj) const state2 = reative(state1)有些情况无法被观测,则直接返回观测对象本身
const canObserve = (value: any): boolean => { return ( !value._isVue && !value._isVNode && observableValueRE.test(toTypeString(value)) && !nonReactiveValues.has(value) ) }设置handlers,即get,set等属性访问器, 注意:collectionHandlers是用来处理观测对象为Set,Map等情况,很少见,这里就不讲了
const handlers = collectionTypes.has(target.constructor) "htmlcode">observed = new Proxy(target, handlers) toProxy.set(target, observed) toRaw.set(observed, target)然后在targetMap做了target ==> Map的映射,这又是干嘛,注意:targetMap是全局的
export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap() if (!targetMap.has(target)) { targetMap.set(target, new Map()) }在这里先给大家卖个关子,targetMap非常重要,是用来保存依赖的地方
讲完了reactive,可以回到一开始的引子
依赖收集
说到依赖收集,不得不提到,依赖的创建,那么Vue3.0是在哪里创建了渲染依赖呢,大家可以找到下面这段代码以及文件
// vue-next\packages\runtime-core\src\createRenderer.ts function setupRenderEffect( instance: ComponentInternalInstance, parentSuspense: HostSuspsenseBoundary | null, initialVNode: HostVNode, container: HostElement, anchor: HostNode | null, isSVG: boolean ) { // create reactive effect for rendering let mounted = false instance.update = effect(function componentEffect() { // ... }, __DEV__ "htmlcode">function run(effect: ReactiveEffect, fn: Function, args: any[]): any { if (!effect.active) { return fn(...args) } if (activeReactiveEffectStack.indexOf(effect) === -1) { cleanup(effect) try { activeReactiveEffectStack.push(effect) return fn(...args) } finally { activeReactiveEffectStack.pop() } } }这里进行了第一步的依赖收集,保存在全局数组中,为了方便触发get的对象,将依赖收集到自己的deps中
然后就是调用patch,进行组件挂载
if (!mounted) { const subTree = (instance.subTree = renderComponentRoot(instance)) // beforeMount hook if (instance.bm !== null) { invokeHooks(instance.bm) } patch(null, subTree, container, anchor, instance, parentSuspense, isSVG) initialVNode.el = subTree.el // mounted hook if (instance.m !== null) { queuePostRenderEffect(instance.m, parentSuspense) } mounted = true }至于它内部实现,我就不讲了,不是本文重点,然后我们去编译的地方看看
//vue-next\packages\runtime-core\src\component.ts function finishComponentSetup( instance: ComponentInternalInstance, parentSuspense: SuspenseBoundary | null ) { const Component = instance.type as ComponentOptions if (!instance.render) { if (Component.template && !Component.render) { if (compile) { Component.render = compile(Component.template, { onError(err) {} }) } else if (__DEV__) { warn( `Component provides template but the build of Vue you are running ` + `does not support on-the-fly template compilation. Either use the ` + `full build or pre-compile the template using Vue CLI.` ) } } if (__DEV__ && !Component.render) { warn( `Component is missing render function. Either provide a template or ` + `return a render function from setup().` ) } instance.render = (Component.render || NOOP) as RenderFunction } // ...其他 }上面的代码是编译部分,我们来看看例子中编译后是什么样
(function anonymous( ) { const _Vue = Vue const _createVNode = Vue.createVNode const _hoisted_1 = { id: "box" } return function render() { with (this) { const { toString: _toString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue return (_openBlock(), _createBlock("div", _hoisted_1, [ _createVNode("button", { onClick: increment }, _toString(state.count), 9 /* TEXT, PROPS */, ["onClick"]) ])) } } })可以看到,编译的代码中,有使用到state.count,那么就会触发get访问器,从而收集依赖,至于为什么能直接访问到属性,原因是由于with设置了上下文,下面我们具体分析get
get
// vue-next\packages\reactivity\src\baseHandlers.ts function createGetter(isReadonly: boolean) { return function get(target: any, key: string | symbol, receiver: any) { const res = Reflect.get(target, key, receiver) if (typeof key === 'symbol' && builtInSymbols.has(key)) { return res } // _isRef if (isRef(res)) { return res.value } track(target, OperationTypes.GET, key) // 如果该属性对应的值还是对象,就继续递归创建响应式 return isObject(res) "htmlcode">// 这种情况 const key = Symbol('key') const state = reative({ [key]: 'symbol value' }) state[key]如果值为Ref返回该值的value,看到这里如果大家有了解过ref api的话就知道了,由于ref它自己实现了自己的get,set,所以不再需要执行后面的逻辑,这个在后面会讲
调用track
递归深度观测,使整个对象都为响应式
下面我会详细讲解
track
在讲它之前,先了解它有哪些参数
target: any, // 目标对象 type: OperationTypes, // 追踪数据变化类型,这里是get key"htmlcode">export function track( target: any, type: OperationTypes, key"htmlcode">const state = reative({ count: 0 }) effect(() => { console.log(state.count) }) // 依赖大致结构(随便写的,不太规范) { target(state):Map { count: Set (componentEffect渲染依赖, user自己添加的依赖) } }如果该对象不存在Map,就初始化一个
如果该Map中属性对应的Set不存在,就初始化一个Set
添加依赖到Set中
添加依赖到effect自身的deps数组中
最后调用onTrack回调
// 调用onTrack钩子 effect.onTrack({ effect, target, type, key })OK,Track实现大体就这样,是不是也很简单,有了这些基础,后面要讲的一些API就很容易理解了
set
当我们点击按钮后,就会触发set属性访问器
function set( target: any, key: string | symbol, value: any, receiver: any ): boolean { value = toRaw(value) const hadKey = hasOwn(target, key) const oldValue = target[key] // 如果旧的值是ref,而新的值不是ref if (isRef(oldValue) && !isRef(value)) { // 直接更改原始ref即可 oldValue.value = value return true } const result = Reflect.set(target, key, value, receiver) // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { /* istanbul ignore else */ if (__DEV__) { const extraInfo = { oldValue, newValue: value } if (!hadKey) { trigger(target, OperationTypes.ADD, key, extraInfo) } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key, extraInfo) } } else { if (!hadKey) { trigger(target, OperationTypes.ADD, key) } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key) } } } return result }判断旧值是ref,新值不是ref
// 这种情况 const val = ref(0) const state = reative({ count: val }) state.count = 1 // 其实state.count最终还是ref,还是能通过value访问 state.count.value // 1调用Reflect.set修改值
开发环境下,拿到新旧值组成的对象,调用trigger,为什么开发环境要这么做呢,其实是为了方便onTrigger能拿到新旧值
trigger(target, OperationTypes.ADD, key, extraInfo)可以看到第二个参数和track是一样的enum,有两种情况,一种我们设置了新的属性和值,另一种修改了原有属性值,下面我们来看看trigger实现。
trigger
export function trigger( target: any, type: OperationTypes, key"htmlcode">// 用来保存将要执行的依赖 const effects: Set<ReactiveEffect> = new Set() // computed依赖,因为trigger不仅是要处理effect,watch,还要处理computed惰性求值的情况 const computedRunners: Set<ReactiveEffect> = new Set()处理三种情况CLEAR,ADD,DELETE,SET(这里没有标识)
// effect set const effects: Set<ReactiveEffect> = new Set() // computed effect set const computedRunners: Set<ReactiveEffect> = new Set() function addRunners( effects: Set<ReactiveEffect>, computedRunners: Set<ReactiveEffect>, effectsToAdd: Set<ReactiveEffect> | undefined ) { if (effectsToAdd !== void 0) { effectsToAdd.forEach(effect => { if (effect.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } }可以看到,三种情况实际上都差不多,唯一的区别就是,如果添加的对象是数组,就会拿到length属性的依赖,用于修改数组长度
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) { const iterationKey = Array.isArray(target) "htmlcode">// 执行set中的effect const run = (effect: ReactiveEffect) => { scheduleRun(effect, target, type, key, extraInfo) } computedRunners.forEach(run) effects.forEach(run)function scheduleRun( effect: ReactiveEffect, target: any, type: OperationTypes, key: string | symbol | undefined, extraInfo: any ) { if (__DEV__ && effect.onTrigger) { effect.onTrigger( extend( { effect, target, key, type }, extraInfo // { oldValue, newValue: value } ) ) } if (effect.scheduler !== void 0) { effect.scheduler(effect) } else { effect() } }最后调用了scheduleRun,它内部会分别执行onTrigger,scheduler,effect
需要注意的是,只有开发环境才会执行onTrigger,这也是为什么,前面要这么判断
if (__DEV__) { const extraInfo = { oldValue, newValue: value } if (!hadKey) { trigger(target, OperationTypes.ADD, key, extraInfo) } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key, extraInfo) } }readonly
有了前面的基础,readonly看起来会非常简单,唯一的区别就是rawToReadonly,rawToReadonly, readonlyHandlers
export function readonly(target: object) { if (reactiveToRaw.has(target)) { target = reactiveToRaw.get(target) } return createReactiveObject( target, rawToReadonly, readonlyToRaw, readonlyHandlers, readonlyCollectionHandlers ) }前两个大家应该能猜出来了,关键是最后这个readonlyHandlers,区别就在set
set(target: any, key: string | symbol, value: any, receiver: any): boolean { if (LOCKED) { if (__DEV__) { console.warn( `Set operation on key "${key as any}" failed: target is readonly.`, target ) } return true } else { return set(target, key, value, receiver) } }它的实现很简单,不过LOCKED有是什么鬼,大家可以找到lock.ts
//vue-next\packages\reactivity\src\lock.ts export let LOCKED = true export function lock() { LOCKED = true } export function unlock() { LOCKED = false }看似简单,但是却非常重要,它能够控制被readonly的对象能够暂时被更改,就比如我们常用的props,它是无法被修改的,但是Vue内部又要对他进行更新,那怎么办,话不多说,我们再源码中看他具体应用
// vue-next\packages\runtime-core\src\componentProps.ts export function resolveProps( instance: ComponentInternalInstance, rawProps: any, _options: ComponentPropsOptions | void ) { const hasDeclaredProps = _options != null const options = normalizePropsOptions(_options) as NormalizedPropsOptions if (!rawProps && !hasDeclaredProps) { return } const props: any = {} let attrs: any = void 0 const propsProxy = instance.propsProxy const setProp = propsProxy "color: #ff0000">computed
export function computed<T>( getterOrOptions: (() => T) | WritableComputedOptions<T> ): any { const isReadonly = isFunction(getterOrOptions) const getter = isReadonly "htmlcode">const isReadonly = isFunction(getterOrOptions) const getter = isReadonly "htmlcode">const runner = effect(getter, { lazy: true, computed: true, scheduler: () => { dirty = true } })然后我们具体来看看,返回的对象
{ _isRef: true, // expose effect so computed can be stopped effect: runner, get value() { if (dirty) { value = runner() dirty = false } trackChildRun(runner) return value }, set value(newValue) { if (setter) { setter(newValue) } else { // TODO warn attempting to mutate readonly computed value } } }先说说set吧,尤大似乎还没写完,只是单纯能修改值
然后是get,注意dirty的变化,如果computed依赖了state中的值,初次渲染时,他会调用依赖,然后dirty = false,关键来了,最后执行了trackChildRun
function trackChildRun(childRunner: ReactiveEffect) { const parentRunner = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] if (parentRunner) { for (let i = 0; i < childRunner.deps.length; i++) { const dep = childRunner.deps[i] if (!dep.has(parentRunner)) { dep.add(parentRunner) parentRunner.deps.push(dep) } } } }由于computed是依赖了state中的属性的,一旦在初始时触发了get,执行runner,就会将依赖收集到activeReactiveEffectStack中,最后才是自己的依赖,栈的顶部是state属性的依赖
if (!dep.has(parentRunner)) { dep.add(parentRunner) parentRunner.deps.push(dep) }所以最后这段代码实现了state属性变化后,才导致了computed依赖的调用,从而惰性求值
ref
const convert = (val: any): any => (isObject(val) "color: #ff0000">最后
终于分析完了,Vue3.0响应系统使用了Proxy相比于Vue2.0的代码真的简洁许多,也好理解,说难不难。其实还有watch并没有讲,它没有在reactivity中,但是实现还是使用了effect,套路都是一样的。最后谢谢大家观看。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
DDR爱好者之家 Design By 杰米广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!DDR爱好者之家 Design By 杰米暂无评论...《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。
更新日志
2024年11月24日2024年11月24日
- 凤飞飞《我们的主题曲》飞跃制作[正版原抓WAV+CUE]
- 刘嘉亮《亮情歌2》[WAV+CUE][1G]
- 红馆40·谭咏麟《歌者恋歌浓情30年演唱会》3CD[低速原抓WAV+CUE][1.8G]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[320K/MP3][193.25MB]
- 【轻音乐】曼托凡尼乐团《精选辑》2CD.1998[FLAC+CUE整轨]
- 邝美云《心中有爱》1989年香港DMIJP版1MTO东芝首版[WAV+CUE]
- 群星《情叹-发烧女声DSD》天籁女声发烧碟[WAV+CUE]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[FLAC/分轨][748.03MB]
- 理想混蛋《Origin Sessions》[320K/MP3][37.47MB]
- 公馆青少年《我其实一点都不酷》[320K/MP3][78.78MB]
- 群星《情叹-发烧男声DSD》最值得珍藏的完美男声[WAV+CUE]
- 群星《国韵飘香·贵妃醉酒HQCD黑胶王》2CD[WAV]
- 卫兰《DAUGHTER》【低速原抓WAV+CUE】
- 公馆青少年《我其实一点都不酷》[FLAC/分轨][398.22MB]
- ZWEI《迟暮的花 (Explicit)》[320K/MP3][57.16MB]