|
1 | | -# render 函数组件 |
| 1 | +# render 函数组件 |
| 2 | + |
| 3 | +## vue组件的渲染过程 |
| 4 | + |
| 5 | + |
| 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 | + |
| 16 | + |
| 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 | + |
| 42 | + |
| 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