Vue3 核心模块源码解析(中)

news2025/1/11 9:54:36

【Vue3 核心模块源码解析(上)】讲到了 Vue2 与 Vue3的一些区别,Vue3 新特性的使用,以及略微带了一点源码。那么这篇文章就要从Vue3 模块源码解析Vue3 执行逻辑解析这两个方面去给大家剖析 Vue3 的深层次,一起学习起来吧!

这里还是想多说一点,源码几句话很难去解释清除,下面的核心代码都有写注释,大家按照思路一起走下去吧,本文末尾会有整体的流程图!

文章目录

    • Vue3 核心源码解析
      • 1. compiler-core
        • 1.1 目录结构
        • 1.2 compile 逻辑
      • 2. reactivity
        • 2.1 目录结构
        • 2.2 reactivity 逻辑
      • 3. runtime-core
        • 3.1 目录结构
        • 3.2 runtime 核心逻辑
      • 4. runtime-dom
        • 4.1 主要功能
      • 5. shared
        • 5.1 代码示例
    • Vue3 执行逻辑解析
      • init —— 组件初始化

Vue3 核心源码解析

为什么要去看源码?可能很多人感觉你在装X,事实并不是这样,就像我们在 【上】中讲到 ref 与 reactive 都可以生成响应式数据,为什么更推荐用 reactive 来代替 ref 生成深层次响应式数据结构呢?读读源码,从宏观的设计角度去考虑,可以更快的加速我们的成长!

在这里插入图片描述

本篇文章主要从 packages -> compiler-core/reactivity/runtime-core 这三个阶段去学习,Vue3是如何实现响应式的Vue3怎么样实现AST模型Vue3如何在运行时做一些Diff
开头提到的 MonoRepo 的包管理方式也在 packages 的包中提现出来了,一个多包的管理方式

在这里插入图片描述

1. compiler-core

Vue3 的编译核心,作用就是将字符串转换成 抽象语法树AST(Abstract Syntax Tree)

1.1 目录结构

  |-src
  |  |—— ast.ts // ts类型定义,比如type,enum,interface等
  |  |—— codegen.ts // 将生成的ast转换成render字符串
  |  |—— compile.ts // compile统一执行逻辑,有一个 baseCompile ,用来编译模板文件的
  |  |—— index.ts // 入口文件
  |  |—— parse.ts // 将模板字符串转换成 AST
  |  |—— runtimeHelpers.ts // 生成code的时候的定义常量对应关系
  |  |—— transform.ts // 处理 AST 中的 vue 特有语法
  |  |—— utils.ts // 工具类
  |  |
  |  |—— transforms // 需要转换的类型
  |			transformElement.ts
  |			transformExpression .ts
  |			transformText.ts
  |
  |
  |——// 测试用例tests
  	  |—— codegen.spec.ts
	  |—— parse.spec.ts
	  |—— transform.spec.ts
	  |
	  |—— snapshots
			 codegen.spec.ts.snap

1.2 compile 逻辑

1.2.1 compile.ts

为了方便阅读与理解,把 TS 的类型部分环境判断断言等相关内容省略了

compile 这个包主要是实现了什么能力呢?下面就是 compiler-core 核心

  1. 把用户输入的内容做了 AST 的转换,
  2. 转译成 Vue 能够识别的语言或者说 Vue 能够识别的语法
import { generate } from './codegen';
import { baseParse } from './parse';
import { transform } from './transform';
import { transformExpression } from './transforms/transformExpression';
import { transformElement } from './transforms/transformElement';
import { transformText } from './transforms/transformText';

export function baseCompile(template, options){
  // 1. 先把 template 也就是字符串 parse 成 ast
  const ast = baseParse(tempalte);
  // 2. 给 ast 加点料 --> 做了一些处理
  transform(
	ast,
	Object.assign(options,
	  { nodeTransforms: [transformElement, transformText, transformExpression] }
	)
  )

  // 3. 生成 render 函数
  return generate(ast)
}

1.2.2 parse.ts

AST 的逻辑 简而言之就是一开始我们是一个模板的语言,之后通过我们的一套解析规则,最后可以生成一个 Tree 或者说是一个对象,对象里面就是对应我们的标签属性,比如type、value、等,可以这么简单的理解AST。
parse.ts 主要就是进行一些 AST 的逻辑处理

import { ElementTypes, NodeTypes ] from "./ast";

const enum TagType {
  start,
  End ,
}

export function baseParse(content: string) {
  // 创建上下文
  const context = createParserContext(content);
  return createRoot(parseChildren(context,[]));
}

function createParserContext(content) {
  console.log("创建 parseContext");
  return {
    // 真实源码会有很多参数,这里省略了
	source: content  
  }
}

