简单的描述一下 Vue 2.x 的运行机制(需要注意分析的是 Runtime + Compiler 的 Vue.js)。
初始化流程:
- 创建 Vue 实例对象
init
过程会初始化生命周期,初始化事件中心,初始化渲染、执行beforeCreate
周期函数、初始化data
、props
、computed
、watcher
、执行created
周期函数等。- 初始化后,调用
$mount
方法对Vue实例进行挂载(挂载的核心过程包括模板编译、渲染以及更新三个过程)。 - 如果没有在 Vue 实例上定义
render
方法而是定义了template
,那么需要经历编译阶段。需要先将template
字符串编译成render function
,template
字符串编译步骤如下 :parse
正则解析template
字符串形成 AST(抽象语法树,是源代码的抽象语法结构的树状表现形式)optimize
标记静态节点跳过 DIFF 算法(DIFF 算法是逐层进行比对,只有同层级的节点进行比对,因此时间的复杂度只有 O(n)。generate
将 AST 转化成render function
字符串
- 编译成
render function
后,调用$mount
的mountComponent
方法,先执行beforeMount
钩子函数,然后核心是实例化一个渲染Watcher
,在它的回调函数(初始化的时候执行,以及组件实例中监测到数据发生变化时执行)中调用updateComponent
方法(此方法调用render
方法生成虚拟 Node,最终调用update
方法更新 DOM)。 - 调用
render
方法将render function
渲染成虚拟的Node(真正的 DOM 元素是非常庞大的,因为浏览器的标准就把 DOM 设计的非常复杂。如果频繁的去做 DOM 更新,会产生一定的性能问题,而 Virtual DOM 就是用一个原生的 JavaScript 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多,而且修改属性也很轻松,还可以做到跨平台兼容),render
方法的第一个参数是createElement
(或者说是h
函数),这个在官方文档也有说明。 - 生成虚拟 DOM 树后,需要将虚拟 DOM 树转化成真实的 DOM 节点,此时需要调用
update
方法,update
方法又会调用pacth
方法把虚拟 DOM 转换成真正的 DOM 节点。需要注意在图中忽略了新建真实 DOM 的情况(如果没有旧的虚拟 Node,那么可以直接通过createElm
创建真实 DOM 节点),这里重点分析在已有虚拟 Node 的情况下,会通过sameVnode
判断当前需要更新的 Node节点是否和旧的 Node 节点相同(例如我们设置的key
属性发生了变化,那么节点显然不同),如果节点不同那么将旧节点采用新节点替换即可,如果相同且存在子节点,需要调用patchVNode
方法执行 DIFF 算法更新 DOM,从而提升 DOM 操作的性能。
需要注意在初始化阶段,没有详细描述数据的响应式过程,这个在响应式流程里做说明。
响应式流程:
- 在
init
的时候会利用Object.defineProperty
方法(不兼容 IE8)监听Vue实例的响应式数据的变化从而实现数据劫持能力(利用了 JavaScript 对象的访问器属性get
和set
,在未来的 Vue3 中会使用 ES6 的Proxy
来优化响应式原理)。在初始化流程中的编译阶段,当render function
被渲染的时候,会读取Vue实例中和视图相关的响应式数据,此时会触发getter
函数进行依赖收集(将观察者Watcher
对象存放到当前闭包的订阅者Dep
的subs
中),此时的数据劫持功能和观察者模式就实现了一个 MVVM 模式中的 Binder,之后就是正常的渲染和更新流程。 - 当数据发生变化或者视图导致的数据发生了变化时,会触发数据劫持的
setter
函数,setter
会通知初始化依赖收集中的Dep
中的和视图相应的Watcher
,告知需要重新渲染视图,Wather
就会再次通过update
方法来更新视图。
可以发现只要视图中添加监听事件,自动变更对应的数据变化时,就可以实现数据和视图的双向绑定了。