封面P站: https://www.pixiv.net/artworks/83495208

响应式原理

这张图片是Vue官方响应式图片、像我这样的菜鸡是很难理解的、所有我通过查阅资料和看视频来理解Vue响应式、虽然并不能完全看懂、但是尽自己所能总结出自己的想法吧!!!

Vue响应式原理解读

什么是响应式?

响应式就是当数据发生改变时、视图也会跟着更新

Vue响应式原理是数据劫持发布订阅模式

  • Vue2.0时使用的ES5中的Object.defineProperty设置对象属性的set/get方法来监听数据的变化
  • Vue3.0的时使用的ES6中的Proxy实现数据劫持
  • 观察者模式

下面我们就通过最直观的方法来了解Vue响应式的原理吧

Object.defineProperty

基本语法

1
Object.defineProperty(obj, prop, descriptor)

参数

  • obj要定义属性的对象。
  • prop要定义或修改的属性的名称或 Symbol
  • descriptor要定义或修改的属性描述符。

descriptor属性修饰符:

修饰符说明
configurabletrue 时,该属性也能从对应的对象上被删除(delete)。 默认为 false
enumerabletrue 可以被遍历。默认为 false
数据描述符还具有以下可选键值:
value该属性对应的值。默认为 undefined
writabletrue 时,属性值value才能被赋值运算符改变。 默认为 false。存在改属性是不能有get、set
存取描述符还具有以下可选键值:
get属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined
set属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined

简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = {};
var value;

Object.defineProperty(obj, "name", {
enumerable: true,
configurable: true,
set(newValue) {
value = newValue;
console.log("触发set方法");
return newValue;
},
get() {
console.log("触发set方法");
return value;
}
})
obj.name = "zykj"
console.log(obj.name);

最终打印:

1
2
3
触发set方法
触发set方法
zykj

在Vue2.0中我们使用Object.defineProperty给data中的属性加上get、set方法实现数据劫持

观察者模式

观察者模式又称发布订阅模式(只是观察者模式的一个别称。),它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。

  • 优点:更加解耦,支持广播通信

  • 缺点:大量观察者,广播有性能问题

图解

这里就简单理解一下 参考: https://blog.csdn.net/qq_25800235/article/details/86659422

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
class Observer  {
constructor(name) {
this.name = name
}
update(data) {
console.log("数据修改了"+data);
}
}

class Subject {
constructor(name) {
this.name = name
//用于存放观察者
this.observers = []
}
addObserver(observer) {
this.observers.push(observer)
}
removeObserver(observer) {
let index = this.observers.indexOf(observer)
this.observers.splice(index, 1)
}
notify(data) {
this.observers.forEach((observer) => {
observer.update(data)
})
}
}

var ob1 = new Observer ('ob1');
var ob2 = new Observer ('ob2');
var sub = new Subject('sub');
sub.addObserver(ob1);
sub.addObserver(ob2);
sub.notify('some context'); // 数据修改了some context

代码实现

我将从以下几个方面来实现:

  • 初始化Vue

  • 编译模板:将指令转换为数据

  • 数据劫持:通过Object.defineProperty对每个数据添加get/set方法

  • 数据代理:可以直接通过vue实例访问data中的数据、例: vue.people.namevue.$data.people.name

  • 发布订阅模式: 通知观察者更新数据

初始化Vue

html

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<input type="text" v-model="people.name">
<div>{{ people.name }}</div>
<ul>
<li>1</li>
<li>2</li>
</ul>
{{ people.age }}
{{ getName }}
<div v-html="message"></div>
<button v-on:click="change">更新</button>
</div>

首先我们需要有一个vue实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const vue = new Vue({
el: "#app",
data: {
people: {
name: 'zykj',
age: 18
},
message: "<h1>哈哈哈哈</h1>"
},
computed: {
getName() {
return "我是" + this.people.name
}
},
methods: {
change() {
this.people.age = 20
}
}
})

那我就创建一个Vue类、模仿Vue默认传入options对象、并且对eldata

1
2
3
4
5
6
class Vue {
constructor(options) {
this.$el = options.el
this.$data = options.data
}
}

编译模板