function parseChildren(context, ancestors) {
  console.log('开始解析 children');

  const nodes: any[] = []

  while (!isEnd(context, ancestors)) {
    const s = context.source
    let node = undefined

    if (startsWith(s, "{{")) {
      // '{{'
      // 看看如果是 {{ 开头的话,那么就是一个插值,那么去解析他
      node = parseInterpolation(context, mode)
    } else if (s[0] === '<') {
      if (s[1] === '/') {
        // https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
        // 这里属于 edge case 可以不用关心
        // 处理结束标签
        if (/[a-z]/i.test(s[2])) {
          // 匹配 </div>
          // 需要改变 context.source 的值 -> 也就是需要移动光标
          parseTag(context, TagType.End)
          // 结束标签就以为这都已经处理完了,所以就可以跳出本地循环了
          continue
        }
      } else if (/[a-z]/i.test(s[1])) {
        node = parseElement(context, ancestors)
      }
    }
    if (!node) {
      node = parseText(context, mode)
    }
    nodes.push(node)
  }

  return nodes
}

function parseInterpolation(context: any,): InterpolationNode | undefined {
  // 1.先获取到结束的 index
  // 2.通过 closeIndex - startIndex 获取到内容的长度 contextLength
  // 3.通过slice 截取内容


  // }} 是插值的关闭
  // 优化点是从 {{ 后面搜索即可
  const openDelimiters = "{{";
  const closeDelimiters = "}}";

  const closeIndex = context.source.indexOf(closeDelimiters, openDelimiters.length)
  // TODO 需要报错
  if (closeIndex === -1) {
    // emitError(context, ErrorCodes.X_MISSING_INTERPOLATION_END)
    return undefined
  }

  // 让代码前进两个长度 可以把 {{ 干掉
  advanceBy(context, open.length)

  const rawContentLength = closeIndex - openDelimiters.length
  const rawContent = context.source.slice(0, rawContentLength)

  const preTrimContent = parseTextData(context, rawContentLength)
  const content = preTrimContent.trim()

  // 最后让代码前进两个长度 可以把 }} 干掉
  advanceBy(context, close.length)

  return {
    type: NodeTypes.INTERPOLATION,
    content: {
      type: NodeTypes.SIMPLE_EXPRESSION,
      content,
    },
  }
}

function parseTag(context: any, type: TagType): any {
  // 发现如果不是 > 的话,那么就把字符都收集起来 ->div
  // 正则 
  const match: any = /^<\/?([a-z][\r\nt f />]*)/i.exec(context.source);
  const tag = match[1];
  // 移动光标
  // <div
  advanceBy(context, match[0].length);

  // 暂时不处理 selfclose 标签的情 ,所以可以直接 advanceBy 1个坐标< 的下一个就是 >
  advanceBy(context, 1);

  if (type === TagType.End) return;

  let tagType = ElementTypes.ELEMENT;

  return {
    type: NodeTypes.ELEMENT,
    tag,
    tagType,
  }
}

function parseElement(context, ancestors) {
  // 应该如何解析 tag 呢
  // <div></div> 
  // 先解析开始 tag

  const element = parseTag(context, TagType.Start);

  ancestors.push(element);

  const children = parseChildren(context, ancestors);
  ancestors.pop();

  // 解析 end tag 是为了检测语法是不是正确的
  // 检测是不是和 start tag 一致
  if (startsWithEndTagOpen(context.source, element.tag)) {
    parseTag(context, TagType.End);

  } else {
    throw new Error(`缺失结束标签:${element.tag}`);
  }
  element.children = children;

  return element;
}

function createRoot(children) {
  return {
    type: NodeTypes.ROOT,
    children,
    helpers: [],
  }
}

function startswith(source: string, searchString: string): boolean {
  return source.startswith(searchString);
}

function isEnd(context: any, ancestors) {
  //检测标签的节点
  // 如果是结束标签的话,需要看看之前有没有开始标签,如果有的话,那么也应该结束
  // 这里的一个 edge case 是 <div><span></div>
  // 像这种情况下,其实就应该报错
  const s = context.source;
  if (context.source.startswith('</')) {
    // 从后面往前面查
    // 因为便签如果存在的话 应该是 ancestors 最后一个元素
    for (let i = ancestors.length - 1; i >= 0; --i) {
      if (startswithEndTagOpen(s, ancestors[i].tag)) {
        return true;
      }
    }
  }
  // 看看 context.source 还有没有值
  return !context.source;
}

function startswithEndTagOpen(source: string, tag: string) {
  // 1.头部 是不是以 </ 开头的
  // 2.看看是不是和 tag 一样
  return (
    startswith(source, '</') && 
    source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
  )
}

1.2.3 transform.ts

transform 方法主要做了一下几点事

  1. 创建 context
  2. 递归遍历 node, 针对不同的类型(NodeTypes)做不同的处理
  3. createRootCodegen --> 创建根节点

