Vue3.0 响应式系统源码逐行讲解

前言

关于响应式原理想必大家都很清楚了,下面我将会根据响应式API来具体讲解Vue3.0中的实现原理, 另外我只会针对get,set进行深入分析,本文包含以下API实现,推荐大家顺序阅读

effect

reactive

readonly

computed

ref

对了,大家一定要先知道怎么用哦~

引子

先来段代码,大家可以直接复制哦,注意引用的文件

        Document   

      const { reactive, computed, effect, watch, createApp } = Vue    const App = {      template: `       
            {{ state.count }}       
      `,      setup() {        const state = reactive({          count: 0        })        function increment(e) {          state.count++        }        effect(() => {          console.log('count改变', state.count);        })        return {          state,          increment        }      }    }    createApp().mount(App, '#app')  复制代码

这段代码,想必大家都看得懂,点击后count增加,视图也随之更新,effect监听了count改变,那么为什么effect能观察到count变化呢,还有为什么reactive可以实现响应式?

effect

为什么要先说这个函数呢,因为它和其他函数都息息相关,只有先了解它才能更好的理解其他响应式API

上源码

exportfunctioneffect(  fn: Function,

  options: ReactiveEffectOptions = EMPTY_OBJ):ReactiveEffect{if((fnasReactiveEffect).isEffect) {    fn = (fnasReactiveEffect).raw  }consteffect = createReactiveEffect(fn, options)if(!options.lazy) {    effect()  }returneffect}复制代码

if判断,判断如果传入的fn函数,它已经是effect了,也就是一个标识,直接获取该函数上的raw属性,这个属性后面会讲到

调用createReactiveEffect

如果options中有lazy,就会立即调用effect,其实本质上调用的还是传入的fn函数

// 了解一下options有哪些{  lazy?: boolean// 是否立即调用fncomputed?: boolean// 是否是computedscheduler?:(run:Function) =>void// 在调用fn之前执行onTrack?:(event: DebuggerEvent) =>void// 在依赖收集完成之后调用onTrigger?:(event: DebuggerEvent) =>void// 在调用fn之前执行,源码上来看和scheduler调用时机一样,只是传入参数不同onStop?:()=>void// 清除依赖完成后调用}复制代码

返回effect

createReactiveEffect

上面提到了createReactiveEffect函数,我们来看看它的实现

