Vue2.x的运行原理可以大致分为以下几步;
- 创建Vue实例,进行数据劫持以及依赖收集
- 将模板编译为render函数
- 运行render函数,生成VNode节点
- 将VNode节点渲染成实际DOM节点
- 数据变化后,比对新的VNode和旧的VNode,只更新变化的部分
setter -> Dep -> Watcher -> patch -> 视图
数据劫持
Vue2.x使用 Object.defineProperty
,将初始化传入的data对象中属性全部遍历,对每个属性设置 setter
和 getter
。
// 设置getter、setter
const dep = new Dep()
function defineReactive(obj,key,val) {
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get() {
dep.addSub(Dep.target)
return val
},
set(newVal) {
if(newVal===val) return;
val = newVal;
// 属性发生变化
dep.notify()
}
})
}
// 监听data对象属性变化
function observer(data) {
if(!value || typeof value!=='object') return;
Object.keys(data).forEach(key=>{
if(typeof data[key]==='object') {
observer(data[key])// 多层对象采用递归遍历
} else {
defineReactive(data,key,data[key]])
}
})
}
依赖收集
Dep负责收集依赖,Watcher和Vue对象一对一的关系,表示该实例的对象属性变化
Dep.target = null
class Watcher() {
constructor() {
Dep.target = this
}
update() {
// 更新DOM节点
}
}
class Vue() {
constructor(data) {
new Watcher()
observer(data)
}
}
class Dep() {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub=>{
sub.update()
})
}
}
在属性变化后先调用dep的notify方法,notify方法中将收集来的依赖对象遍历并调用每个依赖对象中的update方法。
模板编译
使用vue-template-compiler对SFC中的模板进行编译,生成render函数
const compiler = require('vue-template-compiler')
const code = compiler.compile('<div>123</div>')
console.log(code.render)
// ƒ anonymous(
// ) {
// with(this){return _c('div',[_v("123")])}
/ }
VNode对比
Vue2.x中diff算法对同层树节点进行比较,是一种比较高效的算法,时间复杂度为O(n)
批量异步更新
Vue.js在默认情况下,每次触发某个数据的 setter
方法后,对应的 Watcher
对象其实会被 push
进一个队列 queue
中,在下一个 tick 的时候将这个队列 queue
全部拿出来 run
( Watcher
对象的一个方法,用来触发 patch
操作) 一遍。
Vue.js 实现了一个 nextTick
函数,传入一个 cb
,这个 cb
会被存储到一个队列中,在下一个 tick 时触发队列中的所有 cb
事件。
因为目前浏览器平台并没有实现 nextTick
方法,所以 Vue.js 源码中分别用 Promise
、setTimeout
、setImmediate
等方式在 microtask(或是task)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。
遇到同一个属性多次变化的情况,只需要改变一次,所以需要Watcher做去重,在每个Watcher中添加uuid,如果遇到uuid相同的就不去多次触发。