Vue源码的一些理解

549 阅读4分钟

由于对于vue中一些操作的疑惑比如computed存在缓存机制如何实现,带着问题尝试去阅读vue源码,没想到越往后看想要了解的越多,根本停不下继续深入研究的脚步,断断续续花了几周的时间,终于初略地有点理解,如果有不正确的地方希望得到大牛的指正。

在开始前最好能先了解下Vue.componentVue.extendVue.usenextTick的作用。

Vue.component,Vue.use和Vue.extend

  • Vue.extend主要是定义Sub方法,将父类prototype放在子类prototype
var Sub = function VueComponent (options) {
  this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;

进行对于super父级的option和子级option进行merge,将父级的extend,mixin,use放在sub中

  • Vue中内置ASSET_TYPES包含三个默认的属性: ['component', 'directive','filter'],在initGolbalAPI的时候会进行initAssetRegisters,分别给这三个属性赋值function (id, definition)方法,当在调用的时候例如Vue.component的时候会调用extend进行父级的继承

  • Vue.use 主要用于插件的安装Vue.use(plugin),例如Vue.use(Input, options),实际最终会调用Vue.component方法,Vue.component(Input.name, options)最终调用extend进行继承。

// 如果存在plugin.install,则会调用install方法,并传入vue对象和options
export default {
    install(vue, options) {
        // something
    }
}

或者

// 如果plugin为方法,则直接传入vue对象和options
export default (vue, options) => {
    vue.use(...)
}

nextTick

nextTick函数中在callbacks中push匿名函数(内部执行flushSchedulerQueue), 如果为pending则进行异步操作,分为三部进行优雅降序

链接1
链接2

最后在同步更新完成后进行异步操作,执行flushCallbacks,遍历callbacks中的方法,执行flushSchedulerQueue,遍历队列, 执行watcher.run方法进行更新。

Vue实例

一般在开发过程中在main.js文件中我们总是习惯性地写上

new Vue({
  render: h => h(App)
}).$mount("#app");

随之而来总是会有下面的疑惑

  • 为什么传入参数直接写render就可以进行渲染?
  • 进行$mount就可以进行挂载app节点?

顾名思义,广义上理解地话很容易就是进行节点的渲染以及挂载渲染的节点到app上去,网上解释只是初略带过告诉我们用途,但对于我来说感觉没明白原理总是心里很膈应,阅读了源码后才知道原来所谓的render函数,原来所谓的挂载其实还是需要经过一套流程的。

以我个人的理解new vue的作用是初始化Vue实例,将options加到vm对象下的$options,主要经过了initProxyinitLifecycle(vm)initEvents(vm)initRender(vm),之后就是我们所熟悉的调用beforeCreated钩子函数了,在此之后会经过initInjections(vm)initState(vm)初始化datacomputed,methods,props以及watcher,initProvide(vm),之后是调用created钩子函数,这也就能解释为什么在created钩子的时候能够访问到data这个数据了,以上这些初略带过以后进行补充,之后就是比较关键的挂载节点了。

initProxy

主要通过Proxy的方式进行数据监听,Object.defineProperty不能监听数组以及只能劫持对象的属性。可以参考:实现双向绑定Proxy比defineproperty优劣如何,当进行数据获取的时候会调用get方法,继而调用属性上的get方法。

initState

主要进行data,props,computed,methods,watcher的初始化

initData

主要执行data函数得到对象,如果为对象则直接赋值,将属性放到vm._data中,这也就是为什么在子页面中data要用方法的原因了,这样每个vue对象就有各自独立的,之后对data进行代理,这样通过vm.data就能获取到相关的属性,其实本质是获取vm._data.attr,接下来就是进行数据劫持了,说白了就是给data中每个属性都加上get和set,如果属性为对象则继续观察,知道为非对象,Vue3中将使用Proxy,使用Object.defineProperty的话只能对属性进行定义,这样需要一直递归到最底层,使用Proxy就可以方便对对象进行监听

initComputed

将父类super原型上的属性放在sub的prototype
最后将计算属性通过Object.defineProperty(target, key, sharedPropertyDefinition)加到sub.prototype上,其中sharedPropertyDefinition中会寻找_computedWatchers是否有这个key对应的value, 如果会判断dirty是否是true, 本质上computed是基于watcher的,在初始化vue对象的时候会进行初始化computed,并且创建watcher对象,不同的是会将watcher中的lazy设置为true,这会帮助我们判断页面其他属性改变的时候是否需要调用computed里的方法进行更新
如果为true会执行这个computed方法并且传入vm对象进行修改,调用getter方法,触发data的getter获取当前值计算,并将dirty设置为false, 缓存的控制实际上是Watcher中dirty属性来控制的,false时就取缓存中的value值
最后会将依赖都收集进当前watcher对象中
总的来说通过new Watcher、watcher、depend、evaluate这些过程来完成对依赖的收集 例如isA是计算属性

data() {
    return {
    	name: '1'
    }
},
computed: {
    isA() {
    	return this.name === '0'
    }
}

模板会render成_vm.isA ? _c("div", [_vm._v("哈哈哈哈")]) : _vm._e(), 首先会通过proxy代理对象调用get方法去获取isA,_vm即为sub对象通过原型链找到isA这个参数,会调用定义好的computed的getter方法,会执行这个computed方法并且传入vm对象进行修改,通过获取isA里data对象的数据会再次调用绑定那个data属性的getter方法将这个新的watcher加入到dep的依赖里,最后进行evalute是否需要重写计算和依赖收集操作

挂载节点主要流程是创建watcher对象,将updateComponent方法作为watcher的getter,并且调用对象中的getter方法,将这个对象push到targetStack,并且赋值给Dep.target这个方法就是生成虚拟节点并且进行节点patch

updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

vm._render的过程

  • 首先会对节点进行render,获取相关属性调用属性的get方法,在get中会进行相关依赖的收集,将Dep.target对象放入当前属性闭包下的Dep对象中
  • 其次会进行虚拟Vnode节点的创建,通过判断传入tag的类型进行创建不同类型的Vnode,如果为组件类型则会将Ctor方法方法Vnode对象的componentOptions下,Ctor方法通过resolveAssets获取options.component中key对应的value
    最终会形成如下形式的Vnode

patch的过程

  • 首先判断是否存在oldVnode,如果不存在就调用createElm

  • 如果存在如果存在会比较新的节点和老节点是否一样,一样就进行虚拟节点的patch

    patchVnode的作用

    主要是在更新的时候对子Vnode进行patch,因为在在初始化的时候会将当前vm对象存入vm._vnode中,在后续更新的时候就会获取这个值,此外如果节点为组件类型会进行prepatch,也就是通过获取初始化update的时候设置的componentInstance实例进行childComponent update

    Vue精确更新策略里加入了updateChildComponent,主要是进行props和listener以及对于slot做一些更新

    • 在父组件进行render的时候会重新计算子组件的props,属性在传给子组件的时候会绑定在子组件的_props,并做成响应式

      if (propsData && vm.$options.props) {
          toggleObserving(false);
          var props = vm._props;
          var propKeys = vm.$options._propKeys || [];
          for (var i = 0; i < propKeys.length; i++) {
            var key = propKeys[i];
            var propOptions = vm.$options.props; // wtf flow?
            props[key] = validateProp(key, propOptions, propsData, vm);
          }
          toggleObserving(true);
          // keep a copy of raw propsData
          vm.$options.propsData = propsData;
        }
      
    • 更新插槽里的子组件通过vm.$forceUpdate强制更新,也就是帮你提前调用watcher对象进行更新,slot的本质其实是生成VNode

      // resolve slots + force update if has children
      if (needsForceUpdate) {
          vm.$slots = resolveSlots(renderChildren, parentVnode.context);
          vm.$forceUpdate();
      }
      

    最后通过通过oldVnodenewVnode对比进行更新,如果不相等则继续比较oldVnode.childrennewVnode.children

    1. 如果新老节点都存在则进行diff操作,也就是下面的snabbdom算法

    2. 如果只有新节点,则将新节点append到父节点

    3. 如果只有老节点,则删除这个老节点,如果是文本的话将文本设置为空

    snabbdom算法

    通过比较同一层级的两个节点oldVnode, newVnode,分别给两个节点设置头尾指针

    1. 比较oldVnode[oldStart]和newVnode[newStart],如果相等则进行patchVnode,oldStart,newStart分别向后移,否则进入步骤2

    2. 比较oldVnode[oldEnd]和newVnode[newEnd],如果相等则进行patchVnode,oldEnd,newEnd分别向前移,否则进入步骤3

    3. 比较oldVnode[oldStart]和newVnode[newEnd],如果相等则进行patchVnode,将oldVnode[oldStart]这个节点插入到oldVnode[oldEnd]的前面,oldStart向后移动,newEnd向前移动,否则进入步骤4

    4. 比较oldVnode[oldEnd]和newVnode[newStart],如果相等则进行patchVnode,将oldVnode[oldEnd]这个节点插入到oldVnode[oldStart]的前面,oldEnd向前移动,newStart向后移动,否则进入步骤5

    5. 如果上面的都不满足,则从oldVnode中寻找newVnode对于的那个key,不存在说明是新的节点,进行createElm,存在的话就进行patchVnode,将节点插入到当前oldStart所对应Vnode前面

    6. 如果完成遍历后,oldStart>oldEnd这个逻辑判断要先于newStart>newEnd,表示newVnode中有额外新增的节点,将剩下节点全部放入当前oldEnd所对应的Vnode前面,否则,就表示oldVnode中有额外不需要的节点,则进行删除

      snabbdom算法相关链接

  • 如果和老节点不一致的话,就会获取老节点的parentNode,并且调用createElm

createElm的过程

createElm主要做了两件事

  • 判断是否需要创建子类Vnode
  • 如果不需要创建子类component,则创建这个节点的element,将vnode节点上的属性比如style、class、attributes、方法等通过invokeCreateHooks更新到节点上,并将元素挂载到的parentNode上,最后将整个渲染好的节点全部挂载到app这个节点上。

总结

Vue的挂载过程大概是

虽然打了n遍的断点,错失了n次的断点,刷新页面重新断点了n次,但每次阅读源码总会有新的收获,对于自己理解和提高有很大的帮助,希望继续努力保持学习。