Vue3源码解读之Vue3 整体架构

Vue3源码解读 - Vue3 渲染原理

1. Vue3 自定义渲染器

自定义渲染器 的作用是把虚拟 DOM 渲染为特定平台上的真实元素 。在浏览器中,渲染器会把虚拟 DOM 渲染成真实 DOM 元素。

官方 Api:

ts 复制代码
import { createRenderer } from "vue";
const { render, createApp } = createRenderer<Node, Element>({
  patchProp,
  ...nodeOps,
});

定义渲染器可以传入特定于平台的类型,如下所示:

js 复制代码
const { createRenderer, h } = Vue;
const renderer = createRenderer({
  createElement(element) {
    return document.createElement(element);
  },
  setElementText(el, text) {
    el.innerHTML = text;
  },
  insert(el, container) {
    container.appendChild(el);
  },
});
renderer.render(h("h1", "hello world"), document.getElementById("app"));

2. 节点操作 和 比对属性 实现

Vue3 中将runtime模块分为 runtime-core 核心代码 及 其他平台对应的运行时 ,那么VueRuntimeDOM无疑就是解决浏览器运行时 的问题,此包中提供了 DOM 属性操作节点操作一系列接口。

2.1 创建 runtime-dom 包

runtime-dom 针对浏览器运行时,包括 DOM API 、属性、事件处理等

runtime-dom/package.json

json 复制代码
{
  "name": "@vue/runtime-dom",
  "main": "index.js",
  "module": "dist/runtime-dom.esm-bundler.js",
  "unpkg": "dist/runtime-dom.global.js",
  "buildOptions": {
    "name": "VueRuntimeDOM",
    "formats": ["esm-bundler", "cjs", "global"]
  }
}
bash 复制代码
pnpm install @vue/shared@workspace --filter @vue/runtime-dom

2.2 实现节点常用操作 nodeOps

runtime-dom/src/nodeOps 这里存放常见 DOM 操作 API ,不同运行时提供的具体实现不一样,最终将操作方法传递到runtime-core中,所以runtime-core不需要关心平台相关代码~
nodeOps 这里存放着所有的节点操作的方法

js 复制代码
export const nodeOps = {
  // 添加节点
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null);
  },
  // 删除节点
  remove: (child) => {
    const parent = child.parentNode;
    if (parent) {
      parent.removeChild(child);
    }
  },

  // 创建节点
  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
  },

  // 创建文本
  createText: text => doc.createTextNode(text),

  // 创建文本
  createComment: text => doc.createComment(text),
  // 设置文本节点内容
  setText: (node, text) => {
    node.nodeValue = text
  },

  // 设置元素中的内容
  setElementText: (el, text) => {
    el.textContent = text
  },

  // 父亲节点
  parentNode: node => node.parentNode as Element | null,

  // 下一个节点
  nextSibling: node => node.nextSibling,

  // 搜索元素
  querySelector: selector => doc.querySelector(selector),

  // 设置一个作用域id
  setScopeId(el, id) {
    el.setAttribute(id, '')
  },
};

设置一个作用域 id

作用域 id 通常是一个随机生成的字符串,用于确保每个组件的样式都是唯一的,不会影响到其他组件。

js 复制代码
setScopeId(el, id) {
  el.setAttribute(id, '')
},

这个就是我们平常看到的标签上面的自定义属性,只有 key(scopeId),没有 value

2.3 比对属性方法 patchProp

patchProp 源码

此方法主要针对不同的属性提供不同的 patch 操作

js 复制代码
import { patchClass } from "./modules/class"; // 类名处理
import { patchStyle } from "./modules/style"; // 样式处理
import { patchEvent } from "./modules/events"; // 事件处理
import { patchAttr } from "./modules/attrs"; // 属性处理
import { isOn } from "@vue/shared";
export const patchProp = (el, key, prevValue, nextValue) => {
  switch (key) {
    // class类名
    case "class":
      patchClass(el, nextValue);
      break;
    // 样式
    case "style":
      patchStyle(el, prevValue, nextValue);
      break;
    default:
      if (isOn(key)) {
        // 如果是事件
        patchEvent(el, key, nextValue);
      } else {
        // html 标签属性
        patchAttr(el, key, nextValue);
      }
      break;
  }
};