我们需要在el存在时对模板进行编译、这时我们准备一个Compile进行编译模板、构造函数中需要传入el当前对象(this)

  1. 首先是对传入的el(可能传入: #app或者document.querySelector("#app"))进行判断、封装一个函数来判断是否为元素节点(nodeType = 1)

    1
    2
    3
    4
    5
    // 判断是否为元素节点
    isElementNode(node) {
    // nodeType 为 1 是元素节点
    return node.nodeType === 1
    }
  2. 因为我们需要对所有节点进行分析、频繁操作会影响性能、可以创建一个文档碎片 document.createDocumentFragment()、遍历每个节点并且添加appendChild到文档碎片中、每次获取第一个节点firstChild并去除、可以依次放入文档碎片中、对每个元素替换完之后再放回appendChildDom中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //将el中的元素放入文档碎片(内存)中
    node2fragment(node) {
    // 创建文档碎片
    let fragment = document.createDocumentFragment();
    let firstChild;
    // 通过每次获取第一个元素来将所有元素添加到文档碎片中
    while (firstChild = node.firstChild) {
    // appendChild 具有移动性 可以将页面中的元素移动到文档碎片中
    fragment.appendChild(firstChild)
    }
    return fragment
    }
  3. 把内容放入文档碎片后、对文档中的数据进行编译:双大括号里的内容转换成真正的数据、compile方法编译、获取Fragment中的中的所有子节点childNodes(返回一个伪数组)、通过扩展运算符(…) 转换成为数组再进行遍历得到每一项(child) 进行判断是否为元素节点(isElementNode)或者是文本节点

    • 如果是元素节点:我们首先遍历的是第一层元素(并且编译元素节点compileElementNode)

    • 如果还有子元素、我们需要再进行递归子元素、如果是文本节点: 调用compileText

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      //编译模板
      compile(node) {
      // 获取子节点 返回的是一个伪数组
      let childNodes = node.childNodes; // NodeList(7) [text, input, text, div, text, ul, text]
      // 通过...转成 数组
      [...childNodes].forEach((child) => {
      //判断是为元素节点 如 <input type="text" v-model="people.name">
      if (this.isElementNode(child)) {
      // 编译元素节点
      this.compileElementNode(child);
      // 如果是元素 需要判断自己的子节点 (递归)
      this.compile(child)
      } else {
      // 编译文本节点
      this.compileText(child);
      }
      })
      }
  4. 再来实现一下这两个方法

    compileElementNode

    • 到这里了我们可以确定获取到<input type="text" v-model="people.name">、开始获取每个属性值(attributes)

    • 同样进行遍历得到每个属性(是以对象形式的、name为key、value是value)、进行解构赋值(把value重新命名为expr)

    • 判断是否以v-开头的(isDirective)、获取到v-model在进行(-)分割得到对应的指令directive:事件、当然如果是v-on:click需要再进行分隔得到directiveName和事件名eventName、需要一个工具类CompileUtil表达式expr(people.name) 替换成对应的数据、expr去除两边空格

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      // 编译元素
      compileElementNode(node) {
      // 返回的是一个伪数组
      let attributes = node.attributes;
      [...attributes].forEach((attr) => {
      // 通过解构赋值获取 name(type) 和 value(text)
      let { name, value: expr } = attr;
      if (this.isDirective(name)) {
      //获取指令、 如 v-model 获取 model v-on:click
      let [, directive] = name.split("-");
      let [directiveName, eventName] = directive.split(":");
      CompileUtil[directiveName](node, expr.trim(), this.vm, eventName)
      }
      })

      }
      //判断是否为指令
      isDirective(attrName) {
      //ES6 语法 判断是否以 v- 开头
      return attrName.startsWith("v-")
      }

      compileText

      文本节点通过textContent获取文本内容、得到的内容我们需要判断是否含有 {{ people.name }} 、通过正则表达式/\{\{(.+?)\}\}/判断、再通过工具类CompileUtil表达式expr(people.name) 替换成对应的数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 编译文本
      compileText(node) {
      let content = node.textContent; // 获取文本内容
      //匹配 {{ xxx }} .+? 匹配(任意字符重复一次或更多次)再重复0次或一次避免匹配 {{ aaa }} {{ bbb }}
      let reg = /\{\{(.+?)\}\}/;
      if (reg.test(content)) {
      CompileUtil['text'](node, content, this.vm)
      }
      }
  5. 工具类CompileUtil

    通过不同指令执行不同的方法:

    首先对CompileUtil[directiveName](node, expr.trim(), this.vm, eventName)进行分析:

    为了确保能够正确的设置值和获取值我们需要准备getVal(vm, expr)setVal(vm, expr, value)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    getVal(vm, expr) { // 我们以 people.name 进行分析
    return expr.split(".").reduce((data, current) => {
    /* data 前一项数据 current 当前数据
    第一次 从 vm.$data[people] 返回 people: { name: 18 }
    第二次 people[name] 得到 18
    */
    return data[current];
    }, vm.$data/*这里为初始的值*/)
    },
    setVal(vm, expr, value) { // 将 people.name 设置为新值
    expr.split(".").reduce((data, current, index, arr) => {
    if (index == arr.length - 1) {
    // 只有当前索引等于数组的最后一项的索引 将修改数据 people[name] = value
    return data[current] = value
    }
    return data[current]
    }, vm.$data)
    },

    以及修改值的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //工具类替换模板内容
    CompileUtil = {
    //...
    //更新数据
    updater: {
    // v-mdoel 数据修改
    modelUpdater(node, value) {
    node.value = value;
    },
    // 文本数据修改
    textUpdater(node, value) {
    node.textContent = value
    },
    // v-html 数据修改
    htmlUpdater(node,value){
    node.innerHTML = value
    }
    }
    }
    • v-model指令 model方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // v-model 指令  node为元素节点、expr为表达式(people.name)、vm当前实例
      model(node, expr, vm) {
      let fn = this.updater['modelUpdater']
      // 给input 赋予 value 属性 node.value = xxx
      let val = this.getVal(vm, expr)
      //监听事件
      node.addEventListener('input', (event) => {
      //值修改时修改数据
      this.setVal(vm, expr, event.target.value)
      })
      fn(node, val)
      },
    • {{  }} 模板 text方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      text(node, expr, vm) {
      let fn = this.updater['textUpdater']
      // 通过 replace 将 {{ people.name }} 替换为对应的值
      let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      // args[1]: args 是一个数组、第二个参数是匹配到的内容
      return this.getVal(vm, args[1].trim())
      })
      fn(node, content)
      },
    • v-on:click指令on方法

      1
      2
      3
      4
      5
      6
      on(node, expr, vm, eventName) {
      node.addEventListener(eventName, (event) => {
      // 相当于 vue[change]()
      vm[expr].call(vm, event)
      })
      },
    • v-html 指令html方法

      1
      2
      3
      4
      5
      6
      html(node,expr,vm){
      let fn = this.updater['htmlUpdater']
      // 给input 赋予 value 属性 node.value = xxx
      let val = this.getVal(vm, expr)
      fn(node, val)
      },
    • 完整代码

      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
      //工具类替换模板内容
      CompileUtil = {
      getVal(vm, expr) {
      return expr.split(".").reduce((data, current) => {
      return data[current];
      }, vm.$data)
      },
      setVal(vm, expr, value) {
      expr.split(".").reduce((data, current, index, arr) => {
      if (index == arr.length - 1) {
      return data[current] = value
      }
      return data[current]
      }, vm.$data)
      },
      // v-model 指令 node为元素节点、expr为表达式(people.name)、vm当前实例
      model(node, expr, vm) {
      let fn = this.updater['modelUpdater']
      // 给input 赋予 value 属性 node.value = xxx
      let val = this.getVal(vm, expr)
      node.addEventListener('input', (event) => {
      this.setVal(vm, expr, event.target.value)
      })
      fn(node, val)
      },
      on(node, expr, vm, eventName) {
      node.addEventListener(eventName, (event) => {
      vm[expr].call(vm, event)
      })
      },
      html(node,expr,vm){
      let fn = this.updater['htmlUpdater']
      // 给input 赋予 value 属性 node.value = xxx
      let val = this.getVal(vm, expr)
      fn(node, val)
      },
      text(node, expr, vm) {
      let fn = this.updater['textUpdater']
      // 通过 replace 将 {{ people.name }} 替换为对应的值
      let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      // args[1]: args 是一个数组、第二个参数是匹配到的内容
      return this.getVal(vm, args[1].trim())
      })
      fn(node, content)
      },
      //更新数据
      updater: {
      modelUpdater(node, value) {
      node.value = value;
      },
      textUpdater(node, value) {
      node.textContent = value
      },
      htmlUpdater(node,value){
      node.innerHTML = value
      }
      }
      }

