vue3解读—setup与render

1,655 阅读4分钟

setup和render

前言

在比较vue2vue3时,我们总说vue2options apivue3composition api,那么什么是option,什么又是composition呢?

option api

vue2中,有data用于储存响应式的变量与页面交互,有watch监听某个数据前后是否发生变化,有computed通过一些自定义规则对数据进行计算,有methods用于处理页面的交互等逻辑,我们需要在什么场景用到什么方法由我们自己选择,这就是option:可选择的。

option api的优点就是:代码结构上更为清晰,对小白来说更容易理解和上手。当然缺点也较为明显,一旦页面交互逻辑较为复杂,那么上下找代码是件头疼的事;就我而言,页面复杂时加上组件的props,理清一个页面逻辑人都饶晕了。

composition api

vue3中,我们对于页面的响应式数据,接口请求,逻辑处理,甚至各种生命周期等统一放在了setup中,(更加的高内聚,低耦合),这个需要一定的代码规范,每一块函数处理某个逻辑,即composition:组合。

composition api的优点就是:特定功能相关的所有东西都放到一起维护,可以快读定位到某个功能的所有相关代码,维护方便,如果功能复杂,代码量大,我们还可以进行逻辑拆分处理。缺点是对代码能力有一定要求,或者公司需要有相对的代码规范,否则梳理逻辑也会很头疼。 这里引用一份文章《组合式API和选项式API》的图,大家也可以去看看其它文章对这两个api的解读哦。 image.png 下面开始进入正题。

setup

setup函数是vue3为组件提供的新属性,我们可以认为它替代了vue2中的beforeCreatecreated两个钩子函数,以往需要在这两个钩子函数中执行的代码可直接写在setup中,即:setup本身相当于一个生命周期函数,以往我们常用的methodswatchcomputeddata等api也写在了setup函数中。

接下我们尝试实现一下setup和render方法。

方法初写

const App = {
    render(context) {
        effectWatch(() => {
            // reset
            document.body.innerText = '';
            const div = document.createElement("div");
            div.innerText = context.state.count;
            // root
            document.body.append(div);
        });
    },
    setup() {
        const state = reactive({
            count: 0,
        });
        window.state = state; // 用于调试
        return {
            state
        };
    }
}
App.render(App.setup());

setup中,我们定义了一个count数据用于页面渲染,那么当我们通过一些方法逻辑去改变count时,通知页面该值发生了变化,使页面重新渲染出新的count值。这是一个非常简陋的方法实现,我们主要是理解其中的实现逻辑,实际开发时我们需要考虑很多因素,例如:开发者使用框架是否方便、人性化(实例中需要开发者去调用effectWatch?),每次重新渲染的性能(实例中每次清空,再重新添加?)等。

image.png

image.png

方法优化

// App.js
export default {
    render(context) {
            // reset
            const div = document.createElement("div");
            div.innerText = context.state.count;
            // root
            return div;
    },
    setup() {
        const state = reactive({
            count: 0,
        });
        window.state = state;
        return { state };
    },
}
// core/index.js
import { effectWatch } from "./reactivity/index.js";
export function createApp(rootComponent) { // 传入render方法
    return {
        mount(rootContainer) {
            // 将{ state }对象拿到
            const context = rootComponent.setup();
            effectWatch(() => {
                rootContainer.innerHTML = ``;
                // 将state渲染到容器中
                const element = rootComponent.render(context);
                rootContainer.append(element);
            });
        }
    }
}
// index.js
import App from './App.js';
import { createApp } from './core/index.js';

createApp(App).mount(document.querySelector("#app"));

这里我们分为三步走,App.js负责拿到需要渲染的数据,以及将数据渲染到某个容器内部。core/index.js负责将拿到的state数据给渲染到挂载的#app目标上。而我们只需要做得就是写好自己的setup方法即可,再也不用我们去调用effectWatch做到依赖变化重新渲染。

等等,这里其实还有一个问题,简单地文本节点,我们这样做没什么毛病,但是对于复杂的结构呢?每次都innerHTML=''在append是不现实的,这里就要用到我们耳熟能详的vdom了。

什么是vdom

这里我们会分析vue2和vue3的vdom,也能让我们清晰认识到vue3相对于vue2优化了哪些内容。

<template>
  <div>
    <h1>vue2的vdom</h1>
    <p>一起来看看</p>
    <div id="name">{{ name }}</div>
  </div>
</template>
    ||
    ||编译后
    ||
    ∨
var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c("div", [
    _c("h1", [_vm._v("vue2的vdom")]),
    _vm._v(" "),
    _c("p", [_vm._v("一起来看看")]),
    _vm._v(" "),
    _c("div", { attrs: { id: "name" } }, [_vm._v(_vm._s(_vm.name))])
  ])
}