export const patchProp: DOMRendererOptions['patchProp'] = (
  el,
  key,
  prevValue,
  nextValue,
  isSVG = false,
  prevChildren,
  parentComponent,
  parentSuspense,
  unmountChildren
) => {
  if (key === 'class') {
    // class类名
    patchClass(el, nextValue, isSVG)
  } else if (key === 'style') {
    // 样式
    patchStyle(el, prevValue, nextValue)
  } else if (isOn(key)) {
    // 如果是事件,并且忽略v-model的监听事件:onModelUpdate
    if (!isModelListener(key)) {
      patchEvent(el, key, prevValue, nextValue, parentComponent)
    }
  } else if (
    key[0] === '.'
      ? ((key = key.slice(1)), true)
      : key[0] === '^'
      ? ((key = key.slice(1)), false)
      : shouldSetAsProp(el, key, nextValue, isSVG)
  ) {
    patchDOMProp(
      el,
      key,
      nextValue,
      prevChildren,
      parentComponent,
      parentSuspense,
      unmountChildren
    )
  } else {
    // 处理特殊情况下的input元素的v-model指令
    // 因为当input元素的type属性为checkbox时,v-model指令还可以接受两个可选的属性:true-value和false-value
    // 由于非字符串的属性值在DOM中会被强制转换为字符串,所以这里需要将属性值存储到DOM元素的属性中,以便在后续处理中使用
    if (key === 'true-value') {
      ;(el as any)._trueValue = nextValue
    } else if (key === 'false-value') {
      ;(el as any)._falseValue = nextValue
    }
    // html 标签属性:将属性值应用到DOM元素上
    patchAttr(el, key, nextValue, isSVG, parentComponent)
  }
}

2.3.1 patchClass(操作类名)

如果当前元素正在进行过渡动画,就需要将过渡动画中使用的临时 class 也加入到 class 属性中。

如果 class 属性的值为null或undefined,就将 class 属性从 DOM 元素上移除。

如果当前元素为 SVG 元素,则需要使用 setAttribute 方法将 class 属性应用到 DOM 元素上。都不是的话,就直接设置 DOM 元素的 className 属性就好了。

js 复制代码
function patchClass(el: Element, value: string | null, isSVG: boolean) {
  const transitionClasses = (el as ElementWithTransition)[vtcKey]
  if (transitionClasses) {
    value = (
      value ? [value, ...transitionClasses] : [...transitionClasses]
    ).join(' ')
  }
  if (value == null) {
    el.removeAttribute('class')
  } else if (isSVG) {
    el.setAttribute('class', value)
  } else {
    el.className = value
  }
}

2.3.2 patchStyle(操作样式)

patchStyle 源码

js 复制代码
function patchStyle(el: Element, prev: Style, next: Style) {
  const style = (el as HTMLElement).style
  const isCssString = isString(next)
  // 更新style
  if (next && !isCssString) {
    if (prev && !isString(prev)) {
      for (const key in prev) {
        if (next[key] == null) {
          // 老的有,新的没有,那就清空掉
          setStyle(style, key, '')
        }
      }
    }
    // 再用最新的直接覆盖
    for (const key in next) {
      setStyle(style, key, next[key])
    }
  } else {
    const currentDisplay = style.display
    if (isCssString) {
      if (prev !== next) {
        style.cssText = next as string
      }
    } else if (prev) {
      // 老的有新的没有删除
      el.removeAttribute('style')
    }

    // v-show这个指令是用于控制元素的显示和隐藏,是通过控制元素的display属性来实现
    // 但是有个特殊情况:如果元素的display属性已经被其他样式规则控制,那么v-show指令就需要将控制权交给其他样式规则,将display属性设置为当前的display属性值
    if (vShowOldKey in el) {
      style.display = currentDisplay
    }
  }
}

2.3.3 patchEvent(操作事件)

patchEvent 源码

根据事件的旧值(prevValue) 新值(nextValue),来添加、删除或更新事件监听器

