Vue3
官网中有上面这样一张图,根本展现出了Vue3
的渲染原理:
本文会从源码角度来粗率的看一下Vue3
的运行全流程,旨在加深对上图的了解,从上面这个很简略的应用示例开始:
import { createApp, ref } from "vue";createApp({ template: ` <div class="card"> <button type="button" @click="count++">count is {{ count }}</button> </div> `, setup() { const count = ref(0); return { count, }; },}).mount("#app");
通过createApp
办法创立利用实例,传了一个组件的选项对象,包含模板template
、组合式 API
的入口setup
函数,在setup
函数里应用ref
创立了一个响应式数据,而后return
给模板应用,最初调用实例的mount
办法将模板渲染到id
为app
的元素内。后续只有批改count
的值页面就会主动刷新,麻雀虽小,然而也代表了Vue
的外围。
首先调用了createApp
办法:
const createApp = ((...args) => { const app = createRenderer(rendererOptions).createApp(...args); return app;});
通过createRenderer
创立了一个渲染器,rendererOptions
是一个对象,下面次要是操作DOM
的办法:
{ insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null); }, //...}
这么做次要是不便跨平台,比方在其余非浏览器环境,能够替换成对应的节点操作方法。
function createRenderer(options) { return baseCreateRenderer(options);}function baseCreateRenderer(options, createHydrationFns) { // ... return { render, hydrate, createApp: createAppAPI(render, hydrate) };}
baseCreateRenderer
办法十分长,蕴含了渲染器的所有办法,比方mount
、patch
等,createApp
是通过createAppAPI
办法调用返回的:
function createAppAPI(render, hydrate) { return function createApp(rootComponent, rootProps = null) { if (!isFunction(rootComponent)) { rootComponent = Object.assign({}, rootComponent); } const context = createAppContext(); let isMounted = false; const app = (context.app = { _uid: uid$1++, _component: rootComponent, _props: rootProps, _container: null, _context: context, _instance: null, version, get config() {}, set config() {}, use(){}, mixin(){}, component(){}, directive(){}, mount(){}, unmount(){}, provide(){} }); return app; }}
这个就是最终的createApp
办法,所谓的利用实例app
其实就是一个对象,咱们传进去的组件选项作为根组件存储在_component
属性上,另外还能够看到利用实例提供的一些办法,比方注册插件的use
办法,挂载实例的mount
办法等。
context
其实也是一个一般对象:
function createAppContext() { return { app: null, config: { isNativeTag: NO, performance: false, globalProperties: {}, optionMergeStrategies: {}, errorHandler: undefined, warnHandler: undefined, compilerOptions: {} }, mixins: [], components: {}, directives: {}, provides: Object.create(null), optionsCache: new WeakMap(), propsCache: new WeakMap(), emitsCache: new WeakMap() };}
这个上下文对象会保留在利用实例和根VNode
上,可能是后续渲染时会用到。
接下来看一下创立实例后挂载的mount
办法:
mount(rootContainer, isHydrate, isSVG) { // 没有挂载过 if (!isMounted) { // 创立虚构DOM const vnode = createVNode(rootComponent, rootProps); vnode.appContext = context; // 渲染 render(vnode, rootContainer, isSVG); isMounted = true; // 实例和容器元素相互关联 app._container = rootContainer; rootContainer.__vue_app__ = app; // 返回根组件的实例 return getExposeProxy(vnode.component) || vnode.component.proxy; }}
次要就是做了两件事,创立虚构DOM
,而后渲染。
createVNode
办法:
const createVNode = _createVNode;function _createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) { const shapeFlag = isString(type) ? 1 /* ShapeFlags.ELEMENT */ : isSuspense(type) ? 128 /* ShapeFlags.SUSPENSE */ : isTeleport(type) ? 64 /* ShapeFlags.TELEPORT */ : isObject(type) ? 4 /* ShapeFlags.STATEFUL_COMPONENT */ : isFunction(type) ? 2 /* ShapeFlags.FUNCTIONAL_COMPONENT */ : 0; return createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true);}
createVNode
办法会依据组件的类型生成一个标记,后续会通过这个标记做一些优化之类的解决。咱们传的是一个组件选项,也就是一个一般对象,shapeFlag
的值为4
。
而后调用了createBaseVNode
办法:
function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1 /* ShapeFlags.ELEMENT */, isBlockNode = false, needFullChildrenNormalization = false) { const vnode = { __v_isVNode: true, __v_skip: true, type, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, slotScopeIds: null, children, component: null, suspense: null, ssContent: null, ssFallback: null, dirs: null, transition: null, el: null, anchor: null, target: null, targetAnchor: null, staticCount: 0, shapeFlag, patchFlag, dynamicProps, dynamicChildren: null, appContext: null, ctx: currentRenderingInstance }; return vnode;}
能够看到返回的虚构DOM
也是一个一般对象,咱们传进去的组件选项会存储在type
属性上。
虚构DOM
创立完后就会调用render
办法将虚构DOM
渲染为理论的DOM
节点,render
办法是通过参数传给createAppAPI
的:
const render = (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;};
如果要渲染的新VNode
不存在,那么从容器元素上获取之前VNode
进行卸载,否则调用patch
办法进行打补丁,如果是首次渲染,container._vnode
不存在,那么间接将新VNode
渲染为DOM
元素即可,否则会比照新旧VNode
,应用diff
算法进行打补丁,Vue2
中应用的是双端diff
算法,Vue3
中应用的是疾速diff
算法。
打补丁完结后清空了两个回调队列,能够看到事件队列还分为前后两个,那么咱们罕用的nextTick
办法注册的回调在哪个队列呢,实际上,两个都不在:
const resolvedPromise = Promise.resolve();let currentFlushPromise = null;function nextTick(fn) { const p = currentFlushPromise || resolvedPromise; return fn ? p.then(this ? fn.bind(this) : fn) : p;}
Promise.resolve()
办法会创立一个Resolved
状态的Promise
对象。
nextTick
办法就是这么简略,如果currentFlushPromise
有值,那么应用这个Promise
注册回调,否则应用默认的resolvedPromise
将回调放到微工作队列。
currentFlushPromise
会在调用queueFlush
办法时赋值,也就是生成一个新的Promise
对象:
function queueFlush() { if (!isFlushing && !isFlushPending) { isFlushPending = true; currentFlushPromise = resolvedPromise.then(flushJobs); }}
flushJobs
和后面的flushPreFlushCbs
办法里冲刷的都是queue
队列,而flushPostFlushCbs
办法里冲刷的是pendingPostFlushCbs
队列,flushJobs
办法在冲刷完queue
队列后才会冲刷pendingPostFlushCbs
队列。而如果是冲刷中调用nextTick
增加的回调会在这两个队列都清空后才会执行。
扯远了,回到render
办法,接下来看看render
办法里调用的patch
办法:
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = (process.env.NODE_ENV !== 'production') && isHmrUpdating ? false : !!n2.dynamicChildren) => { // 新旧VNode雷同间接返回 if (n1 === n2) { return; } // 如果新旧VNode的类型不同,那么也不须要打补丁了,间接卸载旧的,挂载新的 if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1); unmount(n1, parentComponent, parentSuspense, true); n1 = null; } const { type, ref, shapeFlag } = n2; switch (type) { case Text: // ... break; // ... default: // ... else if (shapeFlag & 6 /* ShapeFlags.COMPONENT */) { processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); } // ... }}
patch
办法就是用来打补丁更新理论DOM
的,switch
外面依据VNode
的类型不同做的解决也不同,因为咱们的例子传的是一个组件选项对象,所以会走processComponent
解决分支:
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => { // 如果旧的VNode不存在,那么调用挂载办法 if (n1 == null) { mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized); } // 新旧都存在,那么进行更新操作 else { updateComponent(n1, n2, optimized); }};
依据是否存在旧的VNode
判断是调用挂载办法还是更新办法,先看mountComponent
办法:
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => { const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense)); setupComponent(instance); setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);}
首先调用createComponentInstance
办法创立组件实例,返回的其实也是一个一般对象:
function createComponentInstance(vnode, parent, suspense) { const type = vnode.type; const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext; const instance = { uid: uid++, vnode, type, parent, appContext, // 还有十分多属性 // ... } return instance;}
而后调用了setupComponent
办法:
function setupComponent(instance, isSSR = false) { const { props, children } = instance.vnode; const isStateful = instance.vnode.shapeFlag & 4; initProps(instance, props, isStateful, isSSR); initSlots(instance, children); const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined; return setupResult;}
初始化props
和slots
,而后如果shapeFlag
为4
会调用setupStatefulComponent
办法,后面说了咱们传的组件选项对应的shapeFlag
就是4
,所以会走setupStatefulComponent
办法:
function setupStatefulComponent(instance, isSSR) { const { setup } = Component; if (setup) { const setupResult = callWithErrorHandling(setup, instance, 0, [instance.props, setupContext]); handleSetupResult(instance, setupResult, isSSR); }}
在这个办法里会调用组件选项的setup
办法,这个函数中返回的对象会裸露给模板和组件实例,看一下handleSetupResult
办法:
function handleSetupResult(instance, setupResult, isSSR) { if (isFunction(setupResult)) { instance.render = setupResult; } else if (isObject(setupResult)) { instance.setupState = proxyRefs(setupResult); } finishComponentSetup(instance, isSSR);}
如果setup
返回的是一个函数,那么这个函数会间接被作为渲染函数。否则如果返回的是一个对象,会应用proxyRefs
将这个对象转为Proxy
代理的响应式对象。
最初又调用了finishComponentSetup
办法:
function finishComponentSetup(instance, isSSR) { const Component = instance.type; if (!instance.render) { if (!isSSR && compile && !Component.render) { const template = Component.template || resolveMergedOptions(instance).template; if (template) { const { isCustomElement, compilerOptions } = instance.appContext.config; const { delimiters, compilerOptions: componentCompilerOptions } = Component; const finalCompilerOptions = extend(extend({ isCustomElement, delimiters }, compilerOptions), componentCompilerOptions); Component.render = compile(template, finalCompilerOptions); } } instance.render = (Component.render || NOOP); }}
这个函数次要是判断组件是否存在渲染函数render
,如果不存在则判断是否存在template
选项,咱们传的组件选项显然是没有render
属性,而是传的模板template
,所以会应用compile
办法来将模板编译成渲染函数。
回到mountComponent
办法,最初调用了setupRenderEffect
,这个办法很重要:
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => { // 组件更新办法 const componentUpdateFn = () => {} // 创立一个effect const effect = (instance.effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update), instance.scope)); // 调用effect的run办法执行componentUpdateFn办法 const update = (instance.update = () => effect.run()); update();}
这一步就波及到Vue3
的响应式原理了,外围就是应用Proxy
拦挡数据,而后在属性读取时将属性和读取该属性的函数(称为副作用函数)关联起来,而后在更新该属性时取出该属性关联的副作用函数进去执行,具体的内容网上曾经有十分多的文章了,有趣味的能够本人搜一搜,或者间接看源码也是能够的。
简化后的ReactiveEffect
类就是这样的:
let activeEffect;class ReactiveEffect { constructor(fn, scheduler = null, scope) { this.fn = fn; } run() { activeEffect = this; try { return this.fn(); } finally { activeEffect = null } } }
执行它的run
办法时会把本身赋值给全局的activeEffect
变量,而后执行副作用函数时如果读取了Proxy
代理后的对象的某个属性时就会将对象、属性和这个ReactiveEffect
示例关联存储起来,如果属性产生扭转,会取出关联的ReactiveEffect
实例,执行它的run
办法,达到自动更新的目标。
咱们应用的是ref
办法创立的数据,ref
办法返回的响应式数据尽管不是通过Proxy
代理的,然而读取批改操作同样是会被拦挡的,和Proxy
代理的数据拦挡时做的事件是一样的。
接下来看看传给它的组件更新办法componentUpdateFn
:
const componentUpdateFn = () => { // 组件没有挂载过 if (!instance.isMounted) { const subTree = (instance.subTree = renderComponentRoot(instance)); patch(null, subTree, container, anchor, instance, parentSuspense, isSVG); initialVNode.el = subTree.el; instance.isMounted = true; } else {// 组件曾经挂载过 const nextTree = renderComponentRoot(instance); patch(prevTree, nextTree, hostParentNode(prevTree.el), getNextHostNode(prevTree), instance, parentSuspense, isSVG); next.el = nextTree.el; }}
组件无论是首次挂载,还是更新,做的事件外围是一样的,先调用renderComponentRoot
办法生成组件模板的虚构DOM
,而后调用patch
办法打补丁。
function renderComponentRoot(instance) { const { type: Component, vnode, proxy, withProxy, props, propsOptions: [propsOptions], slots, attrs, emit, render, renderCache, data, setupState, ctx, inheritAttrs } = instance; let result = render.call(proxyToUse, proxyToUse, renderCache, props, setupState, data, ctx) return result}
renderComponentRoot
外围就是调用组件的渲染函数render
办法生成组件模板的虚构DOM
,而后扔给patch
办法更新就好了。
看完了mountComponent
办法,再来看看updateComponent
办法:
const updateComponent = (n1, n2, optimized) => { const instance = (n2.component = n1.component); if (shouldUpdateComponent(n1, n2, optimized)) { // 须要更新 instance.next = n2; instance.update(); }else { // 不须要更新 n2.el = n1.el; instance.vnode = n2; }}
先调用shouldUpdateComponent
办法判断组件是否须要更新,大抵是通过是否存在过渡成果、是否存在动静slots
、props
是否产生扭转、子节点是否发扭转等来判断。
如果须要更新,那么会执行instance.update
办法,这个办法就是后面setupRenderEffect
办法里保留的effect.run
办法,所以最终执行的也是componentUpdateFn
办法。
到这里,从咱们创立实例到页面渲染,再到更新的全流程就讲完了,总结一下,大抵就是:
1.每个Vue
组件都须要产出一份虚构DOM
,也就是组件的render
函数的返回值,render
函数你能够间接手写,也能够通过template
传递模板字符串,由Vue
外部来编译成渲染函数,平时咱们开发时写的Vue
单文件,最终也会编译成一般的Vue
组件选项对象;
2.render
函数会作为副作用函数执行,也就是如果在模板中应用到了响应式数据(所谓响应式数据就是能拦挡到它的各种读取、批改操作),那么响应式数据和属性会与render
函数关联起来,那么当响应式数据被批改当前,就能找到依赖它的render
函数,那么就能够告诉依赖的组件进行更新;
2.有了虚构DOM
之后,Vue
外部的渲染器就能将它渲染成实在的DOM
,如果是更新的状况,也就是存在新旧两个虚构DOM
,那么Vue
会通过比拟,必要时会应用diff
算法进行高效的更新实在DOM
;
所以只有你实现一个渲染器,能将虚构DOM
渲染成实在DOM
,并且能高效的依据新旧虚构DOM
比照实现更新,再实现一个编译器,能将模板编译成渲染函数,最初再基于Proxy
实现一个响应零碎就能够实现一个Vue3
了,是不是很简略,心动不如口头,下一个框架等你来发明!