上一篇:Vue原理解析(五):完全搞懂虚拟Dom到真实Dom的生成过程html
vue
之因此能数据驱动视图发生变动的关键,就是依赖它的响应式系统了。响应式系统若是根据数据类型区分,对象和数组它们的实现会有所不一样;解释响应式原理,若是只是为了说明响应式原理而说,但不是从总体流程出发,不在vue
组件化的总体流程中找到响应式原理的位置,对深入理解响应式原理并不太好。接下来笔者会从总体流程出发,试着站在巨人的肩膀上分别说明对象和数组的实现原理。vue
对象响应式数据的建立
data
为例,会将传入的数据包装为响应式的数据。对象示例:
main.js
new Vue({ // 根组件
render: h => h(App)
})
---------------------------------------------------
app.vue
<template>
<div>{{info.name}}</div> // 只用了info.name属性
</template>
export default { // app组件
data() {
return {
info: {
name: 'cc',
sex: 'man' // 即便是响应式数据,没被使用就不会进行依赖收集
}
}
}
}
复制代码
接下来的分析将以上面代码为示例,这种结构实际上是一个嵌套组件,只不过根组件通常定义的参数比较少而已,理解这个仍是很重要的。面试
在组件new Vue()
后的执行vm._init()
初始化过程当中,当执行到initState(vm)
时就会对内部使用到的一些状态,如props
、data
、computed
、watch
、methods
分别进行初始化,再对data
进行初始化的最后有这么一句:编程
function initData(vm) { //初始化data
...
observe(data) // info:{name:'cc',sex:'man'}
}
复制代码
这个observe
就是将用户定义的data
变成响应式的数据,接下来看下它的建立过程:数组
export function observe(value) {
if(!isObject(value)) { // 不是数组或对象,再见
return
}
return new Observer(value)
}
复制代码
简单理解这个observe
方法就是Observer
这个类的工厂方法,因此仍是要看下Observer
这个类的定义:bash
export class Observer {
constructor(value) {
this.value = value
this.walk(value) // 遍历value
}
walk(obj) {
const keys = Object.keys(obj)
for(let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // 只传入了两个参数
}
}
}
复制代码
当执行new Observer
时,首先将传入的对象挂载到当前this
下,而后遍历当前对象的每一项,执行defineReactive
这个方法,看下它的定义:app
export function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖管理器
val = obj[key] // 计算出对应key的值
observe(val) // 递归包装对象的嵌套属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
... 收集依赖
},
set(newVal) {
... 派发更新
}
})
}
复制代码
这个方法的做用就是使用Object.defineProperty
建立响应式数据。首先根据传入的obj
和key
计算出val
具体的值;若是val
仍是对象,那就使用observe
方法进行递归建立,在递归的过程当中使用Object.defineProperty
将对象的每个属性都变成响应式数据:函数
...
data() {
return {
info: {
name: 'cc',
sex: 'man'
}
}
}
这段代码就会有三个响应式数据:
info, info.name, info.sex
复制代码
知识点:
Object.defineProperty
内的get
方法,它的做用就是谁访问到当前key
的值就用defineReactive
内的dep
将它收集起来,也就是依赖收集的意思。set
方法的做用就是当前key
的值被赋值了,就通知dep
内收集到的依赖项,key
的值发生了变动,视图请变动吧~工具
这个时候get
和set
只是定义了,并不会触发。什么是依赖咱们接下来讲明,首先仍是用一张图帮你们理清响应式数据的建立过程:oop
依赖收集
什么是依赖了?咱们看下以前mountComponent
的定义:
function mountComponent(vm, el) {
...
const updateComponent = function() {
vm._update(vm._render())
}
new Watcher(vm, updateComponent, noop, { // 渲染watcher
...
}, true) // true为标志,表示是不是渲染watcher
...
}
复制代码
咱们首先说明下这个Watcher
类,它相似与以前的VNode
类,根据传入的参数不一样,能够分别实例化出三种不一样的Watcher
实例,它们分别是用户watcher
,计算watcher
以及渲染watcher
:
用户
(user) watcher
new Vue({
data {
msg: 'hello Vue!'
}
created() {
this.$watch('msg', cb()) // 定义用户watcher
},
watch: {
msg() {...} // 定义用户watcher
}
})
复制代码
这里的两种方式内部都是使用Watcher
这个类实例化的,只是参数不一样,具体实现咱们以后章节说明,这里你们只用知道这个是用户watcher
便可。
计算
(computed) watcher
new Vue({
data: {
msg: 'hello'
},
computed() {
sayHi() { // 计算watcher
return this.msg + 'vue!'
}
}
})
复制代码
渲染
(render) watcher
Watcher
实例,再组件执行vm.$mount
的最后会实例化Watcher
类,这个时候就是以渲染watcher
的格式定义的,收集的就是当前渲染watcher
的实例,咱们来看下它内部是如何定义的:class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
if(isRenderWatcher) { // 是不是渲染watcher
vm._watcher = this // 当前组件下挂载vm._watcher属性
}
vm._watchers.push(this) //vm._watchers是以前初始化initState时定义的[]
this.before = options.before // 渲染watcher特有属性
this.getter = expOrFn // 第二个参数
this.get() // 实例化就会执行this.get()方法
}
get() {
pushTarget(this) // 添加
...
this.getter.call(this.vm, this.vm) // 执行vm._update(vm._render())
...
popTarget() // 移除
}
addDep(dep) {
...
dep.addSub(this) // 将当前watcher收集到dep实例中
}
}
复制代码
当执行new Watcher
的时候内部会挂载一些属性,而后执行this.get()
这个方法,首先会执行一个全局的方法pushTarget(this)
,传入当前watcher
的实例,咱们看下这个方法定义的地方:
Dep.target = null
const targetStack = [] // 组件从父到子对应的watcher实例集合
export function pushTarget (_target) { // 添加
if (Dep.target) {
targetStack.push(Dep.target) // 添加到集合内
}
Dep.target = _target // 当前的watcher实例
}
export function popTarget() { // 移除
targetStack.pop() // 移除数组最后一项
Dep.target = targetStack[targetStack.length - 1] // 赋值为数组最后一项
}
复制代码
首先会定义一个Dep
类的静态属性Dep.target
为null
,这是一个全局会用到的属性,保存的是当前组件对应渲染watcher
的实例;targetStack
内存储的是再执行组件化的过程当中每一个组件对应的渲染watcher
实例集合,使用的是一个先进后出的形式来管理数组的数据,这里可能有点不太好懂,稍等再看到最后的流程图后天然就明白了;而后将传入的watcher
实例赋值给全局属性Dep.target
,再以后的依赖收集过程当中就是收集的它。
watcher
的get
这个方法而后会执行getter
这个方法,它是new Watcher
时传入的第二个参数,这个参数就是以前的updateComponent
变量:
function mountComponent(vm, el) {
...
const updateComponent = function() { //第二个参数
vm._update(vm._render())
}
...
}
复制代码
只要一执行就会执行当前组件实例上的vm._update(vm._render())
将render
函数转为VNode
,这个时候若是render
函数内有使用到data
中已经转为了响应式的数据,就会触发get
方法进行依赖的收集,补全以前依赖收集的逻辑:
export function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖管理器
val = obj[key] // 计算出对应key的值
observe(val) // 递归的转化对象的嵌套属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() { // 触发依赖收集
if(Dep.target) { // 以前赋值的当前watcher实例
dep.depend() // 收集起来,放入到上面的dep依赖管理器内
...
}
return val
},
set(newVal) {
... 派发更新
}
})
}
复制代码
这个时候咱们知道watcher
是个什么东西了,简单理解就是数据和组件之间一个通讯工具的封装,当某个数据被组件读取时,就将依赖数据的组件使用Dep
这个类给收集起来。
当前例子data
内的属性是只有一个渲染watcher
的,由于没有被其余组件所使用。但若是该属性被其余组件使用到,也会将使用它的组件收集起来,例如做为了props
传递给了子组件,再dep
的数组内就会存在多个渲染watcher
。咱们来看下Dep
类这个依赖管理器的定义:
let uid = 0
export default class Dep {
constructor() {
this.id = uid++
this.subs = [] // 对象某个key的依赖集合
}
addSub(sub) { // 添加watcher实例到数组内
this.subs.push(sub)
}
depend() {
if(Dep.target) { // 已经被赋值为了watcher的实例
Dep.target.addDep(this) // 执行watcher的addDep方法
}
}
}
----------------------------------------------------------
class Watcher{
...
addDep(dep) { // 将当前watcher实例添加到dep内
...
dep.addSub(this) // 执行dep的addSub方法
}
}
复制代码
这个Dep
类的做用就是管理属性对应的watcher
,如添加/删除/通知。至此,依赖收集的过程算是完成了,仍是以一张图片加深对过程的理解:
派发更新
若是只是收集依赖,那实际上是没任何意义的,将收集到的依赖在数据发生变化时通知到并引发视图变化,这样才有意义。如如今咱们对数据从新赋值:
app.vue
export default { // app组件
...
methods: {
changeInfo() {
this.info.name = 'ww';
}
}
}
复制代码
这个时候就会触发建立响应式数据时的set
方法了,咱们再补全那里的逻辑:
export function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖管理器
val = obj[key] // 计算出对应key的值
observe(val) // 递归转化对象的嵌套属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
... 依赖收集
},
set(newVal) { // 派发更新
if(newVal === val) { // 相同
return
}
val = newVal // 赋值
observer(newVal) // 若是新值是对象也递归包装
dep.notify() // 通知更新
}
})
}
复制代码
当赋值触发set
时,首先会检测新值和旧值,不能相同;而后将新值赋值给旧值;若是新值是对象则将它变成响应式的;最后让对应属性的依赖管理器使用dep.notify
发出更新视图的通知。咱们看下它的实现:
let uid = 0
class Dep{
constructor() {
this.id = uid++
this.subs = []
}
notify() { // 通知
const subs = this.subs.slice()
for(let i = 0, i < subs.length; i++) {
subs[i].update() // 挨个触发watcher的update方法
}
}
}
复制代码
这里作的事情只有一件,将收集起来的watcher
挨个遍历触发update
方法:
class Watcher{
...
update() {
queueWatcher(this)
}
}
---------------------------------------------------------
const queue = []
let has = {}
function queueWatcher(watcher) {
const id = watcher.id
if(has[id] == null) { // 若是某个watcher没有被推入队列
...
has[id] = true // 已经推入
queue.push(watcher) // 推入到队列
}
...
nextTick(flushSchedulerQueue) // 下一个tick更新
}
复制代码
执行update
方法时将当前watcher
实例传入到定义的queueWatcher
方法内,这个方法的做用是把将要执行更新的watcher
收集到一个队列queue
以内,保证若是同一个watcher
内触发了屡次更新,只会更新一次对应的watcher
,咱们举两个小示例:
export default {
data() {
return { // 都被模板引用了
num: 0,
name: 'cc',
sex: 'man'
}
},
methods: {
changeNum() { // 赋值100次
for(let i = 0; i < 100; i++) {
this.num++
}
},
changeInfo() { // 一次赋值多个属性的值
this.name = 'ww'
this.sex = 'woman'
}
}
}
复制代码
这里的三个响应式属性它们收集都是同一个渲染watcher
。因此当赋值100次的状况出现时,再将当前的渲染watcher
推入到的队列以后,以后赋值触发的set
队列内并不会添加任何渲染watcher
;当同时赋值多个属性时也是,由于它们收集的都是同一个渲染watcher
,因此推入到队列一次以后就不会添加了。
知识点:
vue
仍是挺聪明的,经过这两个实例你们也看出来了,派发更新通知的粒度是组件级别,至于组件内是哪一个属性赋值了,派发更新并不关心,并且怎么高效更新这个视图,那是以后diff
比对作的事情。
队列有了,执行nextTick(flushSchedulerQueue)
再下一次tick
时更新它,这里的nextTick
就是咱们常用的this.$nextTick
方法的原始方法,它们做用一致,实现原理以后章节说明。看下参数flushSchedulerQueue
是个啥?
let index = 0
function flushSchedulerQueue() {
let watcher, id
queue.sort((a, b) => a.id - b.id) // watcher 排序
for(index = 0; index < queue.length; index++) { // 遍历队列
watcher = queue[index]
if(watcher.before) { // 渲染watcher独有属性
watcher.before() // 触发 beforeUpdate 钩子
}
id = watcher.id
has[id] = null
watcher.run() // 真正的更新方法
...
}
}
复制代码
原来是个函数,再nextTick
方法的内部会执行第一个参数。首先会将queue
这个队列进行一次排序,依据是每次new Watcher
生成的id
,以从小到大的顺序。当前示例只是作渲染,并且队列内只存在了一个渲染watcher
,因此是不存在顺序的。可是若是有定义user watcher
和computed watcher
加上render watcher
后,它们之间就会存在一个执行顺序的问题了。
知识点:
watcher
的执行顺序是先父后子,而后是从computed watcher
到user watcher
最后render watcher
,这从它们的初始化顺序就能看出。
而后就是遍历这个队列,由于是渲染watcher
,全部是有before
属性的,执行传入的before
方法触发beforeUpdate
钩子。最后执行watcher.run()
方法,执行真正的派发更新方法。咱们去看下run
干了啥:
class Watcher {
...
run () {
if (this.active) {
this.getAndInvoke(this.cb) // 有一种要抓狂的感受
}
}
getAndInvoke(cb) { // 渲染watcher的cb为noop空函数
const value = this.get()
... 后面是用户watcher逻辑
}
}
复制代码
执行run
就是执行getAndInvoke
方法,由于是渲染watcher
,参数cb
是noop
空函数。看了这么多,其实...就是从新执行一次this.get()
方法,让vm._update(vm._render())
再走一遍而已。而后生成新旧VNode
,最后进行diff
比对以更新视图。
最后咱们来讲下vue
基于Object.defineProperty
响应式系统的一些不足。如只能监听到数据的变化,因此有时data
中要定义一堆的初始值,由于加入了响应式系统后才能被感知到;还有就是常规JavaScript
操做对象的方式,并不能监听到增长以及删除,例如:
export default {
data() {
return {
info: {
name: 'cc'
}
}
},
methods: {
addInfo() { // 增长属性
this.info.sex = 'man'
},
delInfo() { // 删除属性
delete info.name
}
}
}
复制代码
数据是被赋值了,可是视图并不会发生变动。vue
为了解决这个问题,提供了两个API
:$set
和$delete
,它们又是怎么办到的了?原理以后章节分析。
最后惯例的面试问答就扯扯最近工做中遇到趣事吧。对于一个数据不会变动的列表,笔者把它定义再了created
钩子内,不多结对编程,此次例外。
created() {
this.list = [...]
}
复制代码
旁边的妹子接事后:
妹子: 这个列表怎么data里没有阿?在哪定义的?
我:我定义在created钩子里了。
妹子:你怎么定义在这了?
我:由于它是不会被变动的,因此不须要... 算了,那你移到data里吧。
妹子:嗯!? 好。 小声说道:我仍是第一次看见这么写的。
我:...有种被嫌弃了的感受
复制代码
面试官微笑而又不失礼貌的问道:
data
里么?怼回去:
data
中的变量都会被代理到当前this
下,因此咱们也能够在this
下挂载属性,只要不重名便可。并且定义在data
中的变量在vue
的内部会将它包装成响应式的数据,让它拥有变动便可驱动视图变化的能力。可是若是这个数据不须要驱动视图,定义在created
或mounted
钩子内也是能够的,由于不会执行响应式的包装方法,对性能也是一种提高。顺手点个赞或关注呗,找起来也方便~