前言
- runtime-core: 与平台无关的运行时核心 (可以创建针对特定平台的运行时 - 自定义渲染器)
- runtime-dom: 针对浏览器的运行时。包括DOM API,属性,事件处理等
vue3的runtime-core源码地址
vue3的runtime-dom源码地址
- 浏览器的渲染组件的流程(如下图所示)
- 宏观(Vue3是Monorepo管理项目):
- 根据平台的不同, 提供的操作方法不通过
- 平台模块包裹着核心编译模块, 编译时去调用响应模块
- 微观:
- 描述以示例为准
runtime-dom
- 该模块提供浏览器的针对元素, 文本, 事件等 增删改查
- 1.通过
createApp(App, {})(runtime-dom的API)将组件选项和属性选项传入到runtime-core模块
- 2.也将浏览器增删改查的选项传入到
runtime-core模块
- 3.并将挂载元素的id 通过
app.mount('#app')将挂载根元素传入到runtime-core模块
runtime-core
- 1.将组件的选项和属性选项创建虚拟dom(vnode)
const vnode = createVNode(rootComponent, rootProps)
- 2.渲染render
render(vnode, '根元素')
- 3.通过
patch去比对vnode, 老的没传就是生成(递归操作)
- 4.如果是元素,文本 生成元素, 添加元素, 生成文本, 添加文本
- 5.如果是组件, 生成组件实例
createComponentInstance
- 6.处理数据挂载到实例上
setupComponent 如state props attrs render(着重处理)...
- 并对vue2.0API做了兼容处理(未写 循环合并)源码对应
componentOptions.ts -> applyOptions方法
- 7.创建effect函数(引入
reactivity模块的API) 执行render函数setupRenderEffect
浏览器编译结构图
+--------> 模板complier过程(本章不考虑, 后面的章节细讲) ?
|
+----+----+
| |
| vue |
| |
+----+----+ +---------------------+ +----------------------+ +-------------------+
| | | | | | |
+-------->| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity |
| | | | | |
+---------------------+ +----------------------+ +-------------------+
组件初始化渲染流程图
示例
<script src="../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js"></script>
<div id="app"></div>
<script>
let { createApp, reactive, h} = VueRuntimeDOM
let App = {
setup(props, context) {
let state = reactive({ name:'张三' })
let fn = function () {
console.log('点击事件')
state.name = '李四'
}
return () => {
return h('div', {onClick:fn}, state.name)
}
},
}
let app = createApp(App, { name: 'xxxx', age: 30 })
app.mount('#app')
console.log("🚀", app)
</script>
shared
index.ts
export const isObject = (value) => typeof value == 'object' && value !== null
export const extend = Object.assign
export const isArray = Array.isArray
export const isFunction = (value) => typeof value == 'function'
export const isNumber = (value) => typeof value == 'number'
export const isString = (value) => typeof value === 'string'
export const isIntegerKey = (key) => parseInt(key) + '' === key
let hasOwnpRroperty = Object.prototype.hasOwnProperty
export const hasOwn = (target, key) => hasOwnpRroperty.call(target, key)
export const hasChanged = (oldValue,value) => oldValue !== value
shapeFlag.ts
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
}
runtime-dom
modules/attrs.ts
export const patchAttr = (el, key, value) =>{
if(value == null){
el.removeAttribute(key)
}else {
el.setAttribute(key, value)
}
}
modules/class.ts
export const patchClass = (el, value) =>{
if (value == null) {
value = ''
}
el.className = value
}
modules/event.ts
export const patchEvent = (el, key, value) => {
const invokers = el._vei || (el._vei = {})
const exists = invokers[key]
if (value && exists) {
exists.value = value
} else {
const eventName = key.slice(2).toLowerCase()
if (value) {
let invoker = invokers[key] = createInvoker(value)
el.addEventListener(eventName,invoker)
} else {
el.removeEventListener(eventName,exists)
invokers[key] = undefined
}
}
}
function createInvoker(value) {
const invoker = (e) =>{ invoker.value(e) }
invoker.value = value
return invoker
}
modules/style.ts
export const patchStyle = (el, prev, next) => {
const style = el.style
if(next == null){
el.removeAttribute('style')
}else{
if (prev) {
for(let key in prev){
if (next[key] == null) {
style[key] = ''
}
}
}
for (let key in next) {
style[key] = next[key]
}
}
}
patchProp.ts
import { patchAttr } from "./modules/attr"
import { patchClass } from "./modules/class"
import { patchEvent } from "./modules/events"
import { patchStyle } from "./modules/style"
export const patchProp = (el, key, prevValue, nextValue) => {
switch (key) {
case 'class':
patchClass(el, nextValue)
break;
case 'style':
patchStyle(el, prevValue, nextValue)
break;
default:
if (/^on[^a-z]/) {
patchEvent(el, key, nextValue)
} else {
patchAttr(el, key, nextValue)
}
break;
}
}
nodeOps.ts
export const nodeOps = {
createElement: tagName => document.createElement(tagName),
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
insert: (child, parent, anchor = null) => {
parent.insertBefore(child, anchor)
},
querySelector: selector => document.querySelector(selector),
setElementText: (el, text) => el.textContent = text,
createText: text => document.createTextNode(text),
setText: (node, text) => node.nodeValue = text
nextSibling: (node) => node.nextSibling
}
index.ts
import { extend } from "@vue/shared/src"
import { nodeOps } from "./nodeOps"
import { patchProp } from "./patchProp"
import { createRenderer } from '@vue/runtime-core'
export * from '@vue/runtime-core'
const rendererOptions = extend({ patchProp }, nodeOps)
export function createApp(rootComponent, rootProps = {}) {
const app = createRenderer(rendererOptions).createApp(rootComponent,rootProps)
let { mount } = app
app.mount = function (container) {
container = nodeOps.querySelector(container)
container.innerHTML = ''
mount(container)
}
return app
}
runtime-core
index.ts
export * from '@vue/reactivity'
export {
createRenderer
} from './renderer'
export {
h
} from './h'
renderer.ts
import { effect } from "@vue/reactivity/src"
import { ShapeFlags } from "@vue/shared/src"
import { createAppAPI } from "./apiCreateApp"
import { createComponentInstance, setupComponent } from "./component"
import { queueJob } from "./scheduler"
import { normalizeVNode ,Text} from "./vnode"
export function createRenderer(rendererOptions) {
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
nextSibling: hostNextSibling,
} = rendererOptions
const setupRenderEffect = (instance, container) => {
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
let proxyToUse = instance.proxy
let subTree = instance.subTree = instance.render.call(proxyToUse, proxyToUse)
patch(null, subTree, container)
instance.isMounted = true
} else {
}
}, {
scheduler: queueJob
})
}
const mountComponent = (initialVNode, container) => {
const instance = (initialVNode.component = createComponentInstance(initialVNode))
setupComponent(instance)
setupRenderEffect(instance, container)
}
const processComponent = (n1, n2, container) => {
if (n1 == null) {
mountComponent(n2, container)
} else {
}
}
const mountChildren = (children, container) => {
for (let i = 0; i < children.length; i++) {
let child = normalizeVNode(children[i])
patch(null, child, container)
}
}
const mountElement = (vnode, container) => {
const { props, shapeFlag, type, children } = 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, children)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(children, el)
}
hostInsert(el, container)
}
const processElement = (n1, n2, container) => {
if (n1 == null) {
mountElement(n2, container)
} else {
}
}
const processText = (n1, n2, container) => {
if (n1 == null) {
hostInsert((n2.el = hostCreateText(n2.children)), container)
}
}
const patch = (n1, n2, container) => {
const { shapeFlag, type } = n2
switch (type) {
case Text:
processText(n1, n2, container)
break;
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container)
} else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
processComponent(n1, n2, container)
}
break;
}
}
const render = (vnode, container) => {
patch(null, vnode, container)
}
return {
createApp: createAppAPI(render)
}
}
apiCreateApp.ts
import { createVNode } from "./vnode"
export function createAppAPI(render) {
return function createApp(rootComponent, rootProps) {
const app = {
_props: rootProps,
_component: rootComponent,
_container: null,
vnode: null,
mount(container) {
const vnode = createVNode(rootComponent, rootProps)
app.vnode = vnode
render(vnode, container)
app._container = container
}
}
return app
}
}
vnode.ts
import { isArray, isObject, isString, ShapeFlags } from "@vue/shared/src"
export const createVNode = (type, props, children = null) => {
const shapeFlag = isString(type) ?
ShapeFlags.ELEMENT : isObject(type) ?
ShapeFlags.STATEFUL_COMPONENT : 0
const vnode = {
__v_isVnode: true,
type,
props,
children,
component: null,
el: null,
key: props && props.key,
shapeFlag
}
normalizeChildren(vnode, children)
return vnode
}
function normalizeChildren(vnode, children) {
let type = 0
if (children == null) {
} else if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN
} else {
type = ShapeFlags.TEXT_CHILDREN
}
vnode.shapeFlag |= type
}
export const Text = Symbol('Text')
export function normalizeVNode(child) {
if (isObject(child)) return child
return createVNode(Text, null, String(child))
}
export function isVnode(vnode){
return vnode.__v_isVnode
}
component.ts
import { isFunction, isObject, ShapeFlags } from "@vue/shared/src"
import { PublicInstanceProxyHandlers } from "./componentPublicInstance"
export function createComponentInstance(vnode) {
const instance = {
vnode,
type: vnode.type,
props: {},
attrs: {},
slots: {},
ctx: {},
data: {},
setupState: {},
proxy: null,
render: null,
subTree: null,
isMounted: false
}
instance.ctx = { _: instance }
return instance
}
export function setupComponent(instance) {
const { props, children } = instance.vnode
instance.props = props
instance.children = children
let isStateful = instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
if (isStateful) { setupStatefulComponent(instance) }
}
function setupStatefulComponent(instance) {
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
let Component = instance.type
let { setup } = Component
if (setup) {
let setupContext = createSetupContext(instance)
const setupResult = setup(instance.props, setupContext)
handleSetupResult(instance, setupResult)
} else {
finishComponentSetup(instance)
}
}
function handleSetupResult(instance, setupResult) {
if (isFunction(setupResult)) {
instance.render = setupResult
} else if (isObject(setupResult)) {
instance.setupState = setupResult
}
finishComponentSetup(instance)
}
function finishComponentSetup(instance) {
let Component = instance.type
if (!instance.render) {
if(!Component.render && Component.template) {
}
instance.render = Component.render
}
}
function createSetupContext(instance) {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: () => {},
expose: () => {}
}
}
componentPublicInstance.ts
import { hasOwn } from "@vue/shared/src"
export const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
const { setupState, props, data } = instance
if (key[0] == '$') return
if (hasOwn(setupState, key)) {
return setupState[key]
} else if (hasOwn(props, key)) {
return props[key]
} else if (hasOwn(data, key)) {
return data[key]
}
},
set({ _: instance }, key, value) {
const { setupState, props, data } = instance
if (hasOwn(setupState, key)) {
setupState[key] = value
} else if (hasOwn(props, key)) {
props[key] = value
} else if (hasOwn(data, key)) {
data[key] = value
}
return true
}
}
h.ts
import { isArray, isObject } from "@vue/shared/src"
import { createVNode, isVnode } from "./vnode"
export function h(type, propsOrChildren, children) {
const l = arguments.length
if (l == 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
if (isVnode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren])
}
return createVNode(type, propsOrChildren)
} else {
return createVNode(type, null, propsOrChildren)
}
} else {
if (l > 3) {
children = Array.prototype.slice.call(arguments,2)
} else if (l === 3 && isVnode(children)) {
children = [children]
}
return createVNode(type, propsOrChildren, children)
}
}
scheduler.ts
let queue = []
let isFlushPending = false
export function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
queueFlush()
}
}
function queueFlush() {
if (!isFlushPending) {
isFlushPending = true
Promise.resolve().then(flushJobs)
}
}
function flushJobs() {
queue.sort((a, b) => a.id - b.id)
for(let i=0; i < queue.length; i++) {
const job = queue[i]
job()
}
isFlushPending = false
queue.length = 0
}
完