Vue的组件和实例化模式设计思想

我在学习vue的源码的时候有时候会被Component和Instance的关系绕进去,因此编写次笔记用于学习相关的架构设计和思想,本笔记锚定的是runtime-core模块内容,总结的是组件抽象、实例化和递归渲染的内容。

Component

官方术语里这个叫做组件定义或者组件选项对象,我一般叫他组件原型,里面保存的是静态配置,它是先从Component -> VNode -> instance的顺序在工作

export interface Component {
  setup?: (setupProps: SetupProps, setupContext: SetupContext) => any;
  render?: () => VNode;
  expose: Expose;
  data?: () => Record<string, any>;
  props?: string[] | Record<string, any>;
  mounted?: (proxy: any) => void;
}

vnode.type指向静态的Component,vnode.component指向的是运行时的Instance组件,instance.type又指回Component,instance.vnode指回外壳Vnode。

在 initProps 和 updateProps 中,通过 instance.type.props读取声明的字段名单,用来区分外部传入的属性属于 instance.props 还是instance.attrs

defineProps

data在setupComponent中调用Component.data获取初始化状态对象,并用reactive包装后赋给instance.data。mounted在setupComponent中个会被包装并推入instance.m生命周期队列,执行Component.setup(instance.props, setupContext),产出响应式状态setupState或者渲染反函数render,setupComponent中赋给instance.render。

  if (Component.data) {
    instance.data = reactive(Component.data());
  }
    if (Component.mounted) {
    instance.m.push(() => {
      Component.mounted?.call(instance.proxy, instance.proxy);
    });
  }
  if (Component.setup) {
    const setupContext = {
      attrs: instance.attrs,
      slots: instance.slots,
      emit: instance.emit,
      expose(exposed = {}) {
        instance.exposed = exposed;
      },
    };
    setCurrentInstance(instance); // setup-entry
    let setupResult;
    try {
      setupResult = Component.setup(instance.props, setupContext);
    } finally {
      setCurrentInstance(null); // setup-out
    }
    if (isFunction(setupResult)) {
      instance.render = setupResult;
    } else if (isObject(setupResult)) {
      instance.setupState = proxyRefs(setupResult);
    }
  }

ComponentInstance

是框架内部维护的组件的运行时的状态,一个组件从生到死的所有动态数据都挂在它身上,贯穿整个生命周期。

export interface ComponentInstance {
  vnode: VNode;
  data: any;
  slots: Slots;
  props: Record<string, any>;
  attrs: Record<string, any>;
  emit: Emit;
  exposed: Record<string, unknown>;
  proxy: any;
  update: Function | null;
  type: Component;
  setupState: Record<string, any>;
  render: Function | null;
  subTree: VNode | null;
  isMounted: boolean;
  parent: ComponentInstance | null;
  provides: Record<PropertyKey, unknown>;
  bm: LifecycleHooksArray;
  m: LifecycleHooksArray;
  bu: LifecycleHooksArray;
  u: LifecycleHooksArray;
  bum: LifecycleHooksArray;
  um: LifecycleHooksArray;
  isUnmounted: boolean;
  effect: ReactiveEffect | null;
}

在初始化阶段setupComponent,他参与了状态存储,保存了自身的响应式数据data、setupState,以及从父组件接受到的拆分好的props、attrs、slots。

setupResult = Component.setup(instance.props, setupContext);

参与渲染代理instance.proxy,通过Proxy,把setupState、data、props、以及$emit、$attrs、$slots统一代理到一个对象上,作为render函数的this和第一个参数。