数据劫持

在我们判断数据是否存在时、首先需要给所有数据添加set/get、通过Observer类

  • observer(data)方法 首先判断data是否存在并且是否为对象类型、是就进行for in遍历、Recative
  • 通过Object.defineProperty添加get和set方法
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
// 改类实现数据劫持、添加set/get方法
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
if (data && typeof data == "object") {
for (let key in data) {
this.defineRecative(key, data, data[key])
}
}
}
defineRecative(key, data, value) {
// 每次添加set/get方法时还需要递归遍历 如 people:{ zykj: { age: 18 } }
this.observer(value)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
// 因为普通函数的this的指向为当前对象
// 我们使用箭头函数将this指向函数定义在的对象里
set: (newValue) => {
if (newValue !== value) {
// 给修改的新值重新添加get/set
this.observer(newValue)
value = newValue
}
},
get() {
return value
}
})
}
}

数据代理

使用过vue我们可以知道、上面的示例中、可以通过vue.people.name、访问到数据、这样就进行了代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//代理 使得 通过 vue.people.name 访问和修改数据
proxyData(data) {
for (let key in data) {
// 这里第一个参数 this就是vue示例 通过vue访问到 vue.$data中的数据
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
set(newValue) {
data[key] = newValue
},
get() {
return data[key]
}
})
}
}

