vue源码学习笔记
一、变化侦测篇
众所周知,Vue最大的特点之一就是数据驱动视图,数据驱动视图简单来说就是数据变化引起视图变化
在Angular中是通过脏值检查流程来实现变化侦测;在React是通过对比虚拟DOM来实现变化侦测
我们把"谁用到了这个数据"称为"谁依赖了这个数据",我们给每个数据都建一个依赖数组(因为一个数据可能被多处使用),谁依赖了这个数据(即谁用到了这个数据)我们就把谁放入这个依赖数组中,那么当这个数据发生变化的时候,我们就去它对应的依赖数组中,把每个依赖都通知一遍,告诉他们:"你们依赖的数据变啦,你们该更新啦!"。这个过程就是依赖收集
在getter中收集依赖,在setter中通知依赖更新
谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher实例,在创建Watcher实例的过程中会自动的把自己添加到这个数据对应的依赖管理器中,以后这个Watcher实例就代表这个依赖,当数据变化时,我们就通知Watcher实例,由Watcher实例再去通知真正的依赖
我们通过Object.defineProperty方法实现了对object数据的可观测,并且封装了Observer类,让我们能够方便的把object数据中的所有属性(包括子属性)都转换成getter/seter的形式来侦测变化
在getter中收集依赖,在setter中通知依赖更新,以及封装了依赖管理器Dep,用于存储收集到的依赖
我们为每一个依赖都创建了一个Watcher实例,当数据发生变化时,通知Watcher实例,由Watcher实例去做真实的更新操作
双向绑定的原理:
双向绑定其实是通过js的object.defindproperty的方法来实现的,vue实例会遍历data的所有属性,并将其添加上getter和setter的方法,在 getter 方法中收集数据依赖,在 setter 中监听数据变化。一旦数据发生变化,再通知订阅者。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
在此过程中vue通过封装observe类,将data上的所有属性转化‘’可观测‘’的值,通过object.defindproperty的方法把所有属性的读和写分别使用get()和set()进行拦截,在get()中将用到该属性的所有地方都添加为依赖,并放入该属性的依赖数组中,同时我们会将每一个依赖创建一个watcher实例,当数据发生变化时,我们不会直接通知依赖,而是通过watcher来通知视图更新。
二、虚拟DOM篇
虚拟DOM:所谓虚拟DOM,就是用一个JS对象来描述一个DOM节点,我们把这个JS对象就称为是这个真实DOM节点的虚拟DOM节点
VNode类:在Vue中就存在了一个VNode类,通过这个类,我们就可以实例化出不同类型的虚拟DOM节点
可以用JS的计算性能来换取操作DOM所消耗的性能
虚拟DOM产生的原因以及其最大的作用:
当数据发生变化时,我们对比变化前后的虚拟DOM节点,通过DOM-Diff算法计算出需要更新的地方,然后去更新需要更新的视图,最终达到以最少操作真实DOM更新视图的目的
虚拟DOM的产生:
在VUE中存在一个VNode的类,通过这个VNode类我们就可以实例化出不同类型的虚拟DOM了
虚拟DOM的一些基本概念和为什么要有虚拟DOM,其实说白了就是以JS的计算性能来换取操作真实DOM所消耗的性能。接着从源码角度我们知道了在Vue中是通过VNode类来实例化出不同类型的虚拟DOM节点,并且学习了不同类型节点生成的属性的不同,所谓不同类型的节点其本质还是一样的,都是VNode类的实例,只是在实例化时传入的属性参数不同罢了。最后探究了VNode的作用,有了数据变化前后的VNode,我们才能进行后续的DOM-Diff找出差异,最终做到只更新有差异的视图,从而达到尽可能少的操作真实DOM的目的,以节省性能。
三、模板编译篇
模板编译过程:我们把用户在<template></template>标签中写的类似于原生HTML的内容进行编译,把原生HTML的内容找出来,再把非原生HTML找出来,经过一系列的逻辑处理生成渲染函数,也就是render函数的这一段过程称之为模板编译过程
模板编译就会产生VNode类
总结:
首先会先将模版通过解析器,解析成AST(抽象语法树),然后再通过优化器,遍历AST树,将里面的所有静态节点找出来,并打上标志,这样可以避免在数据更新进行重新生成新的Vnode的时候做一些无用的功夫,和diff算法对比时进行一些无用的对比,因为静态节点这辈子是什么样就是什么样的了,不会变化。接着,代码生成器会将这颗AST编译成代码字符串(生成render函数字符串),这段字符串会被Vdom里面的createElement函数调用,最后生成Vnode。
四、生命周期篇
Vue实例的生命周期大致可分为4个阶段:
初始化阶段:为Vue实例上初始化一些属性,事件以及响应式数据;(进行new Vue();创建一个vue实例,并未该实例初始化一些属性、事件和响应式数据等)
模板编译阶段:将模板编译成渲染函数;
挂载阶段:将实例挂载到指定的DOM上,即将模板渲染到真实DOM中;
销毁阶段:将实例自身从父组件中删除,并取消依赖追踪及事件监听器;
export function initMixin (Vue) { Vue.prototype._init = function (options) { const vm = this vm.$options = mergeOptions( //合并属性 resolveConstructorOptions(vm.constructor), options || {}, vm ) vm._self = vm initLifecycle(vm) // 初始化生命周期 initEvents(vm) // 初始化事件 initRender(vm) // 初始化渲染 callHook(vm, 'beforeCreate') // 调用生命周期钩子函数 initInjections(vm) //初始化injections initState(vm) // 初始化props,methods,data,computed,watch initProvide(vm) // 初始化 provide callHook(vm, 'created') // 调用生命周期钩子函数 if (vm.$options.el) { //判断用户是否传入了el选项 vm.$mount(vm.$options.el) //如果传入了则调用$mount函数进入模板编译与挂载阶段,如果没有传入el选项,则不进入下一个生命周期阶段,需要用户手动执行vm.$mount方法才进入下一个生命周期阶段 } } }
初始化阶段:
初始化阶段所做的工作也可大致分为两部分:第一部分是new Vue(),也就是创建一个Vue实例;第二部分是为创建好的Vue实例初始化一些事件、属性、响应式数据等。
1)new Vue() 做了些什么?
合并配置,调用一些初始化函数,触发生命周期钩子函数,调用$mount开启下一个阶段。
2)initLifecycle函数在做什么?
为Vue实例初始化一些属性,包括以 $ 开头的供用户使用的一些外部属性,和以 _ 开头的内部属性。($parent $children $root)
3)initEvents函数的实现过程?
该函数内部首先在实例上新增了_events属性并将其赋值为空对象,用来存储事件。接着通过调用updateComponentListeners函数,将父组件向子组件注册的事件注册到子组件实例中的_events对象里。
4)initInjections函数用来初始化inject选项。
5)initState函数:vue组件中会写一些如props、methods、data、computed、watch选项,我们把这些选项称之为实例的状态选项,initState函数就是在初始化状态选项。
我们在初始化的时候遵循了这种顺序,先初始化props,接着初始化data,最后初始化watch,这也是我们在data中能够使用props,在watch中能够监听props和data的原因
模板编译阶段:
该阶段主要做的工作是,获取到用户传入的模板内容并将其编译成渲染函数。
Vue源码构建的两种版本:完整版本和只包含运行时版本。并且我们知道了模板编译阶段只存在于完整版中,在只包含运行时版本中不存在该阶段,这是因为在只包含运行时版本中,当使用vue-loader或vueify时,*.vue文件内部的模板会在构建时预编译成渲染函数,所以是不需要编译的,从而不存在模板编译阶段
挂载阶段:
在该阶段中所做的主要工作是创建Vue实例并用其替换el选项对应的DOM元素,同时还要开启对模板中数据(状态)的监控,当数据(状态)发生变化时通知其依赖进行视图更新
挂载阶段所做的工作分成两部分进行了分析,第一部分是将模板渲染到视图上,第二部分是开启对模板中数据(状态)的监控。两部分工作都完成以后挂载阶段才算真正的完成了
销毁阶段:
在该阶段所做的主要工作是将当前的Vue实例从其父级实例中删除,取消当前实例上的所有依赖追踪并且移除实例上的所有事件监听器
五、VUE生命周期
beforeCreate :
第一个生命周期函数,表示实例在完全创建出来之前会执行它,在执行它时,data和methods中的数据都还未初始化。
beforeCreate -> created:
初始化vue实例,进行数据观测
created:
第二个生命周期函数,完成数据观测,此时data和methods已经初始化完成,可在此处进行异步请求;此时vm.$el 并没有被创建
created -> beforeMount:
进行模板编译;首先会判断对象是否有el选项,如果有的话就继续向下编译,如果没有,则会停止编译,也就意味着停止了生命周期,直到在改vue实例上调用vm.$mount(el)才会继续编译;
判断是否传入template选项,若果有则会通过一系列的编译生成 render 函数;否则则将外部的HTML作为模板进行编译;这里优先级:render > template > outerHTML
beforeMount:
第三个生命周期函数,表示模板在内存中已经编译好了,但是并没有渲染到页面中。页面显示的还仅仅是模板字符串;在此阶段可获取到vm.el
beforeMount -> mounted:
在此阶段vm.el完成挂载,vm.$el生成的DOM替换了el选项所对应的DOM
mounted:
第四个生命周期函数,此时内存中的模板已经真实的挂载到了页面中,用户可以看到渲染好的页面了;可以在此阶段进行一些dom操作
beforeUpdate:
执行它时,data中的数据已经被更新了,但是页面中的data还未被替换过来,view层还未更新;若在beforeUpdate中再次修改数据,不会再次触发更新方法
updated:
已完成view层的更新,若在updated中再次修改数据,会再次触发更新方法(beforeUpdate、updated),导致内存爆炸
beforeDestroy:
实例被销毁前调用,此时实例属性与方法仍可访问
destroyed:
完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器,定时器等
浙公网安备 33010602011771号