# VUE框架原理

# 1、VUE响应式原理

# 1、总结原理

当创建vue实例时,vue会遍历data里的属性,使用Object.defineProperty为属性添加getter和setter,对数据的读取进行劫持,getter进行依赖收集,setter进行派发更新。

  • 依赖收集
    • 每个组件实例对应⼀个watcher实例
    • 在组件渲染过程中,把“touch”过的数据记录为依赖(触发getter -> 将当前watcher实例收集到属性对应的dep中)
  • 派发更新
    • 数据更新后 -> 会触发属性对应的setter -> 通过dep去通知watcher -> 关联的组件重新渲染
<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', {
 ...
})
-->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 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方法

# 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 观察者对象类
//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(){} //当数据变化时更新视图
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 2、index.html

使用module引入vue进行测试,测试指令及响应式

<!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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 3、vue实例

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
                }
            })
        })

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 4、observer实例

/**
 * 功能:实现数据劫持,利用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()
            }
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# 5、dep实例

/**
 * 发布订阅模式
 * 存储所有观察者,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()
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 6、compiler实例

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])
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

# 7、watcher实例

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)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 5、vue3响应式原理

vue3操作:使用函数式编程

1、effect执行 -> activeEffect 就有值了(值为更新页面的副作用)

2、触发getter -> track() -> 存储activeEffect

3、触发setter -> trigger() -> 执行activeEffect() -> 更新页面

总结:收集副作用 -> 收集的时间(getter)-> 触发副作用执行 -> 触发的时间(setter)

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>
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164

# 2、计算属性的实现原理

computed watcher为计算属性的监听器

computed watcher持有一个dep实例,通过dirty属性标记计算属性是否需要重新求值

当computed的依赖值改变后,就会通知订阅的watcher进行更新,对于computed watcher会将dirty属性设置为true,并且进行计算属性方法的调用。

# 1、computed缓存

计算属性是基于他的响应式依赖进行缓存的,只有依赖发生改变的时候才会重新求值

# 2、缓存意义及使用

应用在方法内部操作非常耗时,优化性能。例如计算属性方法里遍历一个极大的数组,计算一次可能耗时1s,使用计算属性可以很快获取到值。

# 3、计算属性检测

计算属性必须有响应式的依赖,否则无法监听,只有在vue创建初始化时添加监听的东西,才可以被计算属性监听到。

另computed和watch属性:computed用于做简单转换不适合做复杂操作,watch适合监听动作,做复杂操作。

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 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方法实现。

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 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中。

this.$nextTick(() => {
    // 获取数据的操作...
})
1
2
3

所以,在以下情况下,会用到nextTick:

  • 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在nextTick()的回调函数中。
  • 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在nextTick()的回调函数中。因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。
Last Updated: 12/6/2021, 10:04:17 AM