[Vue] 解析 Vue 框架底层源码(上)

2023 年 1 月 27 日 星期五(已编辑)
9

[Vue] 解析 Vue 框架底层源码(上)

温馨提示:在写这篇文章的时候其实是图文并茂的,但由于图片都保存在第三方平台的图床中(notion, juejin),搬运到博客也较为麻烦一些,所以博文中就没有图片,如果对图片感兴趣的小伙伴,可以看我的掘金文章,那里有图文并茂的源码解释

链接: https://juejin.cn/column/7149818417325801503

目录

1. 组件挂载原理

2. 组件更新原理

3. Reactive

4. Computed

5. watch

6. ref

7. emit

8. slot

组件挂载原理

代码示例

<body>
  <script src="../dist/runtime-dom.global.js"></script>
  <div id="app"></div>
  <script>
    const { createApp, h } = VueRuntimeDOM;
    const App = {
      render() {
        return h("div", "hello world");
      },
    };
    let app = createApp(App, {});
    app.mount("#app"); 
  </script>
</body>

组件挂载

第一:执行第一句代码 createApp 函数,传入 App 组件。实际上调用的是 createAppAPI 函数返回的 createApp,在 createApp 函数中中返回 app 对象,app对象上有 mount, use, component, directive, mixin…… 等等常用的方法。 ``` export function createAppAPI( render: RootRenderFunction, hydrate?: RootHydrateFunction ): CreateAppFunction { return function createApp(rootComponent, rootProps = null) { // 组件、属性 const context = createAppContext()// 创造应用的上下文 const installedPlugins = new Set() //要安装的插件 let isMounted = false

  const app: App = (context.app = {  
    _uid: uid++,
    _component: rootComponent as ConcreteComponent,
    _props: rootProps,
    _container: null,
    _context: context,
    version,
    use(plugin: Plugin, ...options: any[]) { // use方法
    },
    mixin(mixin: ComponentOptions) { // mixin方法
    },
    directive(name: string, directive?: Directive) { // 对应的指令
    },
    mount(rootContainer: HostElement, isHydrate?: boolean): any { // 挂载方法
    },
    unmount() {
    },
    provide(key, value) {
    }
  })
  return app
}
  
}

第二:执行 app.mount("#app"), 通过 createVNode 递归创建虚拟 Dom, 我们的 Hello World 场景中是如下情况:
typescript const app = { mount(rootContainer: HostElement, isHydrate?: boolean): any { // 挂载方法 if (!isMounted) { const vnode = createVNode( // 创造vnode节点 rootComponent as ConcreteComponent, rootProps ); render(vnode, rootContainer); // 渲染vnode到容器中 isMounted = true; // 挂载完毕 app._container = rootContainer;
  return vnode.component!.proxy;
}
  
},

}; ```

第三:执行 render 函数将 vnode 渲染到容器当中。在render 函数调用 patch 方法,初次挂载 patch 的第一参数为 null。进入 patch 函数。

const render: RootRenderFunction = (vnode, container, isSVG) => {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  flushPreFlushCbs()
  flushPostFlushCbs()
  container._vnode = vnode
}

第四:在 patch 函数中根据就不同的标签,进行不同的 process。这里是 App 组件标签,会调用 processComponent 函数。每个 process 处理函数都会对应两个逻辑,第一个是挂载,第二个是更新。我们这里调用的是挂载函数 mountComponent。

const processComponent = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  n2.slotScopeIds = slotScopeIds
  if (n1 == null) {
    if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
      ;(parentComponent!.ctx as KeepAliveContext).activate(
        n2,
        container,
        anchor,
        isSVG,
        optimized
      )
    } else {
      mountComponent(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  } else {
    updateComponent(n1, n2, optimized)
  }
}

第五:在 mountComponent 调用 setupRenderEffect 函数进行组件初始化。

const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )
}

第六:调用 setupRenderEffect 函数。

  • 创建 componentUpdateFn 函数,该函数 是 ReactiveEffct 对象的 fn 属性。不懂这句话的意思,可以看之前的文章。Vue3 Reactive原理

  • new ReactiveEffect 函数,创建 ReactiveEffect 对象,并且传入第二个参数 scheduler,进行组件更新的批处理。
  • 绑定 update 方法给 组件 instance。
  • 调用 update 方法,调用 effect.fn 属性,也就是调用 componentUpdateFn 函数。