js 复制代码
function createInvoker(initialValue) {
  const invoker = (e) => invoker.value(e);
  invoker.value = initialValue;
  return invoker;
}

function patchEvent(
  el: Element & { [veiKey]?: Record<string, Invoker | undefined> },
  rawName: string,
  prevValue: EventValue | null,
  nextValue: EventValue | null,
  instance: ComponentInternalInstance | null = null
) {
  // 更新事件
  const invokers = el[veiKey] || (el[veiKey] = {})
  // 如果绑定过,则替换为新的
  const existingInvoker = invokers[rawName]
  if (nextValue && existingInvoker) {
    // 将旧监听器对象的value属性更新为新
    existingInvoker.value = nextValue
  } else {
    // 转化事件为小写的
    const [name, options] = parseName(rawName)
    if (nextValue) {
      // 增加事件监听
      const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
      // 添加该事件的监听器
      addEventListener(el, name, invoker, options)
    } else if (existingInvoker) {
      // 需要删除该事件的监听器
      removeEventListener(el, name, existingInvoker, options)
      invokers[rawName] = undefined
    }
  }
}

看看这里的 createInvoker: 在绑定事件的时候,绑定一个伪造的事件处理函数 invoker,把真正的事件处理函数设置为 invoker.value 属性的值

2.3.4 patchAttr(操作属性)

patchAttr 源码

js 复制代码
function patchAttr(
  el: Element,
  key: string,
  value: any,
  isSVG: boolean,
  instance?: ComponentInternalInstance | null
) {
  if (isSVG && key.startsWith("xlink:")) {
    if (value == null) {
      el.removeAttributeNS(xlinkNS, key.slice(6, key.length));
    } else {
      // 说明属于XML命名空间中的属性,此时需要使用setAttributeNS方法来设置属性
      el.setAttributeNS(xlinkNS, key, value);
    }
  } else {
    if (__COMPAT__ && compatCoerceAttr(el, key, value, instance)) {
      return;
    }

    // 在Vue2中,如果一个布尔属性的值为true,则会将该属性的名称作为属性值应用到元素上。
    // 但是Vue3中,布尔属性的值为true时,只需要将该属性设置为空字符串即可
    const isBoolean = isSpecialBooleanAttr(key);
    if (value == null || (isBoolean && !includeBooleanAttr(value))) {
      el.removeAttribute(key);
    } else {
      el.setAttribute(key, isBoolean ? "" : value);
    }
  }
}

3. Runtime-Dom 实现

runtime-dom 主要提供 dom 操作的方法

3.1 createApp

用户调用的createApp函数就在这里被声明

createApp 源码

js 复制代码
import { extend } from "@vue/shared";
import { patchProp } from "./patchProp";
import { nodeOps } from "./nodeOps";

// runtimeDom中对dom操作的所有选项
const rendererOptions = extend({ patchProp }, nodeOps);

// 用户调用的createApp方法,此时才会创建渲染器
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }

  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      // 这里需要注意的是,由于innerHTML属性可以包含JavaScript代码,因此在使用innerHTML属性时需要注意安全性问题
      // 如果这些模板是由服务器渲染的,那么应该确保它们不包含任何用户数据。
      // 这有点像react的 dangerouslySetInnerHTML 属性
      component.template = container.innerHTML
      if (__COMPAT__ && __DEV__) {
        for (let i = 0; i < container.attributes.length; i++) {
          const attr = container.attributes[i]
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null
            )
            break
          }
        }
      }
    }

    // 清空容器内容
    container.innerHTML = ''
    // 执行挂载逻辑
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

更多关于createApp的原理可以移步Vue3 源码解读之 createApp

3.2 createRenderer(创建渲染器)

最终我们在 index.js中引入写好的方法,渲染选项就准备好了。 稍后将虚拟 DOM 转化成真实 DOM 会调用这些方法

createRenderer 源码

js 复制代码
// -----------这些逻辑移动到core中与平台代码无关--------------
function createRenderer(rendererOptions) {
  return {
    createApp(rootComponent, rootProps) {
      // 用户创建app的参数
      const app = {
        mount(container) {
          // 挂载的容器
        },
      };
      return app;
    },
  };
}

