Vue 双向数据绑定【vue 知识汇点3】

微信扫一扫,分享到朋友圈

Vue 双向数据绑定【vue 知识汇点3】

Vue 三要素:

响应式:例如如何监听数据变化,其中的实现方法就是我们提到的双向绑定
模版引擎:如何解析模版
渲染:Vue 如何将监听到的数据变化和解析后的 HTML 进行渲染

双向绑定
目前业界分为两个大的流派,一个是以 React 为首的单向数据绑定,另一个是以 Angular、Vue 为主的双向数据绑定。
可以实现双向绑定的方法有:

发布者-订阅者模式:一般通过subject、pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set(‘property’, value)
脏值检查:anjular.js 是通过脏值检测的方法比对数据是否有变更,来决定是否更新视图,最简单的方法就是通过 setInterval() 定时轮询检测数据变动
数据挟持:vue.js 采用数据挟持结合发布者-订阅者模式的方法,通过 Object.defineProperty() 来挟持各个属性的 setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

数据挟持
数据挟持的优点:

无需显式调用,可以直接通知变化并驱动视图
可精确得知变化数据,我们挟持了属性的 setter,当 setter 改变时,可以精确获得变化的内容 newVal,因此在这部分不需要额外的 diff 操作。

数据挟持的思路:

利用 Proxy 或者 Object.defineProperty 生成的 Observer 针对对象/对象的属性进行“挟持”,在属性发生变化后通知订阅者
解析器 Compile 解析模板中的 Directive,收集指令所依赖的方法和数据,等待数据变化,然后渲染
Watcher 属于 Observer 和 Compile 桥梁,它将收到的 Observer 产生的变化,并根据 Compile 童工的指令进行视图渲染,使数据变化促使视图变化

实现 mvvm 的双向绑定必要因素
实现 mvvm 的双向绑定,必须要实现以下几点:

实现一个数据监听器 Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
实现一个指令解析器 Compile,对每个元素节点的指令进行扫描和解析,根据指令模版替换数据,以绑定相应的更新函数
实现一个 watcher,作为 compile 和 observer 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
mvvm的入口函数,整合上面三者

实现 observer
observer需要具备的功能:1.监听数据变化 2.数据变化通知订阅者
Object.defineProperty() 来监听属性变动,那么将需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter。
var data = {name: ‘xiaohui’};
observe(data);
data.name = ‘test’;

function observe(data) {
if(!data || typeof data !== ‘object’) {
return;
}
Object.keys(data).forEach((key) => defineReactive(data, key,data[key]))
}
function defineReactive(data, key, val) {
observe(val) // 监听子属性
Object.defineProperty(data, key, {
enumberable: true,
configurable: false,
get: function() {
return val;
},
set: function(newVal) {
console.log( 监听到值变了!${val} ----> ${newVal}
)
val = newVal;
}
})
}
复制代码
此时,我们就可以监听每个数据的变化了,监听之后怎么通知订阅者,就需要实现一个消息订阅器,也就是维护一个数组,用来收集订阅者,数据变动触发 notify,再调用订阅者的 update 方法
function defineReactive(data, key, val) {
var dep = new Dep();
observe(val);
Object.defineProperty(data, key, {
// 省略
set: function(newVal) {
if(val === newVal) {
return;
}
val = newVal;
dep.notify(); //通知所有订阅者
}
})
}
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub)
},
notify: function() {
this.subs.forEach(sub => {
sub.update();
})
}
}
复制代码
此时我们的问题就来了,订阅者是 watcher,那怎么往 subs 里添加订阅者呢?通过 dep 添加订阅者,就必须要在闭包内操作
Object.defineProperty(data, key, {
get: function() {
// 通过 Dep 定义一个全局 target 属性,暂存 watcher,用完移除
Dep.target && dep.addDep(Dep.target)
return val;
}
})
Watcher。prototype = {
get: function(key) {
Dep.target = this;
this.value = data[key] // 会触发getter,从而添加订阅者
Dep.target = null
}
}

复制代码
实现 Compile
Compile 主要是解析模版指令,将模版中的变量替换成数据,然后初始化渲染页面视图,并经每个指令对应的节点绑定更新函数函数,添加监听数据的订阅者,一旦有数据变动,收到通知,更新视图。
// 遍历解析过程中,会多次操作 dom,为了提高性能和效率,会将 vue 实例根结点的 el 转换成 文档碎片 fragment,进行解析编译操作,解析完成,再将 fragment 添加回原来的真实 dom 节点中。