helper 有点类似于 GC 当中的 引用计数 算法很像,这里维护的是一个 Map 对象,比如在 unMount 会判断我们当前 count 是否为0,为0时则删除,也是做垃圾回收用的;

function createTransformContext(root, options): any {
  const context = {
    root,
    nodeTransforms: options.nodeTransforms || [],
    helpers: new Map(),
    helper(name) {
      // 这里会收集调用的次数
      // 收集次数是为了给删除做处理的,(当只有 count 为0的时候才需要真的删除掉)
      // helpers 数据会在后续生成代码的时候用到
      const count = context.helpers.get(name) || 0;
      context.helpers.set(name, count + 1);
      return context;
    },
  };
}

function createRootCodegen(root: any, context: any) {
  const { children } = root;

  // 只支持有一个根节点
  // 并且还是一个 single text node
  const child = children[0];

  // 如果是 element 类型的话,那么我们需要把它的 codegenNode 赋值给 root
  // root 其实是个空的什么数据都没有的节点
  // 所以这里需要额外的处理 codegenNode
  // codegenNode 的目的是专门为了 codegen 准备的 为的就是和 ast 的 node 分离开

  if (child.type === NodeTypes.ELEMENT && child.codegenNode) {
    const codegenNode = child.codegenNode;
    root.codegenNode = codegenNode;
  } else {
    root.codegenNode = child;
  }
}

1.2.4 generate.ts

import { isString } from '@mini-vue/shared';
import { NodeTypes } from './ast';
import { CREATE_ELEMENT_VNODE, 
helperNameMap,TO_DISPLAY_STRING } from './runtimeHelpers'; 

export function generate(ast, options = {}) {
  // 先生成 context
  const context = createCodegenContext(ast, options);
  const { push, mode } = context;

  //1.先生成 preambleContext
  if (mode === "module") {
    genModulePreamble(ast, context);
  } else {
    genFunctionPreamble(ast, context);
  }

  const functionName = "render";

  const args = ["_ctx"];

  // _ctx,aaa,bbb,ccc
  // 需要把 args 处理成 上面的 string
  const signature = args.join(",");
  push(`function ${functionName}(${signature}) {`);

  // 这里需要生成具体的代码内容
  // 开始生成 vNode tree 表达式
  push("return ");
  genNode(ast.codegenNode, context);
  push("}");
  return {
    code: context.code,
  };
}

2. reactivity

reactivity 实现了什么样的逻辑呢?
可以看下面 index.ts 的引入,基本上就是我们在 Vue3 核心模块源码解析(上)
中讲到的实现响应式的 内容reactive、ref、isRef 、effect等;

export {
  reactive,
  readonly,
  shal1owReadonly,
  isReadonly,
  isReactive,
  isProxy,
} from "./reactive";

export { ref, proxyRefs, unRef, isRef } from "./ref";
export { effect, stop, ReactiveEffect } from "./effect";
export { computed } from "./computed";

2.1 目录结构

  |-src
  |  |—— index.ts // 所有响应式 API 的暴露,比如ref、unRef、isRef、effect 等
  |  |—— reactive.ts // reactive 响应式 的实现
  |  |—— ref.ts // ref 响应式 的实现
  |  |—— dep.ts // 
  |  |—— effect.ts // 
  |  |—— baseHandler.ts // 
  |  |—— computed.ts // 
  |  |
  |  |
  |
  |
  |——// 测试用例tests
  	  |—— xxx.spec.ts
	  |—— xxxx.spec.ts // 对应src目录下

2.2 reactivity 逻辑

2.2.1 reactive.ts

Vue3 的响应式基本上都是通过 weakMap 来实现的,最核心的原因是 weakMap 可以使用对象的方式作为键,其次就是弱引用更好的支持垃圾回收。

import {
  mutableHandlers,
  readonlyHandlers,
  shallowReadonlyHandlers,
} from "./baseHandlers";
export const reactiveMap = new WeakMap();
export const readonlyMap = new WeakMap();
export const shallowReadonlyMap = new WeakMap();
export const enum ReactiveFlags {
  IS_REACTIVE = "_v_isReactive",
  IS_READONLY = "_v_isReadonly",
  RAW = "_v_raw",
}
export function reactive(target) {
  return createReactiveobject(target, reactiveMap, mutableHandlers);
}
export function readonly(target) {
  return createReactiveobject(target, readonlyMap, readonlyHandlers);
}
export function shallowReadonly(target) {
  return createReactiveObject(
    target,
    shallowReadonlyMap,
    shallowReadonlyHandlers
  );
}
export function isProxy(value) {
  return isReactive(value) || isReadonly(value);
}
export function isReadonly(value) {
  return !!value[ReactiveFlags.IS_READONLY];
}

export function isReactive(value) {
  // 如果 value 是 proxy 的话
  // 会触发 get 操作,而在 createGetter 里面会判断
  // 如果 value 是普通对象的话
  // 那么会返回 undefined,那么就需要转换成布尔值
  return !!value[ReactiveFlags.IS_REACTIVE];
}

