打个广告🤭 上一篇 体验Vue3.0, 仿一个网易云音乐客户端
为什么要在这个时候写这样的一篇文章?
转眼2021年了,Vue3.0正式发布也已经过去了几个月。但框架仅仅做到会用是不够的,要深入去了解其相关原理,需要知道一下它与之前版本到底有啥区别,优势在哪里。所以今天就一起重新品2.x版本的响应式源码!
以下每个阶段可能有一些方法或者对象实例暂时无法理解的,没关系,当你看完文章完整的流程后,手敲一遍,就会恍然大悟的🤭
Observer、Dep、Watcher作用与关系
用过Vue的同学或多或少都知道尤大是使用数据劫持与订阅-发布模式来实现响应式的,其离不开以下三个对象。
Observer
Observer监听器是给需要响应式的对象进行数据劫持的,即添加getter
和setter
,同时也是Dep与Watcher对数据进行依赖收集与数据更新的中间站。其相关源码与解析如下:
// 在vue初始化的时候会调用
function initData(vm) {
let data = vm.$options.data
// 这里假设data就是个返回了对象的函数,实际这里做了很多判断
data = vm._data = data.call(vm, vm)
const keys = Object.keys(data);
// 将 _data 的数据代理到 this 上
for (let i = 0; i < keys.length; i++) {
proxy(vm, '_data', keys[i]);
}
// 正常做个会有while循环, 其实是后面获取了option中的props与methods 做了重名的判断,这里假设不会有这种情况
observe(data, true)
}
function observe(value, asRootData) {
// 如果不是个对象或者是个虚拟dom节点的 无需观测
if (!isObject(value) || value instanceof VNode) {
return
}
let obj
// 如果已经被监测了,则无需再实例化
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
obj = value.__ob__
} else {
obj = new Observer(value)
}
return obj
}
// 属性代理方法
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
class Observer {
constructor(value) {
this.value = value
// 这里的dep是给数组依赖收集的
this.dep = new Dep()
// 这里的def代理的__ob__很重要,后面数组就会用到这代理的observer对象
def(value, '__ob__', this)
// 如果值是数组的话需要额外处理,因为object.defineProperty 无法监听数组的任何改变数组长度的操作,以及原型上的方法。
// 这也是为什么尤大不对数组进行处理的原因,而是通过$set与代理变异方法来实现
if (Array.isArray(value)) {
// 代理数组的变异方法,重新赋值原型
value.__proto__ = arrayMethods
this.observeArray(value)
} else {
// 否则就遍历对象,为对象的属性进行劫持
this.walk(value)
}
}
// 监听对象
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 监听数组的每一项,即arr[i].xxxx = xxx的时候可以监听到,就是因为这里,但不会监听到arr[i] = xxx
observeArray(array) {
for (let i = 0; i < array.length; i++) {
observe(array[i])
}
}
}
根据注释解析我们大致可以了解到,observe
方法里面的value
应该就是我们的data
或者其子对象,然后通过实例化Obverver
,分别对值是对象与数组进行了相应的处理。
对象劫持
对象的话走walk
方法,遍历对象的key
,将每个key
与原obj
传入了defineReactive
方法,而这个方法就是调用了Object.defineProperty
。其方法分析如下:
/**
每个key对应一个dep
obj是我们需要劫持的对象,例如最开始的data,以及后面data里面的对象
val是手动set的时候传值,默认情况下是不需要的
**/
function defineReactive(obj, key, val) {
const dep = new Dep()
// 获取对象属性的描述对象
const property = Object.getOwnPropertyDescriptor(obj, key)
// 如果对象的值之前被设置过了,且设置为不可更改值,即无需监听
if (property && property.configurable === false) {
return
}
// 如果原本的对象的属性定义了对应的get与set,应该与其保持一致
const getter = property && property.get
const setter = property && property.set
// 非set的情况下,没有定义get,即这时候val会是undefined,需要赋予其原本的值
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 递归进行依赖收集
let ob = observe(val)
Object.defineProperty(obj, key, {
// 可遍历,可修改
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// 这里的Dep是依赖收集器,后面会介绍到,现在只要知道这里的 Dep.target 为一个Watcher实例
if (Dep.target) {
// 依赖收集
dep.depend()
// 如果val是对象或者是数组的时候,ob会是observe对象,否则就是undefined
// 如果是数组的话,通过ob.dep.depend来进行数组方法的依赖收集
if (ob) {
ob.dep.depend()
if (Array.isArray(value)) {
// 这里为什么需要判断其是否是数组呢,因为前面的ob.dep.depend只是对最外层的数组的方法进行的依赖收集
// 但是考虑到值如果也是数组的情况,即多维数组的依赖的收集
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value) return
// 如果对象属性是不能被设置的
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 新数据递归进行响应式处理
ob = observe(newVal)
// 通知数据更新
dep.notify()
}
})
}
// 多维数组变异方法的依赖收集
function dependArray(array) {
for (let i = 0; i < array.length; i++) {
const element = array[i];
// 这里的__obj__就是前面def发挥的作用
element && element.__ob__ && element.__ob__.dep.depend()
if (Array.isArray(element)) {
dependArray(element)
}
}
}
数组劫持
数组的话,情况比较特殊,不会像对象一样每一个值使用Object.defineProperty
进行处理,这也就是为什么this.a[0] = xxx
无效的原因。但对数组的每一个值都会去调用observe
方法对其进行数据监听,这就是为什么this.a[0].xxx = xxx
会有效的原因。而对数组的增删改查操作都是通过劫持其原型上的变异方法,调用方法进行依赖收集来实现的。其实现分析如下:
const arrayMethods = Object.create(Array.prototype)
// 数组的变异方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 这里是变异方法进行劫持
methodsToPatch.forEach((method) => {
// 数组的原方法
const original = arrayMethods[method]
def(arrayMethods, method, function (...args) {
const result = original.apply(this, args)
// 这里的ob 刚刚前面讲Observer的时候已经说过了,def将当前的obsever实例代理到value,而这对于数组来说,value就是数组本身
const ob = this.__ob__
// push/unshift/splice的值
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
})
})
小结
看到这里,抛去里面dep相关的代码,相信你应该对Observer如何对数据进行劫持有了一些了解。不知道你有没有发现,无论是数组还是对象,在Observer.observeArray
、defineReactive
方法里面都会对其子对象或者值调用observe
方法,而observe
方法又会对传入的值进行判断,是对象又会进行实例化一个Observer
实例。正是通过这种‘递归’方式,对我们传入的data
值及其子对象进行了数据监听!
希望你能继续看下去,当你看完Watcher
与Dep
相关源码,在回过头看这里,会有不一样的收获!
Dep
依赖收集器,负责收集响应式对象的依赖关系,从上面可以知道每个响应式对象包括子对象都会实例化一个Dep
实例,对对象来说是在defineReactive
中实例化,对数组来说,前面也说到,是对其变异方法进行劫持,其方法对应的dep
是在Observer
类里面实例化的,而每个Dep
实例里面subs
是Watcher
实例数组,当数据有变更时,会通过dep.notify
通知各个watcher
。
let depId = 0
class Dep {
constructor() {
this.id = depId++
this.subs = []
}
removeSub(sub) {
// 这里的sub是watcher
remove(this.subs, sub)
}
addSub(sub) {
// 这里的sub是watcher
this.subs.push(sub)
}
// watcher实例互相依赖收集
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// 更新通知
subs[i].update()
}
}
}
Dep.target = null
function remove(arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
Watcher
订阅者,或者称为观察者,负责我们的相关更新操作。有三类的Watcher,负责组件渲染的Watcher(renderWatcher)
、计算属性的Watcher(computedWatcher)
、watch属性的Watcher(userWatcher)
,当响应式对象进行更新操作的时候,会触发其setter
方法,进而调用其对应的依赖收集器实例的dep.notify
,调用收集的watcher数组的update
方法,进行对应的更新处理。
class Watcher {
dirty
getter
cb
value
// vm 为vue实例对象
// expOrFn为更新的方法, renderWatcher中为我们的updateComponent更新视图, userWatcher/computedWatcher为重新求值方法
// cb 为回调,主要在userWatcher中及我们的watch属性才需要用到
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
this.id = ++wId
this.cb = cb
// 记录上一次求值的依赖
this.deps = []
// 记录当前求值的依赖
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
// options lazy是computedWatcher的参数, user则是userWatcher的参数
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.dirty = this.lazy
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 如果是watch
this.getter = parsePath(expOrFn)
}
// renderWatcher、userWatcher new watcher的时候,get方法会调用
// 如果是computeWatcher this.lazy为true, get方法不会被调用
this.value = this.lazy ?
undefined :
this.get()
}
get() {
pushTarget(this)
let value
const vm = this.vm
try {
// 这里的getter 对于renderWatcher是updateComponent方法,在watch就是其对应的key, computed是对应的方法
value = this.getter.call(vm, vm)
} catch (error) {
} finally {
// 这个是为watch的deep服务的
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
// 将自己添加到dep里面
addDep(dep) {
const id = dep.id
// 这里两个dep作用是为了防止重复收集依赖
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
// 将现有的所有依赖加入
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
update() {
// 对于computed数据来说,初始化 computedWatcher lazy是为true,且一直是true
// 所以 computedWatcher 不会执行run ,而是依靠其计算方法中的data的属性值改变的时触发其computedWatcher,将dirty置为true。
// 在我们对计算属性进行取值操作的时候,会因为dirty为true,从而调用evaluate获得最新的值。(这是一个很巧妙的设计,等到用到的才会最终去计算取值)
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
// 重新计算值,计算属性才会调用到的方法
evaluate() {
// 对于计算属性来说,get方法会去调用其在vue实例声明的那个computed对象方法
// 例如 computed: { name(){ return this.first + thi.second } },这里的get就会去求name返回的这两个data对象的属性的值的计算结果。
// 在此之前,会将本身这个watcher给加到frist/second两个响应式数据的dep的subs数组里面,当这两个值发生改变的时候,也会触发computed属性发生改变。
// 并且,等会你会看到,更新操作里面会对watcher的id进行排序,因为computedWatcher会比renderWatcher(这个是在挂载的时候才实例化)先实例化,所以其值更改会在渲染之前
// 这也就是为什么页面看到的computed属性的值也是更新了的。
this.value = this.get()
this.dirty = false
}
run() {
const value = this.get()
if (value !== this.value || isObject(value) || this.deep) {
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
// 清除无用的依赖
cleanupDeps() {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
// 如果当前的新的依赖列表里面没有,旧的依赖有的情况,就需要解除依赖收集
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
// 当前的依赖列表作为上一次依赖列表,然后重置当前的依赖列表
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
}
const targetStack = []
// 依赖采集数组
// Dep.target都是一个Watcher
function pushTarget(target) {
targetStack.push(target)
Dep.target = target
}
function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
几个联系上文的点如下
-
依赖收集
前面一直在说的一个概念,到这里可以知道
dep
收集的就是watcher
,如何收集watcher
呢?,从Observer
分析可以知道对象在取值操作的时候会判断当前是否有Dep.target
,从上面Watcher
分析知道了这个要有值的情况是得Watcher
被实例化的时候即调用new Watcher
。而Wacther
在被实例化的时候,会根据watcher
的类型是否调用get
方法,get
方法则会将当前的watcher
实例赋值给Dep.target
,从而完成依赖的收集。接下来我们分析何时会进行Watcher
的实例化。 -
何时会调用new Watcher
- renderWatcher: 负责
data
数据到视图的更新的工作,需要做到每个数据发生改变的时候会更新试图,它必须是在数据与计算属性,以及watch的初始化之后,同时还得对data
的每个属性进行取值getter
操作才会触发依赖收集(渲染视图的时候就会进行取值操作),所以它实例化的地方就是在组件挂载的时候。
// 这里大致实现挂载与更新的方法,其实际远不止这些 const vm = new Vue({ data() { return { name: 'cn', age: 24, wife: { name: 'csf', age: 23 } } }, computed: { husband() { return this.name + this.wife.name } }, watch: { wife: { handler(val, oldVal) { console.log('watch--->',val.name, oldVal.name) }, deep: true, immediate: true } }, render() { return ` <h3>normal key</h3> <p>${this.name}</p> <p>${JSON.stringify(this.wife)}</p> <h3>computed key</h3> <p>${this.husband}</p> `; } }).$mount(null, document.getElementById('root')) function mountComponent(vm, el, hydrating) { vm.$el = el; const _updateComponent = function (vm) { vm._update(vm._render(), hydrating) } // 没错就是在这里,_updateComponent就是我们的更新视图的方法 // 因为这里因为没设置lazy,所以实例化watcher的时候,会直接调用 // _updateComponent -> _render -> 渲染页面 new Watcher(vm, _updateComponent, noop, {}, true) return vm } Vue.prototype.$mount = function ( el, hydrating ) { return mountComponent(this, el, hydrating) }; Vue.prototype._update = function (node, hydrating) { hydrating.innerHTML = node }
- computedWatcher: 负责计算属性的变化。在一开始初始化Vue实例就进行了实例化,发生在renderWatcher之前,原因前面注释也有说明,不在累赘。下面看其如何实例化的。
// initState在Vue.prototype._init里面调用,_init会在Vue被实例化的时候调用 function initState(vm) { // destory的时候用 vm._watchers = []; // 这里的参数就是我们的data、computed、watch、render..... const opts = vm.$options if (opts.data) { // 这里就是初始化data数据了,也就是对数据进行响应式处理,等下会说到 initData(vm) } // 这里就是initComputed了 if (opts.computed) initComputed(vm, opts.computed) if (opts.watch) { // 这里是init我们的watch initWatch(vm, opts.watch) } } function initComputed(vm, computed) { // 计算属性的wacher对象 const watchers = vm._computedWatchers = Object.create(null) for (const key in computed) { const userDef = computed[key] // 因为compouted有函数形式或者 set/get方式的 const getter = typeof userDef === 'function' ? userDef : userDef.get // 在这里就实例化了 watchers[key] = new Watcher( vm, getter || noop, noop, // 注意这里,lazy初始化是true { lazy: true } ) if (!(key in vm)) { defineComputed(vm, key, userDef) } } } function defineComputed(target, key, userDef) { // 暂时默认不是服务端渲染 // const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = createComputedGetter(key) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = createComputedGetter(key) sharedPropertyDefinition.set = userDef.set || noop } // 进行代理,当我们this.computedxxx时候,就会触发下面的computedGetter 进行取值操作 Object.defineProperty(target, key, sharedPropertyDefinition) } function createComputedGetter(key) { return function computedGetter() { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 这里一开始new的时候 dirty 是为true的 // 只要不取值,第一次,computed 值因为 dirty 是true所以是 undefined // 没必要一开始就算出其值(浪费),只有取值了才会调用 evaluate 重新计算值。 if (watcher.dirty) { // 调用evaluate计算值,计算完后会置为false // 除非依赖的数据发生改变,会将 dirty 置为true 否则都无需取计算 watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } }
- userWatcher: watch监听的变化。在一开始初始化Vue实例就进行了实例化,发生在renderWatcher之前,这里可能有点绕,需要多看几遍,多调试几遍,下面看其如何实例化的。
stateMixin(Vue) function stateMixin(Vue) { // expOrFn是我们要监听的值的key cb是我们传的回调 Vue.prototype.$watch = function(expOrFn, cb, options) { const vm = this options = options || {} // watcher 里面的user就是给watch用的 options.user = true // 这里也是会立马调用到watcher的get方法 const watcher = new Watcher(vm, expOrFn, cb, options) // 如果watch配置了立马获取值的话 if (options.immediate) { try { cb.call(vm, watcher.value, null) } catch (error) { } } } } function initWatch(vm, watch) { for (const key in watch) { // 这是user定义的回调 const handler = watch[key] createWatcher(vm, key, handler) } } function createWatcher(vm, expOrFn, handler, options) { // 针对watch值是对象的情况 if(isPlainObject(handler)) { options = handler handler = handler.handler } return vm.$watch(expOrFn, handler, options) } // 下面回顾上面内容 class Watcher { dirty getter cb value constructor(vm, expOrFn, cb, options, isRenderWatcher) { if (options) { this.user = !!options.user } else { this.deep = this.user = this.lazy = this.sync = false } if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // 如果是watch的话是走这里 this.getter = parsePath(expOrFn) } // new watcher的时候,get方法会调用 this.value = this.lazy ? undefined : this.get() } get() { pushTarget(this) let value const vm = this.vm try { // value = this.getter.call(vm, vm) } catch (error) { } finally { // 这个是为watch的deep服务的 // 如果deep的话需要递归遍历其watch的data属性的其下的所有子属性 // 将当前的userWatcher加入到它们的dep中,这样才能深度改变 if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value } run() { const value = this.get() if (value !== this.value || isObject(value) || this.deep) { const oldValue = this.value this.value = value // cb就是我们的回调 this.cb.call(this.vm, value, oldValue) } } } function parsePath(path) { // 解析watch 的a.b.c的情况 const segments = path.split('.'); // 这里的obj会被赋予vm,所以返回的就是watch的data中key的值 return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) { return } obj = obj[segments[i]]; } return obj } } // 为了防止重复依赖收集 const seenObjects = new Set() // 递归遍历watch的需要deep的响应式对象的值,进行依赖收集,这样才能实现deep function traverse(val) { _traverse(val,seenObjects) seenObjects.clear() } function _traverse (val, seen) { let i, keys const isA = Array.isArray(val) if ((!isA && !isObject(val))) { return } if (val.__ob__) { const depId = val.__ob__.dep.id if (seen.has(depId)) { return } seen.add(depId) } if (isA) { i = val.length while (i--) _traverse(val[i], seen) } else { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) } }
- renderWatcher: 负责
-
总结一下
通过上面完整的Watcher对象,以及几种watcher实例的分析,可以大致了解了watcher在什么时候,如何与我们的响应式数据进行一个绑定,从而完成发布->订阅,现在再次引用官网的图,是不是有点明白了。
如果你完整读完前面的内容的话,应该对整个流程有个大致了解,总的来说如下
- data数据劫持:遍历
data
选项的属性,属性值不是数组的话,利用Object.defineProperty
为属性添加getter
和setter
,是数组的则是劫持其变异方法,再递归遍历其子对象、遍历数组,重复操作。 - watcher实例化:不同的
watcher
在不同的阶段实例化,对data
数据进行取值操作,进行依赖收集。 - 修改响应式数据:触发
setter
,调用dep.notify
,遍历其对应的dep上的subs数组,调用watcher.update
。
更新策略
前面的基本都讲了,其实还差一个就是我们的更新操作,即watcher.update
会发生什么。
当我们数据发生变化的时候,走到watcher.update
,并不是立马就去调用watcher.run
方法进行更新操作,而是异步更新,可以想一下,如果不是异步更新,那么每次调用this.xxx = xxx
的时候,就引起了视图更新,如果一次性更新的数据很多,或者我们只是想要最后的那个结果,中间的变换都是无效的,是不是就会出现很多弊端。
所以尤大是这样做的,watcher
观察到数据发生了变化,会开启一个队列queue
,缓冲在同一事件循环中发生的数据改变。如果一个watcher
多次被触发,则只会被推入队列中一次(去除重复的数据,无用的数组,取最后一次)。然后真正的更新是在下一次事件循环的Tick
中触发。具体详情可以参考vue异步更新机制。
let waiting = false
let has = {}
// 缓存watcher数组
const queue = []
// 缓存在一次更新中的watcher
function queueWatcher(watcher) {
const id = watcher.id
if (!has[id]) {
has[id] = true
queue.push(watcher)
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
let index = 0
// flushScheduleQueue函数的作用主要是执行更新的操作
// 它会把queue中所有的watcher取出来并执行相应的更新
function flushSchedulerQueue() {
flushing = true
let watcher, id
// watcher 按先后排序
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
resetSchedulerState()
}
// 重置
function resetSchedulerState() {
index = queue.length = 0
has = {}
waiting = flushing = false
}
// 一般callback都是只有flushSchedulerQueue
// 当我们自己定义了$nextTick的时候也会加入到这里
const callbacks = []
let pending = false
function nextTick(cb, ctx) {
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
}
}
})
// 防止重复执行
if (!pending) {
pending = true
timerFunc()
}
}
const p = Promise.resolve()
// 这里我们默认浏览器支持promise,其源码判断很多种情况
let timerFunc = () => {
p.then(flushCallbacks)
}
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
最后
内容讲起来也是挺多挺绕的,一开始可能会比较懵,但是将整个流程相关的都捋一遍后,其实会发生其设计的妙处。个人感觉注释算是比较齐全了,空讲代码是比较难完全理解的,所以建议拉下下面我手敲的demo例子,对比着一步一步的调试。里面也带上了官方源码,上面的基本都在src/core
文件夹下,看完全的话可以找dist/vue.js
。整理不易,如果有什么说错的地方可以在评论区指出,如果觉得对你有帮助的话,希望能给个三连🤭!