const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {}

  // create reactive effect for rendering
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),
    instance.scope // track it in component's effect scope
  ))

  const update: SchedulerJob = (instance.update = () => effect.run())

  update()
}

第七: componentUpdateFn 函数中,在 componentUpdateFn 中也有两个函数,一个是挂载,一个是更新,这里我们走到挂载的逻辑。

  • 调用 renderComponentRoot 函数,调用组件的 render 方法,得到子节点,放到 组件 instance.subTree 属性上。
  • 继续拿着子节点去 Patch。
const componentUpdateFn = () => {
  if (!instance.isMounted) {
    let vnodeHook: VNodeHook | null | undefined
    const { el, props } = initialVNode
    const { bm, m, parent } = instance
    const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

    const subTree = (instance.subTree = renderComponentRoot(instance))

    patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)

    initialVNode.el = subTree.el
    instance.isMounted = true
  }
}

第八:patch 子节点,也就是 h1 元素,在 patch 函数中根据就不同的标签,进行不同的 process。这里是普通节点标签,会调用 processElement。调用 mountElement 函数。

const processElement = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  isSVG = isSVG || (n2.type as string) === 'svg'
  if (n1 == null) {
    mountElement(
      n2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else {
    patchElement(
      n1,
      n2,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}

第九:在 mountElement 函数中:

  • hostCreateElement 函数,创建 H1 节点赋值给 vnode.el 属性。
  • 深度优先原则去 patch 子节点,这里没有我们默认跳过。
  • hostPatchProp 函数,挂载 props。
  • Object.defineProperty 方法将 vnode 定义为 el.__vnode 属性。
  • hostInsert 函数,传入容器和锚点 anchor 通过 parent.insertBeofore 函数插入节点。自此 h1 被插入到了 div#app 中。
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },
}
const mountElement = (
  vnode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  let el: RendererElement
  let vnodeHook: VNodeHook | undefined | null
  const { type, props, shapeFlag, transition, dirs } = vnode

  el = vnode.el = hostCreateElement(
    vnode.type as string,
    isSVG,
    props && props.is,
    props
  )

  // mount children first, since some props may rely on child content
  // being already rendered, e.g. `<select value>`
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    hostSetElementText(el, vnode.children as string)
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(
      vnode.children as VNodeArrayChildren,
      el,
      null,
      parentComponent,
      parentSuspense,
      isSVG && type !== 'foreignObject',
      slotScopeIds,
      optimized
    )
  }

  // props
  if (props) {
    for (const key in props) {
      if (key !== 'value' && !isReservedProp(key)) {
        hostPatchProp(
          el,
          key,
          null,
          props[key],
          isSVG,
          vnode.children as VNode[],
          parentComponent,
          parentSuspense,
          unmountChildren
        )
      }
    }

    if ('value' in props) {
      hostPatchProp(el, 'value', null, props.value)
    }
    if ((vnodeHook = props.onVnodeBeforeMount)) {
      invokeVNodeHook(vnodeHook, parentComponent, vnode)
    }
  }

  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    Object.defineProperty(el, '__vnode', {
      value: vnode,
      enumerable: false
    })
    Object.defineProperty(el, '__vueParentComponent', {
      value: parentComponent,
      enumerable: false
    })
  }

  hostInsert(el, container, anchor)
}

自此组件挂载完毕。

组件更新

代码示例

<body>
  <script src="../dist/runtime-dom.global.js"></script>
  <div id="app"></div>
  <script>
    const { createApp, h, reactive } = VueRuntimeDOM;

    const Son = {
      props: {
        name: String,
      },
      render() {
        return h("p", this.name);
      },
    };

    const App = {
      setup() {
        const state = reactive({ name: "zf" });
        setTimeout(() => {
          state.name = "jw";
        }, 3000);

        return () => {
          return h(Son, { name: state.name });
        };
      },
    };

    createApp(App).mount("#app");
  </script>
</body>

挂载阶段

第一步,执行第一句代码,createApp(App).mount("#app"); 进行组件挂载。可以查看之前的文章,来看详细流程。

