✋🤚 手写 Vue render
Vue 的设计是渐进式,设计界于 Angular 和 React 之间。Vue 灵活具有多种构建方式,模板写法、JSX 写法,render 函数的写法。这些不同的写法本质是适用不同的场景:
- 模板写法,更加适用于原生 HTML、JS、CSS 写法,更加适合初学 Vue 的小伙伴。
- JSX 写法,JSX 其实也是借助的 webpack 和 Vue 是虚拟 dom 等解析。JSX 的特点是具有 JavaScript 的开发能力,灵活,适用于对 JavaScript 是更加灵活的小伙伴。
- render 函数,render 函数本质就是在创建 VNode,也是虚拟节点,虚拟节点用于创建生成虚拟 dom。大家都知道 dom 操作是昂贵的,但是经过虚拟 dom 处理之后,不在直接操作 dom。虚拟 dom 创建好之后 vue 就可以进入 patch 过程。patch 的过程将生成实际 dom。
render 函数优点和适用场景
- render 函数更加接近于 Vue 的底层,与创建 VNode 直接相连。
- render 函数不在需要 template 模板,可以直接使用 JavaScript 的能力
一个简单的例子
export default {
name: "App",
data() {
return {
a: "this is a"
};
},
render(h) {
h("view", { class: "view", id: "yoi" });
}
};
render 函数参数:
- tag/component/VNode
- VNodeInterface: VNodeData
- Children
在 Vue2 中 render 函数第一个参数是要渲染的标签或者组件。第二个参数是给 render 函数标签上的属性,与 template 上属性相对应。第三个参数用于渲染子元素或者子组件。
render 参数 data 接口
第二个参数要符合 VNodeData 接口:
export interface VNodeData {
key?: string | number;
slot?: string;
scopedSlots?: { [key: string]: ScopedSlot | undefined };
ref?: string;
refInFor?: boolean;
tag?: string;
staticClass?: string;
class?: any;
staticStyle?: { [key: string]: any };
style?: string | object[] | object;
props?: { [key: string]: any };
attrs?: { [key: string]: any };
domProps?: { [key: string]: any };
hook?: { [key: string]: Function };
on?: { [key: string]: Function | Function[] };
nativeOn?: { [key: string]: Function | Function[] };
transition?: object;
show?: boolean;
inlineTemplate?: {
render: Function,
staticRenderFns: Function[]
};
directives?: VNodeDirective[];
keepAlive?: boolean;
}
render VNodeData 详解
render staticClass 静态样式类
h(
"div",
{
staicClass: "yoi-divider yoi-tag"
},
[render]
);
静态的 class 直接渲染到 div 的 class 属性上
render staticStyle
h(
"div",
{
style: {
borderRadius: "2px",
margin: "0px 10px",
background: "blue",
color: "red"
}
},
[render]
);
与静态的 class 同理,直接将静态的 class 渲染到 div 的 内联样式 style 上。
render style
h(
"div",
{
style: {
borderRadius: this.a ? "2px" : '',
margin: this.b ? "0px 10px" : '',
background: this. c ? "blue" : '',
color: this. d ? "red": '';
}
},
[render]
);
动态的 style, 有了变量的控制,在不同的变量下,使用不同的值。
render 动态类
h(
"div",
{
class: this.isDynamic ? "class1" : "class2"
},
[render]
);
h(
"div",
{
class: {
[`yoi-${this.type}`]: !!this.type
}
},
[render]
);
Vue 的 class 支持两种方式,第一中是字符串的方式。第二种是 对象的方式。第二种方式更加的灵活。
render 事件 on
<view data-id='{{id}}'></view>;
h(
"transition",
{
on: {
tap: this.clickHandler
},
nativeOn: {
click: this.nativeClickHandler
}
},
[render]
);
事件需要使用 on、nativeOn 字段 和 methods 进行配合。
组件 props
h(
"div",
{
props: {
name: this.transition,
origin: this.origin,
mode: this.mode
}
},
[render]
);
props 不适用于 element, 适用于组件,需要传递 props 的情况。主要单向数据流,不能在组件内部更改 props。
render attrs
h(
"div",
{
attrs: {
"data-dd": "xxxxx",
"data-id": this.index
}
},
[render]
);
html 属性传递直接使用 attrs 特性,如果对 attrs 和 domProps 分部清楚,何以先弥补这部分知识。
render domProps
h(
"div",
{
innerHTML: "innerHTML"
},
[render]
);
dom 属性,dom 属性属于 dom 属性
render 插槽和作用域插槽
h(
"div",
{
scopedSlots: {
default: props => createElement("span", props.text)
},
slot: "name-of-slot"
},
[render]
);
ref 和 refInFor
h(
"transition",
{
ref: "myRef",
refInFor: true
},
[render]
);
render key
h(
"transition",
{
key: "myKey"
},
[render]
);
key 值,一般用于表示唯一的值。在列表渲染的时候,需要渲染数据,需要绑定一个 key, 这个 key 是 vnode 需要的,不定义在 attrs 或者 domProps 中。
实例
拒绝模板", 我们不使用模板,将 Vue 全部放在 JavaScript 中, 下面是一个 Taro - Vue 的例子:
import "./Article.css";
import Taro from "@tarojs/taro";
import { View } from "@tarojs/components";
export default {
name: "Article",
data() {
return {
title: "my title"
};
},
props: {
type: String
},
methods: {
genContent() {
this.$createElement(View, "this renderContent");
},
clickHandler() {
Taro.showToast({ title: "成功!" });
}
},
render(h) {
const data = {
style: {
borderRadius: "2px",
margin: "0px 10px",
background: "blue",
color: "red"
},
attrs: {
"data-dd": "xxxxx",
"data-id": "455"
},
props: {},
domProps: {},
on: {
tap: this.clickHandler
},
...otherData
};
return h(View, data, [this.genContent()]);
}
};
动态 class 解决方案
- class 的对象绑定方法
- class 动态绑定方法与 Vue 组件的 Computed 配合使用
- classnames 一个简单的 javascript 实用工具,用于有条件地将 classNames 连接在一起
classNames("foo", "bar"); // => 'foo bar'
classNames("foo", { bar: true }); // => 'foo bar'
classNames({ "foo-bar": true }); // => 'foo-bar'
classNames({ "foo-bar": false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'
// lots of arguments of various types
classNames("foo", { bar: true, duck: false }, "baz", { quux: true }); // => 'foo bar baz quux'
// other falsy values are just ignored
classNames(null, false, "bar", undefined, 0, 1, { baz: null }, ""); // => 'bar 1'
render 源码解析
Vue initMixin
vue 先执行 initMixin, 初始化 mixin 中,实现 initRender(vm) 方法。对于 render 而言就是定义了实例 createElement 方法
- initRender 函数中初始化了 createElement
export function initRender(vm: Component) {
// 模板编译使用 vm._c 方法渲染
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
// 用户手写
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
}
所以在实例上是可以直接使用 this.$createElement 方法来直接床架 vnode
render 函数本质是在调用 createElement 方法创建组件或者元素。本质其实是再在创建这些元素或者组件的虚拟 dom, 也就是 VNode。
createElement 方法最终会调用 _createElement 创建 vnode
初始化
Vue 在进行初始化的时候会执行 render 函数的混入:
renderMixin(Vue);
renderMixin 渲染混入的内部在 在 Vue.prototype 上挂载了 _render 方法。 _render 方法中调用 render 函数,这个 render 要么事手写的,要么事编译而成。调用了 call 方法之后,我们会生成一个 vnode
实现:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// ...
vm.$vnode = _parentVnode
// render self
let vnode
try {
currentRenderingInstance = vm
// 核心是调用了 call render
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...other
return vnode
}
_render 创建好 vnode 之后,就是组件将 vnode 渲染到真实的 dom 的流程。
vnode 渲染到真实的 dom
使用到了 Vue.prototype._update, 看看源码实现:
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
const vm: Component = this;
// ...other
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
// other
};
vm.__patch__ 的 patch 的方法 的定义:
Vue.prototype.__patch__ = inBrowser ? patch : noop;
// patch 其实就是根据不同的平台,进行创建的
export const patch: Function = createPatchFunction({ nodeOps, modules });
fuction createPatchFunction() {
// other
function patch (oldVnode, vnode, hydrating, removeOnly) {
// other
return vnode.elm
}
}
patch 理解是相对于 render 是比较复杂的,因为要干更多的事情。
流程:
- new Vue 实例
- Vue 运行时 执行初始化操作,包括 mixins State Render 等等
- 挂载
- 有编译的时候就进行编译
- 得到 render 函数
- render 生成 vnode
- 将 vnode patch成 dom
- 渲染成真实的 dom。
所以我们这里 render 关注点是与 vnode 最为接近的,如果我们使用 template 或者 JSX 书写,还要编译一次才能转换成 render, 然后在调用 render 生成 vnode>