普通视图
Vue2 的响应式原理
Vue2 的响应式原理
Object.defineProperty
Object.defineProperty(obj, prop, descriptor)
obj:必需。要定义或修改的属性的对象。
prop:必需。要定义或修改的属性的属性名。
descriptor:必需。要定义或修改的属性的描述符。
存取器 getter/setter
var obj = {}
var value = 'hello'
Object.defineProperty(obj, 'key', {
// 当获取 obj['key'] 值的时候触发该函数。
get: function() {
return value
},
// 当设置 obj['key'] 值的时候触发该函数。
set: function(newValue) {
value = newValue
}
})
注意:不要在 getter 中获取该属性的值,也不要在 setter 中设置该属性的值,否则会发生栈溢出。
实现数据代理和劫持
class Vue {
constructor(options) {
this.$options = options
this.initData()
}
initData() {
// TODO:this.$options.data 还可能是一个函数。
const data = (this._data = this.$options.data)
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
// 数据代理逻辑。
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return data[keys[i]]
},
set: function proxySetter(newValue) {
data[keys[i]] = newValue
}
})
// 数据劫持逻辑。
let value = data[keys[i]]
Object.defineProperty(data, keys[i], {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log(`获取了 data 的 ${keys[i]} 值。`)
return value
},
set: function reactiveSetter(newValue) {
console.log(`设置了 data 的 ${keys[i]} 值。`)
value = newValue
}
})
}
}
}
实现数据代理和递归劫持
首先将数据递归劫持逻辑抽离到 observe 工厂函数中;然后新定义一个 Observer 类,为后续的工作做铺垫。
class Vue {
constructor(options) {
this.$options = options
this.initData()
}
initData() {
// TODO:this.$options.data 还可能是一个函数。
const data = (this._data = this.$options.data)
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
// 数据代理逻辑。
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return data[keys[i]]
},
set: function proxySetter(newValue) {
data[keys[i]] = newValue
}
})
}
observe(data)
}
}
// 观察 data 数据。
function observe(data) {
const type = Object.prototype.toString.call(data)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
new Observer(data)
}
// TODO:数组的观察逻辑暂时还没实现。
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
}
// 定义 obj 对象的 key 属性的响应式。
// 这里利用了闭包,使得 value 变量一直没有被垃圾回收。
function defineReactive(obj, key, value) {
// 深度优先遍历。
observe(value)
// 数据劫持逻辑。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log(`获取了 ${key} 值。`)
return value
},
set: function reactiveSetter(newValue) {
console.log(`设置了 ${key} 值。`)
observe(newValue)
value = newValue
}
})
}
实现 watch 监听
下面是 Vue 中的 watch 选项与 $watch 方法的实现原理。(暂时只实现了对 vm.$options.data 对象的第一层属性的监听。)
每个响应式属性都有一个属于自己的“筐”。在该响应式属性被其他回调函数依赖的时候,Vue 会通过这个“筐”的 depend 方法把这些回调函数添加到这个“筐”的 subs 属性中。在该响应式属性的值发生变化的时候,Vue 会通过这个“筐”的 notify 方法把这个“筐”的 subs 属性中的这些回调函数取出来全部执行。
在 Vue 中,“筐”被抽象成了 Dep 实例,回调函数被包装成了 Watcher 实例。
class Vue {
constructor(options) {
this.$options = options
this.initData()
this.initWatch()
}
initData() {
// TODO:this.$options.data 还可能是一个函数。
const data = (this._data = this.$options.data)
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
// 数据代理逻辑。
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return data[keys[i]]
},
set: function proxySetter(newValue) {
data[keys[i]] = newValue
}
})
}
observe(data)
}
initWatch() {
const watch = this.$options.watch
if (watch) {
const keys = Object.keys(watch)
for (let i = 0; i < keys.length; i++) {
this.$watch(keys[i], watch[keys[i]])
}
}
}
$watch(exp, cb) {
new Watcher(this, exp, cb)
}
}
// 观察 data 数据。
function observe(data) {
const type = Object.prototype.toString.call(data)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
new Observer(data)
}
// TODO:数组的观察逻辑暂时还没实现。
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
}
// 定义 obj 对象的 key 属性的响应式。
// 这里利用了闭包,使得 value 变量 和 dep 常量一直没有被垃圾回收。
function defineReactive(obj, key, value) {
const dep = new Dep()
// 深度优先遍历。
observe(value)
// 数据劫持逻辑。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 订阅逻辑。
dep.depend()
return value
},
set: function reactiveSetter(newValue) {
if (value === newValue) {
return
}
// 发布逻辑。
dep.notify()
observe(newValue)
value = newValue
}
})
}
// “筐”被抽象成了 Dep 实例。
class Dep {
// 响应式属性当前要订阅的 watcher。
static target = null
constructor() {
// 响应式属性已订阅的 watcher 列表。
this.subs = []
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
notify() {
this.subs.forEach(watcher => {
watcher.run()
})
}
}
// 回调函数被包装成了 Watcher 实例。
// TODO:暂时只实现了对 vm.$options.data 对象的第一层属性的监听。
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm
this.exp = exp
this.cb = cb
this.get()
}
get() {
Dep.target = this
this.vm[this.exp]
Dep.target = null
}
run() {
this.cb.call(this.vm)
}
}
在 Vue 中:1、被包装成 Watcher 实例的回调函数是被异步调用的;2、在该回调函数被异步调用之后和实际执行之前的这个过程中,如果触发该回调函数的响应式属性的值又被修改了,那么这些后续的修改操作将无法再次触发该回调函数的调用。所以 Watcher 类的实现原理,实际如下代码所示:
// 回调函数被包装成了 Watcher 实例。
// TODO:暂时只实现了对 vm.$options.data 对象的第一层属性的监听。
const watcherQueue = []
let watcherId = 0
class Watcher {
constructor(vm, exp, cb) {
this.id = ++watcherId
this.vm = vm
this.exp = exp
this.cb = cb
this.get()
}
get() {
Dep.target = this
this.vm[this.exp]
Dep.target = null
}
run() {
if (watcherQueue.indexOf(this.id) !== -1) {
// 类似于 JavaScript 中的防抖逻辑。
return
}
watcherQueue.push(this.id)
const index = watcherQueue.length - 1
Promise.resolve().then(() => {
this.cb.call(this.vm)
watcherQueue.splice(index, 1)
})
}
}
仍然存在的问题
至此,基本实现了 Vue 中基于发布订阅的 watch 监听逻辑。但目前仍然存在以下问题:1、对象的新增属性没有被添加数据劫持逻辑;2、数组元素的数据劫持逻辑还存在问题。因此在对对象的新增属性和数组元素添加监听逻辑时也会存在问题。
实现 $set 方法
在 Vue 中,如果响应式属性的值是一个对象(包括数组),那么在该响应式属性上就会被挂载一个 _ ob _ 属性,该 _ ob _ 属性的值是一个 Observer 实例,该 Observer 实例的 dep 属性的值是一个 Dep 实例,该 Dep 实例是和 defineReactive 方法的闭包中的 Dep 实例不同的与该响应式属性绑定的另外一个“筐”。
当响应式属性的值是一个对象(包括数组)时,Vue 会把触发该响应式属性的 getter 的 watchers 额外收集一份在该响应式属性的 _ ob _ 属性的 dep 属性的 subs 属性中。这样开发者就可以通过代码命令式地去触发这个响应式属性的 watchers 了。
$set 方法的实现思路基本如下:
1、在创建 Observer 对象的实例去观察响应式属性时,同时也创建一个 Dep 对象的实例。先将该 Dep 对象的实例挂载到该 Observer 对象的实例上,然后把该 Observer 对象的实例挂载到它自己观察的响应式属性上。
2、当响应式属性的 getter 被触发时,把与该响应式属性绑定的“筐”的 depend 方法调用一遍。响应式属性的值为对象或数组时,有两个筐;响应式属性的值不为对象和数组时,有一个筐。
3、当用户调用 $set 方法时,如果 target 为对象,则 Vue 先调用 defineReactive 方法把设置的属性也定义为响应式,然后调用 target._ ob _.dep.notify 方法触发 target 的 watchers。(target 为数组的情况暂时未实现。)
class Vue {
constructor(options) {
this.$options = options
this.initData()
this.initWatch()
}
initData() {
// TODO:this.$options.data 还可能是一个函数。
const data = (this._data = this.$options.data)
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
// 数据代理逻辑。
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return data[keys[i]]
},
set: function proxySetter(newValue) {
data[keys[i]] = newValue
}
})
}
observe(data)
}
initWatch() {
const watch = this.$options.watch
if (watch) {
const keys = Object.keys(watch)
for (let i = 0; i < keys.length; i++) {
this.$watch(keys[i], watch[keys[i]])
}
}
}
$watch(exp, cb) {
new Watcher(this, exp, cb)
}
// TODO:暂时只实现了 target 为对象的情况,target 为数组的情况还未实现。
$set(target, key, value) {
defineReactive(target, key, value)
// 触发依赖 target 的 watchers。
target.__ob__.dep.notify()
}
}
// 观察 data 数据。
function observe(data) {
const type = Object.prototype.toString.call(data)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
return new Observer(data)
}
// TODO:数组的观察逻辑暂时还没实现。
class Observer {
constructor(data) {
this.dep = new Dep()
Object.defineProperty(data, '__ob__', {
enumerable: false,
configurable: false,
value: this,
writable: true
})
this.walk(data)
}
walk(data) {
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
}
// 定义 obj 对象的 key 属性的响应式。
// 这里利用了闭包,使得 value 变量 和 dep 常量一直没有被垃圾回收。
function defineReactive(obj, key, value) {
const dep = new Dep()
// 深度优先遍历。
const objKeyOb = observe(value)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
// 数据劫持逻辑。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 订阅逻辑。
dep.depend()
if (objKeyOb) {
objKeyOb.dep.depend()
}
return value
},
set: function reactiveSetter(newValue) {
if (value === newValue) {
return
}
// 发布逻辑。
dep.notify()
const objKeyOb = observe(newValue)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
value = newValue
}
})
}
// “筐”被抽象成了 Dep 实例。
class Dep {
// 响应式属性当前要订阅的 watcher。
static target = null
constructor() {
// 响应式属性已订阅的 watcher 列表。
this.subs = []
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
notify() {
this.subs.forEach(watcher => {
watcher.run()
})
}
}
// 回调函数被包装成了 Watcher 实例。
// TODO:暂时只实现了对 vm.$options.data 对象的第一层属性的监听。
const watcherQueue = []
let watcherId = 0
class Watcher {
constructor(vm, exp, cb) {
this.id = ++watcherId
this.vm = vm
this.exp = exp
this.cb = cb
this.get()
}
get() {
Dep.target = this
this.vm[this.exp]
Dep.target = null
}
run() {
if (watcherQueue.indexOf(this.id) !== -1) {
// 类似于 JavaScript 中的防抖逻辑。
return
}
watcherQueue.push(this.id)
const index = watcherQueue.length - 1
Promise.resolve().then(() => {
this.cb.call(this.vm)
watcherQueue.splice(index, 1)
})
}
}
实现数组方法的重写
Vue 对数组的处理思路基本如下:
1、对数组本身不使用 Object.defineProperty 方法进行数据劫持,对数组元素依次使用 observe 方法进行数据观察。因此,数组元素不具有响应性,数组元素的属性仍然具有响应性。
2、对数组的 push、pop、shift、unshift、splice、sort、reverse 实例方法进行重写。在这些重写的实例方法中,Vue 先调用数组的原始同名实例方法,然后再调用 this._ ob _.dep.notify 方法去触发该数组的 watchers。
class Vue {
constructor(options) {
this.$options = options
this.initData()
this.initWatch()
}
initData() {
// TODO:this.$options.data 还可能是一个函数。
const data = (this._data = this.$options.data)
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
// 数据代理逻辑。
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return data[keys[i]]
},
set: function proxySetter(newValue) {
data[keys[i]] = newValue
}
})
}
observe(data)
}
initWatch() {
const watch = this.$options.watch
if (watch) {
const keys = Object.keys(watch)
for (let i = 0; i < keys.length; i++) {
this.$watch(keys[i], watch[keys[i]])
}
}
}
$watch(exp, cb) {
new Watcher(this, exp, cb)
}
$set(target, key, value) {
const type = Object.prototype.toString.call(target)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
if (type === '[object Array]') {
const arratProto = Array.prototype
observe(value)
arratProto.splice.call(target, key, 1, value)
} else if (type === '[object Object]') {
defineReactive(target, key, value)
}
// 触发依赖 target 的 watchers。
target.__ob__.dep.notify()
}
}
// 观察 data 数据。
function observe(data) {
const type = Object.prototype.toString.call(data)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
return new Observer(data)
}
class Observer {
constructor(data) {
this.dep = new Dep()
Object.defineProperty(data, '__ob__', {
enumerable: false,
configurable: false,
value: this,
writable: true
})
const type = Object.prototype.toString.call(data)
if (type === '[object Array]') {
data.__proto__ = arrayMethods
this.observeArray(data)
} else if (type === '[object Object]') {
this.walk(data)
}
}
walk(data) {
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
observeArray(array) {
for (let i = 0; i < array.length; i++) {
observe(array[i])
}
}
}
// 定义 obj 对象的 key 属性的响应式。
// 这里利用了闭包,使得 value 变量 和 dep 常量一直没有被垃圾回收。
function defineReactive(obj, key, value) {
const dep = new Dep()
// 深度优先遍历。
const objKeyOb = observe(value)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
// 数据劫持逻辑。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 订阅逻辑。
dep.depend()
if (objKeyOb) {
objKeyOb.dep.depend()
}
return value
},
set: function reactiveSetter(newValue) {
if (value === newValue) {
return
}
// 发布逻辑。
dep.notify()
const objKeyOb = observe(newValue)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
value = newValue
}
})
}
// “筐”被抽象成了 Dep 实例。
class Dep {
// 响应式属性当前要订阅的 watcher。
static target = null
constructor() {
// 响应式属性已订阅的 watcher 列表。
this.subs = []
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
notify() {
this.subs.forEach(watcher => {
watcher.run()
})
}
}
// 回调函数被包装成了 Watcher 实例。
// TODO:暂时只实现了对 vm.$options.data 对象的第一层属性的监听。
const watcherQueue = []
let watcherId = 0
class Watcher {
constructor(vm, exp, cb) {
this.id = ++watcherId
this.vm = vm
this.exp = exp
this.cb = cb
this.get()
}
get() {
Dep.target = this
this.vm[this.exp]
Dep.target = null
}
run() {
if (watcherQueue.indexOf(this.id) !== -1) {
// 类似于 JavaScript 中的防抖逻辑。
return
}
watcherQueue.push(this.id)
const index = watcherQueue.length - 1
Promise.resolve().then(() => {
this.cb.call(this.vm)
watcherQueue.splice(index, 1)
})
}
}
const mutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
mutationMethods.forEach(method => {
arrayMethods[method] = function(...args) {
if (method === 'push' || method === 'unshift' || method === 'splice') {
this.__ob__.observeArray(args)
}
const result = arrayProto[method].apply(this, args)
this.__ob__.dep.notify()
return result
}
})
实现 computed 计算属性
Vue 中 computed 计算属性的特性:
1、计算属性不存在于 data 选项中,因此计算属性需要单独进行初始化。
2、计算属性的值是一个函数运行之后的返回值。
3、计算属性的值“只能取,不能存”,即计算属性的 setter 无效。
4、计算属性所依赖的响应式属性的值,一旦发生变化,便会引起该计算属性的值,一同发生变化。
5、计算属性是惰性的:计算属性所依赖的响应式属性的值发生变化时,不会立即引起该计算属性的值一同发生变化,而是等到该计算属性的值被获取时才会使得 Vue 对它的值进行重新计算。
6、计算属性是缓存的:如果计算属性所依赖的响应式属性的值没有发生变化,即使多次获取该计算属性的值,Vue 也不会对该计算属性的值进行重新计算。
注:对于计算属性 A 依赖计算属性 B 的情况,下面的代码好像已经实现了,但还需进一步的测试验证。
class Vue {
constructor(options) {
this.$options = options
this.initData()
this.initComputed()
this.initWatch()
}
initData() {
// TODO:this.$options.data 还可能是一个函数。
const data = (this._data = this.$options.data)
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
// 数据代理逻辑。
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return data[keys[i]]
},
set: function proxySetter(newValue) {
data[keys[i]] = newValue
}
})
}
observe(data)
}
initComputed() {
const computed = this.$options.computed
if (computed) {
const keys = Object.keys(computed)
for (let i = 0; i < keys.length; i++) {
const watcher = new Watcher(this, computed[keys[i]], function() {}, { lazy: true })
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function computedGetter() {
if (watcher.dirty) {
watcher.get()
watcher.dirty = false
}
watcher.deps.forEach(dep => {
dep.depend()
})
return watcher.value
},
set: function computedSetter() {
console.warn('请不要给计算属性赋值!')
}
})
}
}
}
initWatch() {
const watch = this.$options.watch
if (watch) {
const keys = Object.keys(watch)
for (let i = 0; i < keys.length; i++) {
this.$watch(keys[i], watch[keys[i]])
}
}
}
$watch(exp, cb) {
new Watcher(this, exp, cb)
}
$set(target, key, value) {
const type = Object.prototype.toString.call(target)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
if (type === '[object Array]') {
const arratProto = Array.prototype
observe(value)
arratProto.splice.call(target, key, 1, value)
} else if (type === '[object Object]') {
defineReactive(target, key, value)
}
// 触发依赖 target 的 watchers。
target.__ob__.dep.notify()
}
}
// 观察 data 数据。
function observe(data) {
const type = Object.prototype.toString.call(data)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
return new Observer(data)
}
class Observer {
constructor(data) {
this.dep = new Dep()
Object.defineProperty(data, '__ob__', {
enumerable: false,
configurable: false,
value: this,
writable: true
})
const type = Object.prototype.toString.call(data)
if (type === '[object Array]') {
data.__proto__ = arrayMethods
this.observeArray(data)
} else if (type === '[object Object]') {
this.walk(data)
}
}
walk(data) {
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
observeArray(array) {
for (let i = 0; i < array.length; i++) {
observe(array[i])
}
}
}
// 定义 obj 对象的 key 属性的响应式。
// 这里利用了闭包,使得 value 变量 和 dep 常量一直没有被垃圾回收。
function defineReactive(obj, key, value) {
const dep = new Dep()
// 深度优先遍历。
const objKeyOb = observe(value)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
// 数据劫持逻辑。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 订阅逻辑。
dep.depend()
if (objKeyOb) {
objKeyOb.dep.depend()
}
return value
},
set: function reactiveSetter(newValue) {
if (value === newValue) {
return
}
// 发布逻辑。
dep.notify()
const objKeyOb = observe(newValue)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
value = newValue
}
})
}
// “筐”被抽象成了 Dep 实例。
class Dep {
// 响应式属性当前要订阅的 watcher。
static target = null
constructor() {
// 响应式属性已订阅的 watcher 列表。
this.subs = []
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
addSub(watcher) {
if (this.subs.indexOf(watcher) !== -1) {
return
}
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
// 回调函数被包装成了 Watcher 实例。
// TODO:暂时只实现了对 vm.$options.data 对象的第一层属性的监听。
const watcherQueue = []
let watcherId = 0
const targetStack = []
class Watcher {
constructor(vm, exp, cb, options = {}) {
this.id = ++watcherId
this.vm = vm
this.exp = exp
this.cb = cb
this.deps = []
this.value = null
this.lazy = this.dirty = !!options.lazy
if (!this.lazy) {
this.get()
}
}
get() {
Dep.target = this
targetStack.push(this)
if (typeof this.exp === 'function') {
this.value = this.exp.call(this.vm)
} else {
this.value = this.vm[this.exp]
}
targetStack.pop()
if (targetStack.length > 0) {
Dep.target = targetStack[targetStack.length - 1]
} else {
Dep.target = null
}
}
addDep(dep) {
if (this.deps.indexOf(dep) !== -1) {
return
}
this.deps.push(dep)
dep.addSub(this)
}
update() {
if (this.lazy) {
this.dirty = true
} else {
this.run()
}
}
run() {
if (watcherQueue.indexOf(this.id) !== -1) {
// 类似于 JavaScript 中的防抖逻辑。
return
}
watcherQueue.push(this.id)
const index = watcherQueue.length - 1
Promise.resolve().then(() => {
// 更新 this.value(watcher.value) 的值。
this.get()
this.cb.call(this.vm)
watcherQueue.splice(index, 1)
})
}
}
const mutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
mutationMethods.forEach(method => {
arrayMethods[method] = function(...args) {
if (method === 'push' || method === 'unshift' || method === 'splice') {
this.__ob__.observeArray(args)
}
const result = arrayProto[method].apply(this, args)
this.__ob__.dep.notify()
return result
}
})
Vue 模板响应式更新的原理
Vue 对模板的响应式更新,就是如同代码中的 initRenderWatch 方法这样做的。在 Vue 中,响应式更新模板的 watcher 被称为 render watcher,该 watcher 的求值函数比代码中的 initRenderWatch 方法中的 watcher 的求值函数复杂的多。
class Vue {
constructor(options) {
this.$options = options
this.initData()
this.initComputed()
this.initWatch()
this.initRenderWatch()
}
initData() {
// TODO:this.$options.data 还可能是一个函数。
const data = (this._data = this.$options.data)
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
// 数据代理逻辑。
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return data[keys[i]]
},
set: function proxySetter(newValue) {
data[keys[i]] = newValue
}
})
}
observe(data)
}
initComputed() {
const computed = this.$options.computed
if (computed) {
const keys = Object.keys(computed)
for (let i = 0; i < keys.length; i++) {
const watcher = new Watcher(this, computed[keys[i]], function() {}, { lazy: true })
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function computedGetter() {
if (watcher.dirty) {
watcher.get()
watcher.dirty = false
}
watcher.deps.forEach(dep => {
dep.depend()
})
return watcher.value
},
set: function computedSetter() {
console.warn('请不要给计算属性赋值!')
}
})
}
}
}
initWatch() {
const watch = this.$options.watch
if (watch) {
const keys = Object.keys(watch)
for (let i = 0; i < keys.length; i++) {
this.$watch(keys[i], watch[keys[i]])
}
}
}
initRenderWatch() {
new Watcher(
this,
() => {
document.querySelector('#app').innerHTML = `<p>${this.name}</p>`
},
() => {}
)
}
$watch(exp, cb) {
new Watcher(this, exp, cb)
}
$set(target, key, value) {
const type = Object.prototype.toString.call(target)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
if (type === '[object Array]') {
const arrayProto = Array.prototype
observe(value)
arrayProto.splice.call(target, key, 1, value)
} else if (type === '[object Object]') {
defineReactive(target, key, value)
}
// 触发依赖 target 的 watchers。
target.__ob__.dep.notify()
}
}
// 观察 data 数据。
function observe(data) {
const type = Object.prototype.toString.call(data)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
return new Observer(data)
}
class Observer {
constructor(data) {
this.dep = new Dep()
Object.defineProperty(data, '__ob__', {
enumerable: false,
configurable: false,
value: this,
writable: true
})
const type = Object.prototype.toString.call(data)
if (type === '[object Array]') {
data.__proto__ = arrayMethods
this.observeArray(data)
} else if (type === '[object Object]') {
this.walk(data)
}
}
walk(data) {
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
observeArray(array) {
for (let i = 0; i < array.length; i++) {
observe(array[i])
}
}
}
// 定义 obj 对象的 key 属性的响应式。
// 这里利用了闭包,使得 value 变量 和 dep 常量一直没有被垃圾回收。
function defineReactive(obj, key, value) {
const dep = new Dep()
// 深度优先遍历。
const objKeyOb = observe(value)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
// 数据劫持逻辑。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 订阅逻辑。
dep.depend()
if (objKeyOb) {
objKeyOb.dep.depend()
}
return value
},
set: function reactiveSetter(newValue) {
if (value === newValue) {
return
}
// 发布逻辑。
dep.notify()
const objKeyOb = observe(newValue)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
value = newValue
}
})
}
// “筐”被抽象成了 Dep 实例。
class Dep {
// 响应式属性当前要订阅的 watcher。
static target = null
constructor() {
// 响应式属性已订阅的 watcher 列表。
this.subs = []
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
addSub(watcher) {
if (this.subs.indexOf(watcher) !== -1) {
return
}
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
// 回调函数被包装成了 Watcher 实例。
// TODO:暂时只实现了对 vm.$options.data 对象的第一层属性的监听。
const watcherQueue = []
let watcherId = 0
const targetStack = []
class Watcher {
constructor(vm, exp, cb, options = {}) {
this.id = ++watcherId
this.vm = vm
this.exp = exp
this.cb = cb
this.deps = []
this.value = null
this.lazy = this.dirty = !!options.lazy
if (!this.lazy) {
this.get()
}
}
get() {
Dep.target = this
targetStack.push(this)
if (typeof this.exp === 'function') {
this.value = this.exp.call(this.vm)
} else {
this.value = this.vm[this.exp]
}
targetStack.pop()
if (targetStack.length > 0) {
Dep.target = targetStack[targetStack.length - 1]
} else {
Dep.target = null
}
}
addDep(dep) {
if (this.deps.indexOf(dep) !== -1) {
return
}
this.deps.push(dep)
dep.addSub(this)
}
update() {
if (this.lazy) {
this.dirty = true
} else {
this.run()
}
}
run() {
if (watcherQueue.indexOf(this.id) !== -1) {
// 类似于 JavaScript 中的防抖逻辑。
return
}
watcherQueue.push(this.id)
const index = watcherQueue.length - 1
Promise.resolve().then(() => {
// 更新 this.value(watcher.value) 的值。
this.get()
this.cb.call(this.vm)
watcherQueue.splice(index, 1)
})
}
}
const mutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
mutationMethods.forEach(method => {
arrayMethods[method] = function(...args) {
if (method === 'push' || method === 'unshift' || method === 'splice') {
this.__ob__.observeArray(args)
}
const result = arrayProto[method].apply(this, args)
this.__ob__.dep.notify()
return result
}
})
Vue 对模板响应式更新的处理思路基本如下:
1、**模板编译:**如果传入了 template 或者 DOM,那么在 Vue 实例化的过程中,Vue 首先会把模板字符串转化成渲染函数(vm.$options.render)。
2、**虚拟 DOM:**Vue 借助这个渲染函数去响应式更新模板的时候,如果 Vue 直接去操作 DOM,那么会极大的消耗浏览器的性能。于是 Vue 引入 Virtual-DOM (虚拟 DOM),借助它来实现对 DOM 的按需更新。
实现模板编译
如果传入了 template 或者 DOM,那么在 Vue 实例化的过程中,Vue 首先会把模板字符串转化成渲染函数(vm.$options.render),这个过程就是 Vue 的模板编译。
Vue 模板编译的整体逻辑主要分为三个步骤:1、解析器:将模板字符串转换成 AST。2、**优化器:**对 AST 进行静态节点标记。主要是为了优化渲染性能。(这里不做介绍)3、**代码生成器:**将 AST 转换成 render 函数。
AST
AST,即抽象语法树,是源代码语法结构的抽象表示。JS AST 在线生成
Vue 中 AST 的代码示例如下:
{
children: [{…}], // 叶子节点没有 children 属性。
parent: {}, // 根节点的 parent 属性的值为 undefined。
tag: "div", // 元素节点的专属属性。
type: 1, // 1:元素节点。2:带变量的文本节点。3:纯文本节点。
expression:'"姓名:" + _s(name)', // 文本节点的专属属性。如果 type 值是 3,则 expression 值为 ''。
text:'姓名:{{name}}' // 文本节点的专属属性。text 值为文本节点编译前的字符串。
}
解析器(parser)
源代码被解析成 AST 的过程一般包含两个步骤:词法分析和语法分析。
Vue 中的解析器对模板字符串进行解析时,是每产生一个 token 便会立即对该 token 进行处理,即词法分析和语法分析同时进行,或者说没有词法分析只有语法分析。
下面以最单纯的 HTML 模板为例,阐述 Vue 中的解析器将模板字符串转换成 AST 的原理。(v-model、v-bind、v-if、v-for、@click 以及 HTML 中的单标签元素、DOM 属性、HTML 注释等情况都不予以考虑。)
**解析思路:**以 < 为标识符,代表开始标签或结束标签。使用栈结构去维护当前模板被解析到的层级。如果是开始标签,代表 AST 的层级 push 了一层;如果是结束标签,代表 AST 的层级 pop 了一层。
function parse(template) {
let root = null
let currentParent
const stack = []
// 跳过空白字符串。
template = template.trim()
while (template) {
const ltIndex = template.indexOf('<')
// ltIndex === -1 的情况不会出现。
// ltIndex > 0 标签前面有文本节点。
// ltIndex === 0 && template[ltIndex + 1] !== '/' 开始标签。
// ltIndex === 0 && template[ltIndex + 1] === '/' 结束标签。
if (ltIndex > 0) {
// type:1-元素节点;2-带变量的文本节点;3-纯文本节点。
const text = template.slice(0, ltIndex)
const element = parseText(text)
element.parent = currentParent
if (!currentParent.children) {
currentParent.children = []
}
currentParent.children.push(element)
template = template.slice(ltIndex)
} else if (ltIndex === 0 && template[ltIndex + 1] !== '/') {
const gtIndex = template.indexOf('>')
const element = {
parent: currentParent, // 根节点的 parent 属性值为 undefined。
tag: template.slice(ltIndex + 1, gtIndex), // 只考虑开始标签中没有任何属性的情况。
type: 1
}
if (currentParent) {
if (!currentParent.children) {
currentParent.children = []
}
currentParent.children.push(element)
} else {
root = element
}
currentParent = element
stack.push(element)
template = template.slice(gtIndex + 1)
} else if (ltIndex === 0 && template[ltIndex + 1] === '/') {
const gtIndex = template.indexOf('>')
// parse 函数执行完毕后 stack 值被设置为 []。
stack.pop()
// parse 函数执行完毕后 currentParent 值被设置为 undefined。
currentParent = stack[stack.length - 1]
// parse 函数执行完毕后 template 值被设置为 ''。
template = template.slice(gtIndex + 1)
}
}
return root
}
// 以 {{ 和 }} 为标识符,把 text 中的 '姓名:{{name}}' 转换成 '"姓名:" + _s(name)'。
function parseText(text) {
const originText = text
const tokens = []
// type:2-带变量的文本节点;3-纯文本节点。
let type = 3
while (text) {
const start = text.indexOf('{{')
const end = text.indexOf('}}')
if (start !== -1) {
type = 2
if (start > 0) {
tokens.push(JSON.stringify(text.slice(0, start)))
}
const exp = text.slice(start + 2, end)
tokens.push(`_s(${exp})`)
text = text.slice(end + 2)
} else {
tokens.push(JSON.stringify(text))
text = ''
}
}
const element = {
parent: null,
type,
expression: type === 2 ? tokens.join(' + ') : '',
text: originText
}
return element
}
render
本小结生成的 render 函数的函数体字符串是这样的:
'with (this) { return _c("div", {}, [_c("p", {}, [_v("姓名:" + _s(name))])]) }'。
其中 _c 函数的第三个参数的空值是 [],不是 undefined。
代码生成器(codegenerator)
**生成思路:**1、遍历 AST,对 AST 中的每个节点进行处理。2、遇到元素节点生成 _c("标签名", 属性对象, 后代数组) 格式的字符串。(后代数组为空时为 [],而不是 undefined。)3、遇到纯文本节点生成 _v("文本字符串") 格式的字符串。4、遇到带变量的文本节点生成 _v(_s(变量名)) 格式的字符串。5、为了让字符串中的变量能够在 render 函数中被正常取值,在遍历完 AST 后, 将生成的字符串整体外包一层 with(this)。6、将经过 with(this) 包装处理后的字符串作为函数体,生成一个 render 函数,并将这个 render 函数挂载到 vm.$options 上。
// 将 AST 转换为 render 函数的函数体的字符串。
function generate(ast) {
const code = genNode(ast)
return {
render: `with (this) { return ${code} }`
}
}
// 转换节点。
function genNode(node) {
if (node.type === 1) {
return genElement(node)
} else {
return genText(node)
}
}
// 转换元素节点。
function genElement(node) {
const children = genChildren(node)
// children 的空值是 '[]',不是 'undefined'。
const code = `_c(${JSON.stringify(node.tag)}, {}, ${children})`
return code
}
// 转换文本节点。
function genText(node) {
if (node.type === 2) {
return `_v(${node.expression})`
} else if (node.type === 3) {
return `_v(${JSON.stringify(node.text)})`
}
}
// 转换元素节点的子节点列表中的子节点。
function genChildren(node) {
// node.children 的空值为 undefined。
if (node.children) {
return '[' + node.children.map(child => genNode(child)).join(', ') + ']'
} else {
return '[]'
}
}
class Vue {
constructor(options) {
this.$options = options
this.initData()
this.initComputed()
this.initWatch()
this.initRenderFunction()
// this.initRenderWatch()
}
initData() {
// TODO:this.$options.data 还可能是一个函数。
const data = (this._data = this.$options.data)
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
// 数据代理逻辑。
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return data[keys[i]]
},
set: function proxySetter(newValue) {
data[keys[i]] = newValue
}
})
}
observe(data)
}
initComputed() {
const computed = this.$options.computed
if (computed) {
const keys = Object.keys(computed)
for (let i = 0; i < keys.length; i++) {
const watcher = new Watcher(this, computed[keys[i]], function() {}, { lazy: true })
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function computedGetter() {
if (watcher.dirty) {
watcher.get()
watcher.dirty = false
}
watcher.deps.forEach(dep => {
dep.depend()
})
return watcher.value
},
set: function computedSetter() {
console.warn('请不要给计算属性赋值!')
}
})
}
}
}
initWatch() {
const watch = this.$options.watch
if (watch) {
const keys = Object.keys(watch)
for (let i = 0; i < keys.length; i++) {
this.$watch(keys[i], watch[keys[i]])
}
}
}
initRenderFunction() {
let template
if (this.$options.template) {
template = this.$options.template
} else {
template = document.querySelector(this.$options.el).outerHtml
}
const ast = parse(template)
const code = generate(ast).render
// eslint-disable-next-line no-new-func
this.$options.render = new Function(code)
}
initRenderWatch() {
new Watcher(
this,
() => {
document.querySelector('#app').innerHTML = `<p>${this.name}</p>`
},
() => {}
)
}
$watch(exp, cb) {
new Watcher(this, exp, cb)
}
$set(target, key, value) {
const type = Object.prototype.toString.call(target)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
if (type === '[object Array]') {
const arratProto = Array.prototype
observe(value)
arratProto.splice.call(target, key, 1, value)
} else if (type === '[object Object]') {
defineReactive(target, key, value)
}
// 触发依赖 target 的 watchers。
target.__ob__.dep.notify()
}
}
// 观察 data 数据。
function observe(data) {
const type = Object.prototype.toString.call(data)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
return new Observer(data)
}
class Observer {
constructor(data) {
this.dep = new Dep()
Object.defineProperty(data, '__ob__', {
enumerable: false,
configurable: false,
value: this,
writable: true
})
const type = Object.prototype.toString.call(data)
if (type === '[object Array]') {
data.__proto__ = arrayMethods
this.observeArray(data)
} else if (type === '[object Object]') {
this.walk(data)
}
}
walk(data) {
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
observeArray(array) {
for (let i = 0; i < array.length; i++) {
observe(array[i])
}
}
}
// 定义 obj 对象的 key 属性的响应式。
// 这里利用了闭包,使得 value 变量 和 dep 常量一直没有被垃圾回收。
function defineReactive(obj, key, value) {
const dep = new Dep()
// 深度优先遍历。
const objKeyOb = observe(value)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
// 数据劫持逻辑。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 订阅逻辑。
dep.depend()
if (objKeyOb) {
objKeyOb.dep.depend()
}
return value
},
set: function reactiveSetter(newValue) {
if (value === newValue) {
return
}
// 发布逻辑。
dep.notify()
const objKeyOb = observe(newValue)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
value = newValue
}
})
}
// “筐”被抽象成了 Dep 实例。
class Dep {
// 响应式属性当前要订阅的 watcher。
static target = null
constructor() {
// 响应式属性已订阅的 watcher 列表。
this.subs = []
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
addSub(watcher) {
if (this.subs.indexOf(watcher) !== -1) {
return
}
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
// 回调函数被包装成了 Watcher 实例。
// TODO:暂时只实现了对 vm.$options.data 对象的第一层属性的监听。
const watcherQueue = []
let watcherId = 0
const targetStack = []
class Watcher {
constructor(vm, exp, cb, options = {}) {
this.id = ++watcherId
this.vm = vm
this.exp = exp
this.cb = cb
this.deps = []
this.value = null
this.lazy = this.dirty = !!options.lazy
if (!this.lazy) {
this.get()
}
}
get() {
Dep.target = this
targetStack.push(this)
if (typeof this.exp === 'function') {
this.value = this.exp.call(this.vm)
} else {
this.value = this.vm[this.exp]
}
targetStack.pop()
if (targetStack.length > 0) {
Dep.target = targetStack[targetStack.length - 1]
} else {
Dep.target = null
}
}
addDep(dep) {
if (this.deps.indexOf(dep) !== -1) {
return
}
this.deps.push(dep)
dep.addSub(this)
}
update() {
if (this.lazy) {
this.dirty = true
} else {
this.run()
}
}
run() {
if (watcherQueue.indexOf(this.id) !== -1) {
// 类似于 JavaScript 中的防抖逻辑。
return
}
watcherQueue.push(this.id)
const index = watcherQueue.length - 1
Promise.resolve().then(() => {
// 更新 this.value(watcher.value) 的值。
this.get()
this.cb.call(this.vm)
watcherQueue.splice(index, 1)
})
}
}
const mutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
mutationMethods.forEach(method => {
arrayMethods[method] = function(...args) {
if (method === 'push' || method === 'unshift' || method === 'splice') {
this.__ob__.observeArray(args)
}
const result = arrayProto[method].apply(this, args)
this.__ob__.dep.notify()
return result
}
})
实现虚拟 DOM
什么是虚拟 DOM?
真实 DOM。
<ul>
<li>1</li>
<li>2</li>
</ul>
虚拟 DOM。
{
tag: 'ul',
attrs: {},
children: [
{
tag: 'li',
attrs: {},
children: [
{
tag: null,
attrs: {},
children: [], // children 的空值为 []。
text: '1'
}
]
},
......
]
}
虚拟 DOM 有什么用?
1、**性能优化:**当数据发生变化时,Vue 会先在内存中构建虚拟 DOM 树,然后通过比较新旧虚拟 DOM 树的差异,最终只更新必要的部分到真实 DOM 树中。虚拟 DOM 的使用减少了 Vue 操作真实 DOM 的次数,从而提高了 Vue 渲染页面的性能。
2、**跨平台能力:**虚拟 DOM 是一个与平台无关的抽象层,它的使用使得 Vue 可以在浏览器、移动端和服务端(例如服务端渲染时)等多个环境中运行。
由渲染函数生成虚拟 DOM
定义一个简单的 VNode 类,并实现渲染函数中的 _c、_v、_s 函数。然后运行 vm.$options.render.call(vm) 即可得到虚拟 DOM。
class VNode {
constructor(tag, attrs, children, text) {
this.tag = tag
this.attrs = attrs
this.children = children // children 的空值为 []。
this.text = text
this.elm = undefined
}
}
class Vue {
......
_c(tag, attrs, children) {
return new VNode(tag, attrs, children)
}
_v(text) {
return new VNode(undefind, undefind, undefind, text)
}
_s(value) {
if (value === null || value === undefined) {
return ''
} else if (typeof value === 'object') {
return JSON.stringify(value)
} else {
return String(value)
}
}
......
}
实现 Diff 和 Patch
在 Vue2 中,Diff 和 Patch 是虚拟 DOM 算法的两个关键步骤:1、**Diff(差异计算):**Diff 是指将新旧虚拟 DOM 树进行比较,进而找出它们之间的差异;2、**Patch(补丁应用):**Patch 是指将这些差异映射到真实 DOM 树上,使得真实 DOM 树与新的虚拟 DOM 树保持一致。
通过 Diff 和 Patch 的配合,Vue 可以凭借较少次数的真实 DOM 操作来实现高效地页面更新。
注意,Vue2 中的虚拟 DOM 算法是基于全量比较的,即每次页面更新都会对整个虚拟 DOM 树进行比较,这在大型应用中可能会导致性能问题。为了解决这个问题,Vue3 引入了基于静态分析的编译优化,使用了更高效的增量更新算法。
class Vue {
constructor(options) {
this.$options = options
this.$el = undefined
this._vnode = undefined
this._watcher = undefined
this.initData()
this.initComputed()
this.initWatch()
this.initRenderFunction()
this.$mount(options.el)
}
......
initRenderFunction() {
let template
if (this.$options.template) {
template = this.$options.template
} else {
template = document.querySelector(this.$options.el).outerHtml
}
const ast = parse(template)
const code = generate(ast).render
// eslint-disable-next-line no-new-func
this.$options.render = new Function(code)
}
......
$mount(el) {
this.$el = document.querySelector(el)
this._watcher = new Watcher(
this,
() => {
this._update(this.$options.render.call(this))
},
() => {}
)
}
......
_update(vnode) {
if (this._vnode) {
patch(this._vnode, vnode)
} else {
patch(this.$el, vnode)
}
this._vnode = vnode
}
......
}
Vue 对虚拟 DOM 进行 patch 的逻辑基于 snabbdom 算法。patch 函数接受两个参数:旧的虚拟 DOM 和新的虚拟 DOM。(以下代码不考虑节点的属性和节点的 key。)
function patch(oldVnode, newVnode) {
const el = oldVnode.elm
const parent = el.parentNode
const isRealElement = oldVnode.nodeType
if (isRealElement) {
parent.replaceChild(createElement(newVnode), oldVnode)
return
}
if (!newVnode) {
parent.removeChild(el)
} else if (isChange(newVnode, oldVnode)) {
newVnode.elm = createElement(newVnode)
parent.replaceChild(newVnode.elm, el)
} else if (!isChange(newVnode, oldVnode)) {
// 渲染性能的提升就在这里。
newVnode.elm = el
const newLength = newVnode.children.length
const oldLength = oldVnode.children.length
for (let i = 0; i < newLength || i < oldLength; i++) {
if (i >= oldLength) {
el.appendChild(createElement(newVnode.children[i]))
} else {
patch(oldVnode.children[i], newVnode.children[i])
}
}
}
}
// 由虚拟 DOM 创建真实 DOM。
function createElement(vnode) {
// 文本节点。
if (!vnode.tag) {
const el = document.createTextNode(vnode.text)
vnode.elm = el
return el
}
// 元素节点。
const el = document.createElement(vnode.tag)
vnode.elm = el
// 在父子真实 DOM 之间建立关系。
vnode.children.map(createElement).forEach(subEl => {
el.appendChild(subEl)
})
return el
}
// 判断新的虚拟 DOM 相对于旧的虚拟 DOM 是否发生了变化。
function isChange(newVnode, oldVnode) {
return newVnode.tag !== oldVnode.tag || newVnode.text !== oldVnode.text
}
虚拟 DOM 代码总结
class Vue {
constructor(options) {
this.$options = options
this.$el = undefined
this._vnode = undefined
this._watcher = undefined
this.initData()
this.initComputed()
this.initWatch()
this.initRenderFunction()
this.$mount(options.el)
}
initData() {
// TODO:this.$options.data 还可能是一个函数。
const data = (this._data = this.$options.data)
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
// 数据代理逻辑。
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return data[keys[i]]
},
set: function proxySetter(newValue) {
data[keys[i]] = newValue
}
})
}
observe(data)
}
initComputed() {
const computed = this.$options.computed
if (computed) {
const keys = Object.keys(computed)
for (let i = 0; i < keys.length; i++) {
const watcher = new Watcher(this, computed[keys[i]], function() {}, { lazy: true })
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
get: function computedGetter() {
if (watcher.dirty) {
watcher.get()
watcher.dirty = false
}
watcher.deps.forEach(dep => {
dep.depend()
})
return watcher.value
},
set: function computedSetter() {
console.warn('请不要给计算属性赋值!')
}
})
}
}
}
initWatch() {
const watch = this.$options.watch
if (watch) {
const keys = Object.keys(watch)
for (let i = 0; i < keys.length; i++) {
this.$watch(keys[i], watch[keys[i]])
}
}
}
initRenderFunction() {
let template
if (this.$options.template) {
template = this.$options.template
} else {
template = document.querySelector(this.$options.el).outerHtml
}
const ast = parse(template)
const code = generate(ast).render
// eslint-disable-next-line no-new-func
this.$options.render = new Function(code)
}
$watch(exp, cb) {
new Watcher(this, exp, cb)
}
$set(target, key, value) {
const type = Object.prototype.toString.call(target)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
if (type === '[object Array]') {
const arrayProto = Array.prototype
observe(value)
arrayProto.splice.call(target, key, 1, value)
} else if (type === '[object Object]') {
defineReactive(target, key, value)
}
// 触发依赖 target 的 watchers。
target.__ob__.dep.notify()
}
$mount(el) {
this.$el = document.querySelector(el)
this._watcher = new Watcher(
this,
() => {
this._update(this.$options.render.call(this))
},
() => {}
)
}
_update(vnode) {
if (this._vnode) {
patch(this._vnode, vnode)
} else {
patch(this.$el, vnode)
}
this._vnode = vnode
}
_c(tag, attrs, children) {
return new VNode(tag, attrs, children)
}
_v(text) {
return new VNode(undefind, undefind, undefind, text)
}
_s(value) {
if (value === null || value === undefined) {
return ''
} else if (typeof value === 'object') {
return JSON.stringify(value)
} else {
return String(value)
}
}
}
// 观察 data 数据。
function observe(data) {
const type = Object.prototype.toString.call(data)
if (type !== '[object Object]' && type !== '[object Array]') {
return
}
return new Observer(data)
}
class Observer {
constructor(data) {
this.dep = new Dep()
Object.defineProperty(data, '__ob__', {
enumerable: false,
configurable: false,
value: this,
writable: true
})
const type = Object.prototype.toString.call(data)
if (type === '[object Array]') {
data.__proto__ = arrayMethods
this.observeArray(data)
} else if (type === '[object Object]') {
this.walk(data)
}
}
walk(data) {
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
observeArray(array) {
for (let i = 0; i < array.length; i++) {
observe(array[i])
}
}
}
// 定义 obj 对象的 key 属性的响应式。
// 这里利用了闭包,使得 value 变量 和 dep 常量一直没有被垃圾回收。
function defineReactive(obj, key, value) {
const dep = new Dep()
// 深度优先遍历。
const objKeyOb = observe(value)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
// 数据劫持逻辑。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 订阅逻辑。
dep.depend()
if (objKeyOb) {
objKeyOb.dep.depend()
}
return value
},
set: function reactiveSetter(newValue) {
if (value === newValue) {
return
}
// 发布逻辑。
dep.notify()
const objKeyOb = observe(newValue)
if (objKeyOb && obj[key] && obj[key].__ob__) {
objKeyOb.dep = obj[key].__ob__.dep
}
value = newValue
}
})
}
// “筐”被抽象成了 Dep 实例。
class Dep {
// 响应式属性当前要订阅的 watcher。
static target = null
constructor() {
// 响应式属性已订阅的 watcher 列表。
this.subs = []
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
addSub(watcher) {
if (this.subs.indexOf(watcher) !== -1) {
return
}
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
// 回调函数被包装成了 Watcher 实例。
// TODO:暂时只实现了对 vm.$options.data 对象的第一层属性的监听。
const watcherQueue = []
let watcherId = 0
const targetStack = []
class Watcher {
constructor(vm, exp, cb, options = {}) {
this.id = ++watcherId
this.vm = vm
this.exp = exp
this.cb = cb
this.deps = []
this.value = null
this.lazy = this.dirty = !!options.lazy
if (!this.lazy) {
this.get()
}
}
get() {
Dep.target = this
targetStack.push(this)
if (typeof this.exp === 'function') {
this.value = this.exp.call(this.vm)
} else {
this.value = this.vm[this.exp]
}
targetStack.pop()
if (targetStack.length > 0) {
Dep.target = targetStack[targetStack.length - 1]
} else {
Dep.target = null
}
}
addDep(dep) {
if (this.deps.indexOf(dep) !== -1) {
return
}
this.deps.push(dep)
dep.addSub(this)
}
update() {
if (this.lazy) {
this.dirty = true
} else {
this.run()
}
}
run() {
if (watcherQueue.indexOf(this.id) !== -1) {
// 类似于 JavaScript 中的防抖逻辑。
return
}
watcherQueue.push(this.id)
const index = watcherQueue.length - 1
Promise.resolve().then(() => {
// 更新 this.value(watcher.value) 的值。
this.get()
this.cb.call(this.vm)
watcherQueue.splice(index, 1)
})
}
}
const mutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
mutationMethods.forEach(method => {
arrayMethods[method] = function(...args) {
if (method === 'push' || method === 'unshift' || method === 'splice') {
this.__ob__.observeArray(args)
}
const result = arrayProto[method].apply(this, args)
this.__ob__.dep.notify()
return result
}
})
function parse(template) {
let root = null
let currentParent
const stack = []
// 跳过空白字符串。
template = template.trim()
while (template) {
const ltIndex = template.indexOf('<')
// ltIndex === -1 的情况不会出现。
// ltIndex > 0 标签前面有文本节点。
// ltIndex === 0 && template[ltIndex + 1] !== '/' 开始标签。
// ltIndex === 0 && template[ltIndex + 1] === '/' 结束标签。
if (ltIndex > 0) {
// type:1-元素节点;2-带变量的文本节点;3-纯文本节点。
const text = template.slice(0, ltIndex)
const element = parseText(text)
element.parent = currentParent
if (!currentParent.children) {
currentParent.children = []
}
currentParent.children.push(element)
template = template.slice(ltIndex)
} else if (ltIndex === 0 && template[ltIndex + 1] !== '/') {
const gtIndex = template.indexOf('>')
const element = {
parent: currentParent, // 根节点的 parent 属性值为 undefined。
tag: template.slice(ltIndex + 1, gtIndex), // 只考虑开始标签中没有任何属性的情况。
type: 1
}
if (currentParent) {
if (!currentParent.children) {
currentParent.children = []
}
currentParent.children.push(element)
} else {
root = element
}
currentParent = element
stack.push(element)
template = template.slice(gtIndex + 1)
} else if (ltIndex === 0 && template[ltIndex + 1] === '/') {
const gtIndex = template.indexOf('>')
// parse 函数执行完毕后 stack 值被设置为 []。
stack.pop()
// parse 函数执行完毕后 currentParent 值被设置为 undefined。
currentParent = stack[stack.length - 1]
// parse 函数执行完毕后 template 值被设置为 ''。
template = template.slice(gtIndex + 1)
}
}
return root
}
// 以 {{ 和 }} 为标识符,把 text 中的 '姓名:{{name}}' 转换成 '"姓名:" + _s(name)'。
function parseText(text) {
const originText = text
const tokens = []
// type:2-带变量的文本节点;3-纯文本节点。
let type = 3
while (text) {
const start = text.indexOf('{{')
const end = text.indexOf('}}')
if (start !== -1) {
type = 2
if (start > 0) {
tokens.push(JSON.stringify(text.slice(0, start)))
}
const exp = text.slice(start + 2, end)
tokens.push(`_s(${exp})`)
text = text.slice(end + 2)
} else {
tokens.push(JSON.stringify(text))
text = ''
}
}
const element = {
parent: null,
type,
expression: type === 2 ? tokens.join(' + ') : '',
text: originText
}
return element
}
// 将 AST 转换为 render 函数的函数体的字符串。
function generate(ast) {
const code = genNode(ast)
return {
render: `with (this) { return ${code} }`
}
}
// 转换节点。
function genNode(node) {
if (node.type === 1) {
return genElement(node)
} else {
return genText(node)
}
}
// 转换元素节点。
function genElement(node) {
const children = genChildren(node)
// children 的空值是 '[]',不是 'undefined'。
const code = `_c(${JSON.stringify(node.tag)}, {}, ${children})`
return code
}
// 转换文本节点。
function genText(node) {
if (node.type === 2) {
return `_v(${node.expression})`
} else if (node.type === 3) {
return `_v(${JSON.stringify(node.text)})`
}
}
// 转换元素节点的子节点列表中的子节点。
function genChildren(node) {
// node.children 的空值为 undefined。
if (node.children) {
return '[' + node.children.map(child => genNode(child)).join(', ') + ']'
} else {
return '[]'
}
}
class VNode {
constructor(tag, attrs, children, text) {
this.tag = tag
this.attrs = attrs
this.children = children // children 的空值为 []。
this.text = text
this.elm = undefined
}
}
function patch(oldVnode, newVnode) {
const el = oldVnode.elm
const parent = el.parentNode
const isRealElement = oldVnode.nodeType
if (isRealElement) {
parent.replaceChild(createElement(newVnode), oldVnode)
return
}
if (!newVnode) {
parent.removeChild(el)
} else if (isChange(newVnode, oldVnode)) {
newVnode.elm = createElement(newVnode)
parent.replaceChild(newVnode.elm, el)
} else if (!isChange(newVnode, oldVnode)) {
// 渲染性能的提升就在这里。
newVnode.elm = el
const newLength = newVnode.children.length
const oldLength = oldVnode.children.length
for (let i = 0; i < newLength || i < oldLength; i++) {
if (i >= oldLength) {
el.appendChild(createElement(newVnode.children[i]))
} else {
patch(oldVnode.children[i], newVnode.children[i])
}
}
}
}
// 由虚拟 DOM 创建真实 DOM。
function createElement(vnode) {
// 文本节点。
if (!vnode.tag) {
const el = document.createTextNode(vnode.text)
vnode.elm = el
return el
}
// 元素节点。
const el = document.createElement(vnode.tag)
vnode.elm = el
// 在父子真实 DOM 之间建立关系。
vnode.children.map(createElement).forEach(subEl => {
el.appendChild(subEl)
})
return el
}
// 判断新的虚拟 DOM 相对于旧的虚拟 DOM 是否发生了变化。
function isChange(newVnode, oldVnode) {
return newVnode.tag !== oldVnode.tag || newVnode.text !== oldVnode.text
}
🔥 Vue3 实现超丝滑打字机效果组件(可复用、高定制)
在前端开发中,打字机效果能极大提升页面的交互趣味性和视觉体验,比如 AI 聊天回复、个性化介绍页等场景都非常适用。本文将分享一个基于 Vue3 + Composition API 开发的高性能、高定制化打字机组件,支持打字/删除循环、光标闪烁、自定义样式等核心功能,且代码结构清晰、易于扩展。
🎯 组件特性
- ✅ 自定义打字速度、删除速度、循环延迟
- ✅ 支持光标显示/隐藏、闪烁效果开关
- ✅ 打字完成后自动删除(可选)+ 循环播放
- ✅ 完全自定义样式(字体、颜色、大小等)
- ✅ 暴露控制方法(开始/暂停),支持手动干预
- ✅ 无第三方依赖,纯原生 Vue3 实现
- ✅ 性能优化:组件卸载自动清理定时器,避免内存泄漏
📝 完整组件代码
<template>
<div class="typewriter-container" :style="fontsConStyle">
<!-- 打字文本 - 逐字符渲染 -->
<span class="typewriter-text">
<span
v-for="(char, index) in displayedText"
:key="index"
class="character"
:data-index="index"
>
{{ char }}
</span>
</span>
<!-- 光标 - 精准控制显示/闪烁 -->
<span
v-if="showCursor && isCursorVisible"
class="cursor"
:class="{ 'blink': showBlinkCursor }"
aria-hidden="true"
/>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed, watch, watchEffect } from "vue";
// 组件 Props 定义(带完整校验)
const props = defineProps({
// 要显示的文本内容
text: {
type: String,
required: true,
default: ""
},
// 打字速度(ms/字符)
speed: {
type: Number,
default: 80,
validator: (value) => value > 0
},
// 是否显示光标
showCursor: {
type: Boolean,
default: true
},
// 光标是否闪烁
blinkCursor: {
type: Boolean,
default: true
},
// 是否自动开始打字
autoStart: {
type: Boolean,
default: true
},
// 是否循环播放
loop: {
type: Boolean,
default: false
},
// 循环延迟(打字完成后等待时间)
loopDelay: {
type: Number,
default: 1000,
validator: (value) => value >= 0
},
// 容器样式(自定义字体、颜色等)
fontsConStyle: {
type: Object,
default: () => ({
fontSize: "2rem",
fontFamily: "'Courier New', monospace",
color: "#333",
lineHeight: "1.5"
})
},
// 是否开启删除效果
deleteEffect: {
type: Boolean,
default: false
},
// 删除速度(ms/字符)
deleteSpeed: {
type: Number,
default: 30,
validator: (value) => value > 0
},
// 字符入场动画开关
charAnimation: {
type: Boolean,
default: true
}
});
// 响应式状态
const displayedText = ref(""); // 当前显示的文本
const currentIndex = ref(0); // 当前字符索引
const isPlaying = ref(false); // 是否正在播放
const isDeleting = ref(false); // 是否正在删除
const isCursorVisible = ref(true);// 光标是否显示
// 定时器标识(用于清理)
let intervalId = null;
let timeoutId = null;
let cursorTimer = null;
// 计算属性:控制光标闪烁状态
const showBlinkCursor = computed(() => {
return props.blinkCursor &&
!isDeleting.value &&
(displayedText.value.length === props.text.length || displayedText.value.length === 0);
});
// 工具函数:清除所有定时器
const clearAllTimers = () => {
if (intervalId) clearInterval(intervalId);
if (timeoutId) clearTimeout(timeoutId);
if (cursorTimer) clearInterval(cursorTimer);
intervalId = null;
timeoutId = null;
cursorTimer = null;
};
// 核心方法:开始打字
const startTyping = () => {
// 重置状态
clearAllTimers();
isPlaying.value = true;
isDeleting.value = false;
currentIndex.value = 0;
displayedText.value = "";
isCursorVisible.value = true;
// 打字逻辑
intervalId = setInterval(() => {
if (currentIndex.value < props.text.length) {
displayedText.value = props.text.substring(0, currentIndex.value + 1);
currentIndex.value++;
} else {
// 打字完成
clearInterval(intervalId);
isPlaying.value = false;
// 循环逻辑
if (props.loop) {
if (props.deleteEffect) {
timeoutId = setTimeout(startDeleting, props.loopDelay);
} else {
timeoutId = setTimeout(startTyping, props.loopDelay);
}
}
}
}, props.speed);
};
// 核心方法:开始删除
const startDeleting = () => {
clearAllTimers();
isDeleting.value = true;
intervalId = setInterval(() => {
if (currentIndex.value > 0) {
currentIndex.value--;
displayedText.value = props.text.substring(0, currentIndex.value);
} else {
// 删除完成
clearInterval(intervalId);
isDeleting.value = false;
// 循环打字
if (props.loop) {
timeoutId = setTimeout(startTyping, props.loopDelay);
} else {
isCursorVisible.value = false; // 非循环模式下删除完成隐藏光标
}
}
}, props.deleteSpeed);
};
// 监听文本变化:自动重启打字(适配动态文本场景)
watch(() => props.text, (newText) => {
if (newText && props.autoStart) {
startTyping();
}
}, { immediate: true });
// 初始化光标闪烁(非闪烁模式下固定显示)
watchEffect(() => {
if (props.showCursor && !cursorTimer) {
cursorTimer = setInterval(() => {
if (!showBlinkCursor.value) {
isCursorVisible.value = true;
} else {
isCursorVisible.value = !isCursorVisible.value;
}
}, 500);
}
});
// 组件挂载:自动开始打字
onMounted(() => {
if (props.autoStart && props.text) {
startTyping();
}
});
// 组件卸载:清理所有定时器(避免内存泄漏)
onBeforeUnmount(() => {
clearAllTimers();
});
// 暴露组件方法(供父组件调用)
defineExpose({
start: startTyping, // 手动开始
pause: clearAllTimers, // 暂停
restart: () => { // 重启
clearAllTimers();
startTyping();
},
isPlaying, // 当前播放状态
isDeleting // 当前删除状态
});
</script>
<style scoped>
/* 容器样式 - 适配行内/块级显示 */
.typewriter-container {
display: inline-flex;
align-items: center;
position: relative;
font-size: inherit;
line-height: inherit;
font-family: inherit;
white-space: pre-wrap; /* 支持换行符 */
word-break: break-all; /* 防止长文本溢出 */
}
/* 文本容器 */
.typewriter-text {
display: inline;
font-size: inherit;
line-height: inherit;
font-family: inherit;
color: inherit;
}
/* 字符样式 - 入场动画 */
.character {
display: inline-block;
animation: typeIn 0.1s ease-out forwards;
opacity: 0;
}
/* 字符入场动画 */
@keyframes typeIn {
0% {
transform: translateY(5px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
/* 光标样式 - 垂直居中优化 */
.cursor {
display: inline-block;
width: 2px;
height: 1.2em; /* 匹配字体高度 */
background-color: currentColor;
margin-left: 2px;
vertical-align: middle;
position: relative;
top: 0;
opacity: 1;
}
/* 光标闪烁动画 */
.cursor.blink {
animation: blink 1s infinite step-end;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* 禁用字符动画的样式 */
:deep(.character) {
animation: none !important;
opacity: 1 !important;
}
</style>
🚀 核心优化点说明
1. 功能增强
- 新增
charAnimation属性:可开关字符入场动画,适配不同场景 - 支持动态文本:监听
text属性变化,文本更新自动重启打字 - 优化光标逻辑:非闪烁模式下光标固定显示,避免闪烁干扰
- 新增
restart方法:支持手动重启打字效果 - 文本换行支持:添加
white-space: pre-wrap,兼容带换行符的文本
2. 性能优化
- 定时器统一管理:所有定时器集中清理,避免内存泄漏
- 减少不必要渲染:通过
watchEffect精准控制光标定时器创建/销毁 - 样式优化:使用
currentColor继承文本颜色,光标颜色与文本一致 - 边界处理:添加
word-break: break-all,防止长文本溢出
3. 代码健壮性
- 完善 Prop 校验:所有数值类型添加范围校验,避免非法值
- 状态重置:每次开始打字前重置所有状态,避免多轮执行冲突
- 注释完善:关键逻辑添加注释,提升代码可读性
📖 使用示例
基础使用
<template>
<Typewriter
text="Hello Vue3! 这是一个超丝滑的打字机效果组件✨"
speed="50"
/>
</template>
<script setup>
import Typewriter from './components/Typewriter.vue';
</script>
高级使用(循环+删除效果)
<template>
<div>
<Typewriter
ref="typewriterRef"
text="Vue3 打字机组件 | 支持循环删除 | 自定义样式"
:speed="60"
:deleteSpeed="40"
:loop="true"
:deleteEffect="true"
:loopDelay="1500"
:fontsConStyle="{
fontSize: '1.5rem',
color: '#409eff',
fontFamily: '微软雅黑'
}"
/>
<button @click="handleRestart">重启打字</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Typewriter from './components/Typewriter.vue';
const typewriterRef = ref();
// 手动重启打字
const handleRestart = () => {
typewriterRef.value.restart();
};
</script>
🎨 样式自定义说明
| 属性 | 说明 | 默认值 |
|---|---|---|
| fontSize | 字体大小 | 2rem |
| fontFamily | 字体 | 'Courier New', monospace |
| color | 文本颜色 | #333 |
| lineHeight | 行高 | 1.5 |
你可以通过 fontsConStyle 属性完全自定义组件样式,例如:
fontsConStyle: {
fontSize: "18px",
color: "#e6a23c",
fontWeight: "bold",
background: "#f5f7fa",
padding: "10px 15px",
borderRadius: "8px"
}
🛠️ 扩展方向
- 自定义字符动画:通过 Prop 传入动画类名,支持不同的字符入场效果
- 分段打字:支持数组形式的文本,分段打字+间隔
- 速度渐变:实现打字速度由快到慢/由慢到快的效果
- 暂停/继续:扩展暂停后继续打字的功能(记录当前索引)
- 结合 AI 流式响应:对接 AI 接口的流式返回,实时更新打字文本
📌 总结
这个打字机组件基于 Vue3 Composition API 开发,具备高复用性、高定制性的特点,核心优化点如下:
- 完善的定时器管理,避免内存泄漏
- 精准的状态控制,支持打字/删除/循环全流程
- 灵活的样式自定义,适配不同业务场景
- 暴露控制方法,支持父组件手动干预
组件可直接集成到 Vue3 项目中,适用于 AI 聊天、个人主页、产品介绍等需要打字机效果的场景,开箱即用!
npm install 核心流程
npm install 核心流程
作为前端开发,npm install 天天用,但这行简单的命令背后,npm 其实按固定流程把依赖安装的事安排得明明白白!不用深究底层原理,这篇文章用最直白的话讲清核心步骤,看完秒懂,轻松解决日常安装依赖的小问题~
第一步:先找配置,定好安装规则
执行 npm install 后,npm 第一步不下载,先查找项目和系统的配置文件(比如.npmrc),确定这些关键信息:
- 依赖从哪下载(镜像源,比如国内常用的淘宝镜像)
- 下载的包存在哪(缓存目录,避免重复下载)
- 安装到哪个路径(默认项目根目录的
node_modules)
简单说,就是先“定规矩”,再开始干活~
第二步:核心分支判断!有没有package-lock.json?
这是整个安装流程的关键分叉口,npm 会先检查项目根目录有没有package-lock.json文件(依赖版本快照,记录上一次安装的精确依赖信息),分两种情况处理,核心都是为了保证版本一致、提升安装速度。
情况1:有package-lock.json文件
-
先校验版本一致性 检查lock文件里的依赖版本,是否符合
package.json里的版本范围(比如package.json写^2.0.0,lock文件里2.1.0、2.2.0都算符合)。 符合:按lock文件的精确版本继续; 不符合:忽略旧lock文件,按package.json重新处理。 -
拉取包信息,构建并扁平化依赖树 按lock文件的信息,从镜像源获取依赖的元数据,接着构建依赖树(项目依赖的包是一级依赖,包又依赖的包是二级依赖,以此类推)。 关键操作扁平化处理:把能共享的子依赖提升到
node_modules根目录,避免层级过深、重复安装,省空间又快! -
缓存判断,安装依赖+更新lock文件
- 有缓存:直接把缓存里的包解压到
node_modules,不用重新下载; - 无缓存:从镜像源下载包→检查文件完整性(防止损坏)→存入缓存(下次用)→解压到
node_modules; 最后更新lock文件,保证快照最新。
- 有缓存:直接把缓存里的包解压到
情况2:没有package-lock.json文件
没有lock文件就简单了,直接按package.json来,步骤少了版本校验,其余和上面一致:
拉取远程包信息→构建并扁平化依赖树→缓存判断(有则解压,无则下载+存缓存)→解压到node_modules→生成全新的lock文件,为下一次安装留好精确版本快照。
核心流程一句话总结
输入 npm install → 查找并加载配置文件(.npmrc 等)
→ 检查项目根目录是否有 package-lock.json?
→ 是 → 校验 lock 文件与 package.json 版本是否一致?
→ 一致 → 拉取远程包信息 → 构建依赖树(扁平化)→ 检查缓存?
→ 有 → 解压缓存到 node_modules → 更新 lock 文件
→ 无 → 下载依赖 → 校验完整性 → 存入缓存 → 解压到 node_modules → 更新 lock 文件
→ 不一致 → 按 package.json 重新拉取包信息 → 构建依赖树(扁平化)→ 缓存判断与安装 → 生成/更新 lock 文件
→ 否 → 拉取远程包信息(基于 package.json)→ 构建依赖树(扁平化)→ 缓存判断与安装 → 生成 lock 文件
→ 安装完成
![]()
日常开发
1. 缓存超有用,出问题清一在这里插入图片描述
下
缓存是npm提速的关键,第一次下载的包会存起来,后续安装直接复用。如果遇到安装报错、包损坏,执行npm cache clean --force强制清缓存,重新安装大概率解决。
2. package-lock.json别随便删/改
这个文件是团队协作、生产环境的“版本保障”,删了重新安装可能导致依赖版本变化,项目出问题。真要改版本,先改package.json,再重新npm install自动更新lock文件。
后台管理系统 Vite + elementPlus
弄这个项目缘由
本想学习下vue3 的后台管理项目, 借鉴了vbtn-admin github地址 线上地址, 颜值在线, 但是封装太骚了改代码太累。就自己额外处理了下。
做到简单易懂 开箱即用
这是一个前后端分离的 monorepo 示例项目,使用 pnpm workspace 管理前端(Vite + Tailwind + shadcn-ui)和后端(Node/Express 或自定义后端)。
项目结构
.
├── 📁 backend/ # 后端项目
├── 📁 frontend/ # 前端项目
├── 📄 deploy.sh # 部署脚本
├── 📄 pnpm-lock.yaml # pnpm 锁文件
└── 📄 pnpm-workspace.yaml # pnpm 工作区配置
🚀 快速开始
1. 克隆仓库
git clone https://github.com/hangfengnice/vite-admin-ele.git
cd vite-admin-ele
2. 安装依赖
npm install -g pnpm
pnpm install
3. 启动项目
#同时启动前后端
pnpm run all
#启动前端
pnpm run dev
# 或者
pnpm --filter frontend dev
#启动后端
pnpm run back
# 或者
pnpm --filter backend dev
4. 一键部署阿里云
# 使用 chmod 添加执行权限(第一次)
chmod +x deploy.sh
# 部署
./deploy.sh
5. 开发配置
阿里云需要装
# 镜像 Ubuntu Server 24.04 LTS
# 安装 Node.js 24 LTS
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
sudo apt install -y nodejs
# 安装 PM2
sudo npm install -g pm2
# 安装 Nginx
sudo apt install -y nginx
# 安装 myswl
sudo apt install -y mysql-server
6.本地配置
# 电脑 mac
# node -v
# v24.12.0
# pnpm -v
# 10.28.1
# 本地额外装了mysql
这可能是 Vue 生态最完整的语音智能体组件库
vue3+vite使用unocss和vant做移动端开发适配,使用lib-flexible适配RemToPx
lib-flexible设置根字号
npm i lib-flexible
//然后在mian.js中引入
uno.config.js设置
根目录创建uno.config.js文件,需要安装 npm i @unocss/preset-rem-to-px
import { defineConfig, presetWind3 } from 'unocss'
import presetRemToPx from '@unocss/preset-rem-to-px'
export default defineConfig({
presets: [
presetWind3(), //预设样式
presetRemToPx() // 重点,把预设的rem转成px,这样postCssPxToRem就可以转了 例如预设样式 text-base的font-size:1rem;经过转换变成16px,在经过postCssPxToRem转换成rem
]
})
这个是vite的配置
import postCssPxToRem from 'postcss-pxtorem'
import UnoCSS from 'unocss/vite'
export default defineConfig({
plugins: [
UnoCSS(),
],
css: {
postcss: {
plugins: [
postCssPxToRem({
rootValue: 37.5,
unitPrecision: 6,
minPixelValue: 1,
propList: ['*'],
mediaQuery: false,
})
]
}
}
)}
其他的按照vant文档正常使用就好了
useTemplateRef和ref的区别
useTemplateRef 和 ref 都是用来创建响应式引用(Reactive References)的,但在 Vue 3.5+ 中,useTemplateRef 是一个专门为模板引用(Template Refs)设计的组合式 API (Composable) 。让我们详细对比一下它们:
ref
-
核心功能:
ref是 Vue 最基础的响应式系统 API 之一。它可以包装任何值(原始类型、对象、DOM 元素、组件实例等),使其成为响应式的。 -
用途广泛:
- 存储和响应式地更新本地组件状态(如
count = ref(0))。 - 作为模板引用(虽然在 3.5+ 之前常用)。
- 在任何需要响应式引用的地方。
- 存储和响应式地更新本地组件状态(如
-
在模板引用中的用法 (旧方式) :
<template> <div ref="divRef">Hello World</div> </template> <script setup> import { ref, onMounted } from 'vue'; const divRef = ref(null); // 创建一个 ref onMounted(() => { // divRef.value 现在是 DOM 元素 console.log(divRef.value); // <div>Hello World</div> divRef.value.focus(); // 例如,聚焦到元素上 }); </script>-
问题: 在
<script setup>中,divRef会暴露给模板,即使你只想在脚本内部使用它。这可能会污染模板的上下文。
-
问题: 在
useTemplateRef (Vue 3.5+)
-
核心功能: 专门用于获取对模板中元素或组件的引用。它返回一个getter 函数,而不是一个 ref 对象。
-
目的: 解决
ref作为模板引用时暴露到模板上下文的问题,提供更清晰、更符合直觉的 API。 -
用途: 仅用于模板引用。
-
返回值: 一个 getter 函数,调用它会返回最新的模板引用值。这个函数本身是响应式的,但其返回值(即引用的元素或组件实例)不是。
-
优势:
-
不污染模板上下文:
useTemplateRef返回的 getter 不会被自动暴露到模板中,保持了模板上下文的整洁。 -
意图明确: 使用
useTemplateRef明确表示你正在创建一个模板引用,提高了代码的可读性。 -
类型推断: 在 TypeScript 中,
useTemplateRef能提供更精确的类型推断。
-
不污染模板上下文:
-
在模板引用中的用法 (新方式) :
<template> <div ref="divRef">Hello World</div> </template> <script setup> import { useTemplateRef, onMounted } from 'vue'; // useTemplateRef 返回一个 getter 函数 const getDivRef = useTemplateRef('divRef'); onMounted(() => { // 调用 getter 函数获取 DOM 元素 console.log(getDivRef()); // <div>Hello World</div> getDivRef()?.focus(); // 例如,聚焦到元素上 }); </script>- 注意:在
ref指令中使用的字符串(如'divRef')必须与useTemplateRef的参数完全匹配。 -
getDivRef()返回的是实际的 DOM 元素或组件实例,如果元素未挂载,则可能返回null或undefined。
- 注意:在
对比总结
| 特性 | ref |
useTemplateRef |
|---|---|---|
| 主要目的 | 创建通用的响应式引用 | 专门用于模板引用 |
| 返回值 | 一个包含 .value 属性的 ref 对象 |
一个 getter 函数 |
| 模板暴露 | 会暴露到模板上下文(如果在 <script setup> 中定义) |
不会暴露到模板上下文 |
| 类型推断 | 一般 | 更好(尤其是在 TS 中) |
| 意图表达 | 通用,需看上下文 | 明确 |
| Vue 版本要求 | 3.0+ | 3.5+ |
| 何时使用 | 通用响应式状态、旧项目中的模板引用 | Vue 3.5+ 项目中的模板引用 (推荐) |
结论
- 对于模板引用(获取 DOM 元素或子组件实例),强烈推荐在 Vue 3.5+ 项目中使用
useTemplateRef。它更清晰、更安全、类型更友好。 - 对于通用的响应式状态管理(如计数器、布尔标志等),继续使用
ref。 - 在你的项目中,如果已经升级到了 Vue 3.5 或更高版本,并且需要获取模板引用,请优先考虑
useTemplateRef。