发布订阅模式

Watcher类似vue中的watch、可以监听数据的变化

1
2
3
4
5
watch:{
'data中的数据'(newValue,oldValue)=>{

}
}

此时需要Watcher来监听数据的变化来做出相应的改变

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
// (观察者) 发布订阅模式
class Watcher {
constructor(vm, expr, callback) {
this.vm = vm;
this.expr = expr.trim();
// 回调函数 当数据发送变化时传入新值进行改变
this.callback = callback
// 保存变化之前的值
this.oldValue = this.get()
}
get() {
//依赖注入 将 当前对象存入 Dep.target 中
Dep.target = this
let value = CompileUtil.getVal(this.vm, this.expr)
Dep.target = null
return value
}
update() {
let newValue = CompileUtil.getVal(this.vm, this.expr)
// 当 新值和旧值不相同时才进行回调
if (newValue != this.oldValue) {
this.callback(newValue)
this.oldValue = newValue
}
}
}

Dep 用于存放 Watcher 来通知 数据更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 用于通知观察者更新数据
class Dep {
constructor() {
//用于存放 Watcher
this.subs = []
}
//将观察者添加进一个数组里
addSub(watcher) {
this.subs.push(watcher)
}
// 通知每个 watcher进行数据更新
notify() {
this.subs.forEach((watcher) => watcher.update())
}
}

问题何时 创建 Dep 和 Watcher

创建Dep需要在数据劫持时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
defineRecative(key, data, value) {
this.observer(value)
let dep = new Dep() //给每个属性添加发布订阅的功能
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
// 因为普通函数的this的指向为当前对象
// 我们使用箭头函数将this指向函数定义在的对象里
set: (newValue) => {
if (newValue !== value) {
//给新的值添加get/set
this.observer(newValue)
value = newValue
// 数据修改时对数据进行更新
dep.notify()
}
},
get() {
// Dep.target 为 Dep对象、当Dep.target存在时将dep添加进Wacther的数组中
Dep.target && dep.addSub(Dep.target)
return value
}
})
}

创建Watcher

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
// v-model 指令  node为元素节点、expr为表达式(people.name)、vm当前实例
model(node, expr, vm) {
let fn = this.updater['modelUpdater']
// 给input 赋予 value 属性 node.value = xxx
let val = this.getVal(vm, expr)
new Watcher(vm, expr, (newValue) => {
// 当数据更新时会调用这个方法对数据进行更新
fn(node, newValue)
})
node.addEventListener('input', (event) => {
this.setVal(vm, expr, event.target.value)
})
fn(node, val)
},
html(node, expr, vm) {
let fn = this.updater['htmlUpdater']
// 给input 赋予 value 属性 node.value = xxx
let val = this.getVal(vm, expr)
new Watcher(vm, expr, (newValue) => {
// 当数据更新时会调用这个方法对数据进行更新
fn(node, newValue)
})
fn(node, val)
},
getContentValue(vm, expr) {
// 遍历表达式 {{ peoele.name }} 将表达式替换成数据返回
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1].trim())
})
},
text(node, expr, vm) {
let fn = this.updater['textUpdater']
// 通过 replace 将 {{ people.name }} 替换为对应的值
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
fn(node, this.getContentValue(vm, expr))
})
// args[1]: args 是一个数组、第二个参数是匹配到的内容
return this.getVal(vm, args[1].trim())
})
fn(node, content)
},

最终效果:

每个Dep中存放对应的值依赖

当数据改变时、会调用dep.notify通知 Dep 对象中的所有Wachter数据更新、这时Watcher的第三个参数: 回调函数就会修改数据了

最终代码

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
class Vue {
constructor(options) {
this.$el = options.el
this.$data = options.data
let computed = options.computed
let methods = options.methods
// 我们需要判断 el 是否存在
if (this.$el) {
// 数据劫持
new Observer(this.$data)
//计算属性
for (let key in computed) {
Object.defineProperty(this.$data, key, {
get: () => {
return computed[key].call(this)
}
})
}
// 方法
for (let key in methods) {
Object.defineProperty(this, key, {
get() {
return methods[key]
}
})
}
// 数据代理
this.proxyData(this.$data)
//创建编译模板 传入 el 和 当前对象
new Compile(this.$el, this)
}
}
//代理 使得 通过 vue.people.name 访问和修改数据
proxyData(data) {
for (let key in data) {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
set(newValue) {
data[key] = newValue
},
get() {
return data[key]
}
})
}
}
}