functioncreateReactiveEffect(  fn: Function,

  options: ReactiveEffectOptions):ReactiveEffect{// 又包装了一层函数consteffect =functioneffect(...args):any{returnrun(effectasReactiveEffect, fn, args)  }asReactiveEffect  effect.isEffect =true// 标识effecteffect.active =true// 如果activeeffect.raw = fn// 传入的回调effect.scheduler = options.scheduler  effect.onTrack = options.onTrack  effect.onTrigger = options.onTrigger  effect.onStop = options.onStop  effect.computed = options.computed  effect.deps = []// 用于收集依赖returneffect}复制代码

注意,敲黑板,这里有个run函数,很重要,因为它保存了依赖

functionrun(effect: ReactiveEffect, fn: Function, args: any[]):any{if(!effect.active) {returnfn(...args)  }if(activeReactiveEffectStack.indexOf(effect) ===-1) {    cleanup(effect)try{      activeReactiveEffectStack.push(effect)returnfn(...args)    }finally{      activeReactiveEffectStack.pop()    }  }}复制代码

他把依赖存储在了一个全局的数组中activeReactiveEffectStack, 他以栈的形式存储,调用完依赖后,会弹出,大家要留意一下这里,后面会用到

怎么样,是不是很简单~

reactive

exportfunctionreactive(target: object){// 如果target是已经被readonly对象,那么直接返回对应的proxy对象if(readonlyToRaw.has(target)) {returntarget  }// 如果target是已经被readonly对象,那么直接返回对应的真实对象if(readonlyValues.has(target)) {returnreadonly(target)  }returncreateReactiveObject(    target,    rawToReactive,    reactiveToRaw,    mutableHandlers,    mutableCollectionHandlers  )}复制代码

前两个if是用来处理这种情况的

// 情况一conststate1 = readonly({count:0})conststate2 = reactive(state1)// 情况二constobj = {count:0}conststate1 = readonly(obj)conststate2 = reactive(obj)复制代码

可以看到reactive它的参数是被readonly的对象,reactive不会对它再次创建响应式,而是通过Map映射,拿到对应的对象,即Proxy <==> Object的相互转换。

createReactiveObject创建响应式对象,注意它的参数

createReactiveObject(    target,    rawToReactive,// Object ==> ProxyreactiveToRaw,// Proxy ==> ObjectmutableHandlers,// get set has ...mutableCollectionHandlers// 很少会用,不讲了~)复制代码

以上就是reative一开始所做的一些事情,下面继续分析createReactiveObject

createReactiveObject

functioncreateReactiveObject(  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)}`)    }returntarget  }letobserved = toProxy.get(target)// 如果目标对象已经有proxy对象,直接返回if(observed !==void0) {returnobserved  }// 如果目标对象是proxy的对象,并且有对应的真实对象,那么也直接返回if(toRaw.has(target)) {returntarget  }// 如果它是vnode或者vue,则不能被观测if(!canObserve(target)) {returntarget  }// 判断被观测的对象是否是set,weakSet,map,weakMap,根据情况使用对应proxy的,配置对象consthandlers = collectionTypes.has(target.constructor)    ? collectionHandlers    : baseHandlers  observed =newProxy(target, handlers)  toProxy.set(target, observed)  toRaw.set(observed, target)if(!targetMap.has(target)) {    targetMap.set(target,newMap())  }returnobserved}复制代码

第一个if,判断是否是对象,否则报出警告

toProxy拿到观测对象的Proxy对象,如果存在直接返回

// 这种情况constobj = {count:0}conststate1 = reative(obj)conststate2 = reative(obj)复制代码

toRaw拿到Proxy对象对应的真实对象,如果存在直接返回

// 这种情况constobj = {count:0}conststate1 = reative(obj)conststate2 = reative(state1)复制代码

有些情况无法被观测,则直接返回观测对象本身

constcanObserve = (value: any):boolean=>{return(    !value._isVue &&    !value._isVNode &&    observableValueRE.test(toTypeString(value)) &&    !nonReactiveValues.has(value)  )}复制代码

设置handlers,即get,set等属性访问器, 注意:collectionHandlers是用来处理观测对象为Set,Map等情况,很少见,这里就不讲了

consthandlers = collectionTypes.has(target.constructor)    ? collectionHandlers    : baseHandlers复制代码

然后创建了Proxy对象,并把观测对象和Proxy对象,分别做映射

observed =newProxy(target, handlers)  toProxy.set(target, observed)  toRaw.set(observed, target)复制代码

然后在targetMap做了target ==> Map的映射,这又是干嘛,注意:targetMap是全局的

exportconsttargetMap:WeakMap =newWeakMap()if(!targetMap.has(target)) {    targetMap.set(target,newMap())  }复制代码

在这里先给大家卖个关子,targetMap非常重要,是用来保存依赖的地方

讲完了reactive,可以回到一开始的引子

依赖收集

说到依赖收集,不得不提到,依赖的创建,那么Vue3.0是在哪里创建了渲染依赖呢,大家可以找到下面这段代码以及文件

// vue-next\packages\runtime-core\src\createRenderer.tsfunctionsetupRenderEffect(    instance: ComponentInternalInstance,

    parentSuspense: HostSuspsenseBoundary | null,

    initialVNode: HostVNode,

    container: HostElement,

    anchor: HostNode | null,

    isSVG: boolean

  ){// create reactive effect for renderingletmounted =falseinstance.update = effect(functioncomponentEffect(){// ...}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)  }复制代码

代码特别长,我剪掉了中间部分,大家还记得effect有个选项lazy吗,没错,它默认是false,也就会立即调用传入的componentEffect回调,在它内部调用了patch实现了组件的挂载。

敲黑板,关键来了,还记得effect调用,内部会调用run方法吗

functionrun(effect: ReactiveEffect, fn: Function, args: any[]):any{if(!effect.active) {returnfn(...args)  }if(activeReactiveEffectStack.indexOf(effect) ===-1) {    cleanup(effect)try{      activeReactiveEffectStack.push(effect)returnfn(...args)    }finally{      activeReactiveEffectStack.pop()    }  }}复制代码

这里进行了第一步的依赖收集,保存在全局数组中,为了方便触发get的对象,将依赖收集到自己的deps中

然后就是调用patch,进行组件挂载

if(!mounted) {constsubTree = (instance.subTree = renderComponentRoot(instance))// beforeMount hookif(instance.bm !==null) {        invokeHooks(instance.bm)    }    patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)    initialVNode.el = subTree.el// mounted hookif(instance.m !==null) {        queuePostRenderEffect(instance.m, parentSuspense)    }    mounted =true}复制代码

至于它内部实现,我就不讲了,不是本文重点,然后我们去编译的地方看看

//vue-next\packages\runtime-core\src\component.tsfunctionfinishComponentSetup(  instance: ComponentInternalInstance,

  parentSuspense: SuspenseBoundary | null){constComponent = instance.typeasComponentOptionsif(!instance.render) {if(Component.template && !Component.render) {if(compile) {        Component.render = compile(Component.template, {          onError(err) {}        })      }elseif(__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)asRenderFunction  }// ...其他}复制代码

上面的代码是编译部分,我们来看看例子中编译后是什么样

(functionanonymous(){const_Vue = Vueconst_createVNode = Vue.createVNodeconst_hoisted_1 = {id:"box"}returnfunctionrender(){with(this) {const{toString: _toString,createVNode: _createVNode,openBlock: _openBlock,createBlock: _createBlock } = _Vuereturn(_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.tsfunctioncreateGetter(isReadonly: boolean){returnfunctionget(target: any, key: string | symbol, receiver: any){constres =Reflect.get(target, key, receiver)if(typeofkey ==='symbol'&& builtInSymbols.has(key)) {returnres    }// _isRefif(isRef(res)) {returnres.value    }    track(target, OperationTypes.GET, key)// 如果该属性对应的值还是对象,就继续递归创建响应式returnisObject(res)      ? isReadonly        ?// need to lazy access readonly and reactive here to avoid// circular dependencyreadonly(res)        : reactive(res)      : res  }}复制代码

调用Reflect.get获取属性值

如果key是symbol并且是Symbol的一个属性,就直接返回该值

// 这种情况constkey =Symbol('key')conststate = reative({    [key]:'symbol value'})state[key]复制代码

如果值为Ref返回该值的value,看到这里如果大家有了解过ref api的话就知道了,由于ref它自己实现了自己的get,set,所以不再需要执行后面的逻辑,这个在后面会讲

调用track

递归深度观测,使整个对象都为响应式

下面我会详细讲解

track

在讲它之前,先了解它有哪些参数

target: any,// 目标对象type: OperationTypes,// 追踪数据变化类型,这里是getkey?: string | symbol// 需要获取的keyexportconstenum OperationTypes {      SET ='set',      ADD ='add',      DELETE ='delete',      CLEAR ='clear',      GET ='get',      HAS ='has',      ITERATE ='iterate'}复制代码

exportfunctiontrack(  target: any,

  type: OperationTypes,

  key?: string | symbol){if(!shouldTrack) {return}// 获取activeReactiveEffectStack中的依赖consteffect = activeReactiveEffectStack[activeReactiveEffectStack.length -1]if(effect) {if(type === OperationTypes.ITERATE) {      key = ITERATE_KEY    }// 获取目标对象对应的依赖mapletdepsMap = targetMap.get(target)if(depsMap ===void0) {      targetMap.set(target, (depsMap =newMap()))    }// 获取对应属性的依赖letdep = depsMap.get(keyasstring | symbol)// 如果该依赖不存在if(!dep) {// 设置属性对应依赖depsMap.set(keyasstring | symbol, (dep =newSet()))    }// 如果属性对应依赖set中不存在该依赖if(!dep.has(effect)) {// 添加到依赖set中dep.add(effect)      effect.deps.push(dep)if(__DEV__ && effect.onTrack) {// 调用onTrack钩子effect.onTrack({          effect,          target,          type,          key        })      }    }  }}复制代码

activeReactiveEffectStack我两次提到,从它这里拿到了依赖,注意后面执行完依赖后,会从它里面弹出

如果effect存在

从targetMap中获取对象,对饮的Map,具体的数据结构类似这样

conststate = 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属性访问器

functionset(  target: any,

  key: string | symbol,

  value: any,

  receiver: any):boolean{  value = toRaw(value)consthadKey = hasOwn(target, key)constoldValue = target[key]// 如果旧的值是ref,而新的值不是refif(isRef(oldValue) && !isRef(value)) {// 直接更改原始ref即可oldValue.value = valuereturntrue}constresult =Reflect.set(target, key, value, receiver)// don't trigger if target is something up in the prototype chain of originalif(target === toRaw(receiver)) {/* istanbul ignore else */if(__DEV__) {constextraInfo = { oldValue,newValue: value }if(!hadKey) {        trigger(target, OperationTypes.ADD, key, extraInfo)      }elseif(value !== oldValue) {        trigger(target, OperationTypes.SET, key, extraInfo)      }    }else{if(!hadKey) {        trigger(target, OperationTypes.ADD, key)      }elseif(value !== oldValue) {        trigger(target, OperationTypes.SET, key)      }    }  }returnresult}复制代码

判断旧值是ref,新值不是ref

// 这种情况constval = ref(0)conststate = 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

exportfunctiontrigger(  target: any,

  type: OperationTypes,

  key?: string | symbol,

  extraInfo?: any){constdepsMap = targetMap.get(target)if(depsMap ===void0) {// never been trackedreturn}// effect setconsteffects:Set =newSet()// computed effect setconstcomputedRunners:Set =newSet()if(type === OperationTypes.CLEAR) {    depsMap.forEach(dep=>{      addRunners(effects, computedRunners, dep)    })  }else{// 添加effect到set中if(key !==void0) {      addRunners(effects, computedRunners, depsMap.get(keyasstring | symbol))    }// also run for iteration key on ADD | DELETEif(type === OperationTypes.ADD || type === OperationTypes.DELETE) {constiterationKey =Array.isArray(target) ?'length': ITERATE_KEY      addRunners(effects, computedRunners, depsMap.get(iterationKey))    }  }// 执行set中的effectconstrun =(effect: ReactiveEffect) =>{    scheduleRun(effect, target, type, key, extraInfo)  }  computedRunners.forEach(run)  effects.forEach(run)}复制代码

看到这个函数开始的targetMap,大家应该很清楚要干嘛了吧,没错,拿到对象的Map,它包含了属性的所有依赖

如果没有Map直接返回

创建了两个Set,要干嘛用呢

// 用来保存将要执行的依赖consteffects:Set =newSet()// computed依赖,因为trigger不仅是要处理effect,watch,还要处理computed惰性求值的情况constcomputedRunners:Set =newSet()复制代码

处理三种情况CLEAR,ADD,DELETE,SET(这里没有标识)

// effect setconsteffects:Set =newSet()// computed effect setconstcomputedRunners:Set =newSet()functionaddRunners(  effects: Set<ReactiveEffect>,

  computedRunners: Set<ReactiveEffect>,

  effectsToAdd: Set<ReactiveEffect> | undefined){if(effectsToAdd !==void0) {    effectsToAdd.forEach(effect=>{if(effect.computed) {        computedRunners.add(effect)      }else{        effects.add(effect)      }    })  }}复制代码

可以看到,三种情况实际上都差不多,唯一的区别就是,如果添加的对象是数组,就会拿到length属性的依赖,用于修改数组长度

if(type === OperationTypes.ADD || type === OperationTypes.DELETE) {constiterationKey =Array.isArray(target) ?'length': ITERATE_KEY    addRunners(effects, computedRunners, depsMap.get(iterationKey))}复制代码

执行属性对应的依赖

// 执行set中的effectconstrun =(effect: ReactiveEffect) =>{    scheduleRun(effect, target, type, key, extraInfo)  }  computedRunners.forEach(run)  effects.forEach(run)复制代码

functionscheduleRun(  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 !==void0) {    effect.scheduler(effect)  }else{    effect()  }}复制代码

最后调用了scheduleRun,它内部会分别执行onTrigger,scheduler,effect

需要注意的是,只有开发环境才会执行onTrigger,这也是为什么,前面要这么判断

if(__DEV__) {constextraInfo = { oldValue,newValue: value }if(!hadKey) {        trigger(target, OperationTypes.ADD, key, extraInfo)    }elseif(value !== oldValue) {        trigger(target, OperationTypes.SET, key, extraInfo)    }}复制代码

readonly

有了前面的基础,readonly看起来会非常简单,唯一的区别就是rawToReadonly,rawToReadonly, readonlyHandlers

exportfunctionreadonly(target: object){if(reactiveToRaw.has(target)) {    target = reactiveToRaw.get(target)  }returncreateReactiveObject(    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 "${keyasany}" failed: target is readonly.`,        target      )    }returntrue}else{returnset(target, key, value, receiver)  } }复制代码

