以下文章转载或借鉴vue源码社区,仅供个人学习使用,如有文章涉及侵权及其他问题,请及时联系作者修改或删除,感谢!!!
vue源码中文社区:https://vue-js.com/learn-vue/
一、object变化侦测篇
1.通过Object.defineProperty()改造属性
let car = {}
let val = 3000
Object.defineProperty(car, 'price', {
enumerable: true,
configurable: true,
get(){
console.log('price属性被读取了')
return val
},
set(newVal){
console.log('price属性被修改了')
val = newVal
}
})
源码位置:src/core/observer/index.js
/**
1. Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
// 实例化一个依赖管理器
this.dep = new Dep()
this.vmCount = 0
// 给value新增一个__ob__属性,值为该value的Observer实例
// 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
def(value, '__ob__', this)
// 当value为数组时
if (Array.isArray(value)) {
// hasProto = '__proto__' in {} 判断hasProto是否可用,有 些浏览器不支持该属性
//if (hasProto) {
// const arrayProto = Array.prototype
// export const arrayMethods = Object.create(arrayProto)
// function protoAugment (target, src: Object) {
// target.__proto__ = src
// }
// 覆盖__proto__,拦截器挂载
protoAugment(value, arrayMethods)
} else {
// 获取arrayMethods的所有属性
// const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
// function copyAugment (target: Object, src: Object, keys: Array<string>) {
// for (let i = 0, l = keys.length; i < l; i++) {
// const key = keys[i]
// 间接挂载
// def(target, key, src[key])
// }
// }
copyAugment(value, arrayMethods, arrayKeys)
}
// 把数组中的每一项转换为可观测属性
this.observeArray(value)
} else {
// 把对象的每一项转换为可观测属性
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
/**
2. 使一个对象转化成可观测对象
3. @param { Object } obj 对象
4. @param { String } key 对象的key
5. @param { Any } val 对象的某个key的值
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 生成依赖管理器
const dep = new Dep()
// 定义获取指定对象的自身属性描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
const getter = property && property.get
const setter = property && property.set
// 如果只传了obj和key,那么val = obj[key]
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 依赖收集
dep.depend()
if (childOb) {
childOb.dep.depend()
// 如果为数组递归遍历收集依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 依赖更新
dep.notify()
}
})
}
小结:定义observer类,它用来将一个正常的object转换成可观测的object。并且给value新增一个__ob__属性,值为该value的Observer实例。这个操作相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作。
只有object类型的数据才会调用walk将每一个属性转换成getter/setter的形式来侦测变化。 最后,在defineReactive中当传入的属性值还是一个object时使用new observer(val)来递归子属性,这样我们就可以把obj中的所有属性(包括子属性)都转换成getter/seter的形式来侦测变化。 也就是说,只要我们将一个object传到observer中,那么这个object就会变成可观测的、响应式的object。
2.依赖收集(依赖管理器)
在getter中收集依赖,在setter中通知依赖更新。
// 源码位置:src/core/observer/dep.js
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor() {
this.id = uid++;
this.subs = [];
}
addSub(sub: Watcher) {
this.subs.push(sub);
}
removeSub(sub: Watcher) {
remove(this.subs, sub);
}
// 收集依赖
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
// 通知所有依赖更新
notify() {
const subs = this.subs.slice();
if (process.env.NODE_ENV !== "production" && !config.async) {
subs.sort((a, b) => a.id - b.id);
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
3.Watcher 类
作用:在之后数据变化时,不直接去通知依赖更新,而是通知依赖对应的Watch实例,由Watcher实例去通知真正的视图,可以理解成watch类就代表这个依赖。
// watch类大概实现
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn)
this.value = this.get()
}
get () {
window.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
watch类解析:谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher实例,在创建Watcher实例的过程中会自动的把自己添加到这个数据对应的依赖管理器中,以后这个Watcher实例就代表这个依赖,当数据变化时,我们就通知Watcher实例,由Watcher实例再去通知真正的依赖。
当实例化Watcher类时,会先执行其构造函数;
1. 在构造函数中调用了this.get()实例方法;
2. 在get()方法中,首先通过window.target = this把实例自身赋给了全局的一个唯一对象window.target上,然后通过let value = this.getter.call(vm, vm)获取一下被依赖的数据,获取被依赖数据的目的是触发该数据上面的getter,上文我们说过,在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中,在get()方法最后将window.target释放掉。
3. 而当数据变化时,会触发数据的setter,在setter中调用了dep.notify()方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖的update()方法,也就是Watcher类中的update()实例方法,在update()方法中调用数据变化的更新回调函数,从而更新视图。
总结:Watcher先把自己设置到全局唯一的指定位置(window.target),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个watcher收集到Dep中去。收集好之后,当数据发生变化时,会向Dep中的每个Watcher发送通知。通过这样的方式,Watcher可以主动去订阅任意一个数据的变化。
object侦测总结:
1.Data通过observer转换成了getter/setter的形式来追踪变化。
当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
2.当数据发生了变化时,会触发setter,从而向Dep中的依赖(即Watcher)发送通知。
3.Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
二、Array变化侦测
1.Array型数据的set get
get
array不存在对象的Object.defineProperty方法,get操作是通过如下形式实现的:
data(){
return {
arr:[1,2,3]
}
}
每次获取数据的时候都是通过this.arr获取的,每当通过this.属性获取时,就会触发的array的get属性,收集依赖等。
set
let arr = [1,2,3]
arr.push(4)
Array.prototype.newPush = function(val){
console.log('arr被修改了')
this.push(val)
}
arr.newPush(4)
通过拟定array的push方法,达到可以监听改变的目的。
数组的setget方法如上生成。
array的方法拦截器
// 源码位置:/src/core/observer/array.js
const arrayProto = Array.prototype
// 创建一个新的数组,原型为Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 把methodsToPatch数组内的方法指向mutator方法
methodsToPatch.forEach(function (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)
// 通知更新
ob.dep.notify()
return result
})
})
// def 转化为defineProperty
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
解析: 当调用methodsToPatch数组中的方法时,实际都会指向mutator方法,在mutator方法中执行通知更新操作,而mutator函数内部执行了original函数,这个original函数就是Array.prototype上对应的原生方法。
拦截器挂载
Observer类如下,挂载数组拦截器
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
// target.__proto__ = src 直接挂载
protoAugment(value, arrayMethods)
} else {
// 把数组的方法全部挂到value上
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
function protoAugment (target, src: Object) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
解析: 首先判断当前浏览器是否支持__proto__ ,如果支持直接把__proto__ 重定向为arrayMethods对象,如果不支持,则是需要把拦截器中重写的7个方法循环加入到value上。
2.array依赖收集
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// 返回一个属性在此对象上的操作符
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// observe 判断val是否转换为可观测的属性,如果不是就转换,是就返回__ob__
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 待明确此处收集是什么内容
dep.depend()
if (childOb) {
// 依赖收集
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 在setter中通知依赖更新
dep.notify()
}
})
}
三、虚拟dom
1.虚拟DOM简介
所谓虚拟DOM,就是用一个JS对象来描述一个DOM节点,像如下示例:
<div class="a" id="b">我是内容</div>
{
tag:'div', // 元素标签
attrs:{ // 属性
class:'a',
id:'b'
},
text:'我是内容', // 文本内容
children:[] // 子元素
}
为什么要有虚拟DOM?
Vue是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的时候难免要操作DOM,而操作真实DOM又是非常耗费性能的。
通过JS的计算性能来换取操作DOM所消耗的性能
2.vnode类
// 源码位置:src/core/vdom/vnode.js
export default class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag // 当前节点的标签名
this.data = data // 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息
this.children = children // 当前节点的子节点,是一个数组
this.text = text // 当前节点的子节点,是一个数组
this.elm = elm // 当前虚拟节点对应的真实dom节点
this.ns = undefined // 当前节点的名字空间
this.context = context // 当前组件节点对应的Vue实例
this.fnContext = undefined // 函数式组件对应的Vue实例
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key // 节点的key属性,被当作节点的标志,用以优化
this.componentOptions = componentOptions // 组件的option选项
this.componentInstance = undefined // 当前节点对应的组件的实例
this.parent = undefined // 当前节点的父节点
this.raw = false // 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false
this.isStatic = false // 静态节点标志
this.isRootInsert = true // 是否作为跟节点插入
this.isComment = false // 是否为注释节点
this.isCloned = false // 是否为克隆节点
this.isOnce = false // 是否有v-once指令
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
get child (): Component | void {
return this.componentInstance
}
}
3.VNode的类型
共有六种节点类型,区别:只是在实例化传入的参数不同
注释节点
文本节点
元素节点
组件节点
函数式组件节点
克隆节点
源码位置:src/core/vdom/vnode.js
- 注释节点
// 创建注释节点
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
// 具体的注释信息
node.text = text
// isComment 用来标识一个节点是否是注释节点
node.isComment = true
return node
}
- 文本节点
// 创建文本节点
export function createTextVNode (val: string | number) {
// text属性,用来表示具体的文本信息
return new VNode(undefined, undefined, undefined, String(val))
}
- 元素节点
- 组件节点
组件节点除了有元素节点具有的属性之外,它还有两个特有的属性
componentOptions :组件的option选项,如组件的props等
componentInstance :当前组件节点对应的Vue实例 - 函数式组件节点
函数式组件节点相较于组件节点,它又有两个特有的属性:
fnContext:函数式组件对应的Vue实例
fnOptions: 组件的option选项 - 克隆节点
// 创建克隆节点
// 把一个已经存在的节点复制一份出来,它主要是为了做模板编译优化时使用
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children,
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isComment = vnode.isComment
cloned.fnContext = vnode.fnContext
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.asyncMeta = vnode.asyncMeta
cloned.isCloned = true
return cloned
}
vnode的作用
在视图渲染之前,把写好的template模板编译成vnode并缓存下来,等数据发生变化时,对比上一次的vnode,,找出差异,然后有差异的VNode对应的真实DOM节点就是需要重新渲染的节点,最后根据有差异的VNode创建出真实的DOM节点再插入到视图中,最终完成一次视图更新
4.DOM-Diff
4.1.patch过程
patch简介:在vue中,把DOM-Diff过程叫做patch过程,patch译为补丁的意思,即对旧的vnode进行修补。
绑定旧的vnode之后,更新之后只会通过对比更新旧的vnode,不会直接用新的Vnode直接替换旧的Vnode
patch需要做的事情
- 创建节点:新的Vnode中有而旧的oldVNode中没有,就在旧的中创建
- 删除节点:新的Vnode中没有而旧的oldVNode中用,在旧的中删除
- 更新节点:如果新的旧的都有。则以新的vnode中的为准,更新旧的oldVNode
4.2 创建节点
Vnode类可以创建6种类型的节点,只有元素节点、注释节点、文本节点可以被插入到DOM中
// 源码位置: /src/core/vdom/patch.js
function createElm (vnode, parentElm, refElm) {
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
vnode.elm = nodeOps.createElement(tag, vnode) // 创建元素节点
createChildren(vnode, children, insertedVnodeQueue) // 创建元素节点的子节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text) // 创建注释节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
} else {
vnode.elm = nodeOps.createTextNode(vnode.text) // 创建文本节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
}
}
- 判断是否为元素节点只需判断该VNode节点是否有tag标签即可。如果有tag属性即认为是元素节点,则调用createElement方法创建元素节点,通常元素节点还会有子节点,那就递归遍历创建所有子节点,将所有子节点创建好之后insert插入到当前元素节点里面,最后把当前元素节点插入到DOM中。
- 判断是否为注释节点,只需判断VNode的isComment属性是否为true即可,若为true则为注释节点,则调用createComment方法创建注释节点,再插入到DOM中。
- 如果既不是元素节点,也不是注释节点,那就认为是文本节点,则调用createTextNode方法创建文本节点,再插入到DOM中。
4.3 删除节点
如果某些节点再新的VNode中没有而在旧的oldVNode中有,那么就需要把这些节点从旧的oldVNode中删除。删除节点非常简单,只需在要删除节点的父元素上调用removeChild方法即可。源码如下:
function removeNode (el) {
const parent = nodeOps.parentNode(el) // 获取父节点
if (isDef(parent)) {
nodeOps.removeChild(parent, el) // 调用父节点的removeChild方法
}
}