vue2渲染原理
通过前面的文章,我们了解了vue2的响应式原理和异步更新原理。接下来我们将介绍下vue的渲染原理,一下是一个vue最简单的使用案例,我们将对照案例和源码讲解,vue2是怎么实现从new Vue到页面dom节点渲染的过程。
案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>
姓名: {{ name }}
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
name: '张三'
}
}
})
</script>
</body>
</html>
new Vue
new Vue就是调用Vue的构造函数,看源码
// core/instance/index.js
function Vue (options) {
// 开发环境,错误提示代码忽略
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// 调用 内部实例方法 进行初始化
this._init(options)
}
// 混入 Vue.prototype._init 方法
initMixin(Vue)
// 省略其他 实例方法混入代码
可以看到,new Vue 就仅仅调用了内部实例方法 _init 进行初始化,接下来看 _init 的实现
// core/instance/init.js
// 以下代码省略了部分开发环境错误提示代码
Vue.prototype._init = function (options?: Object) {
// 实例
const vm: Component = this
// 每个 vue 实例都有一个 _uid,并且是依次递增的
vm._uid = uid++
// 避免vm实例被响应式化的标识
vm._isVue = true
// 处理组件配置项
if (options && options._isComponent) {
// 子组件的配置项处理, 忽略
initInternalComponent(vm, options)
} else {
// 合并构造函数的options和当前实例的options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 存下vm, 忽略
vm._self = vm
initLifecycle(vm) // 将生命周期相关的标识初始化
initEvents(vm) // 初始化自定义事件
initRender(vm) // 初始化 渲染相关内容,主要是插槽和 vm.$createElement方法即 h 函数
callHook(vm, 'beforeCreate') // 调用 beforeCreate 生命周期钩子
initInjections(vm) // 处理 injections
initState(vm) // 处理 props、methods、data、computed、watch
initProvide(vm) // 处理 provide
callHook(vm, 'created') // 调用 created 生命周期钩子
// 重点: 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount,反之,没有 el 则必须手动调用 $mount
if (vm.$options.el) {
// 调用$mount方法挂载vue生成的dom
vm.$mount(vm.$options.el)
}
}
}
上面代码看着不少,其实主要就 3 步,1. 处理options,选项合并,2.初始化和处理传进来的各选项参数 ,3.调用$mount挂载dom
先不管初始化的内容,直接看挂载
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
const options = this.$options
// 将template或者el转化成 render函数
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
// compileToFunctions即将template转化成render函数的方法
// staticRenderFns用来处理使用了v-pre指令的节点,会加快编译的,可以暂时忽略
// 大致流程:
// compileToFunctions一共分成四个步骤:
// parse:把template转成AST语法树
// optimize:优化静态节点
// generate:通过ast,重新生成代码
// 通过new Function生成render函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// 挂载 render 函数到 this.$options 上
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 最后调用 mountComponent
return mountComponent(this, el, hydrating)
}
上面的案例经过编译后 render函数就是这样:
function () {
with(this){
return _c('div',{attrs:{"id":"app"}},[_c('div',[_v("\n姓名: "+_s(name)+"\n ")])])
}
}
其中,利用with语法实现了template里的变量的自动代理,效果就是写template时不用写this了。
_c,_v,_s 就是vue的一些编译时的转换帮助函数别名,其中_c就是 createElement,其他的可以参见源码
// core/instance/render-helpers.js
function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
// core/instance/render.js
export function initRender (vm: Component) {
// 省略其他代码
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)
}
生成render函数并挂载到this.$options 上后,就是 调用mountComponent函数继续处理
// core/instance/lifecycle.js
// 省略部分开发环境代码
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean // ssr相关参数,忽略
): Component {
vm.$el = el
// 没有render就建个空的
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
// 调用 beforeMount 生命周期钩子
callHook(vm, 'beforeMount')
// 更新组件的函数
let updateComponent = () => {
// 调用 _render生成VNode, 调用_update将VNode转成dom并挂载
vm._update(vm._render(), hydrating)
}
// 创建 渲染watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// 根组件没有 $vnode, 根组件的 mounted 在这里处理,子组件的在其他位置
if (vm.$vnode == null) {
vm._isMounted = true
// 调用 mounted 生命周期钩子
callHook(vm, 'mounted')
}
return vm
}
上面mountComponent主要就是创建了渲染watcher,渲染watcher会初始化时立即执行一次,所以就会调用updateComponent方法, 然后调用 _render生成VNode,调用_update将VNode转成dom并挂载。
先看_render
// core/instance/render.js
// 省略部分开发环境代码
Vue.prototype._render = function (): VNode {
const vm: Component = this
// 取出先前挂载在$options上的render函数
const { render, _parentVnode } = vm.$options
// 非根组件,处理下slot
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
vm.$vnode = _parentVnode
let vnode
// render函数存在时用户直接编写的情况,所以要做下容错处理
try {
currentRenderingInstance = vm
// 调用render函数,生成VNode
vnode = render.call(vm, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
vnode = vm._vnode
} finally {
currentRenderingInstance = null
}
// 如果render函数返回了多了VNode,则只取第一个,这里就是要求我们写template时最顶层一定只能有一个节点
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
if (!(vnode instanceof VNode)) {
// 返回了非VNode的内容,则直接给个空节点
vnode = createEmptyVNode()
}
// set parent,这里的vnode其实是组件内部template转换出来的虚拟dom,也就是组件的第一元素的vnode,而_parentVnode是整个组件对应的 组件vnode,有点绕可以细细品
vnode.parent = _parentVnode
return vnode
}
可以看到_render就是调用之前根据选项生成的render函数,而render函数内部又会调用_c之类的方法也就是上面的createElement生成VNode,也就是大名鼎鼎的虚拟dom。
// core/vdom/create-element.js
// 省略部分开发环境代码和其他功能代码
function createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 处理is
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
return createEmptyVNode()
}
let vnode
if (typeof tag === 'string') {
let Ctor
// html节点
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
// 组件节点
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
// 未知节点
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// 直接试 component options 或者 constructor 的情况
vnode = createComponent(tag, data, context, children)
}
// 返回 vnode
if (isDef(vnode)) {
return vnode
} else {
return createEmptyVNode()
}
}
createElement方法就是根据tag不同,去掉new VNode生成不同的虚拟dom。
// core/instance/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
devtoolsMeta: ?Object; // used to store functional render context for devtools
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
}
get child (): Component | void {
return this.componentInstance
}
}
以上是VNode的全部内容,其实有的多,我们只用关心// strictly internal之上的字段,其他的很多是内部细节逻辑处理需要的字段,暂时可以跳过。
好的,了解了虚拟dom即VNode的生成过程,在看_update方法的实现,再了解虚拟dom是怎么变成真实dom的
// core/instance/lifecycle.js
// 省略部分其他代码
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// 缓存旧节点
const prevVnode = vm._vnode
vm._vnode = vnode
if (!prevVnode) {
// 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
_update就是调用vm.__patch__方法进行的挂载和更新, vm.__patch__就是patch方法,这么写是为了兼容Weex,所以我们直接看patch的实现,patch同时是支持首次渲染和更新两种情况的
// core/vdom/patch.js
// 简化了部分代码
function patch (oldVnode, vnode, hydrating, removeOnly) {
// 调用到patch时vm.$el会被处理掉,所以这里的oldVnode会不存在,即首次渲染
if (isUndef(oldVnode)) {
// 首次渲染
createElm(vnode)
} else {
// 更新时的操作,这里就是大名鼎鼎的 diff 算法的内容,下一章,我们会讲
}
// 返回生成的真实dom
return vnode.elm
}
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
vnode.isRootInsert = !nested // for transition enter check
// 创建子组件,并挂载子组件, 子组件的逻辑后面的章节会讲
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 正常dom节点
if (isDef(tag)) {
// 调用dom的方法创建节点, nodeOps里封装的就是dom的各种方法,insert也是,不直接使用是为了兼容Weex
vnode.elm = nodeOps.createElement(tag, vnode)
// 创建子节点
createChildren(vnode, children, insertedVnodeQueue)
// 插入进页面
insert(parentElm, vnode.elm, refElm)
// 注释节点
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
// 文本节点
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
至此,vue的首次渲染流程完成。后面就是数据更新后,导致的上面创建的渲染watcher更新。触发patch里的更新,就是大名鼎鼎的diff算法,下一章讲。
总结一下:
new Vue(options) -> this._init 初始化 -> this.$mount 将template编译成 render -> mountComponent创建渲染watcher -> this._render 调用刚刚的render 生成 VNode -> this._update -> patch 生成真实dom 并挂载