共计 6148 个字符,预计需要花费 16 分钟才能阅读完成。
之前写了三篇 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.
这次总结就到这里完结了.