之前写了三篇vue的源码解析,响应式,虚拟dom,和模板编译组件化.
这三篇是比较细的,这里做个总结,先看总结,再看那三篇应该会更好,
这里是把大概流程和前面的例子总结下.
一,首次渲染过程
首先我们导入vue时会初始化实例成员,静态成员
- 全局静态例如config,options,内部工具方法,一些静态方法例如set,nextTick,组件,指令,过滤器方法,然后原型方法例如:mount(内部调用mountComponent挂载),init,_render(方法里默认调用了options里的render,默认传递vm.$createElement提供给用户传入的render当 h函数,生成虚拟dom ,模板编译出来的render内部使用的vm._c 不用传递进去这个),_update等,
- 在init初始化实例成员,例如options,_isVue,uid记录, Vue.extend()初始化组件的构造函数,它继承自vue所有原型方法,合并配置options.
- 实例化 new Vue(),这里会调用 原型上定义的init方法;
this._init()
- 在这里合并options配置,初始化生命周期变量,事件监听自定义事件.
- 执行initRender函数(生成vm._c处理编译生成render,生成vm.$createElement处理用户传入render)
执行钩子回调,对传入的data数据做响应式处理
- 劫持属性
- 生成各个属性节点的dep对象,dep对象用来通知watcher更新,并且劫持数组原型方法.
- 如果有计算属性生成计算watcher,有侦听器,生成侦听watcher
- 生成watcher时会根据传入的方法来决定是否去 取对应data中的值,如果传入方法里获取值了,会触发对应的我们前面数据劫持的get方法,从而把我们当前watcher添加到对应属性的dep的subs数组中,如果当前属性是子对象,对应子对象dep也需要添加watcher(set和数组时会用到).
- 然后触发created创建完成的钩子函数.
- 最后执行$mount挂载.
vm.$mount();
- 这个方法会先查找options.render函数,看用户有没有传入,没有传入的话,使用传入的模板,调用compileToFunctions把模板转换成render函数,这个render函数内部调用的是vm._c来处理模板编译生成vNode
- 把生成的render赋值给options.render,,后续调用_render()时会从options取出render来调用,这个需要vue的编译器版本.
- 用户传入了render的话,后续调用_render时就会直接调用用户传入的render,从options.render上获取执行这会会使用传入的vm.$createElement来当h函数生成虚拟dom,最后调用mountComponent来进行挂载.
mountComponent主要功能
定义updateComponent
- 这个方法作用是更新界面_update(_render()) //render中编译出的_c或 用户传入_$createElement生成虚拟dom
- _render()生成虚拟dom,_update()内部调用patchVnode 用来对比新旧vNode 来进行dom更新
- _render中会调用了对应的编译vm._c或者vm._createElement,生成虚拟dom,在这个过程中,会判断如果里面有自定义组件会调用 createComponent ,createComponent内部会调用extend()返回组件构造函数, 并且创建组件vnode, 然后注册插件init钩子,init钩子里做实例化组件,然后会调用继承自Vue的init初始化方法,最后再调用mount(),生成渲染watcher,并把组件挂载到页面上(vnode.elm,这里验证了一个组件对应一个渲染watcher)
创建渲染watcher实例,传递updateComponent
- 创建watcher实例时会传入updateComponent方法,这里初始化会调用传入的函数,也就是updateComponent来更新界面.
- 在这个过程中会 获取 我们前面data进行属性劫持中的属性,然后会触发对应的get,来把渲染watcher添加到 对应属性的dep的subs数组中.形成属性dep和渲染watcher的互相依赖.(这样就形成了一个观察关系,在这里一个渲染watcher,可能放入多个属性dep的subs数组中,因为一个渲染watcher对应一个组件, 一个属性中的dep的subs数组中也可能会放入多个不同watcher,例如同时存在渲染和计算||侦听属性的watcher)
- 在这里声明下,一个组件对应一个渲染watcher.
- mounted最后执行这个钩子,整体渲染完成
- 到此为止 vue的首次渲染就完成了
二,响应式原理
前面讲到了,我们在new Vue()时调用init做了对数据data的劫持生成属性对应的dep发布者和对应的get和set方法,在实例化watcher时会把自身赋给Dep.target,然后获取属性值时再触发对应的get,通过dep.depend()和 childOb.dep.depend(),来把当前的watcher添加到自身和子对象的dep的subs数组中. 同时watcher也记录一下dep.id防止后续触发get时重复添加.然后改变data中的属性赋值时会触发对应的set,set会判断值是否改变,改变了的话赋给val,然后set 里会判断新赋值的值是否是对象,是的话继续进行数据劫持observe,然后调用dep的notify方法,来调用dep的subs数组中的watcher的update方法.
updata方法中会调用queueWatcher方法
- 这个方法,在这里会使用watcher的id做一个对象的key来判断,是否重复,不重复的话,把当前的watcher放入queue队列中.
然后来调用nextTick方法,传入flushSchedulerQueue方法当作参数
- flushSchedulerQueue方法的作用 是按watcher.id排序watcher,也就是创建顺序(计算,侦听,渲染)排序,然后清空前面用来重复添加对象key的id,再依次执行watcher.run()
- watche.run里执行了 this.get()也就是传入的函数, 渲染watcher的话也就是updateComponent来调用 内部的_update(_render()),来生成Vnode和对比更新.如果是计算或侦听watcher的话,执行完get()传入的方法后,会执行cb传入的回调。
watcher排序的作用如下:
- 在这里首先 组件从父组件更新到子组件 也就是说假如有多个渲染watcher 先更新父的渲染watcher 后执行的子的渲染watcher
- 其次 组件的用户监视程序在渲染监视程序之前运行 因为用户观察者在渲染观察者之前创建 ,也就是说 每一级组件的计算和侦听watcher是在渲染watcher之前执行的,因为渲染watcher中可能会用到 计算属性.
- 最后就是如果一个组件在父组件的监视程序运行期间被销毁,它的观察者可以被跳过
在这里nextTick接收到传入的函数后,生成一个匿名函数(匿名函数中执行当前传入的函数,加了try catch的错误处理)放到一个 callbacks数组中,现在它并不会立即执行callbacks数组中的函数,然后pending属性判断是false,默认是false,如果是false的话,改为true标记为本次的tick的任务,然后用Promise.resolve()生成一个promise的微任务then(flushCallbacks),挂在本次tick事件循环的最后, 在本轮tick事件循环的最后来执行微任务flushCallbacks回调,这个flushCallbacks回调的主要作用就是
- pending状态改为false,标记本轮tick结束
- 生成callbacks数组的副本,然后依次执行callbacks中的函数.
异步promsie 如果浏览器不支持的话会降级成setTimeout
这里也就体现了vue中的更新是异步的,批量的
这里我们用段伪代码来推理一下它的更新流程
<div id="app">
<p id="p" ref="p1">{{ msg }}</p>
{{ name }}<br>
{{ title }}<br>
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'Hello nextTick',
name: 'Vue.js',
title: 'Title'
},
mounted() {
this.msg = 'Hello Worlds'
this.name = 'Hello snabbdom'
this.title = 'Vue.js'
Vue.nextTick(() => {
console.log(this.$refs.p1.textContent)
})
this.msg = 'Hello'
}
})
</script>
更新值,然后msg的dep.notify()//派发更新
- msg的dep.subs数组里的watcher.update()
- 执行queueWatcher方法拿到watcher.id用个对象,记录防止重复,不重复的话这个watcher放到queue队列里,(当前这个watcher是渲染watcher)
- 执行nextTick(flushSchedulerQueue) 然后伪代码传入的函数放入 callbacks.push(()=>{ flushSchedulerQueue() })
- 这时callbacks数组[执行queue中队列watcher更新的方法也就是flushSchedulerQueue,]
- 然后这时pending为false,改为true标记为本次的tick处理 定义一个微任务promise挂在本次tick的最后,等待将来执行(flushCallbacks是then回调函数).
更新值,然后name.dep.notify()//派发更新
- name的dep.subs数组里的watcher.update()
- 同上面一样,但是watcher.id重复,同一个渲染watcher,退出queueWatcher方法 (这里没添加重复的watcher,但是值已经更新了 val = 新值)
更新值,然后title的dep.notify()//派发更新
- title的dep.subs数组里的watcher.update()
- 同上面一样,但是watcher.id重复,同一个渲染watcher,退出queueWatcher方法(这里没添加重复的watcher,但是值已经更新了 val = 新值)
Vue.nextTick(回调)
- 执行nextTick(回调) 然后伪代码传入的函数放入 callbacks.push(()=>{ 回调() })
- 这时callbacks数组[执行queue中队列watcher更新的方法也就是flushSchedulerQueue, 回调方法]
- 然后这时pending为true 退出
更新值,然后msg的dep.notify()//派发更新
- msg的dep.subs数组里的watcher.update()
- 同上面一样,但是watcher.id重复,同一个渲染watcher,退出queueWatcher方法(这里没添加重复的watcher,但是值已经更新了 val = 新值,这时的msg已经是Hello 而不是Hello words)
本次tick最后了来执行属于本次tick的微任务
- 执行flushCallbacks方法 callbacks数组中第一个方法是flushSchedulerQueue,这个方法执行queue队列中的所有watcher的更新,我们现在里面就一个渲染watcher(因为id是相同的),执行渲染wtcher.
- 之后执行 步骤4 Vue.nextTick传入的回调,这时渲染watcher已经执行完成了,内容已经改变了,然后执行回调再去获取对应dom的textContent时就是我们最后一次给msg赋值Hello.
这就是vue数据响应式的原理,以及它的更新过程,以及Vue.nextTick为什么能获取到更新之后的dom值
三,Vue 中模板编译的过程
我们开篇提到过,在调用$mount 挂载时 会调用compileToFunctions 把template模板转换成render函数(内部调用_c()生成虚拟dom)
这个方法主要作用:
- 这里会首先使用parse()方法把模板转换成 ast 抽象语法树,抽象语法树是以js对象形式,用来以树形的方式描述代码结构,这个里就包含了对模板和 v-for v-if ref,等的解析.(v-for ,v-if结构化指令只能在编译阶段处理,render函数里 不会再解析模板,所以要使用js的for 和if).
然后优化生成的 ast 抽象语法树 ,标记静态节点和静态根节点
- 检测子节点是否有是纯静态节点的,一旦检测到纯静态节点,那就是永远不会更改的节点,提升为常量,重新渲染的时候不在重新创建节点
- 在 patch 的时候直接跳过静态子树
- 然后把抽象语法树生成字符串形式的 js 代码 这个js字符串代码是编译出来,也就是render函数代码,里面用的_c生成虚拟dom
- 然后把字符串形式的js代码转换成js方法 赋值给凡出对象的render属性
- 返回编译生成的render函数
- 然后把返回的render函数赋值给options.render 后续渲染时调用的_render()就是这个render
这就是模板的编译渲染.
四,虚拟 DOM 中 Key 的作用和好处。
虚拟dom中的key 主要用来标记两个节点是否是同一个,然后做新旧vNode对比使用,对比vnode的不同来更新老vNode,从而更新 对应的dom,因为在虚拟dom节点的对比时,节点对比规则会根据key来比较,新旧开始,新旧结束,旧开始新结束,旧结束新开始.
如果都不符合然后新的开始 在老的没对比完的同级开始结束位置区间 找相同的 vnode 来跟新差异并根据需要移动位置.
这样看的话 假如没有带Key,举个例子
<ul>
<li v-for="value in arr"
:key="value"
>{{value}}</li>
</ul>
[a,b,c,d]
//更新为
[a,x,b,c,d]
没key的时候,对比开始第一个相同,key时undefined相等,标签相同, 第2,3,4标签相同,内容不同更新dom内容,第5个生成dom 插入d
也就是说 没key时有 一个生成插入操作, 三个更新dom
有key的时候,会对比key,不会key出现undefined的情况, 根据我们上面说的规则,只需要执行一次x的生成插入操作.
从这个例子看出来,如果我们 使用的列表,如果往不同位置插入数据时,没有key的时候,更新的次数要远远大于有key的时候.所以使用列表时尽量来使用Key.
这次总结就到这里结束了.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。