Vue源码翻译之渲染逻辑链
本篇文章主要要记录说明的是,Vue在Vdom的创建上的相关细节。这也是描绘了Vue在界面的创建上的一个逻辑顺序,同时我也非常拜服作者编码的逻辑性,当然或许这么庞大复杂的编码不是一次性铸就的,我想应该也是基于多次的需求变动而不断完善至现在如此庞大的结构和复杂度。
首先我们回顾 上一篇文章 中,讲到了Vue实例initMixin,就是实例初始化,但是,我们在看Vue的源码时,经常会遇到某个变量或方法,好像还没定义,怎么就用上了。那是因为,其实我们在使用Vue,即 new Vue(options) 的时候,其实Vue的整个类,已经在我们js的头部import时,就已经完全定义好了,Vue的类因为过于庞大,内部复杂,并且还有抽象分层,所以类的整个写法,会比较分散,但是当你在用它的时候(new Vue()),其实它已经完全初始化完毕,整个类的装配已经齐全,所以我们在看源码时,是根据工程目录来看,但Vue是建立在文本pack上,所以最终这些工程目录是会整合到一个文件里,所以我们遇到没看到的变量,不要感到困惑,你只要知道,它一定是在其他的某个地方初始化过。
So,我们这次要说的,是整个Vue再界面的绘制逻辑。
整个Vue组件的绘制过程,是这样一个方法链条:
vm.$mount() -> mountComponent -> new Watcher()的构造函数 -> watcher.get() -> vm._update -> vm.__patch__()-> patch.createElm -> patch.createComponent -> componentVNodeHooks.init() -> createComponentInstanceForVnode -> child.$mount
好了,从vm.$mount() -----> child.$mount,我相信大家应该看出个名堂来了,其实这很像递归调用。在执行createComponentInstanceForVnode的时候,就把创建好的Vnode与父级Vnode进行关联,通过这么一长串的递归调用去创建整个Vnode Tree,然后在整个树创建完了以后呢,在patch那部分的代码,会继续后续逻辑,后续逻辑自然就是把这个创建好的局部Vnode树,替换掉对应的旧的Vnode节点,相当于更新了局部的页面内容。但这只是执行界面绘制的动作链条,要理解整个过程,要区分一下,区分成执行,和初始化两个步骤。我们来看看定义是从哪里开始的。
首先要看的肯定是上一篇文章中讲到的 vm._c 以及 vm.$createElement ,这个函数的定义,是整个界面绘制逻辑的入口,但是并不是动作触发的入口,就像这个函数的名字一样,initRender,初始化绘制方法,实际上,就是对绘制动作进行了定义,但是并不是从这里执行。
InitRender
path:src/core/instance/render.js
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 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) // $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data /* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
由此,我们再去查看createElement,这是一个又一个代码的封装,整个方法链的调用是这样子:
createElement -> _createElement -> createComponent
最终返回Vnode对象或Vnode对象数组(应该是在v-for的情况下返回数组)。中间的片段包含着一些校验逻辑,我就不说了,不是什么特别难理解的地方,我们直接看createComponent的方法
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
} const baseCtor = context.$options._base // plain options object: turn it into a constructor
if (isObject(Ctor)) {
// Vue.extend(Component)
Ctor = baseCtor.extend(Ctor)
} // if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
} // async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
} data = data || {} // resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor) // 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
}
} // merge component management hooks onto the placeholder node
mergeHooks(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
}
首先,说明一下,入参Ctor是什么。其实这个Ctor,就是你平时写Vue文件时,components 对象里的那些东西,就是你写的单个Component对象。
这个可以从上层_createElement方法中得知,如下图:
其中调用的resolveAsset方法,就是从你的options,即你写的Component中,获取components属性,并且同时验证一下,与对应的tag是否存在于你定义的文件中,这个tag,是标签,是html标签,我们在使用自定义Vue组件的时候,都是自定义标签或<div is='componentName'></div> 这样的方式。而这个tag就是要吗是is的值,要吗是你使用的html标签。
再来。回到createComponent方法中,可以看到,代码一开始会去判断你这个组件对象是否是undefind,如果是undefind,那就直接退出。再往下看,有一行其实我们很熟悉,但可能有点懵逼的代码,就是 Ctor = baseCtor.extend(Ctor) ,这里怎么感觉有点熟悉,是的,这里其实就是我们经常在文档中看到的 Vue.extend(Component) 这么一个方法。这个baseCtor可以看到是从contentx.$options._base来的,这个contex 上级方法追溯就可以知道是一个vm对象,但是这个_base从何而来?不要着急,前面说了,遇到这种好像没看过的,它一定是在某处已经初始化过了,我们不用怀疑它,只需要找到他。
其实它在 src/core/global-api/index.js文件中,initGlobalAPI方法中就定义了,并且他指的就是Vue对象。
然后我们再回到 createComponent 方法这个主线任务中,继续往下打怪,我们会发现遇到一个函数是mergeHooks,
function mergeHooks (data: VNodeData) {
if (!data.hook) {
data.hook = {}
}
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const fromParent = data.hook[key]
const ours = componentVNodeHooks[key]
data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours
}
}
所谓hook,就是钩子,那再Vue中,这个钩子自然就是在代码中的某处可能会执行的方法,类似Vue实例的生命周期钩子一样。细看这个方法,它涉及到了一个对象,就是componentVNodeHooks对象,这个方法其实就是把这个对象里的init、prepath、insert、destory方法存进data.hook这个对象中罢了,那你回头要问,这个data又是从哪里来?一直追溯你会发现,这个是$createElement函数上的参数,咦?好像线索就断了= =?这个时候如果想要简单理解,只需要查找 Vue文档——深入data对象 你大概就知道这个data是神马了。
而此处正定义了,最开头说的界面渲染的执行动作链条中的递归调用创建子节点的部分。但是大家可能会觉得,奇怪,这个函数最终是走到了$createElement,可是跟先前提到的那个动作链条似乎没有相关,就算定义了data.hook,让动作链条就有componentVNodeHooks.init() 这个方法,可是什么地方触发这个定义呢?最开始的动作链条似乎没有涉及定义这部分呀?没地方触发这些定义的方法呀?
大家稍安勿躁,所以我说真的是很绕,不可能没定义,否则到执行data.hook.init的时候就undefind了。
我们要回头看一下,在Vue进行初始化装配的时候,有执行这么一个方法 renderMixin(Vue) :
export function renderMixin (Vue: Class<Component>) {
// install runtime convenience helpers
installRenderHelpers(Vue.prototype) Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
} 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
}
}
可能有的人就看明白了,我们看看,我们平时写组件的时候,如果你有用到render的方式来写组件样式,那是如何工作的。在Vue.prototype._render这个方法体内,你会看到render从vm.$options中取出(vm.$options就是你写的Component内的那些data、props等等的属性),然后再看上面截出的代码的第31行,render.call(vm._renderProxy,vm.$createElement),然后返回一个vnode,所以说,$createElemment在此处就会被调用,然后进行上面说的那些乱七八糟的代码。但是你可能又会问:render.call?我平时写Component的时候从来没用render函数来做界面绘制呀!这个render又是在什么时候被定义在$options的呢?否则直接从$options中取出肯定是会报错的呀。还是我刚才那句话,不是没定义,只是没找到,实际上是定义了,定义在哪儿了?定义在mountComonent的最开始的部分了。
然后你可能又会想,那按照代码的执行顺序,能确保在使用前就定义了吗?答案自然是肯定的。我们刚才看到$createElement这个方法,是被定义在vm._render当中,别忘了我们还有一个很重要的任务,就是找到$createElement是在哪里被执行的,那也就是说,vm._render()是在哪里被执行的。其实它就在mountComponent当中执行的,而且还一定是在render被定义之后才执行的。
其实这段代码不是简单地从上至下执行那么容易理解,你可以看到updateComponent的写法,其实它只是被定义了,而且在定义的时候,vm._update实际上是没有执行的,并且vm._render()也是没有被执行的,他们实际上是到了下面new Watcher()的构造函数当中才被执行,同时我们也可以看到,整个定义和动作执行两个过程中,在watcher的构造函数里,执行updateComponent方法时,vm._render()一定先执行然后返回一个vnode,然后才是到了vm._update开始执行,也就是说,此时data.hook已经被装填了init等函数,所以在最开始的执行链不会因为属性尚未定义而报出undefind被打断。
哈哈,真的很绕。说实在话,看了良久才看明白这绕来绕去的逻辑。
另外,在我研读这份源码时,我才发现(额,我并木有什么偏见),src/platforms 包下,除了web,多了一个weex。然后我就又回过头理解了一圈,发现vue是把vm.$mount以及相关界面的模块整个都抽出来单独写,然后在不同的平台,就可以使用不同的渲染方式,然后我们在使用webpack打包时,只修要针对自己想要的平台打包对应的模块。如此将界面渲染层分开写,真的是增加了Vue的扩展性,整个工程就很好扩展和管理。拜服大神的设计。
Vue源码翻译之渲染逻辑链的更多相关文章
- Vue源码翻译之组件初始化。
废话不多说. 我们先来看看Vue的入口文件. import { initMixin } from './init' import { stateMixin } from './state' impor ...
- Vue源码--深入模板渲染
原文链接:https://geniuspeng.github.io/2018/02/07/vue-compile/ 之前整理了vue的响应式原理,在这里有一点是一直很模糊的,就是何时去new一个wat ...
- Vue源码中compiler部分逻辑梳理(内有彩蛋)
目录 一. 简述 二. 编译流程 三. 彩蛋环节 示例代码托管在:http://www.github.com/dashnowords/blogs 博客园地址:<大史住在大前端>原创博文目录 ...
- 前端Vue 源码分析-逻辑层
Vue 源码分析-逻辑层 预期的效果: 监听input的输入,input在输入的时候,会触发 watch与computed函数,并且会更新原始的input的数值.所以直接跟input相关的处理就有3处 ...
- Vue源码探究-虚拟DOM的渲染
Vue源码探究-虚拟DOM的渲染 在虚拟节点的实现一篇中,除了知道了 VNode 类的实现之外,还简要地整理了一下DOM渲染的路径.在这一篇中,主要来分析一下两条路径的具体实现代码. 按照创建 Vue ...
- Vue 源码解读(10)—— 编译器 之 生成渲染函数
前言 这篇文章是 Vue 编译器的最后一部分,前两部分分别是:Vue 源码解读(8)-- 编译器 之 解析.Vue 源码解读(9)-- 编译器 之 优化. 从 HTML 模版字符串开始,解析所有标签以 ...
- 【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码
前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一 ...
- vue源码逐行注释分析+40多m的vue源码程序流程图思维导图 (diff部分待后续更新)
vue源码业余时间差不多看了一年,以前在网上找帖子,发现很多帖子很零散,都是一部分一部分说,断章的很多,所以自己下定决定一行行看,经过自己坚持与努力,现在基本看完了,差ddf那部分,因为考虑到自己要换 ...
- Vue 源码解读(3)—— 响应式原理
前言 上一篇文章 Vue 源码解读(2)-- Vue 初始化过程 详细讲解了 Vue 的初始化过程,明白了 new Vue(options) 都做了什么,其中关于 数据响应式 的实现用一句话简单的带过 ...
随机推荐
- springboot工程读取配置文件application.yml的写法18045
现在流行springboot框架的项目,里面的默认配置文件为application.yml,我们怎样读取这个配置文件呢? 先贴上我得配置文件吧 目录结构 里面内容 1 写读取配置文件的工具类 @Con ...
- Vue的配置
一.build:打包的配置文件的文件夹 1.build.js 生产版本的配置文件,一般这个文件我们是不改的 'use strict' //调用检查版本的文件,check-versions的导出直接是 ...
- js-倒计时原理
<!DOCTYPE html><html> <head> <meta charset="UTF-8"> ...
- jdbc的配置(更新中)
MySQL的 JDBC URL 格式 for Connector/J 如下例: 格式如下: jdbc:mysql://[host][,failoverhost...][:port]/[databas ...
- allegro中如何添加安装孔(注:在PCB图纸中添加)
最近再给外国一家公司做某一个小的系统模块的封装,其中这个模块中间是挖空的,这就比较难办,到现在为止我还没有找到如何在封装中添加自己绘制特定形状的过孔,(倒是可以添加软件自带的一些圆形的安装孔)最终解决 ...
- Hive Bug修复:ORC表中array数据类型长度超过1024报异常
目前HVIE里查询如下语句报错: select * from dw.ticket_user_mtime limit 10; 错误如下: 17/07/06 16:45:38 [main]: DEBUG ...
- 【转】基于Token的WEB后台认证机制
原谅地址:http://www.cnblogs.com/xiekeli/p/5607107.html 几种常用的认证机制 HTTP Basic Auth HTTP Basic Auth简单点说明就是每 ...
- 《AngularJS深度剖析与最佳实践》笔记: 第二章 概念介绍
第二章 概念介绍 2.1 什么是UI? 用户界面包括内容(静态信息+动态信息), 外观, 交互. 在前端技术栈中分别由HTML, CSS和JS负责. 进一步抽象, 分别对应于MVC三个主要部分: Mo ...
- [学习笔记]树形dp
最近几天学了一下树形\(dp\) 其实早就学过了 来提高一下打开树形\(dp\)的姿势. 1.没有上司的晚会 我的人生第一道树形\(dp\),其实就是两种情况: \(dp[i][1]\)表示第i个人来 ...
- solr初识
参考资料http://blog.csdn.net/l1028386804/article/details/70199983