export function toRaw(value) {
  // 如果 value 是 proxy 的话那么直接返回就可以了
  // 因为会触发 createGetter 内的逻辑
  // 如果 value 是普通对象的话,
  // 我们就应该返回普通对象
  // 只要不是 proxy ,只要是得到了 undefined 的话,那么就一定是普通对象
  // TODO 这里和源码里面实现的不一样,不确定后面会不会有问题
  if (!value[ReactiveFlags.RAW]) {
    return value;
  }
}

function createReactiveobject(target, proxyMap, baseHandlers) {
  // 核心就是 proxy
  // 目的是可以侦听到用户 get 或者 set 的动作
  // 如果命中的话就直接返回就好了
  // 使用缓存做的优化点
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  const proxy = new Proxy(target, baseHandlers);
  // 把创建好的 proxy 给存起来,
  proxyMap.set(target, proxy);
  return proxy;
}

2.2.2 ref.ts

ref 的大概实现逻辑

import { trackEffects, triggerEffects, isTracking } from "/effect";
import { createDep } from "./dep";
import { isObject, hasChanged } from "@mini-vue/shared";
import { reactive } from "./reactive";

export class RefImpl {
  private _rawValue: any;
  private _value: any;
  public dep;
  public __v__isref = true;

  constructor(value) {
    this.rawValue = value;
    // 看看value 是不是一个对象,如果是一个对象的话
    // 那么需要用 reactive 包裹一下
    this.value = convert(value);
    // 这里会在dep.ts 里面单独声明
    // 其实就是一个 new Set 的结构
    this.dep = createDep();
  }

  get value() {
    // 收集依赖
    // 这里类似于 Vue2 的 watcher 依赖收集
    // dep.add() 不过相比 Vue2 的数组,这里做了 new Set 的优化
    // 收集依赖时会 先判断是否收集过
    trackRefValue(this);
    return this._value;
  }

  set value(newValue) {
    // 当新的值不等于老的值的话
    // 那么才需要触发依赖
    if (hasChanged(newValue, this._rawValue)) {
      // 更新值
      this._value = convert(newValue);
      this._rawValue = newValue;
      // 执行收集到的所有的依赖 effect 的 run 方法
      // 类似于 Vue2 中的 Dep.notify()
      // 内部实际上是用 scheduler 可以让用户自己选择调用时机
      // 在 runtime-core 中,就是使用了 scheduler 实现在 next ticker 中调用的逻辑
      triggerRefValue(newValue);
    }
  }
}

export function ref(value) {
  return createRef(value);
}
function convert(value) {
  // 这里 isobject 非常简单,就是用的 Object.is
  return isobject(value) ? reactive(value) : value;
}

function createRef(value) {
  const refImpl = new RefImpl(value);
  return refImpl;
}
export function triggerRefValue(ref) {
  triggerEffects(ref.dep);
}
export function trackRefValue(ref) {
  if (isTracking()) {
    trackEffects(ref.dep);
  }
}

// 这里没有处理 objectwithRefs 是 reactive 类型的时候
// TODO reactive 里面如果有 ref 类型的 key 的话, 那么也是不需要调用 ref.value 的
// (but 这个逻辑在 reactive 里面没有实现)
export function proxyRefs(objectwithRefs) {
  return new Proxy(objectwithRefs, shallowUnwrapHandlers);
}
// 把 ref 里面的值拿到
export function unRef(ref) {
  return isRef(ref) ? ref.value : ref;
}
export function isRef(value) {
  return !!value.__v__isRef;
}

2.2.3 baseHandler.ts

baseHandler 对响应式的处理,get set

问题:为什么是 readonly 的时候不做依赖收集呢?
readonly 的话,是不可以被 set 的,那不可以被 set 就意味着不会触发 trigger,所以就没有收集依赖的必要了

const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true, true);

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly;
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly;
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow;
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target;
    }

    const targetIsArray = isArray(target);

    if (!isReadonly) {
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key, receiver);
      }
      if (key === "hasOwnProperty") {
        return hasOwnProperty;
      }
    }

    const res = Reflect.get(target, key, receiver);

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res;
    }

    // 问题:为什么是 readonly 的时候不做依赖收集呢?
    // readonly 的话,是不可以被 set 的,那不可以被 set 就意味着不会触发 trigger
    // 所以就没有收集依赖的必要了

    if (!isReadonly) {
      // 在触发get的时候信息依赖收集
      track(target, TrackOpTypes.GET, key);
    }

    if (shallow) {
      return res;
    }

    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value;
    }

    if (isObject(res)) {
      // 把内部所有的是 object 的值都用 reactive 包裹,变成响应式
      // 如果说这个 res 值是一个对象的话,那么我们需要把获取到的 res 也转换成 reactive
      // res 等于 target[key]
      return isReadonly ? readonly(res) : reactive(res);
    }

    return res;
  };
}