我们这里只聊聊组件挂载的核心,详细挂载请看: Vue3 组件挂载原理,执行 setup 函数。

  • 对于 App 组件来讲,挂载时的核心就是会创建组件的 ReactiveEffect 对象。
  • 每次更新和挂载都会调度 instance.update() 也就是 componentUpdateFn 函数。
  • 并且将将返回的函数作为 render 函数,进行调用得到组件的虚拟 Dom 节点,来作为 instance.subtree 属性。
  • Son 组件同理。

第二步:render 函数中 state.name 触发,触发 state 响应式对象的的 getter 操作,收集 组件的 ReaciveEffect 对象作为响应式对象的依赖。如下结构:


{
    { name: "zf" }: {
        name : [组件的 ReaciveEffect, 其他的 ReaciveEffect(比如 watch)]
     }
}

挂载核心操作完毕。

更新阶段

执行第一句代码:state.name = "jw"

第一:state.name = "jw", 原理可以看这篇文章:Vue 3.2 - Reactive 原理,这里几句带过,触发响应式对象 state 的 setter 操作,通过 Reflect.set 方法更新属性,然后触发 trigger 操作,执行 name 属性的 RectiveEffects 对象的 fn 属性,也就是 componentUpdateFn 函数。

第二:在 componentUpdateFn 函数中,调用 renderComponentRoot 函数,该函数中,再次调用 instance.render.call(), 获得新的虚拟 Dom, 再去将旧的和新的虚拟 Dom 进行 patch。

const nextTree = renderComponentRoot(instance)

patch(
  prevTree,
  nextTree,
  // parent may have changed if it's in a teleport
  hostParentNode(prevTree.el!)!,
  // anchor may have changed if it's in a fragment
  getNextHostNode(prevTree),
  instance,
  parentSuspense,
  isSVG
)

第三:由于本次是更新逻辑,所以来到了 updateComponent 方法,在 updateComponent 调用 Son 组件 instace.update(),也就是 componentUpdateFn 函数。 在该函数中调用了 Son 组件 instance.render 方法,拿到最新的虚拟 Dom 进行渲染。

if (shouldUpdateComponent(n1, n2, optimized)) {
    instance.next = n2
    invalidateJob(instance.update)
    instance.update()
  }

this 访问的时候,实际上是对 instance 上属性的代理。这里是可以代理的属性,请大家自行参考。

Reactive

代码示例

<script src="./dist/reactivity.global.js"></script>
<body>
  <div id="app"></div>
  <script>
    const { effect, reactive, ref, watch } = VueReactivity

    const state = reactive({ name: 'cyan', age: 18 })

    effect(() => {
      app.innerHTML = state.name + state.age
    })

    setTimeout(() => {
      state.name = 'tom'
    }, 1000)
  </script>
</body>

挂载阶段

执行第一句代码:const state = reactive({ name: 'cyan', age: 18 })

第一:reactive 会通过调用 createReactiveObject 函数来创建响应式对象。

  export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

第二: createReactiveObject 函数中 通过 proxy 和 baseHandlers 创建响应式对象。并将响应式对象返回给 state。

const proxy = new Proxy(
  target,
  targetType = baseHandlers
)
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {


 // 如果已经是一个响应式对象了,直接返回该响应式对象
  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
}

执行第二句代码 effect(() => {app.innerHTML = state.name + state.age})

第三:调用 effect 函数

  • 通过 new ReactiveEffect 来创建 ReactiveEffect 对象。
  • 执行 ReactiveEffect 对象上的 Run 方法,之后返回 run 方法,暴露给用户进行调度(可以用于批量更新)。
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {

  const _effect = new ReactiveEffect(fn)
  _effect.run()
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

第四:ReactiveEffect 类的 constructor 被调用, 将 effect 的参数作为 ReactiveEffect 对象的 fn 属性。

constructor(
  public fn: () => T,
  public scheduler: EffectScheduler | null = null,
  scope?: EffectScope
) {
  recordEffectScope(this, scope)
}

第五:调用 run 方法,当前 activeEffect 指向 effect 函数创建的 ReactiveEffect 对象,ReactiveEffect 对象的 fn 属性被调用, 调用过程中去访问了响应式对象 state(state.name + state.age),从而触发响应式对象/代理Proxy 对象的 getter 方法。

ReactiveEffect.fn = () => {
        app.innerHTML = state.name + state.age; 
 })

