目录 §

  • 0. 内存图 §
  • 1. options里有什么 §
  • 2. 入门属性 §
  • 3. Vuedata做了什么 §
  • 4. 数据响应式 §
  • 5. Vue2.xbug §
  • 6. Vue.setthis.$set §
  • 7. data中的数组 §
  • 8. ES6手写数组新push方法 V.S. ES5写法 §
  • 9. 总结 §
  • 10. 面试题 §

[toc]


搞清构造选项,Vue 实例

0. 内存图 §

作者习惯将Vue实例命名为vmconst vm = new Vue(options)

  • vm对象封装了对视图的(视图层)所有操作,包括
    • 数据读写
    • 事件绑定
    • DOM更新
    • 不包括网络层的Ajax
  • vm构造函数Vue,按照ES6vm所属的类是Vue
  • vm.__proto__ === Vue.prototype
  • 举例 Vue: #202; 自身属性: (*3?); Vue.prototype: #419; Vue.__proto__: (*6?)
  • vm: #101; 自身属性: (*2?); vm.__proto__: #419
  • Vue.prototype: #419; 自身属性: (*4?); Vue.prototype.__proto__: (*5?);
  • 注意点(*1)(*2)(*3)(*4)(*5)(*6)分别是什么
  • (*1) 初始化传入的参数options是什么
  • (*2) 实例vm有哪些自身属性
  • (*3) Vue函数有哪些自身属性
  • (*4) Vue.prototype有哪些自身属性
  • (*5) Vue.prototype.__proto__的指向
  • (*6): Vue.__proto__ === Function.prototype基本原则:任何函数的__proto__都指向Function.prototype构造函数

options: (*1?)new Vue()的参数,一般称之为选项或构造选项


1. options里有什么 §

文档

  • 英文文档搜options
  • 中文文档搜选项

options的五类属性

