关于vue 2.0源代码分析,已经有不少文档分析功能代码段比如watcher,history,vnode等,但没有一个是分析重点难点的,没有一个是分析大命题的,比如执行router.push之后到底是如何执行代码实现路由切换的?
本文旨在分享本人研究vue 2.0源代码重点难点之结果,不涉及每段源代码具体分析,源代码功能段每个人都可以去分析,只要有耐心,再参考已有高手发表的源代码分析文档,不是太难,主要是要克服一些编程技术问题,比如嵌套回调,递归,对象/数组特殊处理方法等等。
希望本文对那些有兴趣研究vue源代码但遇到困难无解的网友有帮助,研究源代码是个人兴趣个人的事情,需要自己去debug跟踪研究,只是看别人的研究文档并不代表自己真的懂多少,有多少提高,所以
本文不涉及从头到尾每段源代码的具体的分析,那个自己有兴趣去看看研究研究即可,不是太难,只要对js对象编程技术有一定理解就可以。
首先要说的是,vue 2.0的复杂性和难点都是由于采用vnode技术引起的,如果不采用vnode技术,像1.0那样,就没有这些复杂性和难点。
顺带提一下vue 1.0,Vue 1.0最大的迷惑是组件数据变化时如何处触发页面更新?
答案在:
var watcher = new Watcher(vm, expOrFn, cb, options);
以及:
function defineReactive(obj,key,val,customSetter) {
var dep = new Dep(); //每个属性建立一套dep,会复制/引用保存到set/get方法中与属性一起存在
Object.defineProperty(obj, key, {
get: function reactiveGetter () { //创建watcher时会访问执行属性的get方法获取表达式的值!!!
if (Dep.target) { //当前正在创建的watcher实例保存在全局!!!
dep.depend(); //把当前正在创建的watcher实例保存到属性的dep中
set: function reactiveSetter (newVal) {
dep.notify(); //去属性的dep找watcher/update执行更新页面中绑定的指令表达式
顺带,vuex是用computed方法实现的,而computed方法是基于defineReactive实现的,就是defineReactive技术。
vue 1.0具体就不再分析,网上已经有几个文档分析很透彻。
。
2.0从router.push()开始路由切换时执行transitionTo/confirmtransition的代码莫名其妙,似乎不太对劲,到底最关键的代码逻辑流程在哪里?确实很难破解,因为涉及到源代码总体关键设计思想逻辑,甚至可以说是
设计奥秘,vue作者是个了不起的大神,大神的代码都有很隐蔽很深奥的设计逻辑和编程代码难以破解,比如angular,它的模块机制非常复杂深奥,源代码难以破解。
本文命题破解要点:
1)每个组件都会创建new watcher:
vm._watcher = new Watcher(vm, function () {
vm._update(vm._render(), hydrating); //先产生vnode,再更新组件页面
根组件watcher/update方法何时如何被执行?
new Vue()初始化根组件时即会执行,根组件有属性变化时也会触发执行。
keep-alive组件的watcher/update方法何时如何被执行?????
总不能写vm._update()吧? (vm假定是keep-alive组件实例)
keep-alive组件没有template没有data,没法用data属性触发执行watcher/update吧?
答案是在源代码中当初始化keep-alive组件的vnode时(也就是执行vnode.data.hook.prepatch方法)会强制执行vm._update()更新keep-alive组件极其页面,其中vm是keep-alive组件,keep-alive组件的页面就是
路由组件页面,router-view负责切换路由组件并且做为keep-alive的子组件,在keep-alive创建vnode时传递路由组件,然后保存在keep-alive vnode的componentOptions的children中,keep-alive和router-view都是占位/管理组件,它有子节点就是路由组件vnode,keep-alive只负责处理缓存,而router-view负责路由组件切换,也就是创建一个新的路由组件,并且更新页面,但当外套<keep-alive>时,router-view不再处理替换,而是把新建的路由组件vnode传递给keep-alive,keep-alive可以从缓存恢复路由组件的实例,然后再更新页面。
2)根组件的_route属性
从$router.push()开始路由切换,先执行transitionto()以及confirmtransition,这是巨大的坑,这个过程只是处理辅助功能,主要是执行leave和beforeEnter等钩子函数,钩子函数可有可无,这段代码99%都可以
不起任何作用,但看这段代码跟看天书一样,已经有滴滴高手分析了这段代码。
执行transitionto最后会执行回调,在回调代码中会设置根组件的_route属性=当前路由,这是一个关键点,
vue已经针对根组件的_route属性建立了watcher,当set这个属性时,会执行wacther/update,也就是执行
vm._update(vm._render(), hydrating) (其中vm是根组件)
就是从这里开始真正的路由切换处理,首先执行_render()产生根组件的vnode,再执行_update(vnode)方法调用__patch__(vnode)方法更新根组件页面。
假定页面是这样写的:
<keep-alive>
<router-view></router-view>
</keep-alive>
执行_render()方法时,大家首先要知道根组件template编译之后产生的render/code包含有:
_c(‘keep-alive‘,[_c(‘router-view‘)])
首先会执行_c(‘router-view‘)产生router-view的vnode,_c方法会调用_createElement()方法,再调用
createComponent方法(注意有两个createComponent方法),router-view是functionalComponent,会调用createFunctionalComponent方法,然后执行;
var vnode = Ctor.options.render.call(null, h, {
其中render就是router-view的render方法,是vue特殊构造的,不同于普通组件的render代码。
router-view的render方从根组件_route属性获取路由,再获取路由组件数据,再创建路由组件vnode返回,这都顺理成章没有什么问题。
_c(‘router-view‘)执行完之后要执行_c(‘keep-alive‘,注意写法,_c(‘router-view‘)是keep-alive的子节点,
会把router-view的vnode传递给_c(‘keep-alive‘方法,也就是把路由组件vnode传递给_c(‘keep-alive‘,我们来看一下_createElement()代码,这是vue 2.0最关键最难理解的函数代码:
function _createElement (
context,
tag,
data,
children,
needNormalization
) {
会调用createComponent方法,其中有一段代码:
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : ‘‘)),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }
);
return vnode
这就是创建keep-alive组件的vnode,其中tag是"vue-componet-3-keep-alive",children就是路由组件的vnode,context就是keep-alive组件实例(keep-alive组件在初始化根组件时就已经建立一直存在)。
大家可以去看一下function VNode()的代码,其中第七个参数就是componentOptions。
这样keep-alive的vnode就创建了,其中有componentOptions也就是路由组件vnode,这是router-view传递而来的,router-view负责路由切换,只有router-view能创建路由组件vnode,但当它外套<keep-alive>
时,它做为keep-alive组件的子节点传递路由组件vnode,而keep-alive取代它成为占位组件占据根组件vnode树中的那个位置。
到这里跟组件vnode树中就多了一个vnode,就是路由组件vnode,路由组件vnode已经成功插入vnode树。
我们再回到根组件watcher/update方法,执行完_render()产生vnode之后就执行_update(vnode)方法更新根组件页面,会调用__patch__方法更新根组件页面,对于每一个vnode,会调用patchVnode方法处理,patchVnode会递归每一个vnode,而__patch__方法只是更新组件页面,不递归vnode树。
在根组件vnode树种,keep-alive是最底层的vnode,没有子vnode,但它有componentOptions,就是路由组件vnode,keep-alive的使命就是把自身vnode放在自己占的位置上,而vnode中含路由组件vnode,这是非常
关键非常难懂的环节,请继续看下文。
继续patch过程,当执行__patch__/patchVnode更新根组件页面时,当执行到keep-alive的那个vnode时,它有data.hook,会执行vnode.data.hook.prepatch()方法,这个方法会执行_updateFromParent方法,这个方法
的名称跟天书一样难理解,其中有以下代码:
if (hasChildren) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context); //保存路由组件vnode到keep-alive组件
vm.$forceUpdate(); //强制keep-alive组件更新显示新的路由组件页面
这就是把路由组件vnode保存到keep-alive组件实例的$slots中,然后执行keep-alive组件的watcher/update:
vm._update(vm._render(), hydrating);
先执行keep-alive的_render方法,这是vue组件通用方法,有以下代码:
vnode = render.call(vm._renderProxy, vm.$createElement);
其中render就是keep-alive组件的render方法,其中有以下代码:
var KeepAlive = {
render: function render () {
var vnode = getFirstComponentChild(this.$slots.default);
它是从自身实例的$slots取路由组件vnode返回,再执行update(vnode)更新keep-alive组件页面,此时vnode是路由组件vnode,那么页面就更新为路由组件页面。
之前在执行_c(‘keep-alive‘时已经创建keep-alive vnode返回,然后执行vnode.data.hook.prepatch()处理,这里又把keep-alive vnode替换更新为路由组件vnode,路由组件vnode的parent是keep-alivevnode,但在vnode树中keep-alive vnode并没有子vnode(children),它是一个占位组件vnode,路由切换时它变换vnode为路由组件vnode,页面更新显示的是路由组件页面,有没有晕?
再小结一下:
程序中触发路由切换是从修改_route属性开始;
顺便提一下,router中绑定hashchange/pushState是为了针对直接修改浏览器地址栏的情况。
transitionto是跑龙套的“骗人”的,不是关键代码,别误入歧途;
watcher/update是vue触发程序执行的隐蔽的杀手锏,永远要牢记,创建组件时会针对组件new watcher(),顺便提一下,1.0是针对页面表达式new wacther(),不是针对组件new watcher(),组件属性变化时
会自动执行watcher,也可能在源代码中直接执行watcher/update,这就开始一段重要源代码的执行。
根组件编译生成的render/code代码决定了一切,尤其是其中的_c()是vue 2.0精华,与1.0完全不同,_c方法是最重要的切入点,源代码中很少有调用_c的,因此createElement()方法不知道何时如何被调用,
以及如何传递参数,那些神奇的参数数据好像是天上掉下来似的,其实都是执行_c()方法调用createElement再传递参数数据,这个过程是系统自动进行的,没有源代码,像native code一样,导致分析源代码到这个环节
就牺牲了。
keep-alive是组件,有update方法,router-view不是组件,没有update方法! 它们都有render方法,
一个是根据路由找路由组件数据再产生路由组件vnode,一个是直接取路由组件vnode返回到vnode树中再更新组件页面,逻辑设计很清楚啊。
vnode是对象嵌套,以children表示为子节点嵌套,表现为vnode树。
watcher/update方法是最重要的切入点,触发一段程序执行的起点,update更新包括新建都是先产生vnode,再根据vnode更新页面,对于有template的组件,vnode就是与html对应的,对于管理/占位组件或标签比如
router-view/keep-alive,有设计好的render代码,其目的其实就是获取路由组件vnode,之后还干嘛?就是update更新路由组件页面。
时间关系,可能还有些关键细节没有提及,有问题欢迎交流,文中有错误或不妥之处欢迎拍砖指正,欢迎有兴趣的网友一起来探索js框架的神秘世界。