Skip to content

Commit a645621

Browse files
committed
完成渲染函数章节的主要内容
1 parent 468ba3f commit a645621

File tree

5 files changed

+3575
-2665
lines changed

5 files changed

+3575
-2665
lines changed
362 KB
Loading
152 KB
Loading
202 KB
Loading

docs/posts/tutorial/6.md

Lines changed: 293 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,293 @@
1-
# render 函数组件
1+
# render 函数组件
2+
3+
## vue组件的渲染过程
4+
![](/img/vue-render-1.jpg)
5+
![](/lai-ui/img/vue-render-1.jpg)
6+
7+
上图展示了两大部分的内容,一部分是vue的渲染的一个流程,一个是vue本身的一个数据响应系统,数据响应系统不是本章节的主要内容,所以让我们继续关注与渲染有关的内容。
8+
9+
* 模板:vue模板语法是基于vue和html的语法,方便我们申明数据和UI的关系。
10+
* AST: Abstract syntax Tree,其实就是包含描述节点属性的JavaScript的对象。
11+
* virtual dom: 我们常说的虚拟dom,vue在2.0的时候引入了[snabbdom](https://github.com/snabbdom/snabbdom)(虚拟dom)来渲染真实的dom。(虚拟dom的核心主要是diff的算法,其实最终创建和更新dom的方法还是使用浏览器原生的方法)。
12+
13+
我们通常接触的文件都是基于`.vue`的单文件,主要由`template``script``css`这三块的代码组成,也叫vue的模板文件。vue会将模板文件进行解析,形成抽象语法树,编译成render函数,然后通过调用render函数,借由虚拟dom来创建或者更新真实的UI。vue的一个渲染过程大体就是这个样子。
14+
15+
![](/img/vue-render-3.jpg)
16+
![](/lai-ui/img/vue-render-3.jpg)
17+
18+
## 独立构建和运行时构建
19+
20+
* 独立构建:包含模板编译器,渲染过程`html字符串 -> render函数 -> 虚拟DOM -> 真实DOM`
21+
* 运行时构建:不包含模板编译器,渲染过程`render函数 -> 虚拟dom -> 真实dom`
22+
23+
vue其实有两种不同的版本,一种是完整(包含编译器)版本,一种是运行时版本,运行时版本的体积比完整版的小大约30%。当我们使用webpack配置并启动一个vue的项目的时候,通常需要在webpack中添加下列代码:
24+
```js
25+
module.exports = {
26+
// ...
27+
resolve: {
28+
alias: {
29+
'vue$': 'vue/dist/vue.esm.js' // 用 webpack 1 时需用 'vue/dist/vue.common.js'
30+
}
31+
}
32+
}
33+
```
34+
为了保证线上的vue版本足够小,默认引入的vue的版本是运行时的版本,而我们在开发单vue页面项目的时候通常需要处理`.vue`文件,所以需要添加上述代码,让webpack给我们引入完整版本的vue。
35+
36+
关于上边这一点内容,大家也可以查看vue的官方文档:[https://cn.vuejs.org/v2/guide/installation.html#运行时-编译器-vs-只包含运行时](https://cn.vuejs.org/v2/guide/installation.html#运行时-编译器-vs-只包含运行时)
37+
38+
vue实例中有个$mount函数,通常我们在手动挂载组件的时候会用到这个方法,$mount是整个渲染过程的起始点。所谓手动挂载就是去手动让组件渲染,关于$mount的实践我在全局组件章节有提到,之后我们在vue的测试章节也会使用到。
39+
40+
41+
![](/img/vue-render-5.png)
42+
![](/lai-ui/img/vue-render-5.png)
43+
44+
上边是一张反映$mount整个函数内部的逻辑流程图。接下来我们结合一下vue的独立构建$mount的函数源代码来看下,以下代码来自[https://github.com/vuejs/vue/blob/2.6/dist/vue.esm.js#L2852](https://github.com/vuejs/vue/blob/2.6/dist/vue.esm.js#L2852)
45+
46+
```js
47+
var mount = Vue.prototype.$mount; // runtime $mount function
48+
Vue.prototype.$mount = function (
49+
el,
50+
hydrating
51+
) {
52+
el = el && query(el);
53+
54+
/* istanbul ignore if */
55+
if (el === document.body || el === document.documentElement) {
56+
process.env.NODE_ENV !== 'production' && warn(
57+
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
58+
);
59+
return this
60+
}
61+
62+
var options = this.$options;
63+
// resolve template/el and convert to render function
64+
if (!options.render) {
65+
var template = options.template;
66+
if (template) {
67+
if (typeof template === 'string') {
68+
if (template.charAt(0) === '#') {
69+
template = idToTemplate(template);
70+
/* istanbul ignore if */
71+
if (process.env.NODE_ENV !== 'production' && !template) {
72+
warn(
73+
("Template element not found or is empty: " + (options.template)),
74+
this
75+
);
76+
}
77+
}
78+
} else if (template.nodeType) {
79+
template = template.innerHTML;
80+
} else {
81+
if (process.env.NODE_ENV !== 'production') {
82+
warn('invalid template option:' + template, this);
83+
}
84+
return this
85+
}
86+
} else if (el) {
87+
template = getOuterHTML(el);
88+
}
89+
if (template) {
90+
/* istanbul ignore if */
91+
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
92+
mark('compile');
93+
}
94+
95+
var ref = compileToFunctions(template, {
96+
shouldDecodeNewlines: shouldDecodeNewlines,
97+
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
98+
delimiters: options.delimiters,
99+
comments: options.comments
100+
}, this);
101+
var render = ref.render;
102+
var staticRenderFns = ref.staticRenderFns;
103+
options.render = render;
104+
options.staticRenderFns = staticRenderFns;
105+
106+
/* istanbul ignore if */
107+
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
108+
mark('compile end');
109+
measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
110+
}
111+
}
112+
}
113+
return mount.call(this, el, hydrating)
114+
};
115+
```
116+
从代码中不难看出,在没有render函数的前提下,会判断template和el的存在,最终都是要将template或者el的包裹的html字符串编译成render函数。
117+
118+
## render函数
119+
120+
我们在刚学vue或者使用`vue/cli`的脚手架的时候经常会看到下面渲染跟实例的代码:
121+
122+
```js
123+
import App from './App'
124+
new Vue({
125+
el: '#app',
126+
render: h => h(App)
127+
})
128+
```
129+
130+
render函数默认接受一个h的参数,有的时候别人也会将h换成`createElement`,这样的话函数更加明显。
131+
132+
我们回到上边的`$mount`源码的部分,在编译成render函数之后,之后调用了运行时版本的`$mount`这个函数,我们把这个代码截取出来:
133+
134+
```js
135+
Vue.prototype.$mount = function (
136+
el,
137+
hydrating
138+
) {
139+
el = el && inBrowser ? query(el) : undefined;
140+
return mountComponent(this, el, hydrating)
141+
};
142+
```
143+
144+
这个代码在8000多行的样子,之前提到的$mount的代码大约在11000行,一个是运行时版本的$mount函数一个是完整版本的$mount函数。但是最终都会调用运行时的$mount函数。
145+
146+
我们从`mountComponent`这个函数继续追踪代码:
147+
148+
```js
149+
function mountComponent (
150+
vm,
151+
el,
152+
hydrating
153+
) {
154+
vm.$el = el;
155+
if (!vm.$options.render) {
156+
vm.$options.render = createEmptyVNode;
157+
{
158+
/* istanbul ignore if */
159+
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
160+
vm.$options.el || el) {
161+
warn(
162+
'You are using the runtime-only build of Vue where the template ' +
163+
'compiler is not available. Either pre-compile the templates into ' +
164+
'render functions, or use the compiler-included build.',
165+
vm
166+
);
167+
} else {
168+
warn(
169+
'Failed to mount component: template or render function not defined.',
170+
vm
171+
);
172+
}
173+
}
174+
}
175+
callHook(vm, 'beforeMount');
176+
177+
var updateComponent;
178+
/* istanbul ignore if */
179+
if (config.performance && mark) {
180+
updateComponent = function () {
181+
var name = vm._name;
182+
var id = vm._uid;
183+
var startTag = "vue-perf-start:" + id;
184+
var endTag = "vue-perf-end:" + id;
185+
186+
mark(startTag);
187+
var vnode = vm._render();
188+
mark(endTag);
189+
measure(("vue " + name + " render"), startTag, endTag);
190+
191+
mark(startTag);
192+
vm._update(vnode, hydrating);
193+
mark(endTag);
194+
measure(("vue " + name + " patch"), startTag, endTag);
195+
};
196+
} else {
197+
updateComponent = function () {
198+
vm._update(vm._render(), hydrating);
199+
};
200+
}
201+
202+
// we set this to vm._watcher inside the watcher's constructor
203+
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
204+
// component's mounted hook), which relies on vm._watcher being already defined
205+
new Watcher(vm, updateComponent, noop, {
206+
before: function before () {
207+
if (vm._isMounted) {
208+
callHook(vm, 'beforeUpdate');
209+
}
210+
}
211+
}, true /* isRenderWatcher */);
212+
hydrating = false;
213+
214+
// manually mounted instance, call mounted on self
215+
// mounted is called for render-created child components in its inserted hook
216+
if (vm.$vnode == null) {
217+
vm._isMounted = true;
218+
callHook(vm, 'mounted');
219+
}
220+
return vm
221+
}
222+
223+
```
224+
这部分代码我们可以看到很多东西,比如调用组件的生命周期函数,新建一个`Watcher`来监听并且自动更新组件。
225+
226+
我们找到代码中比较重要的一行代码:`var vnode = vm._render(); `,通过调用实例本身的静态方法_render生成vnode(虚拟dom或者虚拟dom节点),我们找到_render函数的代码:
227+
228+
```js
229+
Vue.prototype._render = function () {
230+
var vm = this;
231+
var ref = vm.$options;
232+
var render = ref.render;
233+
var _parentVnode = ref._parentVnode;
234+
235+
if (_parentVnode) {
236+
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject;
237+
}
238+
239+
// set parent vnode. this allows render functions to have access
240+
// to the data on the placeholder node.
241+
vm.$vnode = _parentVnode;
242+
// render self
243+
var vnode;
244+
try {
245+
vnode = render.call(vm._renderProxy, vm.$createElement);
246+
} catch (e) {
247+
handleError(e, vm, "render");
248+
// return error render result,
249+
// or previous vnode to prevent render error causing blank component
250+
/* istanbul ignore else */
251+
if (vm.$options.renderError) {
252+
try {
253+
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
254+
} catch (e) {
255+
handleError(e, vm, "renderError");
256+
vnode = vm._vnode;
257+
}
258+
} else {
259+
vnode = vm._vnode;
260+
}
261+
}
262+
// return empty vnode in case the render function errored out
263+
if (!(vnode instanceof VNode)) {
264+
if (Array.isArray(vnode)) {
265+
warn(
266+
'Multiple root nodes returned from render function. Render function ' +
267+
'should return a single root node.',
268+
vm
269+
);
270+
}
271+
vnode = createEmptyVNode();
272+
}
273+
// set parent
274+
vnode.parent = _parentVnode;
275+
return vnode
276+
};
277+
}
278+
```
279+
我们可以看到实际上vnode是通过组件的render函数来产生的,并且默认给render函数传入实例的静态方法`$createElement`,这样我们就回到了之前的说的 `h` 这个函数。
280+
281+
关于h这个函数也就是creatElement这个函数的用法vue的文档[https://cn.vuejs.org/v2/guide/render-function.html#createElement-参数](https://cn.vuejs.org/v2/guide/render-function.html#createElement-参数)上有做详细的一个说明。我这里大概介绍一下:
282+
283+
#### createElement接受多个参数
284+
285+
`第一个参数:String | Object | function `
286+
287+
`第二个参数是一个可选的数据对象`,这个说明还是看文档吧。
288+
289+
`第三个参数是children: String | Array`
290+
291+
关于createElement这个函数是如何解析我们传入的内容,这部分可以在Vue源代码中搜索`createElement`这个函数自行了解,也可以去网上查阅类似专门分析的文章,我之前没有关心这部分内容,所以这里不做讲解。
292+
293+

0 commit comments

Comments
 (0)