createRenderer 接收渲染所需的方法,h 方法为创建虚拟节点 的方法。这两个方法和平台无关,所以我们将这两个方法在 runtime-core 中实现。

js 复制代码
import { nodeOps } from "./nodeOps";
import { patchProp } from "./patchProp";

// 准备好所有渲染时所需要的的属性
const renderOptions = Object.assign({ patchProp }, nodeOps);
createRenderer(renderOptions).render(
  h("h1", "james"),
  document.getElementById("app")
);

4. Runtime-Core 实现

4.1 创建 runtime-core 包

runtime-core 不关心运行平台。

runtime-core/package.json

json 复制代码
{
  "name": "@vue/runtime-core",
  "module": "dist/runtime-core.esm-bundler.js",
  "types": "dist/runtime-core.d.ts",
  "files": ["index.js", "dist"],
  "buildOptions": {
    "name": "VueRuntimeCore",
    "formats": ["esm-bundler", "cjs"]
  }
}

runtime-core中需要依赖 @vue/shared@vue/reactivity

bash 复制代码
pnpm install @vue/shared@workspace @vue/reactivity@workspace --filter @vue/runtime-core

最后我们将开发环境下的打包入口改为 runtime-dom

4.2 createRenderer

renderer.ts

js 复制代码
import { createAppAPI } from "./apiCreateApp";

export function createRenderer(rendererOptions) {
  // 渲染时所到的api
  const render = (vnode, container) => {
    // 核心渲染方法
    // 将虚拟节点转化成真实节点插入到容器中
  };
  return {
    createApp: createAppAPI(render),
  };
}

4.3 createAppAPI

createAppAPI 源码

js 复制代码
export function createAppAPI(render) {
  return function createApp(rootComponent, rootProps = null) {
    const app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent, // 组件
      _props: rootProps, // 属性
      _container: null,
      _context: context,
      _instance: null,

      mount(rootContainer) {
        // 1.通过rootComponent 创建vnode
        // 2.调用render方法将vnode渲染到rootContainer中
      },
    };
    return app;
  };
}

4.4 虚拟节点的实现

4.4.1 形状标识

通过组合可以描述虚拟节点的类型

js 复制代码
// vue3使用位运算符提供的形状标识
export const enum ShapeFlags {
  ELEMENT = 1,
  FUNCTIONAL_COMPONENT = 1 << 1,
  STATEFUL_COMPONENT = 1 << 2,
  TEXT_CHILDREN = 1 << 3,
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

4.4.2 createVNode 实现

createVNode 源码

js 复制代码
export function isVNode(value: any) {
  return value ? value.__v_isVNode === true : false;
}
export const createVNode = (type, props, children = null) => {
  const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0;
  const vnode = {
    __v_isVNode: true,
    type,
    props,
    key: props && props["key"],
    el: null,
    children,
    shapeFlag,
  };
  if (children) {
    let type = 0;
    if (Array.isArray(children)) {
      type = ShapeFlags.ARRAY_CHILDREN;
    } else {
      children = String(children);
      type = ShapeFlags.TEXT_CHILDREN;
    }
    vnode.shapeFlag |= type;
    // 如果shapeFlag为9 说明元素中包含一个文本
    // 如果shapeFlag为17 说明元素中有多个子节点
  }
  return vnode;
};
export const createVNode = (
  __DEV__ ? createVNodeWithArgsTransform : _createVNode
) as typeof _createVNode

createVNode 的写法比较死板,我们让他变的更灵活些

4.4.3 h函数的实现

js 复制代码
export function h(type, propsOrChildren?, children?) {
  const l = arguments.length;
  if (l === 2) {
    // 只有属性,或者一个元素子节点的时候
    if (isObject(propsOrChildren) && !Array.isArray(propsOrChildren)) {
      if (isVNode(propsOrChildren)) {
        // h('div',h('span'))
        return createVNode(type, null, [propsOrChildren]);
      }
      return createVNode(type, propsOrChildren); // h('div',{style:{color:'red'}});
    } else {
      // 传递子节点列表的情况
      return createVNode(type, null, propsOrChildren); // h('div',null,[h('span'),h('span')])
    }
  } else {
    if (l > 3) {
      // 超过3个除了前两个都是子节点
      children = Array.prototype.slice.call(arguments, 2);
    } else if (l === 3 && isVNode(children)) {
      children = [children]; // 子节点是元素,就将其包装成 h('div',null,[h('span')])
    }
    return createVNode(type, propsOrChildren, children); // h('div',null,'james')
  }
}
// 注意子节点是:数组、文本、null

4.5 createRenderer 实现

render 方法就是采用 runtime-dom 中提供的方法将虚拟节点 转化成真实的DOM 节点渲染到指定容器中。

createRenderer 源码

js 复制代码
export function createRenderer(options) {
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
  } = options;
  const patch = (n1, n2, container) => {
    // 初始化节点和节点的diff算法都在这里
  };
  const render = (vnode, container) => {
    if (vnode == null) {
      if (container._vnode) {
      } // 卸载
    } else {
      patch(container._vnode || null, vnode, container); // 初始化和更新
    }
    container._vnode = vnode;
  };
  return {
    render,
  };
}