instance.proxy = new Proxy(instance, {
    get(target, key) {
      const { setupState, data, props, attrs, slots, emit } = target;
      if (typeof key === 'symbol') {
        return;
      }
      if (hasOwn(setupState, key)) {
        return setupState[key];
      }
      if (hasOwn(data, key)) {
        return data[key];
      }
      if (hasOwn(props, key)) {
        return props[key];
      }
      if (key === '$emit') {
        return emit;
      }
      if (key === '$attrs') {
        return attrs;
      }
      if (key === '$slots') {
        return slots;
      }
    },
    set(target, key, value) {
      const { setupState, data, props } = target;
      if (typeof key === 'symbol') {
        return true;
      }
      if (hasOwn(setupState, key)) {
        setupState[key] = value;
        return true;
      }
      if (hasOwn(data, key)) {
        data[key] = value;
        return true;
      }
      if (hasOwn(props, key)) {
        console.warn('props is readonly');
        return true;
      }
      return true;
    },
  });
};
...
const setupRenderEffect = (
  instance: ComponentInstance,
  vnode: VNode,
  container: Container,
  anchor: Node | null,
  internals: ComponentRendererInternals
) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      invokeLifecycleHooks(instance.bm);
      const subTree = instance.render?.call(instance.proxy, instance.proxy); // 就是这里
      instance.subTree = subTree;
      internals.patch(null, subTree, container, anchor, instance); // render
      vnode.el = subTree.el;
      instance.isMounted = true;
      invokeLifecycleHooks(instance.m);
      // instance.type.mounted?.call(instance.proxy, instance.proxy);
    } else {
      invokeLifecycleHooks(instance.bu)
      const prevTree = instance.subTree;
      const nextTree = instance.render?.call(instance.proxy, instance.proxy);
      instance.subTree = nextTree;
      internals.patch(prevTree, nextTree, container, anchor, instance); // update
      invokeLifecycleHooks(instance.u);
    }
  };

  let update: () => void;
  const reactiveEffect = new ReactiveEffect(componentUpdateFn, () => {
    queueJob(update);
  });
  instance.effect = reactiveEffect;
  update = instance.update = () => {
    if (instance.isUnmounted) {
      return;
    }
    reactiveEffect.run();
  };
  update();
};

这里就可以参与通信,instance.emit预先用emit.bind(null, instance)的方式,绑定好当前的实例,触发事件去instance.vnode.props里找好对应的onXXX回调。

export type Emit = (event: string, ...args: unknown[]) => void;
...

if (key === '$emit') {
return emit;
}

还参与了记录父组件实例,并通过Object.create(parent.provides)的形式跨层级provide/inject的原型链查找。

instance.subTree保存了当前组件render执行后生成的内部虚拟DOM树,instance.effect和instance.update保存了组件的ReactiveEffect实例和触发重绘的update函数,当自身响应式状态改变的时候,或者父组件的updateComponent中更新了Props的时候,都会调用instance.update(),拿旧的instance.subTree和新的nextTree进行patch对比。

  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      invokeLifecycleHooks(instance.bm);
      const subTree = instance.render?.call(instance.proxy, instance.proxy);
      instance.subTree = subTree;
      internals.patch(null, subTree, container, anchor, instance); // render
      vnode.el = subTree.el;
      instance.isMounted = true;
      invokeLifecycleHooks(instance.m);
      // instance.type.mounted?.call(instance.proxy, instance.proxy);
    } else {
      invokeLifecycleHooks(instance.bu)
      const prevTree = instance.subTree;
      const nextTree = instance.render?.call(instance.proxy, instance.proxy);
      instance.subTree = nextTree;
      internals.patch(prevTree, nextTree, container, anchor, instance); // update
      invokeLifecycleHooks(instance.u);
    }
  };

render的时期

通过 instance.render?.call(instance.proxy, instance.proxy) 调用,render方法内的this会指向instance.proxy,这样在render函数内部我们访问诸如count此类的值可以用proxy.count,或者this.count,都会经过Proxy的get拦截器,按照setupState->data->props-> $emit $attrs $slots的顺序查找数据并完成依赖收集。

因为 instance.render 是在 reactiveEffect.run()内部运行的,所以每次运行前都会先执行 cleanupEffect(this)。清空上一次渲染收集的依赖,再在本次 render(),执行过程中重新收集(这样如果某次渲染因为 v-if /三元表达式不再读取某个变量,该变量变化时就不会再触发重绘)。

const subTree = instance.render?.call(instance.proxy, instance.proxy);

通过render会产出subTree,render内部通过调用h方法返回一颗代表组件内部的结构的VNode树,称作subTree。交由internals.patch处理为真实DOM,首次挂载的时候,调用 internals.patch(null, subTree, container, anchor,instance)。因为第一个参数 n1 是 null,patch 会把 subTree 递归创建为真实 DOM节点插入 container,最后把生成出来的根 DOM 元素赋给组件外壳 vnode.el =subTree.el(方便后续移动或卸载组件时拿到真实 DOM)。