第六:在 state 的 getter 方法中

  • 先通过 Reflect.get 取值。
  • 调用 track 函数跟踪当前的 activeEffefcts 依赖对象。
  • 如果是一个引用类型值,继续去通过 reactive 递归创建响应式对象。
  • 返回 Reflect.get 的取值结果。
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)
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

第七:通过 track函数, trackEffects 函数建立双 Map 结构跟踪属性依赖。结果是这样的:我们访问了 state.name, state.age, 所以 name,age 对应的 映射列表里有 当前 activeEffects 也就是 ReactiveEffect 对象。

const targetMap = {
    {name: "cyan", age: 18}: {
        name: [ReactiveEffect],
        age: [ReactiveEffect]
    }
}

图中还有一个细节没有表示,每一个 ReactiveEffefct 对象的 deps 属性,都指向该属性的依赖对象列表。用于每次 run 之后清理依赖对象。

第七:到这里为止,初始化完毕。app.innerHTML 中初始值渲染完成。

更新阶段:

第一:当执行 state.name = "tom" 时,会触发响应式对象的 settter 操作,在 settter 操作中

  • 通过 Reflect.set 改变响应式对象的值。
  • 然后通过 trigger 函数,触发 ReactiveEffects 依赖对象。
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
  
    // 如果新旧值相同,不更新
    let oldValue = (target as any)[key]
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false
    }
    
    const result = Reflect.set(target, key, value, receiver)

    trigger(target, TriggerOpTypes.SET, key, value, oldValue)

    return result
  }

第二:通过 trigger, triggerEffects, triggerEffect 三个函数,来触发更新,通过 cleanEffects 清理上次的依赖对象, 执行每个 ReactEffect 依赖对象的 run 方法,重新调用 ReactEffcts 对象的 fn属性。也就是 effect 函数的第一个参数。 也就是 javascriptReactiveEffect.fn = () => {app.innerHTML = state.name + state.age; })

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  triggerEffects(createDep(effects), eventInfo)
}

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  effect.run()
}

第三:state.name 又去调用了响应式对象的 getter 方法,由于通过 Reflect.set 修改了值,所以再次访问就是最新值,同时 再次收集依赖,返回新值,等待下一次更新。

第四:至此更新完毕。

Computed

代码示例

<script src="./dist/reactivity.global.js"></script>
<body>
  <div id="app"></div>
  <script>
    let { effect, reactive, ref, shallowRef, toRef, computed } = VueReactivity


    let state = reactive({ firstName: 'Tom', lastName: 'Benjamin' }) // 将普通的类型 转化成一个对象,这个对象中有value属性 指向原来的值

    const fullName = computed({
      // getter
      get() {
        return state.firstName + ' ' + state.lastName
      },
      // setter
      set(newValue) {
        ;[state.firstName, state.lastName] = newValue.split(' ')
      }
    })

    effect(() => {
      app.innerHTML = fullName.value
    })

    setTimeout(() => {
      fullName.value = 'Cyan Benjamin'
    }, 2000)

挂载阶段