对比Vue3.x,加粗斜体代表新增,删除线代表移除,斜体代表有改动

  • 数据(Data)
    • data
    • props属性
    • propsData本意是propsValue 单元测试时用,vue3.x 废除
    • computed计算后的
    • methods方法
      • “方法”(面向对象编程,依附于对象obj.fn()
      • “函数”更偏向数学概念,函数式编程,单独出现fn()
    • watch
    • emits
  • DOM
    • el容器/挂载点
    • template模板内容;与render互斥
    • rendertemplate互斥;不手写,加载器生成
    • renderError
  • 生命周期钩子
    • beforeCreated
    • created
    • beforeMount
    • mounted
    • beforeUpdate
    • updated
    • activated
    • deactivated
    • beforeUnmount (beforeDestroy)
    • unmounted (destroyed)
    • errorCaptured
    • renderTracked
    • renderTriggered
  • 资源
    • directives指令
    • components组件
    • filters (建议用computed代替实现) 过滤器;表面上有用,实际上没用
  • 组合
    • mixins混入
    • extends扩展
    • provide提供
    • inject注入
    • setup
    • parent
  • 其他:先不看

属性分阶段

  • 红色属性:好学、必学入门属性
  • 橙色属性:高级属性、单独举例说明
  • 绿色属性:可自学属性
  • 紫红属性:特殊
  • 灰色属性:新版中移除,注意学代替用法
  • 烟白属性:先不看

2. 入门属性 §

  • el 挂载点
  • template
  • data 内部数据
  • methods 方法
  • components组件
  • 四个钩子
  • props 外部数据

el 挂载点

  • el: "#id"
  • 可用.$mount('#id')代替
  • 替换HTML#id中的节点
  • 慢速网络(slow 3G)下,可能可以看见原来写在页面上的节点内容

template

  • 语法v-if v-for

data 内部数据

  • 支持对象和函数,优先用函数(返回值)
  • 初始值 存储记录
  • 数据响应式中 data 有 bug
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Vue = window.Vue
new Vue({
  /*
  data: {
    n: 0
  },
  */
  data() {
    return { // 每次都创建一个新的对象
      n: 0
    }
  },
  template: `
    <div class="red">
      {{n}}
    <button @click="add">+1s</button>
    </div>
  `,
  methods: {
    add() {
      this.n+=1
    }
  },
}).$mount('#xmas')
  • import Demo from './Demo.vue'
  • 把组件传给new Vue({render: h => h(Demo))
  • 引入的组件本质上就是一个对象

为什么推荐用函数声明data() {...},而不是用对象的形式

  • 文档中说 限制:组件的定义只接受 function,而没说明原因
  • 组件是可复用的 Vue 实例
  • 两个组件复用new Vue({render: h => h(X,[h(Demo), h(Demo)]))
  • 如果是使用对象,创建两个相同组件,引用的data就是指向同一个内存地址,对象在这个组件的所有实例中共享
  • 有时组件需要复用,必须使得每个组件都有一份data的拷贝,防止不同组件修改数据时被相互覆盖,用函数返回一个data对象的独立的拷贝
  • 文档的说明:一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝
  • 为了组件间不互相影响数据(如果 Vue 没有这条规则,点击一个按钮就可能会影响到其它所有实例)
  • 注意同一个组件中重复定义的数据指向同一个内存地址

官方示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button v-on:click="count++">
        You clicked me {{ count }} times.
    </button>
  `
})
// 通过 new Vue 创建的 Vue 根实例中,把这个组件作为自定义元素来使用
new Vue({ el: '#components-demo' })
1
2
3
4
5
6
<!-- 组件的复用 -->
<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

demo


methods 方法

  • 事件处理函数或普通函数(可在模板里调用)
  • 绑定的方法必须写到methods属性中,否则会报错function is not defined
  • 可以代替filters,举例 显示数组中偶数
  • 作为普通函数写到模板中时,必须有函数返回值({{filterEven(array)}}
 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
const Vue = window.Vue
const vm = new Vue({
  data() {
    return {
      array: [1,2,3,4,5,6,7,8],
    }
  },
  template: `
    <div>
        <hr>
        {{array.filter(i => i % 2 === 0)}}
        <hr>
        {{filterEven(array)}}
        <hr>
        {{filterOdd()}}
    </div>
  `,
  methods: {
    filterEven(array) {
      return array.filter(i => i % 2 === 0) // JS Array.prototype.filter()
    },
    // 主动在模板中调用
    filterOdd() {
      console.log('执行了 filter 函数')
      return this.array.filter(i => i % 2 !== 0)
    },
  },
})$mount('#xmas')
// [ 2, 4, 6, 8 ]
// [ 2, 4, 6, 8 ]
// [ 1, 3, 5, 7 ]
  • 主动在模板中调用{{filterOdd()}}的缺点是每次渲染就会调用一次
  • 进行其他页面操作也会重新调用
  • computed解决

components组件

什么是组件

  • 抽象概念,可以与其他部分组合使用
  • 模块化

Vue实例对象vm V.S. Vue组件components

  • new Vue创建的是实例
  • Vue.component创建的是组件
  • import xxx From '*.vue'引入的是组件

components组件的三种引入方式 组件注册

  • import方式
  • 全局方式
  • 混合方式

import方式,模块化引入外部单文件组件

  • Vue组件,注意大小写
  • 三种Vue使用方式的最后一种:单文件组件Demo.vue
  • 通过import Demo from './Demo.vue'引入
  • Vue实例中使用另一个组件
  • 命名组件components: {Xmas: Demo}
  • 写到实例vmtemplate
  • template中的形式为<componentName/>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 单文件组件
const Vue = window.Vue
import Demo from './Demo.vue'
const vm = new Vue({
  components: {
    Xmas: Demo, // 定义组件名
  },
  template: `
    <div class="red">
        <Xmas/>
    </div>
  `,
})
vm.$mount('#xmas')

JS方式 全局声明 全局注册

  • 写在全局Vue的属性Vue.component('componentName', options)中,options是一个对象
  • template中的形式为<componentName/>
  • 第二个参数传入一个对象,此对象的和实例中传入new Vue({})的第一个参数是类似的,可以接受的属性一样
  • 可以理解为将一个复杂组件取一个名字(第一个参数),组件对象放到Vue.component()第二个参数中
  • options的写法是一样的
  • 可以简单理解为给实例取一个名字
  • 不需要额外引入,可被所有其他组件引入
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 属性命名组件
const Vue = window.Vue
Vue.component('Demo2', {
  template: `
    <div>demo2</div>
  `,
})
// 可嵌套
const vm = new Vue({
  components: {
    Xmas: Demo,
  },
  template: `
    <div class="red">
        <Demo2/>
    </div>
  `,
})
vm.$mount('#xmas')

全局注册的组件在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中。比如

1
2
3
4
5
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })

new Vue({ el: '#app' })
1
2
3
4
5
<div id="app">
  <component-a></component-a>
  <component-b></component-b>
  <component-c></component-c>
</div>

在所有子组件中也是如此,也就是说这三个组件在各自内部也都可以相互使用

  • 全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生

全局注册的缺点


混合方式(结合以上两种方式)

  • 在实例中的components属性中,直接定义
 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
const Demo4 = {
    template: `
        <div>demo444<div/>
    `,
}

// 在实例中使用组件
const vm = new Vue({
  // 定义组件
  components: {
    Xmas2: {
      data() {
        return {
          n: 0
        }
      },
      template: `
        <div>
          demo333's n
          <span>{{n}}</span>
        </div>
      `,
    },
    Xmas3: Demo4,
  },
  template: `
    <div>
        <Xmas2/>
        <Xmas3/>
    </div>
  `
})
vm.$mount('#xmas')

组件可以理解为实例中的实例

  • 组件options的写法是一样的
  • 优先使用第一种 import方式 ,优点是模块化:将独立的功能放到单独的文件中
  • 取名可以相同:components: {Demo: Demo}
  • 取名相同可以简化(ES6): components: {Demo}
  • 可以导入多个 components: {Demo, App} 分别导入DemoApp
  • template中用相同的名字<Demo/><App/><Demo/>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ES6
import Demo from './Demo.vue'
const vm = new Vue({
    components: {Demo},
    data() {...}.
    template: `
        <div>
            <Demo/>
        </div>
    `,
    methods: {...},
})

组件名取名规范 Vue 风格指南

  • 在单文件组件、字符串模板和 JSX 中使用自闭合组件
  • 组件名就是 Vue.component 的第一个参数
  • 为什么文件名要小写? by 阮一峰
  • 避免重名冲突:直接在 DOM 中使用一个组件 的时候,推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突
  • 字符串模板或单文件组件的时候, 组件名大写,避免在template中分不清<Button/><button><button/>
  • 其他文件名小写
  • 命名中明确包含用途与归属SearchButton

全局注册 V.S. 局部注册


四个钩子

  • created
  • mounted
  • updated
  • destroyed(unmounted vue3.x) 消
  • “钩子”:即切入点

created

  • debugger证明组件或实例出现在内存中,而未出现在页面中
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
new Vue({
  data() {
    return {
      n: 0
    }
  },
  template: `
      <div>
        {{n}}
        <button @click="add">+1</button>
      </div>
  `,
  created() {
    debugger
    console.log('已出现在内存中,未出现在页面中')
  },
  methods: {
    add() {
      this.n += 1
    },
  }
}).$mount('#xmas');

mounted

  • debugger证明组件或实例出现在内存中,并且也出现在页面中
 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
new Vue({
  data() {
    return {
      n: 0
    }
  },
  template: `
      <div>
        {{n}}
        <button @click="add">+1</button>
      </div>
  `,
  created() {
    // debugger
    console.log('已出现在内存中,未出现在页面中')
  },
  mounted() {
    debugger
    console.log('已出现在页面中')
  },
  methods: {
    add() {
      this.n += 1
    },
  }
}).$mount('#xmas');

updated

  • 能保证数据更新才触发当前的事件绑定的函数
 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
new Vue({
  data() {
    return {
      n: 0
    }
  },
  template: `
      <div>
        {{n}}
        <button @click="add">+1</button>
      </div>
  `,
  created() {
    // debugger
    console.log('已出现在内存中,未出现在页面中')
  },
  mounted() {
    // debugger
    console.log('已出现在页面中')
  },
  updated() {
    console.log('已点击 更新页面')
    console.log(this.n)
  },
  methods: {
    add() {
      this.n += 1
    },
  }
}).$mount('#xmas');

destroyed(unmounted vue3.x) 消

  • 需要一个子组件destroy.vue
  • 从页面中消失就是destroy
  • 每次消除后重新创建的都是新的,+1后消除在创建,数据归零

Destroy.vue

 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
<template>
  <div>
    {{n}}
    <button @click="add">+1</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      n: 0
    }
  },
  created() {
    // debugger
    console.log('已出现在内存中,未出现在页面中')
  },
  mounted() {
    // debugger
    console.log('已出现在页面中')
  },
  updated() {
    console.log('已点击 更新页面')
    console.log(this.n)
  },
  destroyed() {
    console.log('已点击 更新页面')
    console.log(this.n)
  },
  methods: {
    add() {
      this.n += 1
    },
  }
}
</script>

怎么使用destroy的?

  • 用最简单的例子说明

main.js中引入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Destroy from './Destroy.vue'
new Vue({
  components: {Destroy},
  data() {
    return {
      isVisible: true,
    }
  }
  ,
  template: `
      <div>
        <button @click="toggle">toggle</button>
        <hr>
        <Destroy v-if="isVisible === true"/>
      </div>
  `,
  methods: {
    toggle() {
      this.isVisible = !this.isVisible // 取反值
    }
  },
}).$mount('#xmas');
  • 从页面中消失就是destroyed
  • v-if监听数据变化
  • 取反this.isVisible = !this.isVisible

props 外部数据

  • 也称属性,是由组件外部传入值的(对比内部数据data,内部自己传值)
  • 从外部接受一个message,并自动绑定到this
  • template中,可写{{this.message}},也可以省略 this ,写成:{{message}}

声明传入内容的类型

  • 字符串<Props message="props"/>
  • 变量:之前加冒号:message="n"传入this.n数据,优先在data里查找变量
  • 方法:fn="add"传入this.add函数

区别 传字符串 V.S. 传数字(其他数据类型)

  • message前加冒号,声明传入的外部数据是变量,可包含数字或其他数据类型<Props :message="0"/><Props :message="true"/><Props :message=" '0' "/>
  • 不加冒号,就只是传字符串

将内部数据data放到组件中的外部数据message使用

  • <Props :message="n"/>
  • :message=""双引号里的是 JS 代码
  • :message="n"传入的变量n,优先在data里查找

传递参数,调用方法

  • <Props :fn="add"/>methods: {add() {...}}<button @click="fn">call fn</button>
  • 按钮被点击时,调用fn
  • fn从外部传入的一个方法props: ['message', 'fn']
  • fn取名符合规范即可
  • 在外部的实例中,组件Props的属性:fn"add"调用了实例的methods: {add() {...}}中的方法
  • 可以认为是main.js实例的方法传给了组件Props
  • 组件Props里的@click="fn"调用add方法

同步更新内外数据

  • <div>{{n}}<Props :fn="add" :message="n"/></div>

main.js

 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
/*
  <div id="xmas"></div>
  <div id="xmas2"></div>
*/
// props
import Props from './Props-demo'
new Vue({
  components: {Props},
  data() {
    return {
      n: 0,
    }
  },
  template: `
    <div>
      {{n}}
      <Props message="我好了 props"/>
      <Props :message="n"/>
      <Props :message="0"/>
      <Props :message="true"/>
      <Props :message="null"/>
      <Props :message="undefined"/>
      <Props :message="20n"/>
      <Props :message="{}"/>
    </div>
  `,
  methods: {
    add() {
      this.n += 1
    }
  },
}).$mount('#xmas');

// props 方法
new Vue({
  components: {Props},
  data() {
    return {
      n: 0,
    }
  },
  template: `
    <div>
    {{n}}
    <Props :fn="add" :message="n"/>
    </div>
  `,
  methods: {
    add() {
      this.n += 1
    }
  },
}).$mount('#xmas2');

Props.demo.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
  <div class="red">
    这里是Props-demo的内部
    {{message}}
    <button @click="fn">call fn</button>
  <!-- {{this.message}}-->
  </div>
</template>

<script>
export default {
  name: "Props-demo",
  props: ['message', 'fn'] // 从外部接受数据,自动绑到this上
}
</script>

<style scoped>
.red {
  color: red;
  border: 1px solid red;
  }
</style>

3. Vuedata做了什么 §

data实验

在实例外面变更data(而非在methods绑定变更的函数 绑到对应节点的事件上)

  • 示例代码 1myData变化
  • 一开始{n: 0},传给new Vue之后立即变成{n: (...)}
 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
const Vue = window.Vue
// 禁用警告
Vue.config.productionTip = false;

const myData = {
  n: 0
};
// 精髓1
console.log(myData)
new Vue({
  data: myData,
  template: `
    <div>
      <div>{{n}}</div>
      <hr>
      <button @click="add">+10</button>
    </div>
  `,
  methods: {
    add() {
      this.n += 10;
    }
  }
}).$mount("#app");

setTimeout(() => {
  myData.n += 10;
  // 精髓2
  console.log(myData)
}, 3000);
// 控制台
// {__ob__: we}
//     n: (...)
//     __ob__: we {value: {...}, dep: ce, vmCount: 1}
//     get n: f ()
//     set n: f (t)
//     __proto__: Object

{n: (...)}是什么,为什么表现和{n: 0}一致

了解ES6的计算属性 gettersetter

第一次打印的和第二次的区别,在后台展开查看用setter设置属性的对象

  • 调用obj1.姓名()要加括号,调用obj2.姓名不用加括号
  • obj2.姓名是属性,只不过使用函数形式定义的
  • 计算属性的setter可以用来设置之前的原始属性
 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
let obj0 = {
  : "高",
  : "圆圆",
  age: 18
};

// 需求一,得到姓名

let obj1 = {
  : "高",
  : "圆圆",
  姓名() {
    return this. + this.;
  },
  age: 18
};

console.log("需求一:" + obj1.姓名());
// 姓名后面的括号能删掉吗?不能,因为它是函数
// 怎么去掉括号?

// 需求二,姓名不要括号也能得出值

let obj2 = {
  : "高",
  : "圆圆",
  get 姓名() {
    return this. + this.;
  },
  age: 18
};

console.log("需求二:" + obj2.姓名);

// 总结:getter 就是这样用的。不加括号的函数,仅此而已。

// 需求三:姓名可以被写

let obj3 = {
  : "高",
  : "圆圆",
  get 姓名() {
    return this. + this.;
  },
  set 姓名(xxx){
    this. = xxx[0]
    this. = xxx.slice(1)
  },
  age: 18
};

obj3.姓名 = '高媛媛'

console.log(`需求三:姓 ${obj3.},名 ${obj3.}`)

// 总结:setter 就是这样用的。用 = xxx 触发 set 函数
console.log(obj0);
console.log(obj3);
  • 可以看到属性{姓名: (...)}
  • 并不存在一个名为n的属性,而是有一个get nset n来模拟对n的读写操作

为什么要使用gettersetter模拟{n: (...)}

Object.defineProperty

  • 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
  • 应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用
  • 语法Object.defineProperty(obj, prop, descriptor)
  • 示例代码 3
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let _xxx = 0 // 用来存储 xxx 的值

Object.defineProperty(obj3, 'xxx', {
  get() {
    return _xxx
  },
  set(value) {
    _xxx = value
  },
})

_xxx是什么

代理和监听

需求一:用 Object.defineProperty 定义 属性n

1
2
3
4
5
6
7
// 需求一:用 Object.defineProperty 定义 n
let data1 = {}
Object.defineProperty(data1, 'n', {
  value: 0
})
console.log(`需求一:${data1.n}`)
// 总结:这煞笔语法把事情搞复杂了?非也,继续看。
  • n的值就是0,而不是value: 0
  • 属性的属性

需求二:属性n 不能小于 0,即 data2.n = -1 应该无效,但 data2.n = 1 有效

  • 用点运算符不能实现
  • setter
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 需求二:n 不能小于 0
// 即 data2.n = -1 应该无效,但 data2.n = 1 有效
let data2 = {}
data2._n = 0 // _n 用来偷偷存储 n 的值

Object.defineProperty(data2, 'n', {
  get() {
    return this._n // 给外部读取值
  },
  set(value) {
    if (value < 0) return // 白写
    this._n = value // 写入符合条件的值
  }
})

console.log(`需求二:${data2.n}`)
data2.n = -1
console.log(`需求二:${data2.n} 设置为 -1 失败`)
data2.n = 1
console.log(`需求二:${data2.n} 设置为 1 成功`)
// 抬杠:那如果对方直接使用 data2._n 呢?
  • this适用于任何命名(data2),表示当前对象
  • setter按条件筛选传入的属性值
  • Object.defineProperty可以在声明完了之后添加gettersetter 计算属性
  • 即在定义完一个对象之后,添加新的计算属性
  • 注意Object.defineProperty定义的属性是“不存在的”
  • Object.definePropertyget() {}中不能返回命名的属性(xxx)本身,会造成死循环
  • 可以实现按条件筛选传入的属性值
  • 理解为属性的属性

需求三:使用代理proxy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 需求三:使用代理
// 不暴露给外部任何可以访问的东西,包括对象名
let data3 = proxy({ data: { n: 0 } }) // 括号里是匿名对象,无法访问

function proxy({ data } /* 解构赋值 */) {
  const proxyObj = {}
  // 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化 // 因为怕看不懂
  Object.defineProperty(proxyObj, 'n', {
    get() {
      return data.n
    },
    set(value) {
      if (value < 0) return
      data.n = value
    }
  })
  return proxyObj // proxyObj 就是代理
}
// data3 就是 proxyObj
console.log(`需求三:${data3.n}`)
data3.n = -1
console.log(`需求三:${data3.n},设置为 -1 失败`)
data3.n = 1
console.log(`需求三:${data3.n},设置为 1 成功`)
  • 参数为匿名的对象{data: {n: 0}}包裹的数据data: {n: 0}
  • 读取proxyObj的属性,就返回data的属性
  • 设置proxyObj的属性,就写入data的属性
  • proxyObj 就是代理
  • data3暴露给外部访问的代理对象
  • 无法擅自更改属性(n

需求四:绕过代理proxy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 需求四

let myData = { n: 0 }
let data4 = proxy({ data: myData }) // 括号里是匿名对象,无法访问

// data3 就是 obj
console.log(`杠精:${data4.n}`)
myData.n = -1
console.log(`杠精:${data4.n},设置为 -1 失败了吗!?`)
// 我现在改 myData,是不是还能改?!你奈我何

需求五:就算用户擅自修改 myData,也要拦截他

  • 限制绕过代理proxy
  • value存储原始的data.n
  • delete data.n删除了原始数据,使得无法修改
  • delete data.n可以不写,Object.defineProperty(data, 'n', {...})声明新的虚拟对象会覆盖原来的
 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
// 需求五:就算用户擅自修改 myData,也要拦截他
let myData5 = { n: 0 }
let data5 = proxy2({ data: myData5 }) // 括号里是匿名对象,无法访问

function proxy2({ data }/* 解构赋值 */) {
  // 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
  // 因为我怕你们看不懂
  let value = data.n // 可变的值
  // delete data.n
  Object.defineProperty(data, 'n', {
    get() {
      return value
    },
    set(newValue) {
      if (newValue < 0) return
      value = newValue
    }
  })
  // 就加了上面几句,这几句话会监听 data

  // 声明代理
  const proxyObj = {}
  Object.defineProperty(proxyObj, 'n', {
    get() {
      return data.n
    },
    set(value) {
      if (value < 0) return // 这句话多余了
      data.n = value
    }
  })

  return proxyObj // proxyObj 就是代理
}

// data3 就是 proxyObj
console.log(`需求五:${data5.n}`)
myData5.n = -1
console.log(`需求五:${data5.n},设置为 -1 失败了`)
myData5.n = 1
console.log(`需求五:${data5.n},设置为 1 成功了`)

// 这代码看着眼熟吗?
// let data5 = proxy2({ data:myData5 })
// let vm = new Vue({data: myData})
  • 监听的逻辑
  • 代理的逻辑
  • 防止绕过条件限制修改数据

注意

  • 研究方法比知识本身更重要(console.log
  • 不读源码,也可以了解真相
  • 初级读源码自杀

data小结

Object.defineProperty

  • 可以给对象添加属性value
  • 可以给对象添加getter/setter
  • getter/setter用于对属性的读写进行监控

代理(设计模式)

  • myData对象的属性读写,全权由另一个对象vm负责
  • 那么vm就是myData的代理
  • 比如myData.n不用,偏要用vm.nthis.n)来操作myData.n

vm = new Vue({data: myData})

  • 1.会让vm成为myData的代理proxy
  • 2.会让myData的所有属性进行监控
  • 为什么需要监控,为了防止myData的属性变了,vm未知
  • Vue可以监听到任何对myData的读写
  • 监控的目的是,vm已知,属性变了就可以调用render(data)
  • 自动更新UI = render(data)
  • 通过this访问vm

示意

  • 将数据用匿名的对象{data:{n: 0}}传参给new Vue()
  • 经过new Vue()的改造加监听,暂存let value = 0,变成{get n() {return value}, set n(v) {value = v}},实现覆盖数据原来的属性
  • 并且作为原来数据的代理,将返回值{get n() {data.n}, set n(v) {data.n = v}}赋值给变量vm
  • 没有删除对象,只是覆盖了对象的属性,没有改变原来的引用,即没有断开与对象的关联

如果data有多个属性n就会有get n/ get m / get k

  • 对所有属性循环进行监听和代理(闭包隔开作用域)

4. 数据响应式 §

什么是响应式

  • 对外界刺激的反应(抽象)

Vuedata是响应式

  • const vm = new Vue({data: {n: 0}})
  • 如果修改(更新)数据vm.n的值,那么UI中的n就会响应用户操作
  • Vue2通过Object.defineProperty来实现数据响应式(ES5+)

响应式网页

  • 如果改变窗口大小,网页内容会做出响应,即响应式网页
  • 比如 示例响应式网站 smashingMagazine
  • 但是注意,一般用户不会拖动网页大小,除非iPad分屏
  • 用响应式浏览器测试 responsively
  • 区别窗口响应式与媒体查询适配设备

5. Vue2.xbug §

Object.defineProperty的问题

  • Object.defineProperty(obj, 'n', {...})
  • 必须要有一个key(即n),才能监听和代理obj.n

没有data.n会发生

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 页面无显示
new Vue({
  data: {
      // 空
  },
  template: `
    <div>{{n}}</div>
  `
}).$mount("#app");
// 控制台报错 not defined but referenced
  • 示例二Vue无警告
  • Vue只会检查第一层属性是否被定义
  • Vue不会深入检查下一层
  • 只有属性a,却要显示属性b
  • Vue并没有监听属性b,只监听了a,即对b的操作都无效
  • 控制台无报错,Vue无警告
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
new Vue({
  data: {
    obj: {
      a: 0, // obj.a 会被 Vue 监听 & 代理
      // b: undefined
    }
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setB">set b</button>
    </div>
  `,
  methods: {
    setB() {
      this.obj.b = 1; // 页面中不显示 1
      // Vue.set(this.obj, 'b', 1)
    }
  }
}).$mount("#app");
  • 此时如果点击set b,视图中不会显示1
  • 因为Vue没法监听一开始不存在的obj.b

解决办法

  • key都声明好,后面不用再加属性b: undefined,即在传入的参数中预留会用到的属性
  • 使用Vue.set或者this.$set(防止重名)
1
2
3
4
5
6
7
setB() {
// ...
  // this.obj.b = 1; // 页面中不显示 1
  Vue.set(this.obj, 'b', 1)
  this.$set(this.obj, 'b', 1)
  console.log(Vue.set === this.$set)
}

6. Vue.setthis.$set §

作用

  • 新增key,在实例化后增加新属性,不用事先写在data
  • 自动创建代理和监听(如果没有创建过)
  • 触发UI更新(副作用,并不会立刻更新,异步更新)
  • Vue.set(this.obj, 'b', 1)this.obj声明传给Object.defineProperty
  • 之后就会响应对数据b的操作,更新 UI

语法

1
2
3
Vue.set(object, propertyName, value)
// 或者
vm.$set(object, propertyName, value) // vm.$set(object, propertyName, value)

举例

  • this.$set(this.object, 'm', 100)

小结

  • Vue只会监听实例化之前传进来的属性和实例化之后用Vue.setthis.$set设置的数据
  • 在实例化后如果直接在对象上增加新属性,Vue不会将新属性转化为getter/setter,因为没有setter,所以在新属性的数据发生改变时,就不会通知watcher重新渲染视图
  • 在传入的参数中预留会用到的属性,这样就可以对属性进行监听
  • 或者是调用Vue提供的方法来添加新属性
  • 添加多个属性,使用Object.assign()_.extend()也不会让新属性被监听,在这种情况下,用原对象与要混合进去的对象的属性一起创建一个新的对象
1
this.obj = Object.assign({}, this.obj, { a: 1, b: 2 })

7. data中的数组怎么破 §

data: {n: 0, obj: {}, array: []}

  • 没法提前声明所有key(‘0’, ‘1’, ‘2’…)
  • 示例 1: 数组的长度可以一直增加,长度无法预测,下标就是key
  • 本质上array: {0: 'a', 1: 'b', 2: 'c', length: ...}
  • 无法提前把数组的key都声明出来
  • Vue也不能检测数组新增的下标
 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
new Vue({
  data: {
    array: ["a", "b", "c"]
  },
  template: `
    <div>
      {{array}}
      <button @click="setD">set d</button>
    </div>
  `,
  methods: {
    setD() {
      // this.array[3] = "d"; // 页面中不显示 'd'
      this.$set(this.array, 3, 'd')
      // this.array.push('d')
      console.log(this.array) // 加了一层__proto__
      Vue.set(this.array, 4, 'e')
      this.array[0] += 0
      this.array[1] += 1
      this.array[2] += 2
      this.array[3] += 3
      this.array[4] += 4
    }
  }
}).$mount("#app");

语法

1
2
3
vm.$set(vm.items, indexOfArray, newValue)
// 或者
Vue.set(vm.items, indexOfArray, newValue)
  • 如何不用每次改数组都要用Vue.set或者this.$set
  • this.$set 作用于数组时,并不会自动添加监听和代理,所以不会更新 UI

尤雨溪的做法

  • 篡改数组的栈方法API,文档中「变更方法」章节
  • 7 个重新封装的APIpush()pop()shift()unshift()splice()sort()reverse()添加到数组新的一层原型链上,覆盖原来同名的方法
  • 例如:新的push会做 1. 调用以前的push操作;2. 通知Vue 添加监听和代理,帮你this.$set(...)
  • 这 7 个API都会被Vue篡改,调用后会更新UI
1
vm.items.splice(indexOfItem, 1, newValue)

改写数组的原型方法:ES6写法 V.S. ES5写法


8. ES6改写数组原型方法 V.S. ES5写法 §

  • 增加一层原型链
 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
// ES6
class VueArray extends Array {
    push(...args) {
        const oldLength = this.length // this 即当前数组
        super.push(...args) // 先调下一层(父类)的原型方法
        // this.length 改变
        console.log('你 push 了')
        // 记录原来下标,遍历新增下标
        for(let i = oldLength; i < this.length; i++) {
            Vue.set(this, i, this[i]) // this 即当前数组
            // 将每个新增的 key 都告诉 Vue
            // i 的值即push的所有元素的下标
            // 将数组每个变动都告诉vue,去添加监听 和代理
        }
    },
    // ...
}

const a =  new VueArray(1, 2, 3)
console.log(a)
a.push(4)

// ES5 - 原型
var vueArrayPrototype = {
    push: function() {
        console.log('你 push 了')
        return Array.prototype.push.apply(this, arguments)
    }
}
VueArrayPrototype.__proto__ = Array.prototype // 重新指向数组的原型 // 非标属性,仅供学习
var array = Object.create(vueArrayPrototype) // 将实例的原型和vueArrayPrototype连起来
array.push(1)

9. 总结 §

对象中新增的key

  • Vue没有办法事先监听和代理
  • 使用set来新增key,创建监听和代理,更新UI
  • 最好提前把属性都写出来,不要新增key
  • 但数组做不到「不新增 key」

数组中新增的key

  • 不可以用set来新增key创建监听和代理,更新UI
  • this.$set 作用于数组时,并不会自动添加监听和代理
  • 不过尤雨溪篡改了 7 个数组 API 方便对数组进行增删
  • 改查不需要,改即改原来的,原来本身就有监听
  • 删除多余的监听器,节省内存
  • 使用 Vue 提供的数组变异 API 时,会自动添加监听和代理,并异步更新UI
  • 结论:数组新增key最好通过 7 个API
  • 深入响应式原理-检测变化的注意事项
  • Vue3.x的解决方案

10. 面试题 §

Vue 文档自测题

对 Vue 数据响应式的理解

  • 数据响应式就是,将 Model 绑定到 View,当更新 Model 时,View 会自动更新
  • 数据响应式强调数据驱动 DOM 生成,而不是直接操作 DOM
  • 即所谓的数据驱动,指视图是由数据驱动生成的,对视图的修改,不会直接操作 DOM,而是通过修改数据
  • Vue 的响应式原理依赖于Object.defineProperty 中的settergetter
  • Vue 通过设定对象属性setter/getter方法来监听并代理数据的变化
  • 将一个 JS 对象作为参数传入到 Vue 中时,Vue 会遍历该对象的所有属性,使用 Object.defineProperty 把这些属性全部转为 getter/setter,实现监听,并且会返回一个组件实例作为该对象的代理
  • 当操作组件实例的数据时,就会同时修改传入的 JavaScript 对象的数据,而且 vue 同时会监听这个 JS 对象,在 JS 对象的数据发生修改时,同时也会更新组件实例的数据
  • 每个组件实例都对应一个watcher 实例,watcher 实例会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染

理解了data才能继续学习computedwatch


参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名