vue-analysis
vue-analysis copied to clipboard
Vue 源码注释版 及 Vue 源码详细解析
前面的文章已经详细记述了Vue的核心执行过程。相当于已经搞定了主线剧情。后续的文章都会对其中没有介绍的细节进行展开。 现在我们就来讲讲其他支线任务:nextTick和microtask。 Vue的nextTick api的实现部分是Vue里比较好理解的一部分,与其他部分的代码也非常的解耦,因此这一块的相关源码解析文章很多。我本来也不准备单独写博客细说这部分,但是最近偶然在别人的文章中了解到: 每轮次的event loop中,每次执行一个task,并执行完microtask队列中的所有microtask之后,就会进行UI的渲染。但是作者似乎对于这个结论也不是很肯定。而我第一反应就是Vue的$nextTick既然用到了MutationObserver(MO的回调放进的是microtask的任务队列中的),那么是不是也是出于这个考虑呢?于是我想研究了一遍Vue的$nextTick,就可以了解是不是出于这个目的,也同时看能不能佐证UI Render真的是在microtask队列清空后执行的。 研究之后的结论:我之前对于$nextTick源码的理解完全是错的,以及每轮事件循环执行完所有的microtask,是会执行UI Render的。 *task/macrotask和microtask的概念自从去年[知乎上有人提出这个问题](https://www.zhihu.com/question/36972010)之后,task和microtask已经被很多同学了解了,我也是当时看到了microtask的内容,现在已经有非常多的中文介绍博客在介绍这部分的知识,最近[这篇火遍掘金、SF和知乎的文章](https://zhuanlan.zhihu.com/p/25407758),最后也是考了microtask的概念。如果你没有看过task/microtask的内容的话,我还是推荐这篇[英文博客](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/),是绝大多数国内博客的内容来源。* ## 先说nextTick的具体实现 先用120秒介绍MutationObserver: MO是HTML5中的新API,是个用来监视DOM变动的接口。他能监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等等。 调用过程很简单,但是有点不太寻常:你需要先给他绑回调: `var mo = new MutationObserver(callback)` 通过给MO的构造函数传入一个回调,能得到一个MO实例,这个回调就会在MO实例监听到变动时触发。 这个时候你只是给MO实例绑定好了回调,他具体监听哪个DOM、监听节点删除还是监听属性修改,你都还没有设置。而调用他的observer方法就可以完成这一步: ```javascript var domTarget = 你想要监听的dom节点 mo.observe(domTarget, { characterData:...
## 源码整体概览 Vue源码构造实例的过程就一行```this._init(options)```,用你的参数对象去执行init初始化函数。init函数中先进行了大量的参数初始化操作```this.xxx = blabla```,然后剩下这么几行代码(**后文所有的英文注释是尤雨溪所写,中文是我添加的,英文注释极其精确、简洁,请勿忽略**) ```javascript this._data = {} // call init hook this._callHook('init') // initialize data observation and scope inheritance. this._initState() // setup event system and option events. this._initEvents()...
我在之前的主线文章中已经多次介绍了,大数据量的重复渲染生成是考量一个前端UI框架性能的主要场景。也大致介绍了一些Vue为优化这个场景下性能所使用的手段。现在我们来完整的看一看这个Vue优化最多、使用缓存最多的指令。 主线文章中说过,对于同一段模板,查找模板中指令的compile过程是不变的,因此只用执行一次,解析出整个模板中的所有指令描述符。这些指令描述符被闭包在linker中。在你给linker函数传递一段通过cloneNode复制出的模板DOM实例,并传入这段模板需要绑定的数据(scope或者vm),那么linker便会将对应的指令描述符生成真正的指令,绑定在你传进来的DOM实例上,同时,每个指令都会生成一个watcher,而watcher则会订阅到你传入的数据上。至此,我们看到了一个完整的响应式的DOM的构建得以完成。而为什么编译阶段只是编译生成指令描述符,而不是建立指令实例也得以解释:每个指令实例是要绑定到具体的DOM上的,而具体的DOM在linker的执行阶段才得到的,因此,compile只是先生成指令描述符,在linker阶段得到DOM之后才为DOM生成指令,指令又建立watcher,watcher又绑定数据。 ## 第一阶段:创建 我们开始说v-for,之前已经多次强调,v-for是一个terminal directive。其会接管其子元素的DOM的编译过程。在v-for的bind和update方法中,真正为数据中的每个元素创建响应式DOM。 比如这么一段模板:```template:`{{element}}``` 那么v-for就要负责为array中的每个element创建响应式的li元素。同时,每当array中的element有变化时,就需要创建/删除新的响应式li元素。因此,上述过程中,必然要反复执行linker。对此,Vue抽象出FragmentFactory和Fragment的两个类(Fragment不是我们常用的document fragment)。 一个v-for指令有一个FragmentFactory实例,在bind阶段创建,FragmentFactory创建过程中会为v-for中的元素(也就是ul中的li)执行compile,生成linker,存放在FragmentFactory实例的linker属性上。 而在v-for指令的update阶段会为数组的每个元素创建scope,scope为继承自当前vm的对象。并在这个对象上存放数组元素的具体内容。 然后调用FragmentFactory实例创建Fragment: ```javascript FragmentFactory.prototype.create = function (host, scope, parentFrag) { var frag = cloneNode(this.template) return new Fragment(this.linker, this.vm, frag, host,...
## compile compile阶段执行的compileRoot函数就是编译我们在transclude阶段说过的,我们分别提取到了el顶级元素的属性和模板的顶级元素的属性,如果是component,那就需要把两者分开编译生成两个link。主要就是对属性编译,后续内容会细说属性编译,所以在此处不细说了,[注释版源码在此](https://github.com/Ma63d/vue-analysis/blob/master/vue%E6%BA%90%E7%A0%81%E6%B3%A8%E9%87%8A%E7%89%88/compiler/compile.js#L222-L293)。后面的resolveSlots出于篇幅考虑,也不再介绍,如有需求,请查看[注释版源码](https://github.com/Ma63d/vue-analysis/blob/master/vue%E6%BA%90%E7%A0%81%E6%B3%A8%E9%87%8A%E7%89%88/compiler/resolve-slots.js)。 我们来说说compile函数,他对元素执行compileNode,对其childNodes执行compileNodeList: ```javascript export function compile (el, options, partial) { // link function for the node itself. var nodeLinkFn = partial || !options._asComponent ? compileNode(el, options) : null...
## _compile 介绍完响应式的部分,算是开了个头了,后面的内容很多,但是层层递进,最终完成响应式精确订阅和批处理更新的整个过程,过程比较流程,内容耦合度也高,所以我们先来给后文的概览,介绍一下大体过程。 我们最开始的代码里提到了Vue处理完数据和event之后就到了$mount,而$mount就是在this._compile后触发编译完成的钩子而已,所以核心就是Vue.prototype._compile。 `_compile`包含了Vue构建的三个阶段,transclude,compile,link。而link阶段其实是放在linkAndCapture里执行的,这里又包含了watcher的生成,指令的bind、update等操作。 我先简单讲讲什么是指令,虽然Vue文档里说的指令是v-if,v-for等这种HTML的attribute,其实在Vue内部,只要是被Vue处理的dom上的东西都是指令,比如dom内容里的`{{a}}`,最终会转换成一个v-text的指令和一个textNode,而一个子组件``也会生成指令,还有slot,或者是你自己在元素上写的attribute比如`hello={{you}}`也会被编译为一个v-bind指令。我们看到,基本只要是涉及dom的(不是响应式的也包含在内,只要是vue提供的功能),不管是dom标签,还是dom属性、内容,都会被处理为指令。所以不要有指令就是attribute的惯性思维。 回过头来,_compile部分大致分为如下几个部分 1. transclude transclude的意思是内嵌,这个步骤会把你template里给出的模板转换成一段dom,然后抽取出你el选项指定的dom里的内容(即子元素,因为模板里可能有slot),把这段模板dom嵌入到el里面去,当然,如果replace为true,那他就是直接替换el,而不是内嵌。我们大概明白transclude这个名字的意义了,但其实更关键的是把template转换为dom的过程(如`{{a}}`字符串转为真正的段落元素),这里为后面的编译准备好了dom。 2. compile compile的的过程具体就是**遍历模板解析出模板里的指令**。更精确的说是解析后生成了指令描述对象。 同时,compile函数是一个高阶函数,他执行完成之后的返回值是另一个函数:link,所以compile函数的第一个阶段是编译,返回出去的这个函数完成另一个阶段:link。 3. link compile阶段将指令解析成为指令描述对象(descriptor),闭包在了link函数里,link函数会把descriptor传入Directive构造函数,创建出真正的指令实例。此外link函数是作为参数传入linkAndCaptrue中的,后者负责执行link,同时取出这些新生成的指令,先按照指令的预置的优先级从高到低排好顺序,然后遍历指令执行指令的_bind方法,这个方法会为指令创建watcher,并计算表达式的值,完成前面描述的依赖收集。并最后执行对应指令的bind和update方法,使指令生效、界面更新。 此外link函数最终的返回值是unlink函数,负责在vm卸载时取消对应的dom到数据的绑定。  是时候回过头来看看Vue官网这张经典的图了,以前我刚学Vue时也是对于Watcher,Directive之类的概念云里雾里。但是现在大家看这图是不是很清晰明了? > 模板中每个指令/数据绑定都有一个对应的 watcher 对象,在计算过程中它把属性记录为依赖。之后当依赖的 setter 被调用时,会触发 watcher 重新计算 ,也就会导致它的关联指令更新 DOM。...
## 依赖变动后的dom更新 ```javascript Dep.prototype.notify = function () { // stablize the subscriber list first var subs = toArray(this.subs) for (var i = 0, l = subs.length; i < l; i++)...
## link compile结束后就到了link阶段。前文说了所有的link函数都是被linkAndCapture包裹着执行的。那就先看看linkAndCapture: ```javascript // link函数的执行过程会生成新的Directive实例,push到_directives数组中 // 而这些_directives并没有建立对应的watcher,watcher也没有收集依赖, // 一切都还处于初始阶段,因此capture阶段需要找到这些新添加的directive, // 依次执行_bind,在_bind里会进行watcher生成,执行指令的bind和update,完成响应式构建 function linkAndCapture (linker, vm) { // 先记录下数组里原先有多少元素,他们都是已经执行过_bind的,我们只_bind新添加的directive var originalDirCount = vm._directives.length linker() // slice出新添加的指令们 var dirs = vm._directives.slice(originalDirCount)...