- 篇幅较长,不过都是精华。
- 不过静下心来通篇吃透,保你收获不小。
- 有任何问题欢迎评论区留言。
- 来吧,话不多说,就是干!
响应式原理回顾
- 当你把一个普通的
JavaScript
对象传入Vue
实例作为 data 选项,Vue
将遍历此对象所有的property
,并使用 Object.defineProperty 把这些property
全部转为 getter/setter。 - 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
- 每个组件实例都对应一个
watcher
实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。 【 依赖收集 】 - 之后当依赖项的
setter
触发时,通知watcher
,从而使它关联的组件重新渲染。 【 派发更新 】
以上原理回顾来自 Vue 官方文档,更多信息可以进官方文档细看。
简介
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。接下来我们通过对部分 Vue 源码(本文主要针对 v2.6.11 的 Vue 源码进行分析)的简单分析和学习来深入了解 Vue 中的数据响应式是怎样实现的。我们将这一部分的代码分析大致分为三部分:怎么让数据变成响应式、依赖收集、派发更新
一、怎么让数据变成响应式?
整体流程图
调试代码调用栈
我自己 debug 了一下,上图流程的调用栈如下图所示:
具体一点的函数调用流程
1、调用 new Vue() 执行构造函数
在 src/core/instance/index
中定义了 Vue
的构造函数,传入的参数是 options
。进行 vue
实例化时主要调用了一个 this._init()
的函数。
2、构造函数中执行 initMixin(Vue) 中定义的 _init() 方法
function Vue (options) {
// ...
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
// ...
export default Vue
复制代码
3、_init() 方法 中 调用 initState(vm)
在 _init() 中对当前传入的 options
进行了一些处理,主要是判断当前实例化的是否为组件,使用 mergeOptions
方法对 options
进行加工,此处不做赘述。然后又调用了一系列方法进行了生命周期、事件、渲染器的初始化等,这里我们主要关注 initState
这个方法:
Vue.prototype._init = function (options?: Object) {
// ...
// 初始化 vm 的 _props/methods/_data/computed/watch
initState(vm)
// ...
}
复制代码
4、initState(vm) 中调用 initData(vm)
initState(vm) 中对 props
、methods
、data
、computed
和 watch
进行了初始化,这些都是 Vue
实例化方法中传入参数 options
对象的一些属性,这些属性都需要被响应式化。而针对于 data
的初始化分了两种情况,一种是 options
中没有 data
属性的,该方法会给 data
赋值一个空对象并进行 observe
,如果有 data
属性,则调用 initData
方法进行初始化。
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe((vm._data = {}), true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
复制代码
5、initData(vm) 方法中 最后 调用了 observe(data, true /* asRootData */)
initData
方法对 options
中的 data
进行处理,主要是有两个目的
- 1、将
data
代理到Vue
实例上,同时检查data
中是否使用了Vue
中的保留字、是否与props
、methods
中的属性同名。 - 2、使用
observe
方法将data
中的属性变成响应式的。
接下来会对 data
中的每一个数据进行遍历,遍历过程将会使用 hasOwn(methods, key)
、hasOwn(props, key)
方法对该数据是否占用保留字、是否与 props
和 methods
中的属性重名进行判断并在浏览器等控制台给出 warn
提示信息。最后再用 !isReserved(key)
判断当前属性名是否是以$和_开头的(即判断是否是 Vue
中的保留字),最后才调用 proxy
方法将其代理到 Vue
实例上。
此处需要注意的是:如果 data
中的属性与 props
、methods
中的属性重名,那么在 Vue
实例上调用这个属性时的优先级顺序是 props > methods > data
。
最后对 data
中的每一个属性调用 observe
方法,该方法赋予 data
中的属性可被监测的特性。
function initData(vm: Component) {
let data = vm.$options.data
// 初始化 _data,组件中 data 是函数,调用函数返回结果
// 否则直接返回 data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' &&
warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' &&
warn(`The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
// 响应式处理
observe(data, true /* asRootData */)
}
复制代码
6、observe(value) 中最后实例化了 Observer 类,并且把需要响应式的数据传入
export function observe(value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
复制代码
7、Observer 类的构造函数中调用了它的实例方法 this.walk(value)
可以看到该构造函数有以下几个目的
- 针对当前的数据对象新建了一个订阅器
- 为每个数据的
value
都添加一个__ob__
属性,该属性不可枚举并指向自身 - 针对数组类型的数据进行单独处理
- 调用
this.walk(value)
constructor(value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0 // 初始化实例的 vmCount 为0
def(value, '__ob__', this) // 将实例挂载到观察对象的 __ob__ 属性
// 数组的响应式处理
if (Array.isArray(value)) {
if (hasProto) {//判断当前浏览器是否支持 __proto__ 这个属性
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 为数组中的每一个对象创建一个 observer 实例
this.observeArray(value)
} else {
// 遍历对象中的每一个属性,转换成 setter/getter
this.walk(value)
}
}
复制代码
8、this.walk(value) 中遍历了 value 中的所有属性,调用 defineReactive(obj, keys[i])
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk(obj: Object) {
// 获取观察对象的每一个属性
const keys = Object.keys(obj)
// 遍历每一个属性,设置为响应式数据
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
复制代码
9、defineReactive(obj, keys[i])
defineReactive()
方法中最终使用 Object.defineProperty
把这些 property
全部转为 getter/setter
。是真正为数据添加 get
和 set
属性方法的方法,它将 data
中的数据定义一个响应式对象,并给该对象设置 get
和 set
属性方法,其中 get
方法是对依赖进行收集, set
方法是当数据改变时通知 Watcher
派发更新。
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean // 浅的,如果为 true 就只监听第一层数据,false 进行深度监听
) {
// 创建依赖对象实例 作用是为当前这个属性收集依赖 也就是收集观察当前这个属性的所有 watcher
const dep = new Dep()
// 获取 obj 的属性描述符对象
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
//当前这个属性是不可配置的
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 判断是否递归观察子对象,并将子对象属性都转换成 getter/setter,返回子观察对象
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 如果预定义的 getter 存在则 value 等于 getter 调用的返回值
// 否则直接赋予属性值
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
/* eslint-disable no-self-compare */
// 如果新值等于旧值或者新值旧值为NaN则不执行
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
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
}
// 判断是否递归观察子对象,观察子对象并返回子对象的 observer 对象
childOb = !shallow && observe(newVal)
// 派发更新(给订阅者发布更新通知)
dep.notify()
}
})
}
复制代码
到此我们就把创建 Vue 实例时传入的 options 中的 data 通过 Object.defineProperty() 转换为一个响应式对象了
二、依赖收集
依赖收集的简要原理就是:当我们的视图被渲染时,会用到我们定义在 data
中的数据,这样就会触发这些数据的 get
属性方法,Vue
通过 get
方法进行依赖收集。get
方法中的代码如下:
get: function reactiveGetter() {
// 如果预定义的 getter 存在则 value 等于 getter 调用的返回值
// 否则直接赋予属性值
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
},
复制代码
可以看到 get 属性方法中调用了一个 dep.depend 方法,该方法正是依赖收集的开始。
Dep 类之订阅器原理
数据对象中的 get
方法主要使用 depend
方法进行依赖收集,而 depend
是 Dep
类中的属性方法,我们来看下 Dep
类的代码是怎样实现的:
export default class Dep {
static target: ?Watcher
id: number
subs: Array<Watcher>
constructor() {
this.id = uid++
this.subs = []
}
// 添加新的订阅者 watcher 对象
addSub(sub: Watcher) {
this.subs.push(sub)
}
// 移除订阅者
removeSub(sub: Watcher) {
remove(this.subs, sub)
}
// 将观察对象和 watcher 建立依赖
depend() {
if (Dep.target) {
// 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
Dep.target.addDep(this)
}
}
// 发布通知
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !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++) {
subs[i].update()
}
}
}
// Dep.target 用来存放目前正在使用的 watcher
// 全局唯一,并且一次也只能有一个watcher被使用
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
// 入栈并将当前 watcher 赋值给 Dep.target
// 父子组件嵌套的时候先把父组件对应的 watcher 入栈,
// 再去处理子组件的 watcher,子组件的处理完毕后,再把父组件对应的 watcher 出栈,继续操作
export function pushTarget(target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
复制代码
Dep
类中定义了三个属性:target
、id
和 subs
,分别表示全局唯一的静态数据依赖的监听器 Watcher
、该属性的 id
以及订阅这个属性数据的订阅者列表 subs
。其中 subs
其实就是存放了所有订阅了该数据的订阅者们。另外还提供了将订阅者添加到订阅者列表的 addSub
方法、从订阅者列表删除订阅者的 removeSub
方法。
我们能看到在执行 dep.depend()
方法之前,其实是需要先判断 Dep.target
这个全局唯一的静态数据依赖的监听器 Watcher
是否存在的,只有当它有值的时候才会去执行 dep.depend()
收集依赖。所以我们得先搞清楚这个 Dep.target
是在哪里被赋值的。
Dep.target 是在哪里被赋值的
我们在 Dep
类的代码中能看到这个 Dep.target
是一个 Watcher
的实例,所以我通过查看源码找到了创建 watcher
对象的地方,发现是在 src/core/instance/lifecycle.js
中的 mountComponent
函数中执行了 new Watcher()
mountComponent()
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)// 是否为渲染watcher的标识
复制代码
watcher 类的构造函数
通过 Watcher
类的构造函数传参可以看到 mountComponent
中创建的 Watcher
是一个渲染 Watcher
,将当前的 getter
设置为了 updateComponent
方法(这也就是重新渲染视图的方法),最后调用了 get
属性方法。我们接下来看看 get
方法中做了什么
this.value = this.lazy //lazy 的含义是-是否延迟加载
? undefined
: this.get()
复制代码
watcher 类的静态方法 get()
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
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
}
复制代码
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
复制代码
get
方法中首先调用了在 Dep
类文件中定义的全局方法 pushTarget
,将 Dep.target
置为当前正在执行的渲染 Watcher
,并将这个 watcher
压到了 targetStack
。接下来就会调用 wathcer
的 getter
方法
由于此时的 getter
就是 updateComponent
方法,执行 updateComponent
方法,也就是 vm._update(vm._render(), hydrating)
进行渲染页面了,渲染过程中就会 touch
到 data
中相关的依赖属性,这样就会触发各个属性中的 get
方法了,然后就会走到方法里面的 dep.depend()
(因为此时 Dep.target
已经是被赋值为当前正在执行的渲染 Watcher
)
// 将观察对象和 watcher 建立依赖
depend() {
if (Dep.target) {
// 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
Dep.target.addDep(this)
}
}
复制代码
depend() 方法很简单,只是执行了一下 Dep.target.addDep(this),这里的 Dep.target 是一个 Watcher 实例,里面的参数 this 也就是当前的 Dep 实例。我们看下在 Watcher 中的 addDep() 方法执行了啥操作
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
复制代码
addDep
方法中做了一些优化,对同一个属性只收集一次依赖。dep.addSub(this)
就是调用Dep
的属性方法addSub
将当前Watcher
添加到依赖数组中,用于之后的派发更新。
// 添加新的订阅者 watcher 对象
addSub(sub: Watcher) {
this.subs.push(sub)
}
复制代码
在本次渲染完之后会将当前的渲染 Watcher
从 targetStack
推出,进行下一个 watcher
的任务。最后会进行依赖的清空,每次数据的变化都会引起视图重新渲染,每一次渲染又会进行依赖收集,又会触发 data
中每个属性的 get
方法。有下面这种情况
<template>
<div>
<span v-if="isShow"> {{ jing }} </span>
<span v-else> {{ hao }} </span>
</div>
</template>
复制代码
第一次当 isShow
为真时,进行渲染,当我们把 isShow
的值改为 false
时,这时候属性 jing
根本不会被使用,所以如果不进行依赖清空,会导致不必要的依赖通知。依赖清空主要是对 newDeps
进行更新,通过对比本次收集的依赖和之前的依赖进行对比,把不需要的依赖清除。
/**
* Clean up for dependency collection.
*/
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
}
复制代码
三、派发更新
debug 实例
页面代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>observe</title>
</head>
<body>
<div id="app">
<span> {{ arr }}</span>
<child-one :arr="arr"></child-one>
<child-two :arr="arr"></child-two>
<child-three :arr="arr"></child-three>
</div>
<script src="../../dist/vue.js"></script>
<script>
Vue.component('child-one', {
props: ['arr'],
template: '<p>{{ arr }}</p>'
})
Vue.component('child-two', {
props: ['arr'],
template: '<div>{{ arr }}</div>'
})
Vue.component('child-three', {
props: ['arr'],
template: '<p>{{ arr }}</p>'
})
const vm = new Vue({
el: '#app',
data: {
arr: [2, 3, 5]
}
})
</script>
</body>
</html>
复制代码
调试效果如下:
可以很清楚的看到 arr
这个数据的 dep.subs
数组中收集了 4
个 Watcher
实例。
dep.notify()
// 发布通知
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !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++) {
subs[i].update()
}
}
复制代码
而当数据变化时,会触发数据的 setter
,在 setter
中调用了 dep.notify()
方法,在dep.notify()
方法中,遍历所有依赖(即watcher实例),执行依赖的 update()
方法,也就是 Watcher
类中的update()
实例方法。
update()
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
复制代码
update
方法是在 Watcher
类文件中,里面使用了队列的方式来管理订阅者的更新派发,其中主要调用了 queueWatcher
这个方法来实现该逻辑:
queueWatcher()
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
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 (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
复制代码
首先使用一个名为 has
的 Map
来保证每一个 watcher
仅推入队列一次。flushing
这个标识符表示目前这个队列是否正在进行更新派发,如果是,那么将这个 id
对应的订阅者进行替换,如果已经路过这个 id
,那么就立刻将这个 id
对应的 watcher
放在下一个排入队列。接下来根据 waiting
这个标识符来表示当前是否正在对这个队列进行更新派发,如果没有的话,就可以调用 nextTick(flushSchedulerQueue)
进行真正的更新派发了。
flushSchedulerQueue()
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
复制代码
这个方法首先对队列中的 watcher
按照其 id
进行了排序,排序的主要目的注释中写的很清楚了。
- 1、
Vue
的组件更新是由父到子,因为父组件的创建过程是先于子的,所以watcher
的创建也是先父后子,执行顺序也应该保持先父后子。 - 2、用户的自定义
watcher
要优先于渲染watcher
执行;因为用户自定义watcher
(initState) 是在渲染watcher
(mountComponent) 之前创建的。 *( *initState 是比 mountComponent 先执行的) - 3、如果一个组件在父组件的
watcher
执行期间被销毁,那么它对应的watcher
都应该被跳过,所以父组件的watcher
应该先执行。
排序之后就遍历这个队列,执行 watcher.run()
方法。
watcher.run()
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
复制代码
watcher.run()
方法中
- 新值与老值不同时会触发回调函数
- 但是如果值是对象或者
deep
为true
时都会触发回调函数
当回调函数重新渲染视图的时候,又会进行下一轮的依赖收集,之后又派发更新......无限套娃
四、🌈🌈🌈💨💨💨
- 好了,我是金同学,这次的分享就到这里了。
- 要是有下一篇,应该就是关于
Vue
虚拟Dom
源码的相关分享了。 - 当然了,希望我们会再次重逢。
- 努力生活是头等大事,祝朋友们岁月静好。
- 希望疫情下的次生灾难少一点、悲剧发生的少一点。🙏🏻🙏🏻🙏🏻