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
1 export function initRender (vm: Component) { 2 vm._vnode = null // the root of the child tree 3 vm._staticTrees = null // v-once cached trees 4 const options = vm.$options 5 const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree 6 const renderContext = parentVnode && parentVnode.context 7 vm.$slots = resolveSlots(options._renderChildren, renderContext) 8 vm.$scopedSlots = emptyObject 9 // bind the createElement fn to this instance 10 // so that we get proper render context inside it. 11 // args order: tag, data, children, normalizationType, alwaysNormalize 12 // internal version is used by render functions compiled from templates 13 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) 14 // normalization is always applied for the public version, used in 15 // user-written render functions. 16 vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) 17 18 // $attrs & $listeners are exposed for easier HOC creation. 19 // they need to be reactive so that HOCs using them are always updated 20 const parentData = parentVnode && parentVnode.data 21 22 /* istanbul ignore else */ 23 if (process.env.NODE_ENV !== 'production') { 24 defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => { 25 !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm) 26 }, true) 27 defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => { 28 !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm) 29 }, true) 30 } else { 31 defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true) 32 defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true) 33 } 34 }
由此,我们再去查看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 constructorif (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 asyncFactoryif (isUndef(Ctor.cid)) {asyncFactory = CtorCtor = 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 & eventsif (isDef(data.model)) {transformModel(Ctor.options, data)}// extract propsconst propsData = extractPropsFromVNodeData(data, Ctor, tag)// functional componentif (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 listenersconst listeners = data.on// replace with listeners with .native modifier// so it gets processed during parent component patch.data.on = data.nativeOnif (isTrue(Ctor.options.abstract)) {// abstract components do not keep anything// other than props & listeners & slot// work around flowconst slot = data.slotdata = {}if (slot) {data.slot = slot}}// merge component management hooks onto the placeholder node mergeHooks(data)// return a placeholder vnodeconst name = Ctor.options.name || tagconst 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,
1 function mergeHooks (data: VNodeData) { 2 if (!data.hook) { 3 data.hook = {} 4 } 5 for (let i = 0; i < hooksToMerge.length; i++) { 6 const key = hooksToMerge[i] 7 const fromParent = data.hook[key] 8 const ours = componentVNodeHooks[key] 9 data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours 10 } 11 }
所谓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) :
1 export function renderMixin (Vue: Class<Component>) { 2 // install runtime convenience helpers 3 installRenderHelpers(Vue.prototype) 4 5 Vue.prototype.$nextTick = function (fn: Function) { 6 return nextTick(fn, this) 7 } 8 9 Vue.prototype._render = function (): VNode { 10 const vm: Component = this 11 const { render, _parentVnode } = vm.$options 12 13 // reset _rendered flag on slots for duplicate slot check 14 if (process.env.NODE_ENV !== 'production') { 15 for (const key in vm.$slots) { 16 // $flow-disable-line 17 vm.$slots[key]._rendered = false 18 } 19 } 20 21 if (_parentVnode) { 22 vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject 23 } 24 25 // set parent vnode. this allows render functions to have access 26 // to the data on the placeholder node. 27 vm.$vnode = _parentVnode 28 // render self 29 let vnode 30 try { 31 vnode = render.call(vm._renderProxy, vm.$createElement) 32 } catch (e) { 33 handleError(e, vm, `render`) 34 // return error render result, 35 // or previous vnode to prevent render error causing blank component 36 /* istanbul ignore else */ 37 if (process.env.NODE_ENV !== 'production') { 38 if (vm.$options.renderError) { 39 try { 40 vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) 41 } catch (e) { 42 handleError(e, vm, `renderError`) 43 vnode = vm._vnode 44 } 45 } else { 46 vnode = vm._vnode 47 } 48 } else { 49 vnode = vm._vnode 50 } 51 } 52 // return empty vnode in case the render function errored out 53 if (!(vnode instanceof VNode)) { 54 if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { 55 warn( 56 'Multiple root nodes returned from render function. Render function ' + 57 'should return a single root node.', 58 vm 59 ) 60 } 61 vnode = createEmptyVNode() 62 } 63 // set parent 64 vnode.parent = _parentVnode 65 return vnode 66 } 67 }
可能有的人就看明白了,我们看看,我们平时写组件的时候,如果你有用到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的扩展性,整个工程就很好扩展和管理。拜服大神的设计。
转载于:https://www.cnblogs.com/wuxinzhe/p/8496256.html
Vue源码翻译之渲染逻辑链相关推荐
- vue源码分析:渲染篇
这篇文章是2017年5月份作者大大写的,快一年了,写得很好,多看看.我们现在用的也是vue2.0,可以从源码中深入理解一下vue是如何渲染页面和渲染过程以及原理. 转载于https://blog.cs ...
- Vue源码中compiler部分逻辑梳理(内有彩蛋)
[摘要] Vue compiler部分逻辑梳理 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 简述 compiler模块Vue框架中用于模板编译 ...
- Vue源码之渲染watcher
1. 前文回顾 在上一篇文章<Vue源码之计算属性watcher>中,我们学习了计算属性watcher是如何与计算属性的computedGetter协作,在计算属性所依赖的数据发生变化时, ...
- [Vue源码分析]谷歌翻译后,Vue双向数据绑定失效了?
前言: 最近运营反馈了一个问题:谷歌浏览器打开第三方储值平台,使用谷歌浏览器自带的翻译功能后,选择商品没有计算总额. 首先可以肯定的是这不是bug,这个平台已经兼容了13种语言,只是运营没有通过语言栏 ...
- 【一套代码小程序NativeWeb阶段总结篇】可以这样阅读Vue源码
前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一 ...
- vue源码(八)揭开数据响应系统的面纱
本文是学习vue源码,之所以转载过来是方便自己随时查看,在这里要感谢HcySunYang大神,提供的开源vue源码解析,写的非常非常好,简单易懂,比自己看要容易多了,他的文章链接地址是http://h ...
- 约2万字-Vue源码解读汇总篇(续更)
约2万字-Vue源码解读汇总篇(续更) 一.前言 1.系列汇总 未完待续... Vue源码解读:06Vue3探索篇 Vue源码解读:05生命周期篇 Vue源码解读:04模板编译篇 Vue源码解读:03 ...
- Vue源码解析系列——数据驱动篇:patch的执行过程
准备 vue版本号2.6.12,为方便分析,选择了runtime+compiler版本. 回顾 如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:<Vue源码分析系列:目录> ...
- 深入剖析Vue源码 - 响应式系统构建(上)
从这一小节开始,正式进入Vue源码的核心,也是难点之一,响应式系统的构建.这一节将作为分析响应式构建过程源码的入门,主要分为两大块,第一块是针对响应式数据props,methods,data,comp ...
最新文章
- android自定义progressbar样式,Android开发中如何实现自定义ProgressBar的样式
- 问卷星作答能不能检测到屏幕共享_云端检测,别样精彩——大厂回族自治县王必屯中心小学线上期末检测纪实...
- 云服务器变成多个挂机宝销售,云服务器变成挂机宝
- [ JS 进阶 ] Repaint 、Reflow 的基本认识和优化 (2)
- malloc、calloc、realloc和alloca各种的区别
- spring restTemplate使用方法
- Java 多线程 南北多人过桥
- java 反射 hold_Go进阶:反射3定律
- opensource项目_一月份的Opensource.com预览
- 一个函数返回参数二进制中1的个数
- UOJ#52. 【UR #4】元旦激光炮(交互)
- ASP.NET------站点地图SiteMapPath
- 剧透和评析之車輪の国、向日葵の少女
- 微讲师录课软件下载、录屏软件下载
- ubuntu 下文件/文件夹 比较工具 DiffMerge
- 论文 计算机教育教学能力,高校计算机教师应具备的能力和培养的方向
- Mac顶部菜单栏(Menubar)卡死
- Arduino - 输出引脚扩流
- 【Python】cannot import name ‘ParserError‘ from ‘dateutil.parser‘
- ChatGpt真的会取代程序员吗,有点过分担心了
热门文章
- java中的集合框架
- 某博数据挖掘:使用Scrapy构建自定义数据采集提取洞察信息
- win7系统如何关闭广告弹窗操作方法教学
- 记录一次Golang逃逸分析
- matlab命令批量重命名,MATLAB中预定义对话框之文件打开对话框以及如何批量重命名...
- Chrome 浏览器
- CAJ与PDF与WORD转化方法
- 美国本土四十八个州府48个州府所在城市的TSP旅行商回路17110km
- java判断点与线与面的关系_高中数学必修二点线面的位置关系与线面平行判定及其性质(精华试题版)...
- 柔性电子 --基于碳纳米管的柔性压力传感器