它的实现很简单,不过LOCKED有是什么鬼,大家可以找到lock.ts

//vue-next\packages\reactivity\src\lock.tsexportletLOCKED =trueexportfunctionlock(){  LOCKED =true}exportfunctionunlock(){  LOCKED =false}复制代码

看似简单,但是却非常重要,它能够控制被readonly的对象能够暂时被更改,就比如我们常用的props,它是无法被修改的,但是Vue内部又要对他进行更新,那怎么办,话不多说,我们再源码中看他具体应用

// vue-next\packages\runtime-core\src\componentProps.tsexportfunctionresolveProps(  instance: ComponentInternalInstance,

  rawProps: any,

  _options: ComponentPropsOptions | void){consthasDeclaredProps = _options !=nullconstoptions = normalizePropsOptions(_options)asNormalizedPropsOptionsif(!rawProps && !hasDeclaredProps) {return}constprops: any = {}letattrs: any =void0constpropsProxy = instance.propsProxyconstsetProp = propsProxy    ?(key: string, val: any) =>{        props[key] = val        propsProxy[key] = val      }    :(key: string, val: any) =>{        props[key] = val      }  unlock()// 省略一些修改props操作。。lock()  instance.props = __DEV__ ? readonly(props) : props  instance.attrs = options    ? __DEV__ && attrs !=null? readonly(attrs)      : attrs    : instance.props}复制代码

这里前后分别调用了unlock和lock,这样就可以控制对readonly属性的修改

那么readonly的讲解就到这了

computed

exportfunctioncomputed(  getterOrOptions: (() =>T) |WritableComputedOptions):any{constisReadonly = isFunction(getterOrOptions)constgetter = isReadonly    ?(getterOrOptionsas(() =>T))    : (getterOrOptionsasWritableComputedOptions).getconstsetter = isReadonly    ?null: (getterOrOptionsasWritableComputedOptions).setletdirty: boolean =trueletvalue: any =undefinedconstrunner = effect(getter, {lazy:true,computed:true,scheduler:()=>{      dirty =true}  })return{_isRef:true,// expose effect so computed can be stoppedeffect: runner,    get value() {if(dirty) {        value = runner()        dirty =false}      trackChildRun(runner)returnvalue    },    set value(newValue) {if(setter) {        setter(newValue)      }else{// TODO warn attempting to mutate readonly computed value}    }  }}复制代码

首先是前面这段

constisReadonly = isFunction(getterOrOptions)constgetter = isReadonly    ?(getterOrOptionsas(() =>T))    : (getterOrOptionsasWritableComputedOptions).getconstsetter = isReadonly    ?null: (getterOrOptionsasWritableComputedOptions).set复制代码

大家都知道computed是可以单独写一个函数,或者get,set访问的,这里不多讲

然后调用了effect,这里lazy设置为true, scheduler可以更改dirty为true

construnner = effect(getter, {lazy:true,computed:true,scheduler:()=>{        dirty =true}})复制代码

然后我们具体来看看,返回的对象

{_isRef:true,// expose effect so computed can be stoppedeffect: runner,    get value() {if(dirty) {        value = runner()        dirty =false}      trackChildRun(runner)returnvalue    },    set value(newValue) {if(setter) {        setter(newValue)      }else{// TODO warn attempting to mutate readonly computed value}    }  }复制代码

先说说set吧,尤大似乎还没写完,只是单纯能修改值

然后是get,注意dirty的变化,如果computed依赖了state中的值,初次渲染时,他会调用依赖,然后dirty = false,关键来了,最后执行了trackChildRun

functiontrackChildRun(childRunner: ReactiveEffect){constparentRunner =    activeReactiveEffectStack[activeReactiveEffectStack.length -1]if(parentRunner) {for(leti =0; i < childRunner.deps.length; i++) {constdep = 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

constconvert = (val: any):any=>(isObject(val) ? reactive(val) : val)exportfunctionref(raw: T):Ref{  raw = convert(raw)constv = {_isRef:true,    get value() {      track(v, OperationTypes.GET,'')returnraw    },    set value(newVal) {      raw = convert(newVal)      trigger(v, OperationTypes.SET,'')    }  }returnvasRef}复制代码

ref的实现真的很简单了,前面已经学习了那么多,相信大家都能看懂了,区别就是convert(raw)对传入的值进行了简单判断,如果是对象就设置为响应式,否则返回原始值。

最后

终于分析完了,Vue3.0响应系统使用了Proxy相比于Vue2.0的代码真的简洁许多,也好理解,说难不难。其实还有watch并没有讲,它没有在reactivity中,但是实现还是使用了effect,套路都是一样的。最后谢谢大家观看。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容