平常的工作就是以vue2.x进行开发,因为我是个实用主义者,以前我就一直觉得,你既然选择了这个框架开发你首先就要先弄懂这玩意怎么用,也就是先熟悉vue语法和各种api,而不是去纠结实现它的原理是什么。甚至我可以这么说,你没有看过源码,只通过官方文档也能用这个框架解决绝大部分业务需要,解决大部分bug,而且大部分情况下,别人是不会管你知不知道原理的。但我不是说阅读源码不好,至少在解决另一小部分bug的时候会让你少走很多弯路,知道为什么会导致这样的bug,还有一点,至少在面试的时候还是很有用的,手动狗头。
先放上vue2.x版本官方文档:https://v2.cn.vuejs.org/v2/guide/instance.html,然后gayhub上的vue2.x源码地址:https://github.com/vuejs/vue/tree/v2.7.10,由于vue2.x还在迭代更新中,目前最新tag是v2.7.10,所以我们这次分析此分支下的代码。本次分析的代码主要在src/core下,建议谷歌浏览器安装Octo tree插件,一款在线以树形格式展示github项目代码结构的插件(如图左侧),效果真的很棒。
- 实例挂载
大家都会在入口文件main.js写上
let app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
其实这块代码套用官方的话呢,就是通过vue函数,给你创建一个vue实例。关于vue这个对象,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/instance/index.ts#L9
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the new
keyword')
}
this._init(options)
}
是不是感觉特别短,它只是说明了vue这个函数,必须要通过new关键字来进行初始化,而且,重头戏在this._init(options)这行代码里,这里调用的_init方法源码是定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/instance/init.ts#L16中的initMixin(),但重点是下面这些代码(L38~L66):
// merge options 合并配置
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options as any)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
if (__DEV__) {
initProxy(vm) // 初始化代理属性
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件中心
initRender(vm) // 初始化渲染
callHook(vm, 'beforeCreate', undefined, false /* setContext */) // 初始化beforeCreate钩子
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化props、methods、data、computed、watch
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 初始化created钩子
2.双向绑定
实现vue双向绑定的3个核心类:observe类,dep类和watcher类,在src/core/observer文件夹下,分别对应index.ts文件、dep.ts文件、watcher.ts文件,首先,我们先看index.ts中对observe类的定义,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/index.ts#L49
export class Observer {
dep: Dep
vmCount: number // number of vms that have this object as root $data
constructor(public value: any, public shallow = false, public mock = false) {
// this.value = value
this.dep = mock ? mockDep : new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (isArray(value)) {
if (!mock) {
if (hasProto) {
/* eslint-disable no-proto */
;(value as any).__proto__ = arrayMethods
/* eslint-enable no-proto */
} else {
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(value, key, arrayMethods[key])
}
}
}
if (!shallow) {
this.observeArray(value)
}
} else {
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock)
}
}
}
observe类会在vue实例被创建的时候,去遍历data里的每一个属性,先调用Array.isArray()判断是不是数组。第一个考点就来了, vue是怎么监控数组的?可以看到,如果属性是数组,就会直接将arrayMethods直接赋值给监控数组的_proto_上以达到重写数组方法的目的,所以实际上我们调用的这几个数组方法已经是经过mutator()重写过了的(所以官方称这些为数组变更方法),在这里重写数组方法的好处是只对想要监控的数组生效,不用担心会污染到全局的Array方法。还有一点,虽然现在的浏览器基本都支持这种非标准属性(proto)的写法,因为这种写法本身就是早期浏览器自身厂商对原型属性规范的实现,但是为了以防有些浏览器不支持,源码这里还是对浏览器做了兼容,如果不支持,就将这些变异方法一个个绑定到监控的数组上。
arrayMethods定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/array.ts,这里写了一个拦截器methodsToPatch用来拦截数组原有的7个方法并进行重写,这就是为什么vue只能通过变异方法来改变data里的数组,而不能使用array[0]=newValue的原因。官网文档说是由于 JavaScript 的限制,Vue 不能检测数组和对象的变化 。其实就是因为defineProperty方法只能监控对象,不能监控数组。
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
})
})
继续接上上面的observe类源码说,如果是属性是对象的话,则会对对象的每一个属性调用defineReactive()。源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/index.ts#L131。其重点是下面这些代码(L157~L213)。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
if (childOb) {
childOb.dep.depend()
if (isArray(value)) {
dependArray(value)
}
}
}
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
val = newVal
}
childOb = !shallow && observe(newVal, false, mock)
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify()
}
}
})
这就是双向绑定最核心的部分了,利用object.defineProperty()给每个属性添加getter和setter。getter里主要是调用了Dep类的depend(),Dep类的源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/dep.ts#L21,depend()主要是调用了Dep.target.addDep(),可以看到Dep类下有个静态类型target,它就是一个DepTarget,这个DepTarget接口是定义在#L10,而Watcher类则是对DepTarget接口的实现,所以addDep()的定义需要在Watcher类中去寻找,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/watcher.ts#L160,它又调用回dep.addSub(),其作用是将与当前属性相关的watcher实例之间的依赖关系存进一个叫subs的数组里,这个过程就是 依赖收集。那么问题来了:为什么这里要调过来调过去,直接调用不行么,这也是考点之一, vue的双向绑定采用的是什么设计模式?看了这段代码,你就知道了,它采用的是发布者-订阅者模式,而不是观察者模式,因为Dep类就充当了发布者订阅者中的一个消息中转站,就是所谓的调度中心,这样发布者和订阅者就不受对方干扰,实现解耦。
然后setter里主要是调用了dep.notify(),notify()源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/dep.ts#L51,其作用是遍历subs数组,然后通知到与当前属性相关的每个watcher实例,调用watcher.update()触发视图更新,这个过程叫做 派发更新。
export default class Dep {
static target?: DepTarget | null
id: number
subs: Array
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: DepTarget) {
this.subs.push(sub)
}
removeSub(sub: DepTarget) {
remove(this.subs, sub)
}
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this)
if (__DEV__ && info && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
...info
})
}
}
}
notify(info?: DebuggerEventExtraInfo) {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (__DEV__ && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
if (__DEV__ && info) {
const sub = subs[i]
sub.onTrigger &&
sub.onTrigger({
effect: subs[i],
...info
})
}
subs[i].update()
}
}
}
然后,我们再看看Watcher类,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/watcher.ts。主要看下列代码,在#L196。
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
前面说到,派发更新会触发相关watcher实例的update(),而update()主要是执行了queueWatcher(),这个queueWatcher()定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/scheduler.ts#L166,代码如下,主要是起到了对watcher实例去重,然后会在flushSchedulerQueue队列中进行排序,并一个个调用了队列中的watcher .run (),最后用nextTick去异步执行flushSchedulerQueue使视图产生更新。
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
if (has[id] != null) {
return
}
if (watcher === Dep.target && watcher.noRecurse) {
return
}
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (__DEV__ && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
这里可以看下wacther.run()的代码,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/watcher.ts#L211,其重点是它调用了Watcher类自身的get(),本质是调用data中的get() ,其作用是开启新一轮的依赖收集。
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e: any) {
if (this.user) {
handleError(e, vm, getter for watcher "${this.expression}"
)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
3.diff算法
vue更新节点并不是直接暴力一个个节点全部更新,而是对新旧节点进行比较,然后进行按需更新:创建新增的节点,删除废除不用的节点,然后对有差异的节点进行修改或移动。diff算法主要是靠patch()实现的,主要调用的是patchVnode()和updateChildren()这两个方法,源码分别定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/vdom/patch.ts#L584和#L413,前者的作用是先对比了新老节点,然后对一些异步占位符节点(#603的oldVnode .isAsyncPlaceholder,这个属性在vnode.ts中没有注释,vnode源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/vdom/vnode.ts#L8,应该是可以理解为异步组件的占位符)或是静态节点(#617的vnode .isStatic)且含有一样key值的节点且是【克隆节点(#620的vnode .isCloned)或v-once指令绑定的节点(#620的vnode .isOnce,只渲染一次)】不予更新,以提升性能。不是文本节点(#638的vnode .text)的话,就需要对比新旧子节点,对新旧子节点进行按需更新:新子节点有旧子节点没有则新建addVnodes(),新子节点没有旧子节点有则删除removeVnodes(),其他的更新updateChildren()。如果节点是文本节点且文本不一样的,直接将旧节点的文本设置为新节点的文本。
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
而后者的作用是进行是定义了新旧子节点数组的头和尾,然后新旧子节点数组头尾交叉对比,它只在同层级进行比较,不会跨层级比较,这是有考量的,因为前端实际的操作中,很少会把dom元素移到其他层级去。比较完子节点之后,就开始递归调用patchVnode()更新子节点了,这里考点就来了: vue的diff算法是深度优先算法还是广度优先算法?从这个更新流程可以看出来,正常调用顺序是patch()->patchVnode()->updateChildren()->patchVnode()->updateChildren()->….这是深度优先算法,同层比较,深度优先。
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (__DEV__) {
checkDuplicateKeys(newCh)
}
while (oldStartIdx newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(
oldStartVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx))
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// New element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(
vnodeToMove,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
oldCh[idxInOld] = undefined
canMove &&
nodeOps.insertBefore(
parentElm,
vnodeToMove.elm,
oldStartVnode.elm
)
} else {
// same key but different element. treat as new element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(
parentElm,
refElm,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
上面updateChildren()源码其实还有一个考点: vue中key的作用,或是说v-for的时候为什么推荐写上key?在#495中,有一个createKeyToOldIdx(),这个方法是创建key=>index的map映射,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/vdom/patch.ts#L56,对于新节点,可以看到如果没有key值得话,它会通过findIdxInOld()遍历旧的节点,而有key值的话,它会直接从map结构中取到对应的节点数据,相对于遍历,map结构明显会更有效率。
对于core文件夹下的源码就分析到这里,完结。
最后说点题外话,这篇文章躺在我的随笔列表里好久了,其实一年半前就开始写这篇文章了,一直缝缝补补,还好现在是写完了。一是因为确实东西很多,不知道从何写起,原本我打算写的是src下所有的文件夹的主要源码分析,现在看来光是这src/core文件夹下的主要源码就花了这么长的时间,当然有一部分原因是我比较懒,至于其他文件夹下的源码,如果以后有时间可能会新开文章写;二是当时写的时候是以main分支源码为基础的写的,但是vue2.x还是有一直更新的,刚开始写的时候vue版本还在v2.6.10+,现在最新版本都到v2.7.10了,更没想到vue2.x也会投入ts的怀抱,这就导致了之前写的文章里的源码与所在链接和行数是不对应的,有种错乱的感觉,所以这次我将v2.7.10作为版本快照固定下来,在最新的tag上进行源码分析,放上对应的源码链接。三就是我前几个月不是有一段面试经历,加入了一点我面试中经常遇到和vue相关的问题,即考点,希望能帮助大家更好的理解源码在实战中的应用。
Original: https://www.cnblogs.com/jdWu-d/p/14558813.html
Author: 他好像一条狗啊
Title: vue2.x核心源码深入浅出,我还是去看源码了
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/801275/
转载文章受原作者版权保护。转载请注明原作者出处!