0%

谈谈Vue的数据绑定原理与实现

Vue 有两个比较核心的特性,一个是非侵入式的响应式数据绑定系统,另一个是组件系统。由于这两个功能非常重要,所以在求职面试的时候,面试官也通常会围绕这两部分进行提问,如数据绑定的底层实现,组件之间的通讯等等。接下来,本篇博客将主要探讨一下Vue的数据绑定原理与实现。

Vue是如何监听数据变化的

当实例化一个Vue组件的时候,我们会把一个普通的JavaScript对象传递给Vue实例的data选项,然后Vue会对这个对象里面的每个属性进行遍历,然后使用ES5的Object.defineProperty给这些属性设置getter和setter。由于Object.defineProperty是ES5无法shim的一个特性,所以Vue也不支持IE8以及更低版本的浏览器。Vue通过getter和setter劫持传入对象的属性后,然后在内部会跟踪依赖,当属性被访问或者被修改时通知变化。

Vue数据绑定原理

先看一张摘自Vue官方文档的图片:Vue的数据绑定是通过Object.defineProperty劫持数据并结合发布者-订阅者的设计模式来实现的。前面也已经提到了,Vue劫持数据后会对数据进行跟踪依赖,也就是监听它们的变化,所以我们需要设置一个Obsver监听器,用来监听所有劫持到的属性,当属性发生变化时,会通知Watcher订阅者来重新计算判断是否需要更新。由于会有很多订阅者,所以需要一个消息订阅器Dependency,用来专门收集这些订阅者,然后Vue在监听器Observer和订阅者Watcher之间进行统一管理。由于要更新组件视图,所以还需要有一个指令解析器Compile,它将对每个节点元素进行解析,识别出绑定在这些元素上的相关指令,同时将这些指令分别初始化为一个订阅者Watcher,并替换掉模板的数据或者绑定相应的更新函数,此时,如果订阅者Watcher计算到到属性的变化,就会执行相应的更新函数,从而更新视图。从上面的分析,我们知道要实现数据绑定,可以通过以下三个步骤完成:

  1. 实现一个监听器Observer,借助Object.defindProperty劫持所有属性,如果有变化,就会通知订阅者
  2. 实现一个订阅者Watcher,每一个订阅者都绑定一个更新函数,订阅者计算属性变化并执行相应的更新函数,从而更新视图
  3. 实现一个解析器Compile,解析和识别每个元素上的指令,并初始化这些包含指令的元素的模板数据以更新视图,并初始化相应的订阅者Watcher

Vue数据绑定的实现

监听器Observer

核心功能是监听数据的变化,实现的核心方法是Object.defineProperty,劫持每个属性的setter和getter属性:

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
// Dep用于订阅者的存储和收集,将在下面实现
import Dep from 'Dep'
// Observer类用于给data属性添加set&get方法
export default class Observer{
constructor(value){
this.value = value
this.walk(value)
}
walk(value){
Object.keys(value).forEach(key => this.convert(key, value[key]))
}
convert(key, val){
defineReactive(this.value, key, val)
}
}
export function defineReactive(obj, key, val){
var dep = new Dep()
// 给当前属性的值添加监听
var chlidOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=> {
console.log('get value')
// 如果Dep类存在target属性,将其添加到dep实例的subs数组中
// target指向一个Watcher实例,每个Watcher都是一个订阅者
// Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法
// 此处的问题是:并不是每次Dep.target有值时都需要添加到订阅者管理员中去管理,需要对订阅者去重,不影响整体思路,不去管它
if(Dep.target){
dep.addSub(Dep.target)
}
return val
},
set: (newVal) => {
console.log('new value seted')
if(val === newVal) return
val = newVal
// 对新值进行监听
chlidOb = observe(newVal)
// 通知所有订阅者,数值被改变了
dep.notify()
}
})
}
export function observe(value){
// 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听
if(!value || typeof value !== 'object'){
return
}
return new Observer(value)
}

消息订阅器Dependency

用于对订阅者进行收集和通知

1
2
3
4
5
6
7
8
9
10
11
12
export default class Dep{
constructor(){
this.subs = []
}
addSub(sub){
this.subs.push(sub)
}
notify(){
// 通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理
this.subs.forEach((sub) => sub.update())
}
}

订阅者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
import Dep from 'Dep'
export default class Watcher{
constructor(vm, expOrFn, cb){
this.vm = vm // 被订阅的数据一定来自于当前Vue实例
this.cb = cb // 当数据更新时想要做的事情
this.expOrFn = expOrFn // 被订阅的数据
this.val = this.get() // 维护更新之前的数据
}
// 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用
update(){
this.run()
}
run(){
const val = this.get()
if(val !== this.val){
this.val = val;
this.cb.call(this.vm)
}
}
get(){
// 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
Dep.target = this
const val = this.vm._data[this.expOrFn]
// 置空,用于下一个Watcher使用
Dep.target = null
return val;
}
}

解析器Compile

解析每个元素上的指令,并将它们对应的节点绑定相应的更新函数,初始化相应的订阅者,或者替换模板数据,初始化视图。

  • 先创建一个fragment片段,并将要解析的dom节点存入fragment片段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function nodeToFragment (el) {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while (child) {
    // 将Dom元素移入fragment中
    fragment.appendChild(child);
    child = el.firstChild
    }
    return fragment;
    }
  • 遍历各个节点,对包含相关指令的节点进行处理:

    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
    function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
    var reg = /\{\{(.*)\}\}/;
    var text = node.textContent;

    if (self.isTextNode(node) && reg.test(text)) { // 判断是否是符合这种形式{{}}的指令
    self.compileText(node, reg.exec(text)[1]);
    }

    if (node.childNodes && node.childNodes.length) {
    self.compileElement(node); // 继续递归遍历子节点
    }
    });
    },
    function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    this.updateText(node, initText); // 将初始化的数据初始化到视图中
    new Watcher(this.vm, exp, function (value) { // 生成订阅器并绑定更新函数
    self.updateText(node, value);
    });
    },
    function (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
    }
  • 本文作者: 前端农民工
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!