当前位置: 首页 > Web前端 > vue.js

vue源码分析过程总结

时间:2023-03-31 15:56:47 vue.js

我写了三篇关于vue源码分析、响应式、虚拟dom、模板编译组件化的文章。这三篇文章比较详细。这是一个摘要。三篇文章应该更好。下面是一般过程和前面例子的总结。一、第一个渲染过程。首先,我们在导入vue的时候,会初始化实例成员,静态成员是全局静态的,比如config、options、内部工具方法,还有一些静态方法。例如set、nextTick、component、instruction、filter方法,原型方法如:mount(内部调用mountComponent挂载)、init、_render(方法中默认调用options中的render,vm.$createElement默认传给用户,传入的render作为h函数生成虚拟dom,模板编译render内部使用的vm._c不需要传入this),_update等,初始化init中的实例成员,如options、_isVue、uid记录、Vue.extend()初始化组件的构造函数,继承自vue的所有原型方法,合并配置选项。实例化newVue(),会调用原型上定义的init方法;this._init()在这里合并options配置,并初始化生命周期变量,events监听自定义事件。执行initRender函数(生成vm._c处理编译和生成render,生成vm.$createElement处理用户传入的render)执行hook回调,对传入的data数据做响应式处理并劫持属性,生成每个的dep对象属性节点,dep对象用于通知watcher更新,劫持数组原型方法。如果有计算属性生成计算观察者,有监听器,生成监听观察者生成观察者时,会根据传入的方法决定是否去获取对应数据中的值。如果在传入的方法中获取到了值,就会触发我们之前数据劫持对应的get方法,从而将我们当前的watcher添加到对应属性的depsubs数组中。如果当前属性是一个子对象,对应的子对象dep也需要添加watcher(在set和array中会用到)。然后触发created创建的钩子函数。最后执行$mount挂载。vm.$挂载();这个方法会先寻找options.render函数,看用户是否传入,如果没有,使用传入的模板,调用compileToFunctions模板被转换为渲染函数。这个render函数内部调用vm._c处理模板编译,生成vNode。生成的渲染被分配给options.render。当随后调用_render()时,渲染将从选项中获取并调用。这需要Compiler版本的vue。如果用户传入render,则后续调用_render会直接调用用户传入的render,从options.render中获取执行。这会使用传入的vm.$createElement作为h函数生成虚拟dom,最后调用mountComponent进行挂载。mountComponent的主要功能是定义updateComponent。该方法的作用是更新render中编译的接口_update(_render())//_c或者用户传入_$createElement生成虚拟dom_render()生成虚拟dom,_update()内部调用patchVnode进行比较旧的和新的vNode来更新dom。在_render中会调用对应编译好的vm._c或vm._createElement生成虚拟dom。这个过程中会判断A自定义组件是否会调用createComponent,createComponent会调用extend()返回组件构造函数,并创建组件vnode,然后注册插件inithook,在init中实例化组件hook,然后调用继承自Vue的init初始化方法,最后调用mount()生成一个renderingwatcher,将组件挂载到页面上(vnode.elm,这里验证一个组件对应一个renderingwatcher)创建一个渲染观察者实例,并通过updateComponent创建一个观察者实例。会传入updateComponent方法,这里初始化会调用传入函数,即updateComponent来更新界面。在这个过程中,会获取到我们之前进行属性劫持的数据中的属性,然后触发相应的get,将renderingwatcher添加到对应属性的dep中,在属性dep的subs数组中,相互之间的依赖关系属性dep和渲染观察者形成。(这样就形成了一个观察关系,这里可能会在多个属性dep的subs数组中放置一个renderingwatcher,因为一个renderingwatcher对应一个component.dep中的一个attributesubs的subs数组中也可以放置多个不同的watcher,比如一个兼具渲染和计算||监听属性的watcher)这里的语句下,一个组件对应一个renderingwatcher.mounted,最后执行这个hook,以及整体渲染完成到此为止,完成了vue的第一次渲染。二、前面提到的响应式原理,我们在newVue()时调用init来劫持data数据生成属性对应的deppublisher和对应的get、set方法。watcher实例化时,会将自己赋值给Dep.target,然后在获取到属性值时触发相应的get,通过dep.depend()和childOb.dep将当前watcher添加到自己和子对象中。depend()在dep的subs数组中。同时watcher也会记录dep.id,防止后面触发get时重复添加。那么当数据中的属性赋值发生变化时,就会触发对应的集合,集合会判断值是否发生变化。如果它发生变化,它将被分配给val。然后set会判断新赋值的值是否是一个对象。如果是,继续观察数据劫持,然后调用dep的notify方法调用dep的subs数组中watcher的update方法。queueWatcher方法将在更新方法中被调用,这里会用watcher的id作为一个object的key来判断是否重复,如果不重复则将当前watcher放入queue队列中。然后调用nextTick方法,将flushSchedulerQueue方法作为参数传入。flushSchedulerQueue方法的作用watcher是按照watcher.id排序的,也就是创建的顺序(计算,监听,渲染),然后清空之前用来重复添加objectkey的id,然后执行watcher.run()在watche.run中执行此操作。get()也是传入的函数,如果watcher渲染了,就是updateComponent调用内部的_update(_render())生成Vnode,比较更新。如果是计算或者监听watcher,执行完get()pass方法后,会执行cb传入的callbackwatcher排序的作用是:首先将组件从父组件更新到子组件。也就是说如果有多个renderingwatcher,先更新父renderingwatcher再执行子renderingwatcher,然后组件的用户监控程序在渲染监控程序之前。运行是因为用户观察者是在渲染观察者之前创建的,也就是说每一级组件的计算和监听观察者都是在渲染观察者之前执行的,因为在渲染观察者中可能会用到计算属性。最后,如果一个组件在运行过程中销毁了父组件的监听程序,这里可以跳过它的观察者。nextTick接收到传入函数后,生成一个匿名函数(当前传入函数在匿名函数中执行,加上trycatch的错误处理)到一个callbacks数组中,现在不会立即执行其中的函数callbacks数组,然后判断pending属性为false,默认为false,如果为false,将其改为true,标记本次tick的task,然后使用Promise.resolve()生成一个promisemicrotaskthen(flushCallbacks),在这个tick事件循环结束时挂起,并在这个tick事件循环结束时执行微任务flushCallbacks回调。这个flushCallbacks回调的主要作用是将pending状态改为false,标志这一轮tick结束生成callbacks数组的副本,然后依次执行callbacks中的函数。如果浏览器不支持,异步promsie将降级为setTimeout。这也体现了vue中的更新是异步的。这里我们用一段伪代码来推理一下它的更新过程{{msg}}

