Vue2.x的运行原理可以大致分为以下几步;

  1. 创建Vue实例,进行数据劫持以及依赖收集
  2. 将模板编译为render函数
  3. 运行render函数,生成VNode节点
  4. 将VNode节点渲染成实际DOM节点
  5. 数据变化后,比对新的VNode和旧的VNode,只更新变化的部分

setter -> Dep -> Watcher -> patch -> 视图

数据劫持

Vue2.x使用 Object.defineProperty,将初始化传入的data对象中属性全部遍历,对每个属性设置 settergetter

  1. // 设置getter、setter
  2. const dep = new Dep()
  3. function defineReactive(obj,key,val) {
  4. Object.defineProperty(obj,key,{
  5. enumerable:true,
  6. configurable:true,
  7. get() {
  8. dep.addSub(Dep.target)
  9. return val
  10. },
  11. set(newVal) {
  12. if(newVal===val) return;
  13. val = newVal;
  14. // 属性发生变化
  15. dep.notify()
  16. }
  17. })
  18. }
  1. // 监听data对象属性变化
  2. function observer(data) {
  3. if(!value || typeof value!=='object') return;
  4. Object.keys(data).forEach(key=>{
  5. if(typeof data[key]==='object') {
  6. observer(data[key])// 多层对象采用递归遍历
  7. } else {
  8. defineReactive(data,key,data[key]])
  9. }
  10. })
  11. }

依赖收集

Dep负责收集依赖,Watcher和Vue对象一对一的关系,表示该实例的对象属性变化

  1. Dep.target = null
  2. class Watcher() {
  3. constructor() {
  4. Dep.target = this
  5. }
  6. update() {
  7. // 更新DOM节点
  8. }
  9. }
  10. class Vue() {
  11. constructor(data) {
  12. new Watcher()
  13. observer(data)
  14. }
  15. }
  16. class Dep() {
  17. constructor() {
  18. this.subs = []
  19. }
  20. addSub(sub) {
  21. this.subs.push(sub)
  22. }
  23. notify() {
  24. this.subs.forEach(sub=>{
  25. sub.update()
  26. })
  27. }
  28. }

在属性变化后先调用dep的notify方法,notify方法中将收集来的依赖对象遍历并调用每个依赖对象中的update方法。

模板编译

使用vue-template-compiler对SFC中的模板进行编译,生成render函数

  1. const compiler = require('vue-template-compiler')
  2. const code = compiler.compile('<div>123</div>')
  3. console.log(code.render)
  4. // ƒ anonymous(
  5. // ) {
  6. // with(this){return _c('div',[_v("123")])}
  7. / }

VNode对比

Vue2.x中diff算法对同层树节点进行比较,是一种比较高效的算法,时间复杂度为O(n)

Vue运行原理 - 图1

批量异步更新

Vue.js在默认情况下,每次触发某个数据的 setter 方法后,对应的 Watcher 对象其实会被 push 进一个队列 queue 中,在下一个 tick 的时候将这个队列 queue 全部拿出来 runWatcher 对象的一个方法,用来触发 patch 操作) 一遍。

Vue运行原理 - 图2

Vue.js 实现了一个 nextTick 函数,传入一个 cb ,这个 cb 会被存储到一个队列中,在下一个 tick 时触发队列中的所有 cb 事件。
因为目前浏览器平台并没有实现 nextTick 方法,所以 Vue.js 源码中分别用 PromisesetTimeoutsetImmediate 等方式在 microtask(或是task)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。

遇到同一个属性多次变化的情况,只需要改变一次,所以需要Watcher做去重,在每个Watcher中添加uuid,如果遇到uuid相同的就不去多次触发。