依旧看这里,这里调用了注入的方法里的patch方法。

  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      invokeLifecycleHooks(instance.bm);
      const subTree = instance.render?.call(instance.proxy, instance.proxy);
      instance.subTree = subTree;
      internals.patch(null, subTree, container, anchor, instance); // render
      vnode.el = subTree.el;
      instance.isMounted = true;
      invokeLifecycleHooks(instance.m);
      // instance.type.mounted?.call(instance.proxy, instance.proxy);
    } else {
      invokeLifecycleHooks(instance.bu)
      const prevTree = instance.subTree;
      const nextTree = instance.render?.call(instance.proxy, instance.proxy);
      instance.subTree = nextTree;
      internals.patch(prevTree, nextTree, container, anchor, instance); // update
      invokeLifecycleHooks(instance.u);
    }
  };
  
export type PatchFunction = (
  n1: VNode | null,
  n2: VNode,
  container: Container,
  anchor?: Node | null,
  parentComponent?: ComponentInstance | null
) => void;

  const patch = (
    n1: VNode | null,
    n2: VNode,
    container: Container,
    anchor: Node | null = null,
    parentComponent: ComponentInstance | null = null
  ) => {
    if (n1 === n2) {
      return;
    }
    if (n1 !== null) {
      if (!isSameVNode(n1, n2)) {
        // // console.log('[patch<VNode>]', n1, n2, 'unmount');
        unmount(n1);
        n1 = null; // 原节点不复用
      }
    }
    const { type, shapeFlag } = n2;
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor);
        break;
      case Fragment:
        processFragment(n1, n2, container, anchor, parentComponent);
        break;
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElemet(n1, n2, container, anchor, parentComponent);
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          // // console.log('[patch]: component');
          processComponent(n1, n2, container, anchor, parentComponent, internals);
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          type.process(n1, n2, container, anchor, {
            mountChildren,
            patchChildren,
            unmountChildren,
            createText: hostCreateText,
            insert: hostInsert,
            querySelector: hostQuerySelector,
          });
        }
        break;
    }
  };

顶层暴露的render并不是这里的render,如果你看了example里的html,会发现最下面有渲染根组件的render调用:

render(
    h({
        type: Parent
    }),
    app
)

这个render函数是真个应用的挂载/卸载入口,当传入的render(vnode, container)时候,他会把上一次挂载在容器上的container,_vnode交给patch方法,并将本次vnode缓存到container._vodce 上,当传入的render(null, container)时,如果 container._vnode 存在,则会调用 unmount(container._vnode) 卸载整棵应用树。

  const render = (vnode: VNode | null, container: Container): void => {
    if (vnode === null) {
      if (container._vnode) {
        unmount(container._vnode);
      }
      container._vnode = null;
      return;
    }
    patch(container._vnode || null, vnode, container);
    container._vnode = vnode;
  };

依赖追踪的周期

我们一般有两种存储依赖的集合,Dep dep: Set<ReactiveEffect>。reactive对象使用的是全局的targetMap三层结构来存储的,WeakMap以key 和 Dep 为kv存放所有依赖该属性的ReactiveEffect。

const targetMap: WeakMap<object, Map<string | symbol, Set<ReactiveEffect>>> = new WeakMap();
export type Dep = Set<ReactiveEffect>;

ref/computed实例因为自身挂一个 this.dep = new Set<ReactiveEffect>() ,所以不需要使用WeakMap。

同时,每个ReactiveEffect实例上也有一个数组叫做public deps: Dep[] = []。这个用来反方向记录自己被哪些属性的Set各地收集过,我们知道ReactiveEffect用来执行响应式副作用的,然后Dep是一系列的响应式副作用,不会重复执行。deps显而易见,是用来反向记录“我被哪些属性的 Set 收集过”。

export let activeEffect: ReactiveEffect | undefined = undefined;
export class ReactiveEffect<T = any> {
  public executeCount = 0;
  public active = true;
  public deps: Dep[] = []; // deps from this ReactiveEffect used
  public running = 0;
  constructor(
    public fn: () => T,
    public scheduler?: () => void
  ) {}
  run(): T {
    this.executeCount++;
    if (!this.active) return this.fn() as T;
    cleanupEffect(this);
    let lastActiveEffect = activeEffect;
    try {
      this.running++;
      activeEffect = this;
      return this.fn() as T;
    } finally {
      this.running--;
      activeEffect = lastActiveEffect;
    }
  }
  stop() {
    if (this.active) {
      cleanupEffect(this);
      this.active = false;
    }
  }
}