function Compile(el) {
this.$el = this.isElementNode(el) ? el : doucment.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(thiis.$fragment);
}
}
Compile.prototype = {
init: function() {
this.compileElement(this.$fragment);
},
node2Fragment: function() {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
// 将原生节点拷贝到fragment
while (child) {
fragment.appendChild(child)
}
return fragment;
},
// 遍历所有节点以及子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定。
compileElement: function(el) {
var childNodes = el.childNodes, me = this;
[].slice.call(childNodes).forEach(node => {
var text = node.textContext;
var reg = /{{(.*)}}/;
if (me.isElementNode(node)) {
me.compile(node)
} else if (me.isTextnode(node) && reg.test(text)) {
me.compileText(node, RegExp.$1)
}
if (node.childNodes && node.childNodes.length) {
me.compileElement(node)
}
})
},
compile: function(node) {
var nodeAttrs = node.attributes, me = this;
[].slice.call(nodeAttrs).forEach(attr => {
var attrName = attr.name;
if(me.isDirective(attrName)) {
var exp = attr.value
// 指令以 v-xxx 命名
var dir = attrName.subString(2)
if (me.isEventDirective(dir)) {
compileUtil.eventHandler(node, me.$vm, exp, dir)
} else {
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp)
}
}
})
}
}
var compileUtil = {
text: function(node, vm. exp) {
this.bind(node, vm, exp, ‘text’)
},
bind: function(node, vm, exp, dir) {
var updateFn = updater[dir + ‘Updater’];
updaterFn && updateFn(node, vm[exp]);
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
})
}
}
var updater = {
textUpdater: function(node, value) {
node.textContent = typeof value === ‘undefined’ ? ” : value;
}
}
复制代码
实现 Watcher
watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:

在自身实例化时往属性订阅器(dep)里面添加自己
自身必须有一个 update() 方法
待属性变动,dep.notify() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调

function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get();
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.get();
var oldValue = this.value;
if (value !== oldValue) {
this.value = value;
this.cb.call(this.vm, value, oldVal) // 执行 Compile 中绑定的回调,更新视图
}
},
get: function() {
Dep.target = this; // 将当前订阅者指向自己
var value = this.vm[exp]; // 触发 getter,添加自己到属性订阅起中
Dep.target = null; //添加完毕,重置
return value;
}
}
复制代码
实现MVVM
MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模版指令,最终利用 Watcher 搭起 Oberver 和 Compile 之间的桥梁,达到数据变化 -> 视图更新;视图交互变化 -> 数据model 变更的双向绑定效果。
function MVVM (options) {
this.$options = options;
var data = this._data = this.$options.data, me = this;
Object.keys(data).forEach(key => me._proxy(key))
observe(data, this)
this.$compile = new Compile(options.el || document.body, this)
}
MVVM.prototype = {
_proxy: function(key) {
var me = this;
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
})
}

}
复制代码
Object.defineProperty 和 proxy 区别
Object.defineProperty的缺陷:

不能监听数组变化

只能劫持对象的属性,属性值如果也是对象,需要深度遍历
Proxy 就是在被劫持的对象之前加了一层拦截,它的特性是:

可以直接监听对象,而非属性

可以直接监听数组的变化

proxy 返回一个新对象,我们可以只操作新对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改

劣势是浏览器的兼容性问题,所以vue 3.0 才会用 Proxy 重写
总之,defineProperty 是劫持对象的属性,当新增属性时,需要重新劫持。proxy 是代理对象,所有的对象属性变更都能访问到,对于目前defineProperty 所存在的问题,都能提供完美的解决方案
Vue2.x 中数组和对象观察时的特殊处理
Vue 2.0 中响应式数据是通过 defineProperty 实现,因此无法检测数组/对象的新增和删除,当调用数组的push、splice、pop 等方法改变数组元素时,并不会触发数组的 setter,所以,Vue 2.0 做了一些特殊处理,使用函数挟持的方式,重写了数组的方法,vue 将 data 中的数组进行原型链重写,指向自己定义的数组原型方法。这样当调用数组的 api 时,可以通知依赖更新。如果数组中包含引用类型,会对数组中的引用类型再次递归遍历进行监控。
至于目前存在的“无法通过索引改变数组”的问题,是因为性能问题,性能代价和获得的用户体验收益不成正比

微信扫一扫,分享到朋友圈

Vue 双向数据绑定【vue 知识汇点3】

《八佰》密钥延期一个月 延长上映至2020年10月21日

上一篇

云服务器上nginx配置端口号和别名

下一篇

你也可能喜欢

Vue 双向数据绑定【vue 知识汇点3】

长按储存图像,分享给朋友