第一:第一篇文章详细介绍了 reactive 创建响应式流程,我们这里默认已经创建好了响应式对象 obj。 [[Vue 源码] Vue 3.2 - Reactive 原理](https://juejin.cn/post/7208708762265550885)

执行 第二句代码: const fullName = computed({ // getter get() { return state.firstName + ' ' + state.lastName }, // setter set(newValue) { ;[state.firstName, state.lastName] = newValue.split(' ') } }) 第二:调用 computed 函数, 在 computed 函数中初始化 getter 和 setter函数,然后通过 new ComputedRefImpl 创建 computedRef 对象。

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  getter = getterOrOptions.get
  setter = getterOrOptions.set

  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
  return cRef as any
}

第三: 执行 ComputedRefImpl 的 constructor 方法

  • 创建 ReactiveEffect 对象 赋值给 computed 对象的 effect 属性。创建过程 [[Vue 源码] Vue 3.2 - Reactive 原理](https://juejin.cn/post/7208708762265550885) 有详细说明。
  • 默认创建好了 ReactiveEffect 对象 后 将 computedRef 的 getter 作为 ReactiveEffect 对象对象的 fn 属性。
  • 创建ReactiveEffect 对象时 传入第二个参数 -> scheduler 函数。
  • 初始化 dirry 属性 false 来表示是否为脏数据。
  • 返回 computed 对象 > dirty 表示是否为脏数据来缓存计算属性,第一次或者说默认取值是脏数据执行 effect.run 方法取值,二次/多次取值 dirty 不是脏数据, 取缓存数据,等待依赖变了 scheduler 将 dirty 置为 true, 下次取值会重新执行 effect.run 方法来计算缓存值。
public _dirty = true
public _cacheable: boolean

constructor(
  getter: ComputedGetter<T>,
  private readonly _setter: ComputedSetter<T>,
  isReadonly: boolean,
  isSSR: boolean
) {
  this.effect = new ReactiveEffect(getter, () => {
    if (!this._dirty) {
      this._dirty = true
      triggerRefValue(this)
    }
  })
    
  this.effect.computed = this
  this.effect.active = this._cacheable = !isSSR
  this[ReactiveFlags.IS_READONLY] = isReadonly
}

第三句代码调用:effect(() => {app.innerHTML = fullName.value}) 第四:

  • effect 函数调用,创建 ReactiveEffect 对象,执行 ReactiveEffect 对象上的 Run 方法,将 activeEfffect 对象只为当前创建的ReactiveEffect 对象,清除上一次的依赖列表, 执行 () => {app.innerHTML = fullName.value}
  • 执行 fullname.value 触发 fullname.value 的 getter 函数。 javascript get value() { trackRefValue(self) if (self._dirty || !self._cacheable) { self._dirty = false self._value = self.effect.run()! } return self._value }
  • getter 函数中 调用 trackRefValue(self)。
    • trackRefValue 中调用 trackEffects 函数,使用 computedRef 对象的 dep 属性来收集依赖对象。也就是将 activeEffect 对象也就是 effect 产生的 ReacttiveEffects 传入到 computedRef 对象的依赖对象列表当中去。
    • 执行
export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    if (__DEV__) {
      trackEffects(ref.dep || (ref.dep = createDep()), {
        target: ref,
        type: TrackOpTypes.GET,
        key: 'value'
      })
    } else {
      trackEffects(ref.dep || (ref.dep = createDep()))
    }
  }
}
  • getter 函数中 调用 self.effect.run()。也就是调用 computed 的 getter 或者说 ReactiveEffects 对象的 fn属性,也就是 get() { return state.firstName + ' ' + state.lastName }

  • 执行 obj.firstName 和 obj.lastName 触发 obj 的 getter ,将当前 computed 的 ReactEffect 对象收集到 firstName, lastName 属性的 effects Set 集合当中。通过 Reflect.get 方法得到值。

const targetMap  = {
{ firstName: 'Tom', lastName: 'Benjamin' }: {
    firstName: [ReactEffect],
    lastName: [ReactEffect]
}
}
  • 返回计算值赋值给 computed 对象的 _value 属性。

至此初次渲染完毕。

更新阶段

第一:fullname.value = "Cyan Benjamin", 触发 computedRef 对象 value 属性的 setter 方法,调用 this.settter 函数,传入新值。

set value(newValue: T) {
  this._setter(newValue)
}

第二:

  • setter 函数中通过 Reflect.set 给响应式对象赋值
  • 触发响应式对象 的 setter 操作, 通过 Reflect.set() 修改 lastname 和 firstname。
  • 继而触发 trigger 操作,不过这一次并没有执行该 ReactiveEffect 对象的run 方法,而是优先执行了 scheduler 方法。
if (effect.scheduler) {
  effect.scheduler()
} else {
  effect.run()
}

第三:scheduler 方法中执行 triggerRefValue ,

var scheduler = () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    }
  • 在 triggerRefValue 中 调用了 triggerEffects 函数,trigerEffects 执行了 triggerEffect 函数。
  • triggerEffect 函数中遍历 computedRef 对象的依赖列表。
  • 依次然后调用依赖对象的 run 方法
  • run 方法中 先通过 cleanEffect 函数清空依赖。 然后让 activeEffect 指向当前的 ReactiveEffect 对象(computed 创建的 ReactiveEffect 对象), 执行依赖对象的 fn 函数,也就是 getter 方法, 也就是 get() { return state.firstName + ' ' + state.lastName }
  • getter 方法中拿到最新值,同时 stata.firstName , state.lastName 又一次触发了 响应式对象 触发 getter 操作, 再一次跟踪activeEffect 依赖对象(computed 创建的 ReactiveEffect 对象)。
  • 返回并渲染最新值,等待下一次更新。

