数据劫持
对data
进行初始化时,会调用observe
方法对数据实现劫持
observe
方法是通过维护Observer
对象实现的
在Observer
中,主要是对对象按key进行遍历,逐个属性进行数据拦截
而defineReactive
则会进一步对data
内的对象进行深度劫持(shallow默认是false的)
而数组是用函数拦截的方式去实现的,直接在数组数据的原型上挂载自己拦截的push
等操作
可以看到直接把整一个数组的操作都给重写了
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { TriggerOpTypes } from '../../v3'
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ARRAY_MUTATION,
target: this,
key: method
})
} else {
ob.dep.notify()
}
return result
})
})
而对于数组中的数据,则是通过一次遍历,来对数组中的数据进行遍历实现深层拦截,核心方法是observeArray
数据拦截处理完后,要实现能够通过this.msg
访问data.msg
,所以在initData
中有这个操作
这里只需要对第一层进行proxy
即可
这里的Object.defineProperty
对vm
进行proxy
,这样就实现了vm.msg
或this.msg
时,获取到的实际上是this._data.msg
注意_data
早就在一开始就声明挂载好了
模板编译
初始化数据完成后,会调用$mount
进行模板编译
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
__DEV__ &&
warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (__DEV__ && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (__DEV__) {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// @ts-expect-error
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: __DEV__,
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
首先会读取el
属性,获取到对应要挂载的节点,然后使用template
进行模板解析
可以看到解析的优先级为
render > template > el(getOutterHTML)
,如果有render,就不会进行模板解析(因为render方法执行就可以获取到VNode了)
进入compileToFunctions
,跳了比较多的层,但核心是parse
和generate
方法,里面的核心是parseHTML
方法
parse
方法就是对template
进行解析的核心方法,其中的
parseHTML
做的就是使用while
循环遍历模板代码,通过使用正则表达式来匹配各个标签、文本,最终转为AST抽象语法树
AST
语法树解析要点:
- 通过
textEnd
判断当前解析的是标签还是文本 textEnd === 0
代表为标签,textEnd > 0
调用parseXXXTag
解析出用对象表示的标签,比如
const match = {
tagName: 'div',
attrs: [],
start: xx,
end: xx
}
解析完成后会调用
advance
方法对while
循环的html
字符串进行裁剪
- 调用
createASTElement
重新构建一个AST
对象,对该对象设置attrs、tag
等信息后入栈(识别startTag
是入栈,识别endTag
是出栈) - 最终可以获取到一个根节点,里面的
children
数组记录了HTML树节点的父子关系
生成render函数
获取到上一步的AST
对象(一个根节点)后,调用generate
方法将AST
拼接为字符串形式
进入到genElement
,分别调用genXXX
等处理各种不同的情况,最后一个else
处理元素或组件的情况
可以看到最后要生成的是以_c
方法调用的字符串形式,这里的data
、children
也是通过genData
、genChildren
等方法计算而来
而_c
对应的就是createElement
方法,即生成VNode
的方法
除了设置_c(xxx,xxx,xxx...)
的形式,还有_v
、_g
等方法,这些也都是用vm
上挂载的方法进行设置的
最终大概会获取到类似下面这样的结构:
获得到上面这种形式的字符串后,最终会走到createCompileToFunctionFn
方法中,在这个方法中就把render
函数字符串使用newFunction
变成了方法
上面得到的最终的字符串,是直接访问的
_c
,之所以可以这么使用,是因为code
里写的是with(this)
,这样在调用的时候,传入vm
,即可把作用域限制在vm
下
render函数调用
获取到render
函数,挂载到options
上,随后开始执行mount
渲染
这里的mount
对应初始挂载的mountComponent
方法,其中核心为_update
与_render
_render
实际上就是调用render
方法,同时把createElement
方法传给render
当作参数,这样就获取到了VNode
这里结合上面的with(this)
,就成功把作用域限制在了renderProxy
下,而renderProxy
实际上就是vm本身而已
生成虚拟DOM
调用render
函数,即执行了_c('div', xxx)
这样的语句,就等于调用了createElement
函数来创建VNode
Vnode
就是将传入的tag
、data
等放到VNode
类里,声明一堆变量而已
AST与VNode有什么不同?
- AST 是描述语法的, 他并没有用户自己的逻辑,只有语法解析出来的内容;并且AST还适用于跨平台的渲染和使用,比如用在
weex
里- Vnode 是描述dom结构的 ,可以自己去扩展,比如塞入一堆状态控制的变量
虚拟DOM变为真实DOM
调用_update
方法,就会用_render
生成的VNode
去执行patch
,patch
便是把虚拟dom
通过document
的原生API
转为真实DOM进行渲染的方法
而update
调用的时机,就是watch
监听到页面变化时进行调用
patch
生命周期合并与调用
一般声明生命周期的地方,除了组件内部,还有Vue.mixin({created()})
的方式进行注入,此时会对options
进行合并
在mergeOptions
方法中,会遍历两个要合并的options
的对象,对每个key
的对应值按照策略模式进行处理
这里所谓的策略模式,其实就是优化了if
的判断,将parent
和child
中相同key的值整合成数组
而对于data
或watch
的合并,也有对应的处理
同理的,在Vue
初始化时,也会调用mergeOptions
进行合并操作
生命周期的调用,则是通过callHooks
实现
callhooks
便是对所有的hook
进行遍历和调用
dep与watcher
通过发布-订阅模式来完成依赖收集
- dep:
data
上有多少响应式数据的属性,就有多少个dep
(对象里的每一个属性都有一个dep)(发布者) - watcher:视图上用了几处,就有多少
watcher
(订阅者) - dep与watcher是多对多关系
首先需要注意在数据初始化的时候就对data
里的数据完成了数据劫持,每个数据被劫持时,就会创建一个dep
实例,对于对象而言,则会进一步深层地进行数据劫持与创建dep
dep.depend
实际上就是给watcher
里绑上对应的dep
基本数据类型的依赖收集:
以页面初次渲染来举例,数据拦截完成后,到了渲染阶段时,会执行mountComponent
时,执行时本身就会创建一个watcher
而走new Watcher
构造函数时,就会自调用一次get
方法进行计算,get
里执行的getter
就是传入的updateComponent
方法,updateComponent
实际上就是跑_s
、_c
等方法来把虚拟DOM变成真实DOM渲染到页面上
当执行get
方法时,首先要执行pushTarget
方法将这个watcher
实例暂存到Dep
的原型上
页面渲染,那么getter
执行的方法就是updateComponent
,也就是把虚拟DOM渲染成真实DOM
假设渲染的模板字符串为带有{{ msg }}
,这就意味着要访问data
上的msg
属性,这样就会触发msg
的get
拦截,在get
拦截中会调用它的dep
的depend
方法来收集依赖
depend
方法调用了watcher
实例的addDep
,在watcher
的addDep
方法里就完成了 “dep
挂载到watcher
实例上” 的操作与 “watcher
绑定到dep
中” 的操作
执行完对每个属性的依赖收集后,调用popTarget
将当前绑定的dep.target
给pop
出去
这样就完成了对msg
属性的依赖收集与页面渲染,页面会自动用msg
对应的值替换模板字符串{{msg}}
这里主要是通过
_xx
的方法来实现的,至于是哪个,不确定
对象的依赖收集
对于对象而言,比如{{ info.name }}}
实际上就是对info
里的name
和其他属性都进行一次observe
的递归
可以想到,他既对info
对象的每个属性进行了深层拦截,也在触发info
对象的get
时,同时将对象内的各个属性作依赖收集(childOb.dep.depend()
)
这里的childOb.dep
,就来源于对Observer
构造时声明的一个dep
实例
数组的依赖收集
observe
一个数组时,实际上就是对数组push
等操作方法进行拦截,并且这里把__ob__
绑定到了这个数组的原型上,值为该Observer
实例
observeArray
就是对数组里的每个值进行observe
那么可以想到,对于数组内的基本数据类型,就会不符合条件从而不作任何的拦截与监听;而如果是[{a: 1}]
或[[a: 1]]
才会对应的进行数据拦截
这也是为什么调用arr[1] = xx不会触发vue的监听的原因
对于下面这个例子:
data () {
return {
info: [1, 2, {a: 1}]
}
}
首先info
这个数组本身会被作数据拦截,设置info = [3,4,5]
时是响应式的,然后对数组内的数据进行监听,获取到内部数据的Observer
,也就是childOb
这样在读取info
时,除了收集info
的依赖,也会同时收集数组值的依赖,调用对应的depend
方法
调用了数组的push
等方法时(等同于对象中属性的set
),就是靠__ob__
去调用dep.notify
来通知所有的watcher
进行更新的
这也解释了为什么Observer
本身要声明一个this.dep = new Dep
异步队列处理与nextTick
发布订阅模式是只要发现修改,就进行更新,所以为了节省计算性能,应该用一个机制来处理多次修改的情况,让更新只更新一次
data.push(1)
data.push(2)
data.push(3)
data.push(4)
上面这段,如果不处理,那么会更新4次,即调用watcher
里的getter
4次
当修改数据触发set
或者调用数组的push
等方法时,便会触发dep.notify
,通知所有watcher
进行更新
在Watcher
的update
方法里,正常情况下会执行queueWatcher
queueWatcher
实际上是将watcher
放入一个队列中,以id
区分,并在最后通过nextTick
来执行队列中的所有watcher
,watcher
调用的是run
方法
先看run
方法,暂时只关注第一句,执行的就是getter
而已,即触发视图的计算,下面的是watch
对应的回调
由于watcher
已经在queueWatcher
中去重了,所以每次run
都是特定的唯一watcher
进行run
,即更新视图
flushSchedulerQueue
是放到nextTick
中执行的,nextTick
通过异步的方式来对传入的callback
进行调用
timerFunc
是提前实现的,根据浏览器环境的支持与否,使用Promise > MutationObserver > setImmediate > setTimeout
来实现异步调用callbacks
举个实际的例子结合事件循环机制来讲:
// 代码操作
methods: {
someMethod () {
this.arr.push(1)
this.arr.push(2)
this.arr.push(3)
this.arr.push(4)
this.$nextTick(() => {
console.log(this.$refs.someRef.innerHTML)
})
}
}
// 页面
<div>{{ arr }}</div>
<span>{{ arr }}</span>
比如对于上面这段代码,页面渲染之后有一个watcher
,他的getter
就是把div
和span
渲染出来;
执行this.arr.push
4次,那么就调用了4次dep.notify
,等价于调用4次这个watcher
的update
方法,也等于调用4次queueWatcher
,而watcher
的队列有去重处理,所以队列中只有一个这个watcher
第一次进入queueWatcher
,waiting
是false
,调用一次nextTick(flushSchedulerQueue)
,随后几次调用都不会进来,事件循环机制此时是主脚本(代码)执行阶段
nextTick
里执行了timerFunc
,所以实际上的队列应该是
push x4 -> queueWatcher -> nextTick -> timerFunc -> queueWatcher(退出) -> queueWatcher(退出) -> queueWatcher(退出)
跑完上面这一段后,退出脚本执行,进入下一个事件循环
那么进入到下一个事件循环时,就会执行timerFunc
里调用的微任务,也就是调用所有的callback
,也就是实际地执行flushSchedulerQueue
而flushSchedulerQueue
就是调用watcher
队列的run
来触发视图更新,这样就确保了只更新一次视图,大大节省了性能
另外,用户调用this.$nextTick
时,实际上也是调用nextTick
并把回调放到nextTick
的执行栈中处理,等到下一个事件循环中再触发更新
明确了flushSchedulerQueue
是处理watcher
的更新(遍历调用watcher
的run
方法)后,可以看到在这个方法中有声明周期的队列调用,即抛出activated
钩子和updated
钩子
watch源码
用户使用watch
时,可以传多种形式,比如:
watch: {
a () {}
// a: 'handleWatch'
// a: [handler1, handler2]
// a: {
// handler () {},
// immediate: true
// }
}
所以第一步是对同一属性的所有watch
进行统一,调用的是initWatch
方法
处理时,调用createWatcher
进行处理,当handler
传的是字符串时,实际上就是去Vue
实例上获取对应的methods
,然后调用$watch
来实现
$watch
首先判断handler
是否为对象,这种情况一般在用户手动调用this.$watch
时才出现
然后创建一个Watcher
实例,注意它将options.user
设置为了true
,这是用来区分用户写的watch
和渲染页面时使用的watcher
的
进入到Watcher
的构造函数,可以看到如果expOrFn
传字符串时,比如a: 'handleWatch'
的情况,那么就会去Vue
实例上找到这个方法来设置Watcher
实例里的getter
,以便后续调用
进到parsePath
里可以看到,实际上就是去vm
上不断地查找对应的属性值,比如c.c.c
,实际上就是vm['c'] -> vm['c']['c']
同时因为Watcher
本身在声明时会自调用一次,这样就会获取到一个初始值并保存在this.value
中
那么可以想到,当属性值被修改时,触发该值的dep.notify
,然后所有的Watcher
队列被拿出来执行,走run
方法,在里面会先获取最新的值,然后拿来做对比,判断是否需要进行更新
当判断前后两次获取的值不同时,就调用了用户的回调,这样就实现了watch