目前公司主要技术栈是Vue,为了更好的使用,完成的了解Vue的原理是很有必要的。刚开始直接阅读Vue源码时,发现自己阅读的效率很低。偶然间(其实不偶然)
在Github中发现了这份笔记 如何学习Vue2源码,完整的记录了实现Vue框架的完整过程。
我fork了作者的项目,跟着作者的思路如何学习Vue2源码,完整走了一遍如何实现一个2.X版本的Vue。阅读过程中我拉了一个新分支如何学习Vue2源码(带注释),添加了详细的注释,方便自己记忆。通过阅读作者的github,节省自己大量时间~
下面内容为自己的笔记,自己整理思路用。
建议阅读这位大神的github:如何学习Vue2源码。
我的阅读记录: 如何学习Vue2源码(带注释)
Virtual DOM
- 生成virtual DOM
- 用js对象模拟DOM树(html语法树?)
- render真实DOM
- virtual对象的render函数
- 生成新的virtual DOM
- render新的真实DOM
- diff新旧真实DOM,保存变更的patches
- diff算法复杂度问题
- 深度优先遍历,每遍历一个node同时跟新的树比较,记录差异
- 差异类型,不同类型处理方式不一致
- 列表对比算法,调换顺序最优解?
- 在旧的真实DOM上应该patches
- 不同的patch类型,不同的处理方式
HTML parser
- Token解析
- HTML字符串 --> Token数组
- 语法树解析
- Token数组 --> HTML AST
- VNode树
- Dom树
首先有2个重要的概念要留意,将会贯穿后续的文章:静态、动态(runtime)。
我们给一下以下简单的定义:
静态: 任何时刻运行转换函数,同一个输入得到的数据结构都是一致的
动态: 存在某一时刻运行转换函数,同一个输入得到的数据结构是不一致的
所以上边所有涉及到的数据结构可以归为:
字符串 是 静态 的
HTMLElment Token 流 是 静态 的
AST 树 是 静态 的
VNode 树 是 动态 的
DOM 树 是 动态 的
构建一个最简单的数据绑定的Vue
AST树、VNode render、VNode三者之间的渐进关系
VNode 的属性 attrs 和 props
控制语句
v-if v-else-if v-else v-for
新增语法步骤同上及几章:
- parse阶段增加解析v-for v-if部分的代码,在AST上添加这部分语法的属性
- 生成render函数阶段,同样要增加将AST转换成render函数的fn,
- 生成Vnode阶段,在单用render函数的时候,需要将v-if语法转换为实际的VNode对象
- path阶段 将VNode树转换成DOM树
响应式原理
基于Object.defineProperty实现data的数据监听,set 写操作的时候,我们要调用 vm._update() 进行视图更新。
深度追踪依赖变化
obsever、dep、watch之间的关系:https://github.com/coderzzp/vue-come-true/tree/master/vue-come-true-First
详细源码分析:https://segmentfault.com/a/1190000014360080
- 收集依赖流程:
observe ->
walk ->
defineReactive ->
get ->
dep.depend() ->
watcher.addDep(new Dep()) ->
watcher.newDeps.push(dep) ->
dep.addSub(new Watcher()) ->
dep.subs.push(watcher)
- 视图更新流程:
set ->
dep.notify() ->
subs[i].update() ->
watcher.run() || queueWatcher(this) ->
watcher.get() || watcher.cb ->
watcher.getter() ->
vm._update() ->
vm.__patch__()
事件处理
时间的处理与之前属性的处理基本一致:
a. 以下 HTML:
<button v-on:click="clickme">click me</button>
b. 解析后得到的 AST 节点:
evtAstElm = {
type: 1,
tag: 'button',
events: {
'click': { value: 'clickme' }
},
children: [ /* blabla.. */ ]
}
c. 生成的 render code:
_c('button', {
on: { "click": clickme }
}, [ _v("click me")] )
d. 得到一个带属性的 VNode 节点:
VNode {
tag: 'button',
data: {
on: { "click": clickme }
},
children: [ /* blabla.. */ ]
}
c. 生成的 render code ( clickme 函数需要代理到当前的 vm 对象上,同时绑上 vm 这个运行时 context):
_c('button', {
on: { "click": clickme }
}, [ _v("click me")] )
e. 最后渲染在 dom 上的时候:
buttonDom.addEventListener('click', clickme)
源码的事例 https://github.com/raphealguo/how-to-learn-vue2-blob/blob/master/articles/2.4.1.md
生命周期
- new Vue()
- 初始化生命周期、往vue实例上绑事件、方法
这里只的事件、方法,并不是用户自己绑定的method、data之类的数据,而是Vue实例本身的一些内部方法。 - 执行钩子:
beforeCreate()
beforeCreate()
钩子中无法访问到,data、method、$el等数据
$el挂载点,也就是vue生成的dom挂载的地方 - 初始化状态
简单来说就是初始化method、data,监听数据之类的操作 - 执行钩子:
created()
data、method方法已经能访问了,但是还没有挂载点信息 - 是否有挂载点?
有:获取模板信息(有无template,两种情况),编译 -> vnode -> render函数 -> Dom -> diff -> patch
无:暂停执行,知道使用.$mount()
手动挂载后,继续执行 - 执行钩子:
beforeMount()
.$el
中已经有挂载点了,但是dom资源还没有渲染 - 将VNode渲染成Dom,并挂载到挂载点上。
$el
得到更新。 - 执行钩子
mounted()
挂载完成。 - data改变时,触发监听。
- 执行
beforeUpdate()
此时仅监听到了data改变,还未做出操作 diff VNode -> patch
对比VNode变化,更新dom- 检测到页面销毁(
.$destroy()
被执行) - 执行钩子
beforeDestroy()
- 移除data、mothod、watcher、components、event listener等
- 执行钩子
destroyed()