Skip to content

Vue框架原理

参考资料:

手写vue2核心源码

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、三个核心类

  1. Observer:给对象的属性添加getter和setter,用于依赖收集派发更新
  2. Dep:用于收集当前响应式对象的依赖关系,每个响应式对象都有一个dep实例。dep.subs = watcher[],当数据发生变更的时候,会通过dep.notify()通知各个watcher.
  3. Watcher:观察者对象,render watcher(渲染),computed watcher(计算属性),user watcher(用户使用watch)。
  • 依赖收集
    • initState,对computed属性初始化时,会触发computed watcher依赖收集
    • initState,对监听属性初始化时,触发user watcher依赖收集
    • render,触发render watcher依赖收集
  • 派发更新
    • 组件中对响应的数据进行了修改,会触发setter逻辑
    • 执行dep.notify()
    • 遍历所有subs,调用每一个watcher的update方法

img

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。

  1. 支持顺序:Promise.then -> MutationObserver -> setImmediate -> setTimeout.
  • 使用:Promise.then或者MutationObserver时,为微任务,宏任务 -> 微任务(dom更新未渲染,回调函数中已经可以拿到更新的dom)-> UI render
  • 使用:setImmediate、setTimeout时,为宏任务,宏任务 -> UI render -> 宏任务,dom已更新,可拿到更新的dom
  1. 什么时候使用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,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:

  1. 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
  2. compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
  3. Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
    1. 在自身实例化时往属性订阅器(dep)里面添加自己
    2. 自身必须有一个update()方法
    3. 待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退
  4. 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()的回调函数中。