Appearance
Vue框架原理
参考资料:
1、VUE响应式原理
1、总结原理
当创建vue实例时,vue会遍历data里的属性,使用Object.defineProperty为属性添加getter和setter,对数据的读取进行劫持,getter进行依赖收集,setter进行派发更新。
- 依赖收集
- 每个组件实例对应⼀个watcher实例
- 在组件渲染过程中,把“touch”过的数据记录为依赖(触发getter -> 将当前watcher实例收集到属性对应的dep中)
- 派发更新
- 数据更新后 -> 会触发属性对应的setter -> 通过dep去通知watcher -> 关联的组件重新渲染
vue
<template>
<div>
<div>{{ a }}</div>
<div>{{ info.name }}</div>
</div>
</template>
<script>
export default App extends Vue{
data(){
return {
a: 'tes',
info: {
name: "xiaoming"
}
}
}
}
</script>
<!--
const dep1 = new Dep()
Object.defineProperty(this.$data, 'a', {
get(){
dep1.depend() //收集依赖
return value
},
set(newValue){
if(newValue === value) return
value = newValue
dep1.notify() //通知依赖
}
})
const dep2 = new Dep()
Object.defineProperty(this.$data, 'info', {
...
})
const dep3 = new Dep()
Object.defineProperty(this.$data.info, 'name', {
...
})
-->2、三个核心类
- Observer:给对象的属性添加getter和setter,用于依赖收集和派发更新。
- Dep:用于收集当前响应式对象的依赖关系,每个响应式对象都有一个dep实例。dep.subs = watcher[],当数据发生变更的时候,会通过dep.notify()通知各个watcher.
- Watcher:观察者对象,render watcher(渲染),computed watcher(计算属性),user watcher(用户使用watch)。
- 依赖收集
- initState,对computed属性初始化时,会触发computed watcher依赖收集
- initState,对监听属性初始化时,触发user watcher依赖收集
- render,触发render watcher依赖收集
- 派发更新
- 组件中对响应的数据进行了修改,会触发setter逻辑
- 执行dep.notify()
- 遍历所有subs,调用每一个watcher的update方法