自此更新完毕

watch

代码示例

<script src="./dist/vue.global.js"></script>

<body>
  <div id="app"></div>
  <script>
    let { reactive, watch } = Vue
    let state = reactive({ age: 0 })

    watch(
      () => state.age,
      (newValue, oldValue) => {
        app.innerHTML = `count is ${newValue} `
      }, {flush: "sync"}
    )

    setInterval(() => {
      state.age++
    }, 1000)
  </script>

挂载阶段

第一:第一篇文章详细介绍了 reactive 创建响应式流程,我们这里默认已经创建好了响应式对象 obj。 [[Vue 源码] Vue 3.2 - Reactive 原理](https://juejin.cn/post/7208708762265550885 "https://juejin.cn/post/7208708762265550885")

执行 第二句代码:调用 watch 函数。

watch(
       () => state.age,
       (newValue, oldValue) => {
         app.innerHTML = `count is ${newValue} `
       }, {flush: "sync"}
     )

第一:watch 函数调用了 doWatch 函数,

export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  return doWatch(source as any, cb, options)
}

第二:调用 doWatch 函数, 下面是 doWatch 函数流程

  • 在 doWatch 函数中 初始化 getter // 初始化 getter if (isRef(source)) { getter = () => source.value forceTrigger = isShallow(source) } else if (isReactive(source)) { getter = () => source deep = true }else if (isFunction(source)) { if (cb) { // getter with cb getter = () => source() } }
  • 如果是 reactive 响应式对象,则需要深度监听,deep = true
  • 初始化清理函数 clearUp 可以执行上一个 watch 的 cb 函数