4.6 创建真实 DOM

源代码的实现考虑的非常全面,下面只跳出重要的几个地方来解释下:

js 复制代码
const mountChildren: MountChildrenFn = (
  children,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized,
  start = 0
) => {
  for (let i = start; i < children.length; i++) {
    const child = (children[i] = optimized
      ? cloneIfMounted(children[i] as VNode)
      : normalizeVNode(children[i]))
    patch(
      null,
      child,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}
const mountElement = (vnode, container) => {
  const { type, props, shapeFlag } = vnode;
  let el = (vnode.el = hostCreateElement(type)); // 创建真实元素,挂载到虚拟节点上
  if (props) {
    // 处理属性
    for (const key in props) {
      // 更新元素属性
      hostPatchProp(el, key, null, props[key]);
    }
  }
  // 文本节点
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 文本
    hostSetElementText(el, vnode.children);
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // 多个子节点
    mountChildren(vnode.children, el);
  }
  hostInsert(el, container); // 插入到容器中
};
const patch = (n1, n2, container) => {
  // 初始化节点和节点的diff算法都在这里
  if (n1 == n2) {
    return;
  }
  if (n1 == null) {
    // 初始化的情况
    mountElement(n2, container);
  } else {
    // diff算法
  }
};

4.7 卸载 DOM

js 复制代码
createRenderer(renderOptions).render(null, document.getElementById("app"));
js 复制代码
const unmount = (vnode) => {
  hostRemove(vnode.el);
};
const render = (vnode, container) => {
  if (vnode == null) {
    if (container._vnode) {
      // 卸载
      unmount(container._vnode); // 找到对应的真实节点将其卸载
    }
  } else {
    patch(container._vnode || null, vnode, container); // 初始化和更新
  }
  container._vnode = vnode;
};

总结

本文我们学习了 Vue3 自定义渲染器,作用是将虚拟 DOM 渲染为真实的DOM元素

然后渲染成展示真实的DOM元素 之前,涉及到节点操作比对属性

节点操作包括dom节点的创建,更新和删除等方法

比对属性包括节点的class,style和事件

还有就是Runtime-Dom包提供了createApp的方法

Runtime-Core包则是提供创建渲染器createRenderercreateAppAPI、以及虚拟节点的实现、创建真实 DOM 和 卸载 DOM等方法

相关推荐
醉の虾21 分钟前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧29 分钟前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm39 分钟前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
chusheng184043 分钟前
Java项目-基于SpringBoot+vue的租房网站设计与实现
java·vue.js·spring boot·租房·租房网站
asleep7011 小时前
第8章利用CSS制作导航菜单
前端·css
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
游走于计算机中摆烂的1 小时前
启动前后端分离项目笔记
java·vue.js·笔记
幼儿园的小霸王2 小时前
通过socket设置版本更新提示
前端·vue.js·webpack·typescript·前端框架·anti-design-vue
疯狂的沙粒2 小时前
对 TypeScript 中高级类型的理解?应该在哪些方面可以更好的使用!
前端·javascript·typescript
码蜂窝编程官方2 小时前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游