关于vue 2.0源代码分析,已经有不少文档分析功能代码段比如watcher,history,vnode等,但没有一个是分析重点难点的,没有一个是分析大命题的,比如执行router.push之后到底是如何执行代码实现路由切换的? 本文旨在分享本人研究vue 2.0源代码重点难点之结果,不涉及每段源代码具体分析,源代码功能段每个人都可以去分析,只要有耐心,再参考已有高手发表的源代码分析文档,不是太难,主要是要克服一些编程技术问题,比如嵌套回调,递归,对象/数组特殊处理方法等等。
vue 2.0 路由切换以及组件缓存源代码重点难点分析:
首先要说的是,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具体就不再分析,网上已经有几个文档分析很透彻,本人刚开始也是看了一个自己写简单例子实现watcher的 文档受到启发,测试运行那个简单例子,才研究搞明白所谓响应式实现原理的,花费了很长时间,因为它的封装 写得层次太多太复杂了,把人都绕晕了,不知道谁谁dep谁谁把谁的watcher保存到谁谁谁了。
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负责路由组件切换,也就是创建一个 新的路由组件,并且更新页面,但当外套时,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)方法更新根组件页面。 假定页面是这样写的:
执行_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组件的子节点传递路由组件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.forceUpdate(); //强制keep-alive组件更新显示新的路由组件页面
这就是把路由组件vnode保存到keep-alive组件实例的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更新路由组件页面。
大致逻辑挺简单的,但要把源代码走通很难,因为源代码太分散,设计逻辑和编程技术高超,超出一般想象, 有些源代码是异步同时执行的,有些函数调用方法是系统隐蔽的复杂方式,不是a=b()显示方式调用,无法追朔 debug看重要关键参数数据是怎么来的,注释蜻蜓点水几乎没有注释,已经开源了,再详细注释,那真的是全心全意 为人民服务了,可以理解,只恨自己水平太低看不懂老想放弃分析,分析懂了也没什么用,只是个人兴趣。
文中可能有错误或不妥之处,欢迎拍砖! 有问题欢迎同学们交流!