export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

export const readonlyHandlers: ProxyHandler<object> = {
  get: readonlyGet,
  set(target, key) {
    if (__DEV__) {
      // readonly 的响应式对象不可以修改
      warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  }
}
export const shallowReactiveHandlers = /*#__PURE__*/ extend(
  {},
  mutableHandlers,
  {
    get: shallowGet,
    set: shallowSet
  }
)

3. runtime-core

runtime-core: 整个 runtime 的核心,runtime-domruntime-test 等都是为runtime-core提供DOM操作的能力

3.1 目录结构

  |-src
  |  |—— index.ts // 主文件,暴露 Vue 运行时所需要的各种方法、enum、VNode等
  |  |—— apiCreateApp.ts // 创建根节点 
  |  |—— vnode.ts // 定义节点的结构并处理这些结构
  |  |—— component.ts // 组件返回的所有内容,暴露给用户 -> getCurrentInstance
  |  |—— componentEmits.ts // emit 方法的处理
  |  |—— componentProps.ts // props 的处理
  |  |—— componentPublicInstance.ts // 共用的 instance
  |  |—— componentSlots.ts // 组件插槽处理
  |  |—— apiInject.ts // inject 方法的处理
  |  |—— apiWatch.ts // watch 方法的处理
  |  |—— renderer.ts // 核心点,diff 的初始化及所有的初始化,下一篇中会详细讲解 Diff
  |  |—— rendererTemplateRef.ts 
  |  |—— hmr.ts 
  |  |—— h.ts  
  |  |—— hydration.ts
  |  |—— profiling.ts
  |  |—— directives.ts
  |  |—— devtools.ts
  |  |—— customFormatter.ts
  |  |—— componentOptions.ts
  |  |—— compat
  |  |—— helpers
  |——// 测试用例tests
  	  |—— xxx.spec.ts
	  |—— xxxx.spec.ts // 对应src目录下

3.2 runtime 核心逻辑

runtime-core 代码片段都比较长,此处挑一些精简过的核心

3.2.1 index.ts

export {
  // core
  reactive,
  ref,
  readonly,
  // utilities
  unref,
  proxyRefs,
  isRef,
  toRef,
  toRefs,
  isProxy,
  isReactive,
  isReadonly,
  isShallow,
  // advanced
  customRef,
  triggerRef,
  shallowRef,
  shallowReactive,
  shallowReadonly,
  markRaw,
  toRaw,
  // effect
  effect,
  stop,
  ReactiveEffect,
  // effect scope
  effectScope,
  EffectScope,
  getCurrentScope,
  onScopeDispose
} from '@vue/reactivity'
export { computed } from './apiComputed'
export {
  watch,
  watchEffect,
  watchPostEffect,
  watchSyncEffect
} from './apiWatch'

3.2.2 apiCreateApp.ts

import { createVNode } from "./vnode";

export function createAppAPI(render) {
  return function createApp(rootComponent) {
    const app = {
      component: rootComponent,
      mount(rootContainer) {
        console.log("基于根组件创建vnode");
        const vnode = createVNode(rootComponent);
        console.log("调用 render,基于 vnode 进行开箱");
        render(vnode, rootContainer);
      },
    };
    return app;
  };
}

3.2.3 componentEmits.ts

import { camelize, hyphenate, toHandlerKey } from "@mini-vue/shared";

export function emit(instance, event: string, ...rawArgs) {
  // 1.emit 是基于 props 里面的 onXXX 的函数来进行匹配的
  // 所以我们先从 props 中看看是否有对应的 event handler
  const props = instance.props;
  // ex: event -> cick 那么这里取的就是 onclick
  // 让事情变的复杂一点如果是中划线命名的话,需要转换成change-page -> changePage
  // 需要得到事件名称
  let handler = props[toHandlerKey(camelize(event))];

  // 如果上面没有匹配的话 那么在检测一下 event 是不是 kebab-case 类型
  if (!handler) {
    handler = props[toHandlerKey(hyphenate(event))];
  }
  if (handler) {
    handler(...rawArgs);
  }
}

3.2.4 componentProps.ts

export function initProps(instance, rawProps) {
    console.log("initProps");
    // TODO
    // 应该还有 attrs 的概念
    //attrs
    // 如果组件声明了 props 的话,那么才可以进入 props 属性内//
    // 不然的话是需要存储在 attrs 内
    // 这里暂时直接赋值给 instance.props 即可
    instance.props = rawProps;
}

3.2.5 component.ts

import { initProps } from "./componentProps";
import { initslots } from "./componentslots";
import { emit } from "./componentEmits";
import { PublicInstanceProxyHandlers } from "./componentPublicInstance";
import { proxyRefs, shallowReadonly } from "@mini-vue/reactivity";

export function createComponentInstance(vnode, parent) {
  const instance = {
    type: vnode.type,
    vnode,
    next: null, // 需要更新的 ynode,用于更新 component 类型的组件props: (]
    parent,
    provides: parent ? parent.provides : {}, // 取 parent 的 provides 作为当前组件的初始化值,这样就可以继承parent.provied
    isMounted: false,
    attrs: {}, // 存放 attrs 的数据
    slots: {}, // 存放插槽的数据
    ctx: {}, // context 对象
    setupstate: {}, // 存储 setup 的返回值
    emit: () => {},
  };

  // 在 prod 坏境下的 ctx 只是下面简单的结构
  // 在 dev 环境下会更复杂
  instance.ctx = {
    _: instance,
  };

  // 赋值 emit
  //这里使用 bind 把 instance 进行绑定
  // 后面用户使用的时候只需要给 event 和参数即可
  instance.emit = emit.bind(null, instance) as any;
  return instance;
}

// 组件 setup 的初始化
export function setupComponent(instance) {
  // 1.处理 props
  // 取出存在 vnode 里面的 props
  const { props, children } = instance.vnode;
  initProps(instance, props);
  // 2。处理 slots
  initslots(instance, children);

  // 源码里面有两种类型的 component
  // 一种是基于 options 创建的
  // 还有一种是 function 的
  // 这里处理的是 options 创建的
  // 叫做 stateful 类型
  setupStatefulComponent(instance);
}

function setupStatefulComponent(instance) {
  // todo
  // 1,先创建代理 proxy
  console.log("创建 proxy");
  // proxy 对象其实是代理了 instance.ctx 对象
  // 我们在使用的时候需要使用 instance.proxy 对象
  // 因为 instance.ctx 在 prod 和 dev 坏境下是不同的
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
  // 用户声明的对象就是 instance.type
  // const Component = {setup(),render()} ....
  const Component = instance.type;
  // 2,调用 setup

  // 调用 setup 的时候传入 props
  const { setup } = Component;
  if (setup) {
    // 设置当前 currentInstance 的值
    // 必须要在调用 setup 之前
    setCurrentInstance(instance);
    const setupContext = createSetupContext(instance);
    // 真实的处理场景里面应该是只在 dev 环境才会把 props 设置为只读的
    const setupResult =
      setup && setup(shallowReadonly(instance.props), setupContext);
    setCurrentInstance(null);
    // 3。处理 setupResult
    handleSetupResult(instance, setupResult);
  } else {
    finishComponentsetup(instance);
  }
}

function handleSetupResult(instance, setupResult) {
  // setup 返回值不一样的话,会有不同的处理
  // 1.看看 setupResult 是个什么
  if (typeof setupResult === "function") {
    // 如果返回的是 function 的话,那么绑定到 render 上
    // 认为是 render 逻辑
    // setup()f return  ()=>(h("div")) }

    instance.render = setupResult;
  } else if (typeof setupResult === "object") {
    // 返回的是一个对象的话
    // 先存到 setupstate 上
    // 先使用 @vue/reactivity 里面的 proxyRefs
    //后面我们自己构建
    // proxyRefs 的作用就是把 setupResult 对象做一层代理
    // 方便用户直接访问 ref 类型的值
    // 比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了, 而不需要在count.value
    // 这里也就是官网里面说到的自动结构 Ref 类型
    instance.setupState = proxyRefs(setupResult);
  }
  finishComponentsetup(instance);
}

4. runtime-dom

runtime-dom 包:Vue 的底层为什么通过 AST 转换,然后可以在上层供我们的Native、H5、小程序(mpvue)使用;
Vue 是通过 Virtual DOM 实现,runtime-dom 我们可以理解为,给我们VDOM提供了具有真实DOM一样的能力,就是,比如:createElement、createApp、createRenderer等等

4.1 主要功能

此处只是列举

  createElement: (tag, isSVG, is, props): Element => {
    const el = isSVG
      ? doc.createElementNS(svgNS, tag)
      : doc.createElement(tag, is ? { is } : undefined)

    if (tag === 'select' && props && props.multiple != null) {
      ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
    }

    return el
  },

5. shared

shared 包主要会返回一些通用的逻辑,比如: isObject()、isString()、camelize()、isOn()等等,实际上和 utils 没什么区别

  |-src
  |  |—— index.ts // 核心方法库,类似于 utils
  |  |—— shapeFlags.ts // enum 类型文件
  |  |—— toDiaplayString.ts // 通用转换方法
  |  |

5.1 代码示例

这里面的方法很多,这里只是列举一个

const camelizeRE = /-(\w)/g;
/**
 * @private
 *  把中划线命名方式转换成驼峰命名方式
 */

export const camelize = (str: string): string => {
  return str.replace(camelizeRE, (_, c) => (c ? c.toupperCase() : ""));
};

Vue3 执行逻辑解析

init —— 组件初始化

调用 patch ,基于 vNode 类型 进行不同类型的组件处理
调用 patch ,基于 vNode 类型 进行不同类型的组件处理
开始
1.创建 App
2.进行初始化
1.基于 rootComponent 生成 vNode
2.进行 render
处理 shapeFlag & ShapeFlag.COMPONENT 类型
处理 shapeFlag & ShapeFlag.ELEMENT 类型
组件初始化
组件更新
1.创建 component instance 对象
2.setup component
初始化 props
初始化 slot
初始化 setup
初始化 render 函数
3.setupRenderEffect
1. 调用 render 函数获取 vnode -- 子组件
2. 触发生命周期 beforeMount Hook
3. 调用 patch 初始化子组件
4. 触发生命周期 mounted Hook
检测是否需要更新 对比props
提前更新组件 component 的数据,更新props,更新 slots
生成最新的 subTree
调用 patch 递归处理 subTree
element 初始化
element 更新
1. 调用 beforeCreateElement 创建真实 Element
2. 处理 children 节点
3. 调用 hostPatchProp 设置元素的prop
4. 触发beforeMount 钩子
5. 渲染 hostInsert 插入真实的 dom 树
6. 触发 Mounted 钩子
对比 props
对比 children 递归遍历所有children 调用 patch

结语:到此 【Vue3 核心模块源码解析(中)】结束,本篇主要是以 繁琐的代码块为主,配合上 init 整体的流程图,分享了精简后大致的源码;
当然,最关键的 Diff 还没有讲到,Vue2、Vue3、React 的DIff 有什么区别,Vue3 中的 Diff是如何升级的;


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/391181.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

6.深入理解SecurityConfigurer

深入理解SecurityConfigurer 一、SecurityConfigurer SecurityConfigurer 在 Spring Security 中是一个非常重要的角色。在前面的内容中曾经多次提到过&#xff0c; Spring Security 过滤器链中的每一个过滤器&#xff0c;都是通过 xxxConfigurer 来进行配置的&#xff0c;而这…

文件跳过回收站删除了常见原因|找回方法

演示机型&#xff1a;技嘉 H310M HD22.0系统版本&#xff1a;Windows 10 专业版软件版本&#xff1a;云骑士数据恢复软件3.21.0.92你有没有遇到文件跳过回收站而直接删除的情况&#xff1f;如果有的话&#xff0c;你是知道是什么原因吗&#xff1f;文件跳过回收站删除了怎么办&…

【JavaScript速成之路】JavaScript数组

&#x1f4c3;个人主页&#xff1a;「小杨」的csdn博客 &#x1f525;系列专栏&#xff1a;【JavaScript速成之路】 &#x1f433;希望大家多多支持&#x1f970;一起进步呀&#xff01; 文章目录前言1&#xff0c;初识数组1.1&#xff0c;数组1.2&#xff0c;创建数组1.3&…

SCI期刊收不收费也有门道,你知道吗?

什么是OA期刊? OA期刊是在互联网上在线出版的学术刊物&#xff0c;英文全称是OpenAccess Journal&#xff0c;中文译为“开放存取期刊”。OA期刊不同于传统的学术期刊如《自然》、《科学》等&#xff0c;采取的是向读者收费的运营模式&#xff0c;读者只有付费订阅&#xff0…

【MySQL】表的数据处理

哈喽&#xff0c;大家好&#xff01;我是保护小周ღ&#xff0c;本期为大家带来的是 MySQL 数据表中数据的基本处理的操作&#xff0c;数据表的增删改查&#xff0c;更多相关知识敬请期待&#xff1a;保护小周ღ *★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★*一、 添加数据&a…

极简RSS订阅器Miniflux

什么是 Miniflux &#xff1f; Miniflux 是一个极简主义的 RSS 阅读器。它简单、快速、轻便且非常易于使用。Miniflux 是静态编译的单个二进制文件&#xff0c;没有使用任何复杂的框架&#xff0c;也没有外部依赖&#xff0c;简单、快速、轻巧且超级容易安装。支持 Atom、RSS 1…

Docker(五)--Docker网络--源生网络、自定义网络

文章目录一、源生网络1.docker 的网桥---bridge2.host网络模型3.none 网络模型二、自定义网络模型1.bridge驱动2.指定网关和子网3.自定义网络其中内嵌dns解析4.不同网段的容器通信一、源生网络 我们先把server7上的harbor仓库down掉&#xff0c;然后查看网络&#xff0c;可以看…

微服务导学

一、微服务导学1.1 对比单体架构与分布式架构单体架构将业务的所有功能集中再一个项目中开发&#xff0c;打成一个包部署。分布式架构缺点&#xff1a; 模块多导致部署麻烦&#xff1b;架构复杂&#xff0c;难度大1.2 微服务经过良好架构设计的分布式架构方案&#xff0c;微服务…

【Springboot系列】解析Springboot事件机制,从入门到大师

系列文章&#xff1a;Spring Boot学习大纲&#xff0c;可以留言自己想了解的技术点 继续写Springboot系列&#xff0c;争取早点结束。 1、是什么 Spring的事件&#xff08;Application Event&#xff09;为Bean与Bean之间的消息通信提供了支持 事件机制中有三种角色&#x…

RTOS中事件集的实现原理以及实用应用

事件集的原理 RTOS中事件集的实现原理是通过位掩码来实现的。事件集是一种用于在任务之间传递信号的机制。在RTOS中&#xff0c;事件集通常是一个32位的二进制位向量。每个位都代表一个特定的事件&#xff0c;例如信号、标志、定时器等。 当一个任务等待一个或多个事件时&…

Hbase备份与恢复工具Snapshot的基本概念与工作原理

数据库都有相对完善的备份与恢复功能。备份与恢复功能是数据库在数据意外丢失、损坏下的最后一根救命稻草。数据库定期备份、定期演练恢复是当下很多重要业务都在慢慢接受的最佳实践&#xff0c;也是数据库管理者推荐的一种管理规范。HBase数据库最核心的备份与恢复工具——Sna…

Spark+Vue+Springboot 协同过滤额音乐推荐大数据深度学习项目

一、项目背景 随着互联网的发展,大数据的到来,传统的音乐行业受到了很大的冲击,原有的音乐数字化给人们生活带来了极大的便利。随着数字音乐的兴起,各大音乐平台层出不穷,人们在音乐平台上收听音乐的时,常常因为歌曲信息繁杂,而不能找到自己想听的音乐。为了解决这个问题,音乐…

Elasticsearch:使用 Logstash 构建从 Kafka 到 Elasticsearch 的管道 - Nodejs

在我之前的文章 “Elastic&#xff1a;使用 Kafka 部署 Elastic Stack”&#xff0c;我构建了从 Beats > Kafka > Logstash > Elasticsearch 的管道。在今天的文章中&#xff0c;我将描述从 Nodejs > Kafka > Logstash > Elasticsearch 这样的一个数据流。在…

modbus转profinet网关连接ABB变频器在博图程序案例

在博图里PLC无需编程利用兴达易控modbus转Profinet网关,将ABB变频器接入到西门子网络中.用到设备为西门子1200PLC&#xff0c;ABB变频器及兴达易控Modbus转profinet网关一个;兴达易控Modbus转profinet协议转换器&#xff08;XD-MDPN100&#xff09;一台 打开博图添加1200PLC&am…

121.(leaflet篇)leaflet结合echarts4迁徙图

听老人家说:多看美女会长寿 地图之家总目录(订阅之前建议先查看该博客) 文章末尾处提供保证可运行完整代码包,运行如有问题,可“私信”博主。 效果如下所示: 下面献上完整代码,代码重要位置会做相应解释 <!DOCTYPE html> <html>

【数据挖掘与商务智能决策】第二章 特征工程与数据预处理

数据预处理 非数值类型数据处理 Get_dummies哑变量处理 1. 简单示例&#xff1a;“男”和“女”的数值转换 import pandas as pd df pd.DataFrame({客户编号: [1, 2, 3], 性别: [男, 女, 男]}) df客户编号性别01男12女23男 df pd.get_dummies(df, columns[性别]) df客户…

DetectGPT:使用概率曲率的零样本机器生成文本检测

DetectGPT的目的是确定一段文本是否由特定的llm生成&#xff0c;例如GPT-3。为了对段落 x 进行分类&#xff0c;DetectGPT 首先使用通用的预训练模型&#xff08;例如 T5&#xff09;对段落 ~xi 生成较小的扰动。然后DetectGPT将原始样本x的对数概率与每个扰动样本~xi进行比较。…

浏览器主页被hao123劫持的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。喜欢通过博客创作的方式对所学的知识进行总结与归纳,不仅形成深入且独到的理…

Redis哨兵(Sentinel)模式

前言 上一期实现了Redis的主从复制架构&#xff0c;由于主从模式在主节点宕机故障时整个Redis服务都不能再执行写操作&#xff0c;而无法保证Redis在整个系统中的高可用。 Redis提供了Sentinel哨兵机制来解决以上问题&#xff0c;当哨兵服务监测到master下线或宕机&#xff0…

汽车标定知识整理(二):CCP报文基本命令介绍

目录 一、基本命令 CRO命令报文的基本命令表&#xff1a; 二、基本命令与可选命令帧格式介绍 1、CONNECT——建立连接&#xff08;0x01&#xff09; 2、GET_CPP_VERSION——获取CCP版本&#xff08;0x1B&#xff09; 3、SET_MTA——设置内存传输地址&#xff08;0x02&#…