// 清理函数
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
  cleanup = effect.onStop = () => {
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  }
}
  • 初始化 job 函数 const job: SchedulerJob = () => { if (!effect.active) { return } if (cb) { // watch(source, cb) const newValue = effect.run() if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) ) { // cleanup before running cb again if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, // pass undefined as the old value when it's changed for the first time oldValue === INITIAL_WATCHER_VALUE ? undefined : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, onCleanup ]) oldValue = newValue } } }
  • job 函数 作为 ReactEffective 对象的 scheduler 函数。

let scheduler: EffectScheduler
if (flush === 'sync') {
  scheduler = job as any // the scheduler function gets called directly
} 

const effect = new ReactiveEffect(getter, scheduler)
  • 调用 ReactiveEffect 对象的 run 方法
    • cleanEffect 清理依赖列表
    • 将 activeEffects 置为 watch 创建的 ReactiveEffect 对象
    • 调用 getter 方法,也就是 () => state.num , 继而触发 state 响应式对象的getter 方法,将依赖收集到 num 属性的依赖列表里 {num: [ReactiveEffect]}
    • Reflect.get 返回值,成功初始化 oldValue。
    • 返回 Unwatch 函数。 // initial run oldValue = effect.run() const unwatch = () => { effect.stop() if (instance && instance.scope) { remove(instance.scope.effects!, effect) } } ) return unwatch } 自此 挂载阶段完毕

更新阶段

第一:调用 setInterval(() => { state.age++ }, 1000) state.age 触发响应式的 setter 操作。

第二:

  • setter 函数中, 通过 Reflect.set() 修改 state.age值。
  • 继而触发 trigger 操作,不过这一次并没有执行该 ReactiveEffect 对象的run 方法,而是优先执行了 scheduler 方法。
if (effect.scheduler) {
  effect.scheduler()
} else {
  effect.run()
}

第三:scheduler 方法中执行 job 函数(挂载的时候)

第四:job 函数中执行 effect.run() 函数.

  • 再次触发 getter 拿到最新值 newValue。
  • 调用 cleanUp 清理函数。
  • 请用 cb 函数,传入 oldValue 和 newValue
  • 等待下一次更新时再返回第一步。 const job: SchedulerJob = () => { if (!effect.active) { return } if (cb) { // watch(source, cb) const newValue = effect.run() if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) ) { // cleanup before running cb again if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, // pass undefined as the old value when it's changed for the first time oldValue === INITIAL_WATCHER_VALUE ? undefined : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, onCleanup ]) oldValue = newValue } } }

    自此更新完毕。

Ref

代码示例

<script src="./dist/reactivity.global.js"></script>
<body>
  <div id="app"></div>
  <script>
    let { ref, effect } = VueReactivity
    let state = ref(false)

    effect(() => {
      app.innerHTML = `ref is ${state.value} `
    })

    setTimeout(() => {
      state.value = true
    }, 2000)
  </script>

执行第一句代码:ref(false), 调用 ref 函数 第一: ref函数,调用 createRef 函数。

export function ref(value?: unknown) {
  return createRef(value, false)
}

第二:createRef 函数中, 通过 new RefImpl 实例化 ref对象。

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

第三:调用 RefImpl 的 constructor 构造函数。构造函数中初始化 ref 实例对象的 _value 和 _rawValue, 本例中是将 false 传给 toRective, 如果 value 是一个对象则将 通过 reactive 函数包装成 响应式对象,本例中直接返回 false。

第四:返回创建好的 ref 对象。 constructor(value: T, public readonly __v_isShallow: boolean) { this._rawValue = __v_isShallow ? value : toRaw(value) this._value = __v_isShallow ? value : toReactive(value) }

第四:开始执行第二句代码 effect(() => { app.innerHTML = `ref is ${state.value} ` }), effect 函数调用创建 ReactiveEffect 对象,将 activeEffect 对象置为当前的 ReactiveEffect 对象。调用 run 方法,调用 () => { app.innerHTML = `ref is ${state.value}` } 函数

第五:在该函数中触发 state 响应式 ref 对象 value 属性的 getter 操作。

  • 通过 trackRefValue 函数 收集依赖到 ref.deps 当中。 {dep: [ReactiveEffect]}

  • 返回_value 值,也就是 false get value() { trackRefValue(this) return this._value }

  • app.innerHTML = -_value.

初始渲染完成。

更新阶段

执行第一句代码:setTimeout(() => { state.value = true }, 2000)

  
 
触发 响应式对象 ref 的 value 属性的 setter 操作。
  • 修改 _value 为新值

`` javascript this._rawValue = newVal this._value = useDirectValue ? newVal : toReactive(newVal) ```

  • triggerRefValue 来触发依赖列表 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) } }

  • triggerRefValue 函数中 触发一系列 trigger 操作,我们再前面的文章多次聊到过。这里直接执行 ref 对象依赖列表 dep属性 中 ReactiveEffec 对象的 run 方法。也就是 () => { app.innerHTML = ref is ${state.value} }


    • 再次通过 state.value 触发 ref 对象 value 属性的 getter 操作,返回最新值,渲染,然后再次追踪依赖。
  • 等待下一次的更新。

自此更新完毕。

emit

代码示例

<script>
  let { createApp, reactive, toRefs, h } = Vue

  const useCount = () => {
    const state = reactive({ count: 0 })
    const setCount = () => {
      state.count++
    }

    return { state, setCount }
  }

  const MyCpn = {
    props: {
      count: {},
      onChildUpdate: {}
    },
    setup(props, { emit }) {
      return () =>
        h(
          'button',
          {
            onClick: () => {
              emit('childUpdate')
            }
          },
          [props.count]
        )
    }
  }

  const App = {
    setup() {
      let { state, setCount } = useCount()
      return {
        ...toRefs(state),
        setCount
      }
    },

    render() {
      return h(MyCpn, {
        count: this.count,
        onChildUpdate: () => {
          this.setCount()
        }
      })
    }
  }

  createApp(App).mount('#app')
</script>

emit 原理

第一:挂载逻辑可以看以前的文章:Vue 组件挂载原理, 这里我们聊关键的部分。

  • 挂载子组件的时候,将 count, onChildUpdate 自定义事件函数 作为了子组件的 props, 根据上图也可以得知。
  • 当我们在子组件中 emit 的时候,实际上调用调用的是 instance.emit 函数。
    • 在 emit 函数中,对于本例子来说,emit("childUpdate"), emit 函数会将 childUpdate 进行 handleKey, camelize 函数处理得到 onChildUpdate 事件名字,然后去 Props 当中去寻找。并且调用。
    • 另一种,情况是 v-model 的情况,也就是以update 开头的事件名字,Vue 同样也会对这种情况从 Props 中去找 update:modleValue 事件名, 同时也会找受控值 modelValue。
export function emit(
  instance: ComponentInternalInstance,
  event: string,
  ...rawArgs: any[]
) {
  const props = instance.vnode.props || EMPTY_OBJ

  let args = rawArgs
  const isModelListener = event.startsWith('update:')

  // for v-model update:xxx events, apply modifiers on args
  const modelArg = isModelListener && event.slice(7)
  if (modelArg && modelArg in props) {
    const modifiersKey = `${
      modelArg === 'modelValue' ? 'model' : modelArg
    }Modifiers`
    const { number, trim } = props[modifiersKey] || EMPTY_OBJ
    if (trim) {
      args = rawArgs.map(a => (isString(a) ? a.trim() : a))
    }
    if (number) {
      args = rawArgs.map(looseToNumber)
    }
  }

  let handlerName
  let handler =
    props[(handlerName = toHandlerKey(event))] ||
    // also try camelCase event handler (#2249)
    props[(handlerName = toHandlerKey(camelize(event)))]
  // for v-model update:xxx events, also trigger kebab-case equivalent
  // for props passed via kebab-case
  if (!handler && isModelListener) {
    handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
  }

  if (handler) {
    callWithAsyncErrorHandling(
      handler,
      instance,
      ErrorCodes.COMPONENT_EVENT_HANDLER,
      args
    )
  }

slots

代码示例

<script src="./dist/vue.global.js"></script>

<body>
  <div id="app"></div>
  <script>
    let { createApp, reactive, Fragment, toRefs, h, getCurrentInstance } = Vue

    const MyCpn = {
      setup(props, { emit, slots }) {
        console.log(getCurrentInstance(), 'child')

        return () =>
          h(Fragment, [
            h('div', slots.header()),
            h('div', slots.main()),
            h('div', slots.footer())
          ])
      }
    }

    const App = {
      render() {
        return h(MyCpn, null, {
          header: () => {
            return h('h1', 'header')
          },
          main: () => {
            return h('h1', 'main')
          },
          footer: () => {
            return h('h1', 'footer')
          }
        })
      }
    }

    createApp(App).mount('#app')
  </script>

第一:挂载子组件的时候,通过 h, createVNode 函数,创建虚拟 Dom 的时候,在 normalizeChildren 函数中 判断第三个参数是对象,则给子组件的示例上 ShapeFlags 标记了 SLOTS_CHILDREN。

type = ShapeFlags.SLOTS_CHILDREN
vnode.shapeFlag |= type

第二:挂载子组件的时候,调用 mountComponent 时,调用 initSlots 函数,来给 instance.slots 上赋值 children 属性。也就是:

{slots: {
          header: () => {
              return h('h1', 'header')
            },
          main: () => {
              return h('h1', 'main')
            },
          footer: () => {
              return h('h1', 'footer')
            }
}}
export const initSlots = (
  instance: ComponentInternalInstance,
  children: VNodeNormalizedChildren
) => {
  if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
    const type = (children as RawSlots)._
    if (type) {
      // users can get the shallow readonly version of the slots object through `this.$slots`,
      // we should avoid the proxy object polluting the slots of the internal instance
      instance.slots = toRaw(children as InternalSlots)
      // make compiler marker non-enumerable
      def(children as InternalSlots, '_', type)
    } else {
      normalizeObjectSlots(
        children as RawSlots,
        (instance.slots = {}),
        instance
      )
    }
  } else {
    instance.slots = {}
    if (children) {
      normalizeVNodeSlots(instance, children)
    }
  }
  def(instance.slots, InternalObjectKey, 1)
}

第三:在子组件上通过 调用 slots.header(), 再调用 h 函数 获得虚拟 Dom,挂载的时候调用 render 函数进行 patch(这里没有 render 函数,就会将 setup 返回的函数当作 render 函数)。

自此挂载完毕。

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...