//编译模板
class Compile {
constructor(el, vm) {
// 当用户传入 #app 与 document.querySelector('#app') 进行判断
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 因为我们需要操作每个元素、频繁操作会导致页面回流重绘多次、避免发送需要把元素添加到内存中
let fragment = this.node2fragment(this.el)
// 把节点中需要替换的内容进行替换
// 编译模板
this.compile(fragment)
// 再把内容放回页面
this.el.appendChild(fragment)
}
// 判断是否为元素节点
isElementNode(node) {
// nodeType 为 1 是元素节点
return node.nodeType === 1
}
//将el中的元素放入文档碎片(内存)中
node2fragment(node) {
// 创建文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
// 通过每次获取第一个元素来将所有元素添加到文档碎片中
while (firstChild = node.firstChild) {
// appendChild 具有移动性 可以将页面中的元素移动到文档碎片中
fragment.appendChild(firstChild)
}
return fragment
}
//编译模板
compile(node) {
// 获取子节点 返回的是一个伪数组
let childNodes = node.childNodes; // NodeList(7) [text, input, text, div, text, ul, text]
// 通过...转成 数组
[...childNodes].forEach((child) => {
//判断是为元素节点 如 <input type="text" v-model="people.name">
if (this.isElementNode(child)) {
this.compileElementNode(child);
// 如果是元素 需要判断自己的子节点 (递归)
this.compile(child)
} else {
this.compileText(child);
}
})
}
// 编译元素
compileElementNode(node) {
// 返回的是一个伪数组
let attributes = node.attributes;
[...attributes].forEach((attr) => {
// 通过解构复制获取 name(type) 和 value(text)
let { name, value: expr } = attr;
if (this.isDirective(name)) {
//获取指令、 如 v-model 获取 model v-on:click
let [, directive] = name.split("-");
let [directiveName, eventName] = directive.split(":");
CompileUtil[directiveName](node, expr.trim(), this.vm, eventName)
}
})
}
// 编译文本
compileText(node) {
let content = node.textContent; // 获取文本内容
//匹配 {{ xxx }} .+? 匹配(任意字符重复一次或更多次)再重复0次或一次避免匹配 {{ aaa }} {{ bbb }}
let reg = /\{\{(.+?)\}\}/;
if (reg.test(content)) {
CompileUtil['text'](node, content, this.vm)
}
}
//判断是否为指令
isDirective(attrName) {
//ES6 语法 判断是否以 v- 开头
return attrName.startsWith("v-")
}
}

//工具类替换模板内容
CompileUtil = {
getVal(vm, expr) {
return expr.split(".").reduce((data, current) => {
return data[current];
}, vm.$data)
},
setVal(vm, expr, value) {
expr.split(".").reduce((data, current, index, arr) => {
if (index == arr.length - 1) {
return data[current] = value
}
return data[current]
}, vm.$data)
},
// v-model 指令 node为元素节点、expr为表达式(people.name)、vm当前实例
model(node, expr, vm) {
let fn = this.updater['modelUpdater']
// 给input 赋予 value 属性 node.value = xxx
let val = this.getVal(vm, expr)
new Watcher(vm, expr, (newValue) => {
// 当数据更新时会调用这个方法对数据进行更新
fn(node, newValue)
})
node.addEventListener('input', (event) => {
this.setVal(vm, expr, event.target.value)
})
fn(node, val)
},
on(node, expr, vm, eventName) {
node.addEventListener(eventName, (event) => {
vm[expr].call(vm, event)
})
},
html(node, expr, vm) {
let fn = this.updater['htmlUpdater']
// 给input 赋予 value 属性 node.value = xxx
let val = this.getVal(vm, expr)
new Watcher(vm, expr, (newValue) => {
// 当数据更新时会调用这个方法对数据进行更新
fn(node, newValue)
})
fn(node, val)
},
getContentValue(vm, expr) {
// 遍历表达式 {{ peoele.name }} 将表达式替换成数据返回
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1].trim())
})
},
text(node, expr, vm) {
let fn = this.updater['textUpdater']
// 通过 replace 将 {{ people.name }} 替换为对应的值
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
fn(node, this.getContentValue(vm, expr))
})
// args[1]: args 是一个数组、第二个参数是匹配到的内容
return this.getVal(vm, args[1].trim())
})
fn(node, content)
},
//更新数据
updater: {
modelUpdater(node, value) {
node.value = value;
},
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
}
}
}

// 改类实现数据劫持、添加set/get方法
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
if (data && typeof data == "object") {
for (let key in data) {
this.defineRecative(key, data, data[key])
}
}
}
defineRecative(key, data, value) {
this.observer(value)
let dep = new Dep() //给每个属性添加发布订阅的功能
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
// 因为普通函数的this的指向为当前对象
// 我们使用箭头函数将this指向函数定义在的对象里
set: (newValue) => {
if (newValue !== value) {
//给新的值添加get/set
this.observer(newValue)
value = newValue
dep.notify()
}
},
get() {
Dep.target && dep.addSub(Dep.target)
return value
}
})
}
}

