setup和render
前言
在比较vue2和vue3时,我们总说vue2是options api,vue3是composition api,那么什么是option,什么又是composition呢?
option api
在vue2中,有data用于储存响应式的变量与页面交互,有watch监听某个数据前后是否发生变化,有computed通过一些自定义规则对数据进行计算,有methods用于处理页面的交互等逻辑,我们需要在什么场景用到什么方法由我们自己选择,这就是option:可选择的。
option api的优点就是:代码结构上更为清晰,对小白来说更容易理解和上手。当然缺点也较为明显,一旦页面交互逻辑较为复杂,那么上下找代码是件头疼的事;就我而言,页面复杂时加上组件的props,理清一个页面逻辑人都饶晕了。
composition api
在vue3中,我们对于页面的响应式数据,接口请求,逻辑处理,甚至各种生命周期等统一放在了setup中,(更加的高内聚,低耦合),这个需要一定的代码规范,每一块函数处理某个逻辑,即composition:组合。
composition api的优点就是:特定功能相关的所有东西都放到一起维护,可以快读定位到某个功能的所有相关代码,维护方便,如果功能复杂,代码量大,我们还可以进行逻辑拆分处理。缺点是对代码能力有一定要求,或者公司需要有相对的代码规范,否则梳理逻辑也会很头疼。
这里引用一份文章《组合式API和选项式API》的图,大家也可以去看看其它文章对这两个api的解读哦。
下面开始进入正题。
setup
setup函数是vue3为组件提供的新属性,我们可以认为它替代了vue2中的beforeCreate 、created两个钩子函数,以往需要在这两个钩子函数中执行的代码可直接写在setup中,即:setup本身相当于一个生命周期函数,以往我们常用的methods、watch、computed、data等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?),每次重新渲染的性能(实例中每次清空,再重新添加?)等。
方法优化
// 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 };
}
}