上面是vue2的代码被编译之后的样子,我们都知道template标签中的代码会被编译成render函数进行渲染,其中每一个_c(***)都是一个虚拟节点,当template中的元素发生变化时,vue2会使用diff算法比对前后两次节点的变化,对有变化的节点做出更新。但是我们在vue2的vdom中能看到,它每次都会对静态节点进行比对,从而造成了一些性能上的损耗

下面我们看一下vue3中变化:

<template>
  <div>
    <h1>vue3的vdom</h1>
    <p>一起来看看</p>
    <div id="name">{{ name }}</div>
  </div>
</template>
    ||
    ||编译后
    ||
    ∨
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=d496ed49";
const _hoisted_1 = /* @__PURE__ */ _createElementVNode("h1", null, "vue3\u7684vdom", -1);
const _hoisted_2 = /* @__PURE__ */ _createElementVNode("p", null, "\u4E00\u8D77\u6765\u770B\u770B", -1);
const _hoisted_3 = { id: "name" };
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return _openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _hoisted_2,
    _createElementVNode("div", _hoisted_3, _toDisplayString($setup.name), 1)
  ]);
}

仔细看vue3对template的编译结果,我们就会发现与vue2的不同,对于静态节点使用PURE标记,意为——纯净的,后续编译时不会对其进行遍历比对;而对于以来响应式数据的动态节点,_createVNode传递第四个参数1,只有带这个参数的,才会被真正的追踪,进行比对。所以vue3在性能优化是及其明显的且强大的。

这里借用一篇文章《# 尤大Vue3.0直播虚拟Dom总结》,简单说明下第四个参数有哪些和对应含义:

export const enum PatchFlags {
  TEXT = 1,  // 表示具有动态textContent的元素
  CLASS = 1 << 1,  // 表示有动态Class的元素
  STYLE = 1 << 2,  // 表示动态样式(静态如style="color: red",也会提升至动态)
  PROPS = 1 << 3,  // 表示具有非类/样式动态道具的元素。
  FULL_PROPS = 1 << 4,  // 表示带有动态键的道具的元素,与上面三种相斥
  HYDRATE_EVENTS = 1 << 5,  // 表示带有事件监听器的元素
  STABLE_FRAGMENT = 1 << 6,   // 表示其子顺序不变的片段(没懂)。 
  KEYED_FRAGMENT = 1 << 7,  // 表示带有键控或部分键控子元素的片段。
  UNKEYED_FRAGMENT = 1 << 8,  // 表示带有无key绑定的片段
  NEED_PATCH = 1 << 9,  // 表示只需要非属性补丁的元素,例如ref或hooks
  DYNAMIC_SLOTS = 1 << 10,  // 表示具有动态插槽的元素
}

方法结合vdom

相对于方法优化环节,这里我们只是将手动创建元素节点这一步,优化成传递虚拟节点参数,通过虚拟节点构造器方法将虚拟节点创建成为一个真实的元素节点。

具体的作者已经在代码中进行了注释,欢迎大家进行讨论。对于前面有提到的虚拟dom和diff算法,将会在下一章与大家见面。

// core/h.js
export default function(tag, props, children) {
    return {
        tag,
        props,
        children,
    };
}
// core/renderer/index.js
export function mountElement(vnode, container) {
    const { tag, props, children} = vnode;
    // 创建tag标签
    const el  = document.createElement(tag);

    // 创建props属性
    if (props) {
        for(const key in props) {
            const val = props[key];
            el.setAttribute(key, val);
        }
    }

    // 创建children,支持字符和数组
    // 1. 字符
    if (typeof children === 'string') {
        const textNode = document.createTextNode(children);
        el.append(textNode);
    } else if (Array.isArray(children)) {
        // 2. 数组
        children.forEach(child => {
            mountElement(child, el);
        })
    }

    // 插入根元素
    container.append(el);
}
// core/index.js
import { effectWatch } from "./reactivity/index.js";
import { mountElement } from "./renderer/index.js";
export function createApp(rootComponent) {
    return {
        mount(rootContainer) {
            // 将{ state }对象拿到
            const context = rootComponent.setup();
            effectWatch(() => {
                rootContainer.innerHTML = ``;
                // 将state渲染到容器中
                const subTree = rootComponent.render(context);
                mountElement(subTree, rootContainer);
            });
        }
    }
}
// App.js
import h from './core/h.js';
export default {
    render(context) {
        // 可以传数组或者字符
        // return h('div', { id: 'appId', class: 'name' }, String(context.state.count));
        return h(
            'div',
            { id: 'appId', class: 'name' },
            [h('p', { class: 'text' }, 'p标签'),
            h('span', null, 'span标签')]
        );
    },
    setup() {
        const state = reactive({
            count: 0,
        });
        window.state = state;
        return { state };
    }
}