// this.$watch('监听的数据',(newValue,oldValue)=>{
//
//})

// (观察者) 发布订阅模式
class Watcher {
constructor(vm, expr, callback) {
this.vm = vm;
this.expr = expr.trim();
this.callback = callback
// 保存变化之前的值
this.oldValue = this.get()
}
get() {
//依赖注入 将 当前对象存入 Dep.target 中
Dep.target = this
let value = CompileUtil.getVal(this.vm, this.expr)
Dep.target = null
return value
}
update() {
let newValue = CompileUtil.getVal(this.vm, this.expr)
if (newValue != this.oldValue) {
this.callback(newValue)
this.oldValue = newValue
}
}
}

// 用于通知观察者更新数据
class Dep {
constructor() {
//用于存放 Watcher
this.subs = []
}
//将观察者添加进一个数组里
addSub(watcher) {
this.subs.push(watcher)
}
// 通知每个 watcher进行数据更新
notify() {
this.subs.forEach((watcher) => watcher.update())
}
}

源码分析

代码: https://github.com/vuejs/vue/tree/v2.6.11

我们从 new Vue的时候开始分析(执行 new Vue 时会依次执行以下方法):

  1. Vue.prototype._init(option)

  2. initState(vm)

    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
    // 代码位置 https://github.com/vuejs/vue/blob/v2.6.11/src/core/instance/state.js
    export function initState (vm: Component) {
    vm._watchers = []
    const opts = vm.$options
    if (opts.props) initProps(vm, opts.props)
    if (opts.methods) initMethods(vm, opts.methods)
    if (opts.data) {
    // 有数据就会执行 initData方法
    initData(vm)
    } else {
    observe(vm._data = {}, true /* asRootData */)
    }
    if (opts.computed) initComputed(vm, opts.computed)
    if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
    }
    }

    function initData (vm: Component) {
    let data = vm.$options.data
    data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
    if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
    'data functions should return an object:\n' +
    'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
    vm
    )
    }
    // proxy data on instance
    const keys = Object.keys(data)
    const props = vm.$options.props
    const methods = vm.$options.methods
    let i = keys.length
    while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
    if (methods && hasOwn(methods, key)) {
    warn(
    `Method "${key}" has already been defined as a data property.`,
    vm
    )
    }
    }
    if (props && hasOwn(props, key)) {
    process.env.NODE_ENV !== 'production' && warn(
    `The data property "${key}" is already declared as a prop. ` +
    `Use prop default value instead.`,
    vm
    )
    } else if (!isReserved(key)) {
    // 1. data属性代理
    proxy(vm, `_data`, key)
    }
    }
    // observe data
    // 2.对data调用observe
    observe(data, true /* asRootData */)
    }
    • 1.中通过 while 循环内调用proxy函数把data的属性代理到vue实例上。之后可以通过 vue.key 访问到 data.key
    • 2.中 之后对data调用observe方法、数据将会变成响应式
  3. observe(vm._data)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // https://github.com/vuejs/vue/blob/v2.6.11/src/core/observer/index.js
    export function observe (value: any, asRootData: ?boolean): Observer | void {
    if (!isObject(value) || value instanceof VNode) {
    return
    }
    let ob: Observer | void
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
    } else if (
    // 3.检测当前的数据是否是对象或者数组,如果是,则生成对应的Observer
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
    ) {
    // 3.
    ob = new Observer(value)
    }
    if (asRootData && ob) {
    ob.vmCount++
    }
    return ob
    }

    3.中对传入的数据对象进行了判断、只对对象和数组类型生成Observer

  4. new Observer(data)

    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
    // 代码位置: https://github.com/vuejs/vue/blob/v2.6.11/src/core/observer/observer.js
    export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; // number of vms that have this object as root $data

    constructor (value: any) {
    this.value = value
    // 生成了一个消息订阅器dep实例 关于dep的结构稍后详细介绍
    this.dep = new Dep()
    this.vmCount = 0
    // def函数给当前数据添加不可枚举的__ob__属性,表示该数据已经被observe过
    def(value, '__ob__', this)
    // 4.对数组类型的数据 调用observeArray方法;对对象类型的数据,调用walk方法
    if (Array.isArray(value)) {
    if (hasProto) {
    protoAugment(value, arrayMethods)
    } else {
    copyAugment(value, arrayMethods, arrayKeys)
    }
    this.observeArray(value)
    } else {
    this.walk(value)
    }
    }

    /**
    * Walk through all properties and convert them into
    * getter/setters. This method should only be called when
    * value type is Object.
    */
    walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
    }
    }

    /**
    * Observe a list of Array items.
    */
    /* observe数组类型数据的每个值, */
    observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
    }
    }
    }

    /* defineReactive的核心思想改写数据的getter和setter */
    export function defineReactive (
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
    ) {
    // 生成一个dep实例,注意此处的dep和前文Observer类里直接添加的dep的区别
    const dep = new Dep()

    // 检验该属性是否允许重新定义setter和getter
    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
    return
    }

    // cater for pre-defined getter/setters
    // 获取原有的 getter/setters
    const getter = property && property.get
    const setter = property && property.set
    if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
    }
    // 此处对val进行了observe
    let childOb = !shallow && observe(val)
    // 下面的代码利用Object.defineProperty函数把数据转化成getter和setter,并且在getter和setter时,进行了一些操作
    Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
    // dep.depend()其实就是dep和watcher进行了互相绑定,而Dep.target表示需要绑定的那个watcher,任何时刻都最多只有一个,后面还会解释
    dep.depend()
    if (childOb) {
    // 当前对象的子对象的依赖也要被收集
    childOb.dep.depend()
    if (Array.isArray(value)) {
    dependArray(value)
    }
    }
    }
    return value
    },
    set: function reactiveSetter (newVal) {
    const value = getter ? getter.call(obj) : val
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
    return
    }
    /* eslint-enable no-self-compare */
    if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
    }
    // #7981: for accessor properties without setter
    if (getter && !setter) return
    if (setter) {
    setter.call(obj, newVal)
    } else {
    val = newVal
    }
    // 观察新的val并通知订阅者们属性有更新
    childOb = !shallow && observe(newVal)
    dep.notify()
    }
    })
    }

    在Observer类代码中,首先给当前数据添加了一个dep实例,存放于对象或者数组类型数据的,然后把_ob_挂在该数据上

    它是该数据项被observe的标志、可以看的每个data上都有_ob_(observe只对对象和数组有效)

    随后,对于数组和对象类型的数据做不同处理:

    • 对于数组类型的数: 调用observeArray方法

    • 对于对象,我们执行walk()方法,而就是对于当前数据对象的每个key,执行defineReactive()方法、这个方法就是给data添加get/set方法

      图解

  5. 接下来看一下dep

    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
    // 代码位置: https://github.com/vuejs/vue/blob/v2.6.11/src/core/observer/observer.js
    let uid = 0
    /**
    * A dep is an observable that can have multiple
    * directives subscribing to it.
    */
    export default class Dep {
    static target: ?Watcher;
    id: number;
    subs: Array<Watcher>;

    constructor () {
    this.id = uid++
    this.subs = []
    }
    // 添加一个watcher
    addSub (sub: Watcher) {
    this.subs.push(sub)
    }
    // 移除一个watcher
    removeSub (sub: Watcher) {
    remove(this.subs, sub)
    }
    // 让当前watcher收集依赖 同时Dep.target.addDep也会触发当前dep收集watcher
    depend () {
    if (Dep.target) {
    Dep.target.addDep(this)
    }
    }
    // 通知watcher们对应的数据有更新
    notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
    subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
    }
    }
    }

    这个类有两个属性:

    • 第一个是id,在每个vue实例中都从0开始计数

    • 另一个是subs数组,用于存放wacther

      前面我们知道: 一个数据对应一个Dep,所以subs 里存放的也就是依赖该数据需要绑定的wacther

      Dep.target属性是全局共享的,表示当前在收集依赖的那个Watcher,在每个时刻最多只会有一个

  6. 接下来看一下watcher

    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
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    // 代码位置: https://github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js
    export default class Watcher {
    vm: Component;
    expression: string;
    cb: Function;
    id: number;
    deep: boolean;
    user: boolean;
    lazy: boolean;
    sync: boolean;
    dirty: boolean;
    active: boolean;
    deps: Array<Dep>;
    newDeps: Array<Dep>;
    depIds: SimpleSet;
    newDepIds: SimpleSet;
    before: ?Function;
    getter: Function;
    value: any;

    constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
    ) {
    this.vm = vm
    if (isRenderWatcher) {
    vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
    this.deep = !!options.deep
    this.user = !!options.user
    this.lazy = !!options.lazy
    this.sync = !!options.sync
    this.before = options.before
    } else {
    this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
    ? expOrFn.toString()
    : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
    this.getter = expOrFn
    } else {
    this.getter = parsePath(expOrFn)
    if (!this.getter) {
    this.getter = noop
    process.env.NODE_ENV !== 'production' && warn(
    `Failed watching path: "${expOrFn}" ` +
    'Watcher only accepts simple dot-delimited paths. ' +
    'For full control, use a function instead.',
    vm
    )
    }
    }
    this.value = this.lazy
    ? undefined
    : this.get()
    }

    /**
    * Evaluate the getter, and re-collect dependencies.
    */
    get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
    value = this.getter.call(vm, vm)
    } catch (e) {
    if (this.user) {
    handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
    throw e
    }
    } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
    traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    }
    return value
    }

    /**
    * Add a dependency to this directive.
    */
    addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
    dep.addSub(this)
    }
    }
    }

    /**
    * Clean up for dependency collection.
    */
    cleanupDeps () {
    let i = this.deps.length
    while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
    dep.removeSub(this)
    }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
    }

    /**
    * Subscriber interface.
    * Will be called when a dependency changes.
    */
    update () {
    /* istanbul ignore else */
    if (this.lazy) {
    this.dirty = true
    } else if (this.sync) {
    this.run()
    } else {
    queueWatcher(this)
    }
    }

    /**
    * Scheduler job interface.
    * Will be called by the scheduler.
    */
    run () {
    if (this.active) {
    const value = this.get()
    if (
    value !== this.value ||
    // Deep watchers and watchers on Object/Arrays should fire even
    // when the value is the same, because the value may
    // have mutated.
    isObject(value) ||
    this.deep
    ) {
    // set new value
    const oldValue = this.value
    this.value = value
    if (this.user) {
    try {
    this.cb.call(this.vm, value, oldValue)
    } catch (e) {
    handleError(e, this.vm, `callback for watcher "${this.expression}"`)
    }
    } else {
    this.cb.call(this.vm, value, oldValue)
    }
    }
    }
    }

    /**
    * Evaluate the value of the watcher.
    * This only gets called for lazy watchers.
    */
    evaluate () {
    this.value = this.get()
    this.dirty = false
    }

    /**
    * Depend on all deps collected by this watcher.
    */
    depend () {
    let i = this.deps.length
    while (i--) {
    this.deps[i].depend()
    }
    }

    /**
    * Remove self from all dependencies' subscriber list.
    */
    teardown () {
    if (this.active) {
    // remove self from vm's watcher list
    // this is a somewhat expensive operation so we skip it
    // if the vm is being destroyed.
    if (!this.vm._isBeingDestroyed) {
    remove(this.vm._watchers, this)
    }
    let i = this.deps.length
    while (i--) {
    this.deps[i].removeSub(this)
    }
    this.active = false
    }
    }
    }

    watcher用于watcher用来解析表达式,收集依赖,并且当表达式的值改变时触发回调函数,用在$watch() api 和指令之中。

  7. 模板渲染

    这里的分析来自: https://zhuanlan.zhihu.com/p/168768245

    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
    // new Vue 执行流程。
    // 1. Vue.prototype._init(option)
    // 2. vm.$mount(vm.$options.el)
    // 3. render = compileToFunctions(template) ,编译 Vue 中的 template 模板,生成 render 方法。
    // 4. Vue.prototype.$mount 调用上面的 render 方法挂载 dom。
    // 5. mountComponent

    // 6. 创建 Watcher 实例
    const updateComponent = () => {
    vm._update(vm._render());
    };
    // 结合上文,我们就能得出,updateComponent 就是传入 Watcher 内部的 getter 方法。
    new Watcher(vm, updateComponent);

    // 7. new Watcher 会执行 Watcher.get 方法
    // 8. Watcher.get 会执行 this.getter.call(vm, vm) ,也就是执行 updateComponent 方法
    // 9. updateComponent 会执行 vm._update(vm._render())

    // 10. 调用 vm._render 生成虚拟 dom
    Vue.prototype._render = function (): VNode {
    const vm: Component = this;
    const { render } = vm.$options;
    let vnode = render.call(vm._renderProxy, vm.$createElement);
    return vnode;
    };
    // 11. 调用 vm._update(vnode) 渲染虚拟 dom
    Vue.prototype._update = function (vnode: VNode) {
    const vm: Component = this;
    if (!prevVnode) {
    // 初次渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
    } else {
    // 更新
    vm.$el = vm.__patch__(prevVnode, vnode);
    }
    };
    // 12. vm.__patch__ 方法就是做的 dom diff 比较,然后更新 dom,这里就不展开了。

    到这里,我们就知道了 Watcher 其实是在 Vue 初始化的阶段创建的,属于生命周期中 beforeMount 的位置创建的,创建 Watcher 时会执行 render 方法,最终将 Vue 代码渲染成真实的 DOM。

最后附上了一张图:同样来自https://zhuanlan.zhihu.com/p/168768245

到这里、Vue响应式的原理就差不多明白了、虽然还是很菜、呜呜呜。

参考资料: