一、new Vue()的执行过程
1.1 数据劫持和初步渲染
function Vue (options) {
// 初始化
this._init(options)
// 执行render函数
this.$mount()
}
Vue.prototype._init = function (options) {
const vm = this
// 把options挂载到this上
vm.$options = options
if (options.data) {
// 数据响应式
initState(vm)
}
if (options.computed) {
// 初始化计算属性
initComputed(vm)
}
if (options.watch) {
// 初始化watch
initWatch(vm)
}
}
在initState()
中实现数据劫持和代理:
function initState(vm) {
// 拿到配置的data属性值
let data = vm.$options.data;
// 判断data 是函数还是别的类型
data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {};
// 数据代理
const keys = Object.keys(data);
let i = keys.length;
while(i--) {
// 从this上读取的数据全部拦截到this._data到里面读取
// 例如 this.name 等同于 this._data.name
proxy(vm, '_data', keys[i]);
}
// 数据观察
observe(data);
}
// 数据观察函数
function observe(data) {
if (typeof data !== 'object' && data != null) {
return;
}
return new Observer(data)
}
// 从this上读取的数据全部拦截到this._data到里面读取
// 例如 this.name 等同于 this._data.name
function proxy(vm, source, key) {
Object.defineProperty(vm, key, {
get() {
return vm[source][key] // this.name 等同于 this._data.name
},
set(newValue) {
return vm[source][key] = newValue
}
})
}
class Observer{
constructor(value) {
// 给每一个属性都设置get set
this.walk(value)
}
walk(data) {
let keys = Object.keys(data);
for (let i = 0, len = keys.length; i < len; i++) {
let key = keys[i]
let value = data[key]
// 给对象设置get set
defineReactive(data, key, value)
}
}
}
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
return value
},
set(newValue) {
if (newValue == value) return
observe(newValue) // 给新的值设置响应式
value = newValue
}
})
// 递归给数据设置get set
observe(value);
}
initComputed()
以及initWatch()
后面再实现,这时候new Vue()
中的this._init(options)
执行完成,接着执行this.$mount()
// 挂载方法
Vue.prototype.$mount = function () {
const vm = this
new Watcher(vm, vm.$options.render, () => {}, true) // 这里的参数true表示是渲染Watcher
}
在这里可以看到new
了一个Watcher
,此处的Watcher
就是渲染Watcher
,先看看Watcher
的初步实现:
let wid = 0
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm // 把vm挂载到当前的this上
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn // 把exprOrFn挂载到当前的this上,这里exprOrFn 等于 vm.$options.render
}
this.cb = cb // 把cb挂载到当前的this上
this.options = options // 把options挂载到当前的this上
this.id = wid++ // 每个Watcher都有其id
this.value = this.get() // 相当于运行 vm.$options.render()
}
get() {
const vm = this.vm
let value = this.getter.call(vm, vm) // 把this指向到vm,此时的getter即是render函数
return value
}
}
可以看到,当在this.$mount()
中new Watcher()
(渲染Watcher
)时,会立刻执行this.get()
方法去获取数据渲染,至此,首页渲染就完成了
1.2 Dep Watcher
在首页渲染的过程中,是data
订阅的时期,以实现数据修改时能够修改视图
Dep
类的实现:
// 依赖收集
let dId = 0
class Dep{
constructor() {
this.id = dId++ // 每次实例化都生成一个id
this.subs = [] // 让这个dep实例收集watcher
}
depend() {
// Dep.target 就是当前的watcher
if (Dep.target) {
Dep.target.addDep(this) // 让watcher,去存放dep,然后里面dep存放对应的watcher,两个是多对多的关系
}
}
notify() {
// 触发更新
this.subs.forEach(watcher => watcher.update())
}
addSub(watcher) {
this.subs.push(watcher)
}
}
let stack = []
// push当前watcher到stack 中,并记录当前watcer
function pushTarget(watcher) {
Dep.target = watcher
stack.push(watcher)
}
// 运行完之后清空当前的watcher
function popTarget() {
stack.pop()
Dep.target = stack[stack.length - 1]
}
发现Dep
中有一个depend()
实例方法,该方法核心代码为Dep.target.addDep(this)
,即调用当前栈顶Watcher
的addDep()
,目的是实现每一个Watcher
记录所有它订阅的dep
以及每一个dep
记录它所有收集的Watcher
的双向保存,看看Watcher
的改进实现:
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn
}
this.cb = cb
this.options = options
this.id = wid++
+ this.deps = []
+ this.depsId = new Set() // dep 已经收集过相同的watcher 就不要重复收集了
this.value = this.get()
}
get() {
const vm = this.vm
+ pushTarget(this)
// 执行函数
let value = this.getter.call(vm, vm)
+ popTarget()
return value
}
+ addDep(dep) {
+ let id = dep.id
+ if (!this.depsId.has(id)) { // 当watcher有dep时,也意味着dep中已有该watcher,就不再双向保存了,保证了首页渲染后数据修改,触发执行渲染watcher时,不会再添加渲染watcher到属性的dep中
+ this.depsId.add(id)
+ this.deps.push(dep) // 当前watcher保存dep
+ dep.addSub(this) // dep保存当前watcher
+ }
+ }
+ update(){
+ this.get()
+ }
}
Dep
和Watcher
都有了,接下来就是watcher
什么时候订阅:
function defineReactive(data, key, value) {
let dep = new Dep()
Object.defineProperty(data, key, {
get() {
+ if (Dep.target) { // 如果取值时有watcher
+ dep.depend() // 让watcher保存dep,并且让dep 保存watcher,双向保存
+ }
return value
},
set(newValue) {
if (newValue == value) return
observe(newValue) // 给新的值设置响应式
value = newValue
+ dep.notify() // 通知渲染watcher去更新
}
})
// 递归给数据设置get set
observe(value);
}
- 在
initState()
时,每一个属性都创建了自己的dep
- 在首页渲染时,创建
渲染Watcher
,执行get()
,pushTarget(this)
, 此时栈顶是渲染Watcher
let value = this.getter.call(vm, vm)
,即执行render
函数,会执行每个属性的get()
方法,这个时候每个属性把渲染Watcher
都添加到自己的dep
中,渲染Watcher
中也添加了每一个自己订阅的依赖器dep
- 当数据变化时,变化属性执行
dep.notify()
,变化属性的dep
中有渲染Watcher
,因此执行渲染Watcher
的update()
,即get()
,此时重新执行render函数
,视图得以更新(这里的Watcher
还不完善,看后面逐步实现完整的Watcher
)
二、Computed
computed
特征是缓存,仅有在数据有变化时才会执行计算取值,否则都是在缓存中取的- 初始化渲染时会执行一次
执行代码:
const root = document.querySelector('#root')
var vue = new Vue({
data() {
return {
name: '张三',
age: 10
}
},
computed: {
info() {
return this.name + this.age
}
},
render() {
root.innerHTML = `${this.name}----${this.age}----${this.info}`
}
})
function changeData() {
vue.name = '李四'
vue.age = 20
}
执行initComputed
// 初始化computed
function initComputed(vm) {
// 拿到computed配置
const computed = vm.$options.computed
// 给当前的vm挂载_computedWatchers属性,后面会用到
const watchers = vm._computedWatchers = Object.create(null)
// 循环computed每个属性
for (const key in computed) {
const userDef = computed[key]
// 判断是函数还是对象
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 给每一个computed创建一个computed watcher 注意{ lazy: true }
// 然后挂载到vm._computedWatchers对象上
watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true })
if (!(key in vm)) {
defineComputed(vm, key, userDef) // 在vm上定义key属性,如果key已在vm中,则会报错警告
}
}
}
computed
是有缓存的,所以创建watcher
的时候,会传一个配置{ lazy: true }
,同时也可以区分这是computed watcher
,然后到watcer
里面接收到这个对象,我们继续看watcher
的实现:
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn
}
+ if (options) {
+ this.lazy = !!options.lazy // 为computed 设计的
+ } else {
+ this.lazy = false
+ }
+ this.dirty = this.lazy
this.cb = cb
this.options = options
this.id = wId++
this.deps = []
this.depsId = new Set()
+ this.value = this.lazy ? undefined : this.get()
}
// 省略很多代码
}
从上面这句this.value = this.lazy ? undefined : this.get()
代码可以看到,computed
创建watcher
的时候是不会指向this.get
的,第一次执行get
是在render
即初次渲染时获取计算属性时
computed watcher
创建好了之后,把计算属性挂载到vm
实例上,即执行defineComputed(vm, key, userDef)
:
// 设置comoputed的 set个set
function defineComputed(vm, key, userDef) {
let getter = null
// 判断是函数还是对象
if (typeof userDef === 'function') {
getter = createComputedGetter(key)
} else {
getter = userDef.get
}
Object.defineProperty(vm, key, { // 劫持计算属性
enumerable: true,
configurable: true,
get: getter,
set: function() {} // 又偷懒,先不考虑set情况哈,自己去看源码实现一番也是可以的
})
}
// 创建computed函数
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {// 给computed的属性添加订阅watchers
watcher.evaluate()
}
// 把渲染watcher 添加到属性的订阅里面去,这很关键
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
可以看到,读取计算属性时主要执行了createComputedGetter(key)
(这里只考虑了getter
是函数的情况),核心代码是watcher.evaluate()
和watcher.depend()
,接下来在watch
中实现:
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn
}
if (options) {
this.lazy = !!options.lazy // 为computed 设计的
} else {
this.lazy = false
}
this.dirty = this.lazy
this.cb = cb
this.options = options
this.id = wId++
this.deps = []
this.depsId = new Set() // dep 已经收集过相同的watcher 就不要重复收集了
this.value = this.lazy ? undefined : this.get()
}
get() {
const vm = this.vm
pushTarget(this)
// 执行函数
let value = this.getter.call(vm, vm)
popTarget()
return value
}
addDep(dep) {
let id = dep.id
if (!this.depsId.has(id)) {
this.depsId.add(id)
this.deps.push(dep)
dep.addSub(this);
}
}
update(){
if (this.lazy) {
this.dirty = true
} else {
this.get()
}
}
// 执行get,并且 this.dirty = false
+ evaluate() {
+ this.value = this.get()
+ this.dirty = false
+ }
// 所有的属性收集当前的watcer
+ depend() {
+ let i = this.deps.length
+ while(i--) {
+ this.deps[i].depend()
+ }
+ }
}
执行流程:
- 1、首先在
render函数
里面会读取this.info
,这个会触发createComputedGetter(key)
中的computedGetter(key)
; - 2、然后会判断
watcher.dirty
,执行watcher.evaluate()
; - 3、进到
watcher.evaluate()
,执行this.get
方法,这时候会执行pushTarget(this)
把当前的computed watcher
push
到stack
里面去,并且把Dep.target
设置成当前的computed watcher
; - 4、然后运行
this.getter.call(vm, vm)
相当于运行computed
的info
:function() { return this.name + this.age }
,这个方法; - 5、
info函数
里面会读取到this.name
,这时候就会触发数据响应式this.name
的Object.defineProperty.get
的方法,这里name
会进行依赖收集,把watcher
收集到对应的dep
上面,computed watcher
此时也把name
的dep
记录在了自己的deps
中;并且返回name = '张三'
的值,age
收集同理; - 6、依赖收集完毕之后执行
popTarget()
,把当前的computed watcher
从栈清除,返回计算后的值('张三+10')
,并且this.dirty = false
; - 7、
watcher.evaluate()
执行完毕之后,就会判断Dep.target
是不是true
,如果有就代表还有渲染watcher
,就执行watcher.depend()
,然后让watcher
里面的deps
都收集渲染watcher
,这就是双向保存的优势。 - 8、此时
name
都收集了computed watcher
和渲染watcher
。那么设置name
的时候都会去更新执行watcher.update()
- 9、首页渲染后,数据更改,执行
watcher,update()
,如果是computed watcher
的话不会重新执行一遍,只会把this.dirty
设置成true
,等渲染的时候需要用到对应计算属性再执行watcher.evaluate()
进行info
更新。没有变化的话this.dirty
就是false
,不会执行watcher.evaluate()
方法。这就是computed
缓存机制
三、watch
执行代码:
const root = document.querySelector('#root')
var vue = new Vue({
data() {
return {
name: '张三',
age: 10
}
},
computed: {
info() {
return this.name + this.age
}
},
watch: {
name(newValue, oldValue) {
console.log(oldValue, newValue)
}
},
render() {
root.innerHTML = `${this.name}----${this.age}----${this.info}`
}
})
function changeData() {
vue.name = '李四'
vue.age = 20
}
initWatch()
:
function initWatch(vm) {
let watch = vm.$options.watch
for (let key in watch) {
const handler = watch[key]
new Watcher(vm, key, handler, { user: true })
}
}
修改watcher
:
let wId = 0
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn
} else {
+ this.getter = parsePath(exprOrFn) // user watcher
}
if (options) {
this.lazy = !!options.lazy // 为computed watcher设计的
+ this.user = !!options.user // 为user watcher设计的
} else {
+ this.user = this.lazy = false
}
this.dirty = this.lazy
this.cb = cb
this.options = options
this.id = wId++
this.deps = []
this.depsId = new Set() // dep 已经收集过相同的watcher 就不要重复收集了
this.value = this.lazy ? undefined : this.get()
}
get() {
const vm = this.vm
pushTarget(this)
// 执行函数
let value = this.getter.call(vm, vm) // 如果是watch的话getter函数的作用就是获取值而已,在获取值时就在对应属性dep上添加了该watch watcher
popTarget()
return value
}
addDep(dep) {
let id = dep.id
if (!this.depsId.has(id)) {
this.depsId.add(id)
this.deps.push(dep)
dep.addSub(this);
}
}
update(){
if (this.lazy) {
this.dirty = true
} else {
+ this.run() // 除了computed都执行这个
}
}
// 执行get,并且 this.dirty = false
evaluate() {
this.value = this.get()
this.dirty = false
}
// 所有的属性收集当前的watcer
depend() {
let i = this.deps.length
while(i--) {
this.deps[i].depend()
}
}
+ run () {
+ const value = this.get()
+ const oldValue = this.value
+ this.value = value
// 执行cb
+ if (this.user) { // watch执行回调
+ try{
+ this.cb.call(this.vm, value, oldValue)
+ } catch(error) {
+ console.error(error)
+ }
+ } else {
+ this.cb && this.cb.call(this.vm, oldValue, value)
+ }
+ }
}
function parsePath (path) {
const segments = path.split('.')
return function (obj) { // 如name.age,则name和name.age的dep里都有了watch watcher,但如果修改的是name.age.sex,则不会触发watch watcher了,需要使用deep:true选项
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
watch
默认特征是初始渲染时会获取值,执行get()
方法以给检测属性dep
绑定上watch watcher
,但不会执行回调cb
,而且不会执行深检测- 深检测:配置
deep: true
选项,原理就是遍历值继续绑定,大致代码
// 由于配置选项时,handler不是函数,因此得考虑对象写法,就不详细写了,只写出大致思路,这里deepHas = !deep
class Watcher {
get() {
const vm = this.vm
pushTarget(this)
// 执行函数
let value = this.getter.call(vm, vm) // 如果是watch的话getter函数的作用就是获取值而已,在获取值时就在对应属性dep上添加了该watch watcher
if(!deepHas) {
this.deepBind(value)
deepHas = true
}
popTarget()
return value
}
deepBind (obj) {
if(typeof obj === 'object') {
for(let key in obj) {
this.deepBind(obj[key]) // obj[key]去读取值,此时就绑定上了
}
}
}
}
- 立即执行,配置
immediate: true
选项
// 由于配置选项时,handler不是函数,因此得考虑对象写法,就不详细写了,只写出大致思路,这里immediateHas = !immediate
class Watcher {
get() {
const vm = this.vm
pushTarget(this)
// 执行函数
let value = this.getter.call(vm, vm) // 如果是watch的话getter函数的作用就是获取值而已,在获取值时就在对应属性dep上添加了该watch watcher
if(!immediateHas) {
this.run()
immediateHas = true
}
popTarget()
return value
}
}
总结一下执行流程:
- 执行
new Vue()
- 执行
this._init(options)
- 执行
initState()
proxy
observe
- 创建属性自身的
dep
- 创建属性自身的
- 执行
initComputed()
- 创建
computed watcher
- 不会执行
watcher
中的get()
方法,在这一步仅仅创建了computed watcher
,即new Watcher
- 不会执行
defineComputed()
- 这一步通过
Object.defineProperty
将计算属性劫持,设置获取计算属性的get()
方法为createComputedGetter()
,即
Object.defineProperty(vm, key, { // 劫持计算属性 enumerable: true, configurable: true, get: createComputedGetter(key) }
- 这一步通过
- 创建
- 执行
initWatch()
- 创建
use watcher
- 执行
watcher
中的get()
方法,栈顶此时是use watcher
,对应属性dep
就和use watcher
实现了双向保存
- 执行
- 创建
- 执行
- 执行
this.$mount()
- 创建
渲染 watcher
,执行watcher
中的get()
方法,栈顶此时是渲染watcher
,执行render函数
取值渲染,渲染过程中取值会引起对应属性dep
添加watcher
以及watcher
添加对应属性dep
的双向保存 - 取计算属性时:会执行
createComputedGetter()
,最终即执行
根据if (watcher) { if (watcher.dirty) {// 给computed的属性添加订阅watchers watcher.evaluate() } // 把渲染watcher 添加到属性的订阅里面去,这很关键 if (Dep.target) { watcher.depend() } return watcher.value }
watcher.dirty
决定是取缓存还是计算取值,因为是第一次渲染,所以是计算取值,取值过程中就实现了computed watcher
与对应属性dep
的双向保存。继续判断Dep.target
,此时是渲染 watcher
,因此执行watcher.depend()
,这时对应属性的dep
上就都挂载了渲染watcher
。在这里对应属性的dep
上就都挂载了渲染watcher
和computed watcher
- 取普通属性时,此时栈顶时
渲染watcher
,因此对应属性dep
和渲染watcher
实现了双向保存 - 渲染完毕,
渲染watcher
出栈,此时栈为空
- 创建
- 执行
- 属性变动时,就会通知
dep
中的watcher
去更新,根据入dep
的顺序以及调用的顺序,user watcher
先执行,然后是computed watcher
,最后是渲染 watcher
,这样,所有数值更新后,最后再进行页面渲染,页面就是最新的
参考:https://segmentfault.com/a/1190000023196603