追踪的全流程以effect.run到trackEffects为例。当执行run的时候会用局部变量lastActiveEffect 保存外层的activeEffect,然后把全局 activeEffect 设为当前 this,再执行 this.fn()。执行完后在 finally 块里恢复 activeEffect = lastActiveEffect(index.ts:32-40)

为什么要用 lastActiveEffect? 为了支持父子组件嵌套渲染或 computed 嵌套:父组件render 一半去挂载子组件时,activeEffect切为子组件;子组件挂载完恢复为父组件,不会弄丢父组件的上下文。

拦截读取用track,在this.fn执行期间,读取reactive对象的属性会触发Proxy的get 拦截器,调用track(target, key),如果当前没有activeEffect,说明只是普通代码读取,不在effect/render/computed/watch内部,直接return。

否则从 targetMap 中取出(或新建)该 target[key] 对应的 dep 集合,交给trackEffects(dep)。

const mutableHandlers: ProxyHandler<any> = {
  get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) return true;
    if (key === ReactiveFlags.IS_READONLY) return false;
    if (key === ReactiveFlags.RAW) return target;

    // effect track
    track(target, key);
    let result = Reflect.get(target, key, receiver);
    if (isObject(result)) {
      result = createReactiveObject(result);
    }
    return result;
  },
  set(target, key, value, receiver) {
    let oldValue = Reflect.get(target, key, receiver);
    // 相同值不需要触发更新,但赋值本身仍然是成功的。
    if (oldValue === value) return true;
    // update
    let result = Reflect.set(target, key, value, receiver);
    if (result) {
      trigger(target, key);
    }
    return result;
  },
};

export function track(target: object, key: string | symbol) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  trackEffects(dep);
}
export function trackEffects(dep: Set<ReactiveEffect>) {
  if (dep.has(activeEffect!)) {
    return;
  }
  dep.add(activeEffect!);
  activeEffect!.deps.push(dep);
}
function createReactiveObject(target: object) {
  if (!isObject(target)) return target;
  if (reactiveMap.has(target)) return reactiveMap.get(target);
  let proxy = new Proxy(target, mutableHandlers);
  reactiveMap.set(target, proxy);
  return proxy;
}

依赖收集的过程中标,如果现在执行的副作用存在于obj的某个属性的dep里,就不管,如果不存在就收集进去,执行依靠setter钩子里的trigger触发器,当obj的depsMap里有某个属性的deps时,就拷贝一份执行,逻辑清晰明了。

export function trigger(target: object, key: string | symbol) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  console.log('[trigger]', key, dep);
  if (!dep) return;
  // const effects = new Set(dep);
  // console.log('[execute]', effects);
  triggerEffects(dep);
}
/**
 * 触发依赖
 * @param dep 依赖集合
 */
export function triggerEffects(dep: Set<ReactiveEffect>) {
  const effects = new Set(dep);
  effects.forEach((effect) => {
    if (effect !== activeEffect) {
      if (effect.scheduler) {
        {
          if (!effect.running) {
            // prevent infinite loop
            effect.scheduler();
          }
        }
      } else {
        effect.run();
      }
    }
  });
}

清理方法是:

export function cleanupEffect(effect: ReactiveEffect) {
  const deps = effect.deps;
  deps.forEach((dep) => dep.delete(effect));
  deps.length = 0;
}

在trigger内执行effect.run之后,在正式执行委托的this.fn之前会做一次清理,cleanupEffect,然后running计数器+1, 将正在激活的effect设置为自己,最后出去的时候计数器-1,将之前我们设置的activceEffect又设置回去,执行了之后操作obj的相关属性又会添加到deps里去, activeEffect!.deps.push(dep); 。

计数器是为了递归保护,如果有人分在effect里或schedular内部写了自增操作,比如Obj.count++,那么就会读get 和写set,这样触发triggerEffects的时候外层的if (effect !== activeEffect) 和内部的调度器的if (!effect.running) 就会拦截这次触发,防止run没跑完,内部又在无限递归调用导致栈溢出。