Open lihongxun945 opened 6 years ago
上面我们讲了mount整体流程,那么下面我们来看看 render 函数到底是如何工作的?为了能比较容易理解,我们来写一个简单的例子:
render
Vue.component('current-time', { data () { return { time: new Date() } }, template: `<span>{{time}}</span>` }) var app = new Vue({ el: '#app', template: ` <div class="hello" @click="click"> <span>{{message}}</span> <current-time></current-time> </div> `, data: { message: 'Hello Vue!' }, methods: { click() { this.message += '1' } } })
在这个例子中,我们注册了一个自定义组件 current-time,在 #app 中就有一个DOM元素和一个自定义组件。为什么要这样呢?因为 Vue 在创建 VNODE 的时候,对这两种处理是不一样的。
current-time
#app
Vue
VNODE
我们依然从 _render 函数为入口开始看代码(依旧省略部分不影响我们理解的代码):
_render
core/instance/render.js
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options // set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode // render self let vnode try { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { // 省略 vnode = vm._vnode } // set parent vnode.parent = _parentVnode return vnode }
最核心的代码是下面这一句:
vnode = render.call(vm._renderProxy, vm.$createElement)
这里的 render 其实就是我们根据模板生成的 options.render 函数,两个参数分别是:
options.render
_renderProxy
$createElement
vnode
对于我们的例子来说,我们的render函数编译出来是这个样子的:
(function anonymous() { with (this) { return _c('div', { staticClass: "hello", on: { "click": click } }, [_c('span', [_v(_s(message))]), _v(" "), _c('current-time')], 1) } } )
显然,这里的 this 就是 _renderProxy,在它上面就有 _c, v 等函数。这些函数就是一些 renderHelpers ,比如 _v 其实是创建文本节点的:
this
_c
v
renderHelpers
_v
core/instance/render-helpers/index.js
target._v = createTextVNode
仔细观察会发现 $createElement 其实没用到。为什么呢? 因为这是给我们自己写 render 的时候提供的,而这个函数其实就是 this._c,因此编译出来的 render 直接用了 _c 而不是用了 createElement。
this._c
createElement
我们知道 _c 就是 createElement, 而 createElement 其实会调用 _createElement 来创建 vnode,我们来看看 _createElement 的代码:
_createElement
core/vdom/create-element.js
export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { // 省略大段 if (typeof tag === 'string') { if (config.isReservedTag(tag)) { // 如果是保留的tag // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag); } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ); } //省略 } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() } }
首先我们来理解参数,假设我们现在是创建如下所示的最外层 div元素:
div
<div class="hello" @click="click"> <span>{{message}}</span> </div>
那么这几个参数分别是:
context
vm
tag
data
{ staticClass: 'hello', on: {}
children
_c('span')
span
我们在看函数体,几个条件判断有一点点绕,但是最终都是为了判断到底是需要创建一个 vnode 还是需要创建一个 component。我画了一个图来表示上面的条件判断: 解释下 resolveAsset 其实就是看 tag 有没有在 components 中定义,如果已经定义了那么显然就是一个组件。
component
resolveAsset
components
对这段逻辑:比较常见的情况是:如果我们的 tag 名字是一个保留标签,那么就会调用 new VNode 直接创建一个 vnode 节点。如果是一个自定义组件,那么调用 createComponent创建一个组件。而保留标签其实就可以理解为 DOM 或者 SVG 标签。
new VNode
createComponent
因此在我们的例子中 span 是一个保留标签,所以会调用 new VNode() 直接创建一个vnode 出来。VNode 类其实非常简单,他就是把传入的参数都记录了下来而已。因为代码比较长所以这里只贴出一部分代码,有兴趣的话可以去 **core/vdom/vnode.js` 里面看看:
new VNode()
VNode
core/vdom/vnode.js
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; // 省略很多属性 constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { this.tag = tag this.data = data this.children = children // 省略很多属性 } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } }
那么如果是第二种情况,我们创建的是一个自定义的组件要怎么办呢?我们看看 createComponent 的代码:
core/vdom/create-component.js
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // 省略 // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor) // 合并 options, 就是把我们自定义的 options 和 默认的 `options` 合并 // transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data) } // extract props const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component if (isTrue(Ctor.options.functional)) { return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { // abstract components do not keep anything // other than props & listeners & slot // work around flow const slot = data.slot data = {} if (slot) { data.slot = slot } } // install component management hooks onto the placeholder node installComponentHooks(data) // return a placeholder vnode const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) // Weex specific: invoke recycle-list optimized @render function for // extracting cell-slot template. // https://github.com/Hanks10100/weex-native-directive/tree/master/component /* istanbul ignore if */ if (__WEEX__ && isRecyclableComponent(vnode)) { return renderRecyclableComponentTemplate(vnode) } return vnode }
最前面一大段都是对 options, model, on 等的处理,我们暂且跳过这些内容,直接看 vnode 的创建:
options
model
on
const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory )
也就是说,其实自定义组件current-time也是创建了一个 vnode ,那么和 span 这种原生标签肯定有区别的,最大的区别在 componentOptions 上,如果我们是自定义组件,那么会在 componentOptions 中保存我们的组件信息,而 span 这种原生标签就没有这个数据:
componentOptions
显然,对于 span 和 current-time 的更新机制肯定是不同的。由于我们知道了 createComponent 最终也会创建一个 vnode,前面的一张图中我们可以增加一个箭头,改成这样:
回到最开头的 _render,我们知道它最终返回了一个 vnode 节点组成的虚拟DOM树,树中的每一颗节点都会存储渲染的时候需要的信息,比如 context, children 等。那么Vue是如何把 vnode 渲染成真实的DOM呢?我们在下一章讲解
下一章:Vue2.x源码解析系列十:Patch和Diff 算法
大佬
创建 VNode
上面我们讲了mount整体流程,那么下面我们来看看
render
函数到底是如何工作的?为了能比较容易理解,我们来写一个简单的例子:在这个例子中,我们注册了一个自定义组件
current-time
,在#app
中就有一个DOM元素和一个自定义组件。为什么要这样呢?因为Vue
在创建VNODE
的时候,对这两种处理是不一样的。我们依然从
_render
函数为入口开始看代码(依旧省略部分不影响我们理解的代码):core/instance/render.js
最核心的代码是下面这一句:
这里的
render
其实就是我们根据模板生成的options.render
函数,两个参数分别是:_renderProxy
是我们render
函数运行时的上下文$createElement
作用是创建vnode
节点对于我们的例子来说,我们的render函数编译出来是这个样子的:
显然,这里的
this
就是_renderProxy
,在它上面就有_c
,v
等函数。这些函数就是一些renderHelpers
,比如_v
其实是创建文本节点的:core/instance/render-helpers/index.js
仔细观察会发现
$createElement
其实没用到。为什么呢? 因为这是给我们自己写render
的时候提供的,而这个函数其实就是this._c
,因此编译出来的render
直接用了_c
而不是用了createElement
。我们知道
_c
就是createElement
, 而createElement
其实会调用_createElement
来创建vnode
,我们来看看_createElement
的代码:core/vdom/create-element.js
首先我们来理解参数,假设我们现在是创建如下所示的最外层
div
元素:那么这几个参数分别是:
context
,这是vm
本身,因为有这个context
的存在所以我们才能在模板中访问vm
上的属性方法tag
就是div
data
是attributes被解析出来的配置{ staticClass: 'hello', on: {}
children
, 其实就是_c('span')
返回的span
对应的vnode
,被数组包了一下我们在看函数体,几个条件判断有一点点绕,但是最终都是为了判断到底是需要创建一个
vnode
还是需要创建一个component
。我画了一个图来表示上面的条件判断: 解释下resolveAsset
其实就是看 tag 有没有在components
中定义,如果已经定义了那么显然就是一个组件。对这段逻辑:比较常见的情况是:如果我们的
tag
名字是一个保留标签,那么就会调用new VNode
直接创建一个vnode
节点。如果是一个自定义组件,那么调用createComponent
创建一个组件。而保留标签其实就可以理解为 DOM 或者 SVG 标签。因此在我们的例子中
span
是一个保留标签,所以会调用new VNode()
直接创建一个vnode
出来。VNode
类其实非常简单,他就是把传入的参数都记录了下来而已。因为代码比较长所以这里只贴出一部分代码,有兴趣的话可以去 **core/vdom/vnode.js` 里面看看:core/vdom/vnode.js
那么如果是第二种情况,我们创建的是一个自定义的组件要怎么办呢?我们看看
createComponent
的代码:core/vdom/create-component.js
最前面一大段都是对
options
,model
,on
等的处理,我们暂且跳过这些内容,直接看 vnode 的创建:也就是说,其实自定义组件
current-time
也是创建了一个vnode
,那么和span
这种原生标签肯定有区别的,最大的区别在componentOptions
上,如果我们是自定义组件,那么会在componentOptions
中保存我们的组件信息,而span
这种原生标签就没有这个数据:显然,对于
span
和current-time
的更新机制肯定是不同的。由于我们知道了createComponent
最终也会创建一个vnode
,前面的一张图中我们可以增加一个箭头,改成这样:回到最开头的
_render
,我们知道它最终返回了一个vnode
节点组成的虚拟DOM树,树中的每一颗节点都会存储渲染的时候需要的信息,比如context
,children
等。那么Vue是如何把vnode
渲染成真实的DOM呢?我们在下一章讲解下一章:Vue2.x源码解析系列十:Patch和Diff 算法