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办法将模板渲染到idapp的元素内。后续只有批改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办法十分长,蕴含了渲染器的所有办法,比方mountpatch等,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;}

初始化propsslots,而后如果shapeFlag4会调用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办法判断组件是否须要更新,大抵是通过是否存在过渡成果、是否存在动静slotsprops是否产生扭转、子节点是否发扭转等来判断。

如果须要更新,那么会执行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了,是不是很简略,心动不如口头,下一个框架等你来发明!