3、注意事项
1、对象
- vue无法检测对象的添加
- 解决方案:this.$set(this.someObject, 'b', 2)
- 注意:Vue不允许动态添加根级别的响应式property
2、数组
- Object.defineProperty无法监听数组索引值的变化,比如this.a[0] = 4
- 解决方案:this.$set(this.a, 0, 44) |this.a.splice(0, 1, 44)
- 数组长度的变化无法检测
- 解决方案:this.a.splice(newLength)删除从newLength之后的数据
- 重写了数组的方法:push\pop\shift\unshift\splice\sort\reverse
3、其他
- 递归的循环data中的属性修改,可能导致性能问题
- 对于一些数据获取后不更改,只用于展示,可以使用Object.freeze(data.city)优化性能
4、手写vue响应式原理
1、整体结构
响应式原理
1、初始化时,遍历对象对所有属性进行拦截,并编译模版,发现有响应式数据时创建watcher对象:html-> <h1>``</h1> -> compiler发现有
2、创建watcher实例时触发getter,利用dep进行依赖收集watcher实例:new Watcher(vm, 'count', ()=>renderToView(count)) -> count的getter触发 -> Dep.target && dep.add(Dep.target)
3、响应式数据变化时,触发setter,利用dep进行派发更新,执行依赖的相关watcher实例对象的update函数,进行页面更新:this.count++ -> dep.notify() -> watcher.update() -> this.cb(newValue)
申明整体核心类及方法,确认整体框架结构:
- index.html 主页面
- vue.js Vue主文件
- compiler.js 编译模版,解析指令(v-model等)
- dep.js 收集依赖关系,存储观察者,以发布订阅模式实现
- observer.js 实现数据劫持
- watcher.js 观察者对象类
js
//vue主文件
export default class Vue {
constructor(options = {}){
/**
* 1. vue构造函数,接收各种配置参数等
* 2. options里的data挂载至根实例
* 3. 实例化observer对象,监听数据变化,利用dep进行依赖收集和派发更新
* 4. 实例化compiler对象,简析指令和模版表达式
*/
...
this._proxyData(this.$data)
new Observer(this.$data)
new Compiler(this)
}
}
//observer.js:实现数据劫持
export default class Observer {
constructor(data){
this.traverse(data)
}
traverse(data){} //递归遍历data里的所有属性
defineReactive(obj, key, val){} //给传入的数据设置getter/setter, 利用dep实现依赖收集和派发更新
}
//compiler.js:编译模版,解析指令
export default class Compiler {
constructor(vm){
this.compiler(vm.$el)
}
compiler(el){} //编译模版时为每个响应式对象建立watcher对象,并将watcher推送进dep用于依赖收集
}
//dep.js:收集依赖关系,存储观察者
export default class Dep {
constructor(){ //存储所有观察者
this.subs = []
}
addSub(watcher){} //添加观察者
notify(){} //发送通知
}
//watcher.js:观察者对象类
export default class Watcher {
constructor(vm, key, cb){} //vm实例、key属性、cb回调函数
update(){} //当数据变化时更新视图
}2、index.html
使用module引入vue进行测试,测试指令及响应式
html
<!DOCTYPE html>
<html lang="cn">
<head>
<title>my vue</title>
</head>
<body>
<div id="app">
<h1>模版表达式测试</h1>
<h3>{{msg}}</h3>
<h3>{{count}}</h3>
<br/>
<h1>v-text测试</h1>
<div v-text="msg"></div>
<br/>
<h1>v-html测试</h1>
<div v-html="innerHtml"></div>
<br/>
<h1>v-model测试</h1>
<input type="text" v-model="msg">
<input type="text" v-model="count">
<button v-on:click="handler">按钮</button>
</div>
<script src="./index.js" type="module"></script>
</body>
</html>js
//index.js
import Vue from './vue.js'
const vm = new Vue({
el: "#app",
data: {
msg: "Hello, vue",
count: "100",
innerHtml: "<ul><li>good job</li></ul>"
},
methods: {
handler(){
alert(1111)
}
}
})
console.log(vm)3、vue实例
js
import Observer from './observer.js'
import Compiler from './compiler.js'
export default class Vue {
constructor(options = {}){
this.$options = options
this.$data = options.data
this.$methods = options.methods
this.initRootElement(options)
//options里的data挂载至根实例
this._proxyData(this.$data)
//实例化observer对象,监听数据变化
new Observer(this.$data)
//实例化compiler对象,简析指令和模版表达式
new Compiler(this)
}
/**
* 获取根元素,并存储到vue实例,校验传入的el是否合规
*/
initRootElement(options){
if(typeof options.el === 'string'){
this.$el = document.querySelector(options.el)
}else if(options.el instanceof HTMLElement){
this.$el = options.el
}
if(!this.$el){
throw new Error('input el error, you should input css selector or HTMLElement')
}
}
/**
* 利用Object.defineProperty将options传入的data注入vue实例中
* 给vm设置getter和setter
* vm.a触发getter,获取this.$data[key]
* vm.a=2触发setter,设置this.$data[key]=2
*/
_proxyData(data){
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get(){
return data[key]
},
set(newValue){
if(data[key] === newValue){
return
}
data[key] = newValue
}
})
})
}
}4、observer实例
js
/**
* 功能:实现数据劫持,利用dep进行依赖收集和派发更新
* 1、调用时机:vue实例化时调用,监听data数据变化,new Observer(this.$data)
* 2、实现机制:Object.defineProperty(this.$data, key, {})
* 3、使用dep完成依赖收集dep.addSub和派发更新dep.notify机制
* 编译模版:
* a.为每个组件建立watch对象,eg:<div v-text="good"></div>
* new Watcher(this.vm, key, newValue => {node.textContent = newValue}
* b.建立watch时,获取oldvalue,设置Dep.target,获取this.vm."good"值,触发vm的getter
* c.获取this.$data["good"],触发this.$data的getter,添加值dep依赖中
* d.设置Dep.target = null,清除脏数据
* 4、数据更新:
* this.flag = 1 -> vm的setter -> vm.$data.flag = 2 -> vm.$data.setter -> dep.notify -> 所有相关watcher.update
*/
import Dep from "./dep.js"
export default class Observer {
constructor(data){
this.traverse(data)
}
/**
* 递归遍历data里的所有属性
*/
traverse(data){
if(!data || typeof data !== 'object'){
return
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
/**
* 给传入的数据设置 getter/setter,响应式改造
* 1、给vm.$data对象里的每个属性递归设置getter和setter
* 2、使用dep进行依赖收集dep.addSub和派发更新dep.notify
* @param {*} obj
* @param {*} key
* @param {*} val
*/
defineReactive(obj, key, val){
this.traverse(val) //子元素是对象,递归处理
const that = this
const dep = new Dep() //dep存储观察者
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get(){
Dep.target && dep.addSub(Dep.target) //收集依赖,只有当watcher初始化时才会添加依赖
return val
},
set(newValue){
if(newValue === val){
return
}
val = newValue
that.traverse(newValue)//设置的时候可能设置了对象
dep.notify()
}
})
}
}5、dep实例
js
/**
* 发布订阅模式
* 存储所有观察者,watcher
* 每个watcher都有一个update
* 通知subs里的每个watcher实例,触发update方法
*/
/**
* 问题:
* 1、dep 在哪里实例化,在哪里addsub:observer实例化并给this.$data添加getter和setter时初始化,用于收集依赖关系,存储观察者
* 2、dep notify在哪里调用:数据变化时,this.$data.setter里调用
*/
export default class Dep {
constructor(){
//存储所有观察者
this.subs = []
}
/**
* 添加观察者
*/
addSub(watcher){
if(watcher && watcher.update){
this.subs.push(watcher)
}
}
/**
* 发送通知
*/
notify(){
this.subs.forEach(watcher => {
watcher.update()
})
}
}6、compiler实例
js
import Watcher from './watcher.js'
/**
* 功能:模版编译
* 1、模版编译时为每个组件添加一个watcher实例,设置回调函数为更新数据
* 2、watcher初始化时,传入实例、key、回调
*/
export default class Compiler {
constructor(vm){
this.el = vm.$el
this.vm = vm
this.methods = vm.$methods
this.compiler(vm.$el)
}
/**
* 编译模版
* @param {} el
*/
compiler(el){
const childNodes = el.childNodes //nodeList伪数组
Array.from(childNodes).forEach(node => {
if(this.isTextNode(node)){//文本节点
this.compilerText(node)
}else if(this.isElementNode(node)){//元素节点
this.compilerElement(node)
}
//有子节点,递归调用
if(node.childNodes && node.childNodes.length > 0){
this.compiler(node)
}
})
}
/** 判断文本节点 */
isTextNode(node){
return node.nodeType === 3
}
/** 判断元素节点 */
isElementNode(node){
return node.nodeType === 1
}
/** 判断元素属性是否是指令 */
isDirective(attrName){
return attrName.startsWith('v-')
}
/** 编译文本节点,{{text}} */
compilerText(node){
const reg = /\{\{(.+?)\}\}/;
const value = node.textContent;
if(reg.test(value)){
const key = RegExp.$1.trim() //$1取到匹配内容,text
node.textContent = value.replace(reg, this.vm[key]) //this.vm[key]即vm[text]
new Watcher(this.vm, key, (newValue)=> {
node.textContent = newValue //更新视图
})
}
}
/** 编译元素节点 */
compilerElement(node){
if(node.attributes.length){
Array.from(node.attributes).forEach(attr => { //遍历节点的属性
const attrName = attr.name //属性名
if(this.isDirective(attrName)){ //v-model="msg"、v-on:click="handle"
let directiveName = attrName.indexOf(':') > -1 ? attrName.substr(5) : attrName.substr(2) //指令名,model、click
let key = attr.value //msg\handle,属性值
this.update(node, key, directiveName) //更新元素节点
}
})
}
}
/**
* 更新节点
* @param {*} node
* @param {*} key 指令值:msg、handle
* @param {*} directiveName 指令名,model
*/
update(node, key, directiveName){
//v-model\v-text\v-html\v-on:click
const updateFn = this[directiveName + 'Updater']
updateFn && updateFn.call(this, node, this.vm[key], key, directiveName) //
}
/** 解析v-text,编译模版,添加watcher */
textUpdater(node, value, key){
node.textContent = value
new Watcher(this.vm, key, newValue => {
node.textContent = newValue
})
}
/** 解析v-model */
modelUpdater(node, value, key){
node.value = value
new Watcher(this.vm, key, newValue => {
node.value = newValue
})
node.addEventListener('input', ()=>{
this.vm[key] = node.value
})
}
/** 解析v-html */
htmlUpdater(node, value, key){
node.innerHTML = value
new Watcher(this.vm, key, newValue => {
node.innerHTML = newValue
})
}
/** 解析v-on:click */
clickUpdater(node, value, key, directiveName){
node.addEventListener(directiveName, this.methods[key])
}
}7、watcher实例
js
import Dep from "./dep.js"
/**
* 功能:观察者对象类
* 1、watcher初始化获取oldvalue的时候,会做哪些操作
* 2、通过vm[key]获取oldvalue时,为什么将当前实例挂载在dep上获取后设置为null
* 3、update方法在什么时候执行的:dp.notify()
*/
export default class Watcher {
/**
* @param {*} vm vue实例
* @param {*} key data中的属性名
* @param {*} cb 负责更新视图的回调函数
*/
constructor(vm, key, cb){
this.vm = vm
this.key = key
this.cb = cb
//每次watcher初始化时,添加target属性
Dep.target = this
//触发get方法,在get方法里会取做一些操作
this.oldValue = this.vm[key]
Dep.target = null //可能会出现脏数据,清空操作
}
/**
* 当数据变化时更新视图
*/
update(){
let newValue = this.vm[this.key]
if(this.oldValue === this.newValue){
return
}
this.cb(newValue)
}
}5、vue3响应式原理
vue3操作:使用函数式编程
1、effect执行 -> activeEffect 就有值了(值为更新页面的副作用)
2、触发getter -> track() -> 存储activeEffect
3、触发setter -> trigger() -> 执行activeEffect() -> 更新页面
总结:收集副作用 -> 收集的时间(getter)-> 触发副作用执行 -> 触发的时间(setter)
js
function isObject(data){
return data && typeof data === 'object'
}
//类似于vue2的dep对象
let targetMap = new WeakMap() //存储相关依赖
let activeEffect
/**
* {
* target: {
* key: [effect, effect, effect, effect]
* }
* }
* @param {*} key
*/
function track(target, key){ //dep.add
let depsMap = targetMap.get(target)
if(!depsMap) targetMap.set(target, (depsMap = new Map()))
let dep = depsMap.get(key)
if(!dep) depsMap.set(key, (dep = new Set()))
if(!dep.has(activeEffect)) dep.add(activeEffect)
}
function trigger(target, key){//dep.notify
const depsMap = targetMap.get(target)
if(!depsMap) return
depsMap.get(key).forEach(e => e && e())
}
/**
* 注册副作用函数
* @param {*} fn
* @param {*} options
* @returns
*/
function effect(fn, options={}){ //compiler + watcher
const __effect = function(...args){
activeEffect = __effect
return fn(...args) //this.cb()
}
if(!options.lazy){ //computed
__effect()
}
return __effect
}
/**
* const a = reactive({count: 0})
* a.count++
* @param {*} data
* @returns
*/
export function reactive(data){
if(!isObject(data)) return
return new Proxy(data, {
get(target, key, receiver){
//反射 target[key] -> 继承关系清情况下有问题
const ret = Reflect.get(target, key, receiver)
// todo,依赖收集
track(target, key)
return isObject(ret) ? reactive(ret) : ret //返回值时对象递归处理
},
set(target, key, val, receiver){
Reflect.set(target, key, val, receiver)
//todo 触发更新
trigger(target, key)
return true
},
deleteProperty(target, key){
const ret = Reflect.defineProperty(target, key, receiver)
// todo
trigger(target, key)
return ret
}
})
}
/**
* 功能:基本类型代理,基本类型无法使用reflect
* const count = ref(0)
* count.value++
*/
export function ref(target){
let value = target
const obj = {
get value(){
track(obj, 'value')
return value
},
set value(newValue){
if(value === newValue) return
value = newValue
trigger(obj, 'value')
}
}
return obj
}
//延迟计算:只考虑函数情况
//执行c.value时,函数才会执行
export function computed(fn){
//延迟计算 const c = computed(() => `${count.value} + !!!`); c.value
let __computed
const run = effect(fn, {lazy: true})
__computed = {
get value() {
return run()
}
}
return __computed
}
export function mount(instance, el){
//注册副作用更新函数
effect(function(){
instance.$data && update(instance, el)
})
instance.$data = instance.setup()
update(instance, el)
function update(instance, el){
el.innerHTML = instance.render() //直接插入,未添加compiler
}
}
/*
<script type='module'>
import { mount, ref, reactive, computed } from './index.js'
const App = {
$data: null,
setup() {
let count = ref(0)
let time = reactive({ seconds: 0 })
let cc = computed(() => `computed : ${count.value + time.seconds}`)
window.timer = setInterval(() => {
time.seconds++
}, 1000)
setInterval(() => {
count.value++
}, 2000)
return {
count,
time,
cc
}
},
render() {
return `
<h1>How Reactive?</h1>
<p>this is reactive work: ${this.$data.time.seconds}</p>
<p>this is ref work: ${this.$data.count.value}</p>
<p>${this.$data.cc.value}</p>
`
}
}
mount(App, document.querySelector('#app'))
</script>
*/2、计算属性的实现原理
computed watcher为计算属性的监听器
computed watcher持有一个dep实例,通过dirty属性标记计算属性是否需要重新求值
当computed的依赖值改变后,就会通知订阅的watcher进行更新,对于computed watcher会将dirty属性设置为true,并且进行计算属性方法的调用。
1、computed缓存
计算属性是基于他的响应式依赖进行缓存的,只有依赖发生改变的时候才会重新求值
2、缓存意义及使用
应用在方法内部操作非常耗时,优化性能。例如计算属性方法里遍历一个极大的数组,计算一次可能耗时1s,使用计算属性可以很快获取到值。
3、计算属性检测
计算属性必须有响应式的依赖,否则无法监听,只有在vue创建初始化时添加监听的东西,才可以被计算属性监听到。
另computed和watch属性:computed用于做简单转换不适合做复杂操作,watch适合监听动作,做复杂操作。
vue
<template>
<div>
{{storageMsg}} is {{time}}
</div>
</template>
<script>
//storageMsg和time都无法更新
export default MyVue extends Vue {
computed: {
storageMsg: function(){
return sessionStorage.getItem("key")
},
time: function(){
return Date.now()
}
}
}
</script>3、Vue.nextTick的原理
vue是异步执行dom更新的,一旦观察到数据的变化,把同一个事件循环中观察数据变化的watcher推送到队列中。在下一次事件循环时,vue清空异步队列,进行dom的更新。eg:vm.someData = 'new value', dom并不会马上更新,而是在异步队列被清除时才会更新dom。
- 支持顺序:Promise.then -> MutationObserver -> setImmediate -> setTimeout.
- 使用:Promise.then或者MutationObserver时,为微任务,宏任务 -> 微任务(dom更新未渲染,回调函数中已经可以拿到更新的dom)-> UI render
- 使用:setImmediate、setTimeout时,为宏任务,宏任务 -> UI render -> 宏任务,dom已更新,可拿到更新的dom
- 什么时候使用nextTick呢?
在数据变化后要执行某个操作,而这个操作依赖因你数据改变而改变后的dom,这个操作应该放置在vue.nextTick回调中。可以使用update钩子或setTimeout方法实现。
vue
<template>
<div v-if="loaded" refs="test"></div>
</template>
<script>
export default MyVue extends Vue {
methods: {
async showDiv(){
this.loaded = true
//直接执行this.$refs.test无法拿到更新的dom
await Vue.nextTick()
this.$refs.test
/* Vue.nextTick(function(){
this.$refs.test
}) */
}
}
}
</script>4、核心考察点
1、Object.defineProperty与proxy
题目:Vue 的响应式原理中 Object.defineProperty 有什么缺陷?为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
解答:
- Object.defineProperty无法低耗费的监听到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
- Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历。如果属性值是对象,还需要深度遍历。Proxy 可以劫持整个对象, 并返回一个新的对象。
- Proxy 不仅可以代理对象,还可以代理数组,还可以代理动态增加的属性。
扩展:
- Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组:push、pop、shift、unshift、splice、sort、reverse,由于只针对了以上八种方法进行了hack处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
- Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择,而要取代它的Proxy有以下两个优点:可以劫持整个对象,并返回一个新对象,有13种劫持操作。
2、双向数据绑定的原理?
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
- 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
- compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
- Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
- 在自身实例化时往属性订阅器(dep)里面添加自己
- 自身必须有一个update()方法
- 待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退
- MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
3、Computed 和 Watch 的区别?
对于Computed:
- 它支持缓存,只有依赖的数据发生了变化,才会重新计算
- 不支持异步,当Computed中有异步操作时,无法监听数据的变化
- computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的。
- 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
- 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。
对于Watch:
- 它不支持缓存,数据变化时,它就会触发相应的操作
- 支持异步监听
- 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
- 当一个属性发生变化时,就需要执行相应的操作
- 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
- immediate:组件加载立即触发回调函数
- deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。
- 当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。
总结:
- computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
- watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
运用场景:
- 当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。
- 当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
4、$nextTick 原理及作用?
Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。
nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。
nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理
nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因:
- 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染
- 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要
用法:Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick了。由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick中。
js
this.$nextTick(() => {
// 获取数据的操作...
})所以,在以下情况下,会用到nextTick:
- 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在nextTick()的回调函数中。
- 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在nextTick()的回调函数中。因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。