{{name}}
{{title}}
更新值,然后dep.notify()ofmsg//dispatchthedepof更新.subs数组中的msgwatcher.update()执行queueWatcher方法来获取watcher.id的对象,并记录以防止重复。如果不重复,则将watcher放入queue队列(当前watcher为渲染watcher),执行nextTick(flushSchedulerQueue),然后将伪代码传入的函数放入callbacks.push(()=>{flushSchedulerQueue()})此时callbacks数组【执行队列中queuewatcher更新的方法是flushSchedulerQueue,】然后pending此时为false,改为true标记,为本次tick处理定义了一个microtaskpromise在此滴答结束时挂起,等待将来执行(flushCallbacks是then回调函数)。更新值,然后name.dep.notify()//派发更新name的dep.subs数组中的watcher.update()同上,只是重复了watcher.id,同样的渲染watcher,退出queueWatcher方法(这里没有添加重复的watcher,但是值已经更新了val=newvalue)更新值,然后title的dep.notify()//调度更新watcher.update()中的title的dep.subs数组同上,只是watcher.id重复了,同一个渲染watcher,exitqueueWatcher方法(这里没有添加重复的watcher,但是已经更新了valueval=newvalue)Vue.nextTick(callback)执行nextTick(callback)然后把伪代码传入的函数放到callbacks.push(()=>{callback()})此时callbacks数组【执行队列中queuewatcher更新的方法为flushSchedulerQueue,回调方法】然后pending此时为true退出更新值,并且thendep.notify()ofmsg//分发更新msg的dep。subs数组中的watcher.update()同上,只是watcher.id重复了,同一个renderwatcher,退出queueWatcher方法(这里没有添加重复的watcher,但是值已经更新了val=newvalue,而此时的msg已经是ItisHelloinsteadofHellowords)Thistickislasttoexecutethemicrotasks属于这个tick。执行flushCallbacks方法。回调数组中的第一个方法是flushSchedulerQueue。此方法执行队列中所有观察者的更新。我们现在有一个renderingwatcher(因为id一样),执行renderingwatcher。然后执行第4步传递的回调Vue.nextTick。这时renderingwatcher已经执行完毕,内容发生了变化。然后执行回调得到对应的domtextContent就是我们上次给msg赋值Hello。这就是vue数据响应的原理,以及它的更新过程,也是为什么Vue.nextTick可以拿到更新后的dom值。我们已经看到调用$mount时会调用compileToFunctions将template模板转换为render函数(内部调用_c()生成虚拟dom)。语法树,抽象的语法树是js对象的形式,用于以树的形式描述代码结构,包括模板解析和v-forv-ifref等(v-for,v-if结构指令只能在编译阶段处理,在render函数中不会解析模板,所以使用jsforandif)。然后优化生成的ast抽象语法树,标记静态节点和静态根节点,检测子节点是否为纯静态节点。一旦检测到纯静态节点,它就是一个永远不会改变的节点。会被提升为常量,重新渲染时不会重新创建节点,打补丁时直接跳过静态子树,然后从抽象语法树中生成字符串形式的js代码。编译这段js字符串代码,即render函数代码。其中使用的_c生成virtualdom,然后将string形式的js代码转换成js方法,赋值给输出对象的render属性,返回编译好的render函数,然后对返回的render函数进行赋值到options.render。后续渲染时调用的_render()就是这个render,也就是模板的编译渲染。.4.VirtualDOM中Key的作用和好处VirtualDOM中的Key主要用于标记两个节点是否相同,然后比较新旧vNode,通过比较vnode之间的差异来更新旧vNode,从而更新相应的dom。因为在虚拟dom节点的比较中,节点比较规则会按照key,新旧start,新旧end,oldstart和newend,oldend和newstart进行比较。如果不匹配,则新的开始将与未比较的旧开始同时进行。在level区间的开始和结束找到相同的vnode来匹配新的差异,并根据需要移动位置。如果您没有这种方式的Key,例如
    {{value}}
[a,b,c,d]//没有key时更新为[a,x,b,c,d],比较开始时第一个相同,当key为undefined时,undefined相等,标签相同。第2、3、4个标签相同,内容不同。更新dom内容,第5个生成dominsertd。,key会被比较,key不会是undefined。根据我们上面提到的规则,只需要对x进行一次生成和插入操作。从这个例子来看,如果我们使用列表,如果我们在不同的位置插入数据,没有key的时候,更新的次数要比有key的时候大很多。所以在使用list的时候尽量使用key。本总结到此结束。