重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
正文
render
在 Vue实例挂载的实现 中可以看到 render
函数是Vue实例挂载渲染的重点,那本篇过说一下 vm
的 render
方法的内部逻辑,看看它是怎么实现的。
它是定义在 src/core/instance/render.js
里面,有一个 Vue.prototype._render
方法:
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options // reset _rendered flag on slots for duplicate slot check if (process.env.NODE_ENV !== 'production') { for (const key in vm.$slots) { // $flow-disable-line vm.$slots[key]._rendered = false } } if (_parentVnode) { vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject } // 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) { handleError(e, vm, `render`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { if (vm.$options.renderError) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) } catch (e) { handleError(e, vm, `renderError`) vnode = vm._vnode } } else { vnode = vm._vnode } } else { vnode = vm._vnode } } // return empty vnode in case the render function errored out if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode }
首先拿到 $options
里面的 render
函数和一个 _parentVNode
函数,重点看下这个 $options
的 render
。后面有一句 render.call(vm._renderProxy, vm.$createElement)
,第一个参数 vm._renderProxy
在生产环境中表示vm,在开发环境中表示一个proxy 对象,后面的 vm.$createElement
可以从上面的代码中这个js文件中找到:
// bind the createElement fn to this instance // so that we get proper render context inside it. // args order: tag, data, children, normalizationType, alwaysNormalize // internal version is used by render functions compiled from templates vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // normalization is always applied for the public version, used in // user-written render functions. vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
它定义在 initRender
方法中, initRender
被 initMixin
调用,initMixin
是在Vue初始化的时候被调用了。这里知道就好, 来看下 $createElement
函数做了什么事情。
上面的 vm._c
是被编译生成的 render
函数使用的方法,vm.$createElement
是我们手写 render
函数的时候提供的一个可以生成 VNode
的一个方法。既然它是给手写的 render
使用的,那写个例子看看它是怎样被使用的(写法参考 官网说明):
<!-- index.html --> <html> <body> <div id="app"></div> </body> </html>
//main.js import Vue from "vue"; var app = new Vue({ el: "#app", render(createElement){ //render会返回一个VNode,这里用渲染函数写法 return createElement("div", { attrs:{ id: "#app1" } }, this.name) }, data(){ return { name: "abc" } } })
这种写法也可以渲染到页面上,而且没有使用双大括号把变量渲染成字符串并添加到页面上这个过程,因为之前的写法是在 里面先定义了一个比如说 #app
的元素,它在不执行 vue 的时候把这个元素渲染出来,然后在 new Vue
之后,执行 mount
再把定义的插值(这里是name)把它给替换掉,然后显示真实的数据。而通过定义 render
函数,就不用在页面上显示 #app
这个插值,当 render
执行结束就把 name给替换上去。还记得之前说的 render
的参数来源可以是 template
转化成 el
么,这里就不用 template
了,相当于省去了这一步,当然效果是一样的,最终生成的元素的 id
是 app1
。
从这里可以看出来,挂载的元素,其实是会替换掉原来的 #app
元素,也就是说生成的东西会把整个 #app
标签给替换掉,所以这也就是为什么不能在 上做事情,不然就替换了整个 了。到此 createElement
的作用完毕,再来看下 vm._renderProxy
是干嘛的。
在 src/core/instance/init.js
中的 initMixin
方法里有这段代码:
/* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm }
接着后面调用了 initRender
方法,也就是刚才声明 vm.$createElement
的地方。如果是生产环境,就直接把 vm
赋值给 vm._renderProxy
,开发环境就调用 initProxy
函数,它定义在 src/core/instance/proxy.js
内:
initProxy = function initProxy (vm) { if (hasProxy) { // determine which proxy handler to use const options = vm.$options const handlers = options.render && options.render._withStripped ? getHandler : hasHandler vm._renderProxy = new Proxy(vm, handlers) } else { vm._renderProxy = vm }
判断了一个 hasProxy
,整个参数定义在上面:
const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy) if (hasProxy) { const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact') config.keyCodes = new Proxy(config.keyCodes, { set (target, key, value) { if (isBuiltInModifier(key)) { warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`) return false } else { target[key] = value return true } } }) } const hasHandler = { has (target, key) { const has = key in target const isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_') if (!has && !isAllowed) { warnNonPresent(target, key) } return has || !isAllowed } } const getHandler = { get (target, key) { if (typeof key === 'string' && !(key in target)) { warnNonPresent(target, key) } return target[key] } }
就是判断了一下浏览器支不支持 Proxy
。支持的话就声明一个 proxy
劫持,劫持之后做了的事情都在 hashandler
里面,里面主要就是走 warnNonPresent
方法:
const warnNonPresent = (target, key) => { warn( `Property or method "${key}" is not defined on the instance but ` + 'referenced during render. Make sure that this property is reactive, ' + 'either in the data option, or for class-based components, by ' + 'initializing the property. ' + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', target )
会报这个错,这个错应该也比较熟悉,报错原因就是说在属性和方法中使用了一个没有定义的值。到此 proxy
劫持参数完毕。
来简单的捋一遍 render
方法里面的 vnode = render.call(vm._renderProxy, vm.$createElement)
都做了什么事情:第一个参数做一个代理劫持,用来报错,第二个参数用来创建一个插值,最终,render
生成了一个 vnode
。
知道 render
会生成一个vnode
之后,继续看 render
方法的后面逻辑。判断 vnode
有没有在 VNode
上,如果没有并且 vnode
是一个数组,就报错:
warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm
说明模板有多个根节点,就是说有多个 vnode
,这里就报了一个错,说 root
根节点只能有一个 node
。
所以最终, render
方法通过内部的 createElement
函数,会生成一个 vnode
,而 vnode
其实就是一个 Virtual DOM
,所以在了解 createElement
做了什么事情之前先来说一下 Virtual DOM
是个什么东西。
Virtual DOM
Virtual DOM
应该都不陌生,出现 Virtual DOM
的原因是因为浏览器加载DOM是比较 “昂贵“ 的,可以看下一个 div
大概有多少东西:
可见一个 DOM 元素里面的东西很多很多,这也是浏览器的设计标准,所以我们如果频繁的操作 DOM ,对浏览器的性能损耗是一个很大的问题。而 Virtual DOM
就是通过JS来描述记录一个 dom 节点,所以这个代价要小得多。下面看下在 vue 中, Virtual DOM
是如何定义一个 vnode 的,在 src/core/vdom/vnode.js
中:
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching fnScopeId: ?string; // functional scope id support 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 this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } }
可以看到,里面定义了标签tag,数据VNodeData(包括了slot,tag,attrs等等,在 ~/vue/flow/vnode.js
中),子节点children,文本text,持有的节点elm等等。
所以总的来看, VNode
其实就是包含了真实节点的一堆东西,以及增加了和 vue 有关的属性,从而扩展 VNode
的灵活性。由于 VNode
只是用来映射到 真实DOM 的渲染,不需要包含操作 DOM 的方法,所以它特别的轻量。
在定义好 VNode
之后,要映射到真实DOM 要经历 VNode
的 create
、diff
、 patch
等过程,而上面提到的 createElement
就是 create
。在知道了 render
和 Virtual DOM
之后,下篇说下 createElement
是如何添加元素的。