Vue2.0实现双向绑定
所谓的双向数据绑定主要是mvvm设计模式中数据层(m)和视图层(v)的同步应用,在写入数据的同时,视图层也会自动同步更新,我们可以通过下面一张图来进行观察这种绑定原理:
image.png经过查资料我们可以看到网上的各种原理,最多的答案就是下面这样的解释:
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来监听自己的model数据变化,通过compile来解析编译模版,
最终利用watcher搭起observer和compile之间的通信桥梁,达到数据变化->更新视图:视图交互变化->数据model变更的双向绑定效果
复制代码
补充:
ECMAScript中有两种属性: 数据属性和访问器属性, 数据属性一般用于存储数据数值, 访问器属性对应的是set/get操作, 不能直接存储数据值, 存储的一般是一个函数形式的数据
Object.defineProperty(), 这个方法接收三个参数:
属性所在对象,
属性的名字,
描述符对象;
复制代码
为对象定义多个属性的话,就用函数的复数写法:Object.defineProperties();
实现代码步骤如下: 第一步(observer实现对vue各个属性进行监听):
function observer(obj, vm){
//通过Object.key对属性进行遍历
Object.keys(obj).forEach(function(key){
defineReactive(vm, key, obj[key])
})
}
// Object.defineProperty改写各个属性
function defineReactive( obj, key, val ) {
// 每个属性建立个依赖收集对象,get中收集依赖,set中触发依赖,调用更新函数
var dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
// 收集依赖 Dep.target标志
Dep.target && dep.addSub(Dep.target)
return val
},
set: function(newVal){
if(newVal === val) return
// 触发依赖
dep.notify()
val = newVal
}
})
}
复制代码
第二步(dep实现):
function Dep(){
this.subs = []
}
Dep.prototype = {
constructor: Dep,
addSub: function(sub){
this.subs.push(sub)
},
notify: function(){
this.subs.forEach(function(sub){
sub.update() // 调用的Watcher的update方法
})
}
}
复制代码
第三步compiler实现对vue各个指令模板的解析器,生成抽象语法树,编译成Virtual Dom,渲染视图
// 编译器
function compiler(node, vm){
var reg = /\{\{(.*)\}\}/;
// 节点类型为元素
if(node.nodeType ===1){
var attr = node.attributes;
// 解析属性
for(var i=0; i< attr.length;i++){
if(attr[i].nodeName == 'v-model'){
var _value = attr[i].nodeValue
node.addEventListener('input', function(e){
//给相应的data属性赋值,触发修改属性的setter
vm[_value] = e.target.value
})
node.value = vm[_value] // 将data的值赋值给node
node.removeAttribute('v-model')
}
}
new Watcher(vm,node,_value,'input')
}
// 节点类型为text
if(node.nodeType ===3){
if(reg.test(node.nodeValue)){
var name = RegExp.$1;
name = name.trim()
new Watcher(vm,node,name,'input')
}
}
复制代码
第四步:Watcher 连接observer和compiler,接受每个属性变动的通知,绑定更新函数,更新视图
function Watcher(vm,node,name, nodeType){
Dep.target = this; // this为watcher实例
this.name = name
this.node = node
this.vm = vm
this.nodeType = nodeType
this.update() // 绑定更新函数
Dep.target = null //绑定完后注销 标志
}
Watcher.prototype = {
get: function(){
this.value = this.vm[this.name] //触发observer中的getter监听
},
update: function(){
this.get()
if(this.nodeType == 'text'){
this.node.nodeValue = this.value
}
if(this.nodeType == 'input') {
this.node.value = this.value
}
}
}
复制代码
完整实现代码:
function Vue(options){
this.date = options.data
var data = this.data
observer(data, this) // 监测
var id = options.el
var dom = nodeToFragment(document.getElmentById(id),this) //生成Virtual Dom
// 编译完成后,生成视图
document.getElementById(id).appendChild(dom)
}
function nodeToFragment(node, vm){
var flag = document.createDocumentFragment()
var child
while(child = node.firstChild){
compiler(cild, vm)
flag.appendChild(child)
}
return flag
}
// 调用
var vm = new Vue({
el: "app",
data: {
msg: "hello word"
}
})
复制代码
Vue3.0实现双向绑定
但是上述的解释只是在Vue2.0的时候进行的检测,由于Object.defineProperty本身存在一定的缺陷,比如说是不支持:
Object.defineProperty的缺陷:
1)无法检测到对象属性的新增或删除
由于js的动态性,可以为对象追加新的属性或者删除其中某个属性,
这点对经过Object.defineProperty方法建立的响应式对象来说,
只能追踪对象已有数据是否被修改,无法追踪新增属性和删除属性,
这就需要另外处理。
2)不能监听数组的变化(对数组基于下标的修改、对于 .length 修改的监测)
vue在实现数组的响应式时,它使用了一些hack,
把无法监听数组的情况通过重写数组的部分方法来实现响应式,
这也只限制在数组的push/pop/shift/unshift/splice/sort/reverse七个方法,
其他数组方法及数组的使用则无法检测到,
解决方法主要是使用proxy属性,这个proxy属性是ES6中新增的一个属性,
proxy属性也是一个构造函数,他也可以通过new的方式创建这个函数,
表示修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种元编程
proxy可以理解为在目标对象之前架设一层拦截,外界对该对象的访问,都必须经过这层拦截,
因此提出了一种机制,可以对外界的网文进行过滤和改写,proxy这个词是代理,
用来表示由他代理某些操作,可以译为代理器
Proxy,字面意思是代理,是ES6提供的一个新的API,用于修改某些操作的默认行为,
可以理解为在目标对象之前做一层拦截,外部所有的访问都必须通过这层拦截,
通过这层拦截可以做很多事情,比如对数据进行过滤、修改或者收集信息之类。
借用proxy的巧用的一幅图,它很形象的表达了Proxy的作用。
proxy代理的特点:
proxy直接代理的是整个对象而非对象属性,
proxy的代理针对的是整个对象而不是像object.defineProperty针对某个属性,
只需要做一层代理就可以监听同级结构下的所有属性变化,
包括新增的属性和删除的属性
proxy代理身上定义的方法共有13种,其中我们最常用的就是set和get,但是他本身还有其他的13种方法
proxy的劣势:
兼容性问题,虽然proxy相对越object.defineProperty有很有优势,但是并不是说proxy,就是完全的没有劣势,主要表现在以下的两个方面:
1)proxy有兼容性问题,无完全的polyfill:
proxy为ES6新出的API,浏览器对其的支持情况可在w3c规范中查到,通过查找我们可以知道,
虽然大部分浏览器支持proxy特性,但是一些浏览器或者低版本不支持proxy,
因此proxy有兼容性问题,那能否像ES6其他特性有polyfill解决方案呢?,
这时我们通过查询babel文档,发现在使用babel对代码进行降级处理的时候,并没有合适的polyfill
2)第二个问题就是性能问题,proxy的性能其实比promise还差,
这就需要在性能和简单实用上进行权衡,例如vue3使用proxy后,
其对对象及数组的拦截很容易实现数据的响应式,尤其是数组
虽然proxy有性能和兼容性处理,但是proxy作为新标准将受到浏览器厂商重点持续的性能优化,
性能这块会逐步得到改善
复制代码
Vue算法及模型比较
之前在北森面试的时候,被问到Vue的模型建立及他们是怎么实现的,问完之后真的是一脸懵,但是面试官说这里是必问的,当时也是没回答出来,以为面试要凉凉了,没想到后来又打电话,给出了几道算法题,后来查了一下,怎么实现,才知道面试官可能想问的是AST模型和diff算法,还说在Vue的官方文档上,我是真心没找到,在网上查找了好多资料,在这里总结一下
AST模型
AST就是抽象语法树,他是js代码另一种结构映射,可以将js拆解成AST,也可以把AST转成源代码。这中间的过程就是我们的用武之地。 利用 抽象语法树(AST) 可以对你的源代码进行修改、优化,甚至可以打造自己的编译工具。其实有点类似babel的功能。
AST也是一种数据结构,这种数据结构其实就是一个大的json对象,json我们都熟悉,他就像一颗枝繁叶茂的树。
[
{
"name": 12,
"children": [
{
"name": 4,
"children": [
{
"name": 1,
"children": [
{
"name": 0
},
{
"name": 2
}
]
},
{
"name": 8,
"children": [
{
"name": 7
},
{
"name": 9
}
]
}
]
},
{
"name": 18,
"children": [
{
"name": 16,
"children": [
{
"name": 15
},
{
"name": 17
}
]
},
{
"name": 20,
"children": [
{
"name": 19
},
{
"name": 24
}
]
}
]
}
]
}
]
复制代码
大概就是这种形式的树状结构,对AST语法树进行深度优先和广度优先遍历
// 深度遍历, 使用递归
function getName(data) {
const result = [];
data.forEach(item => {
const map = data => {
result.push(data.name);
data.children && data.children.forEach(child => map(child));
}
map(item);
})
return result.join(',');
}
复制代码
// 广度遍历, 创建一个执行队列, 当队列为空的时候则结束
function getName2(data) {
let result = [];
let queue = data;
while (queue.length > 0) {
[...queue].forEach(child => {
queue.shift();
result.push(child.name);
child.children && (queue.push(...child.children));
});
}
return result.join(',');
}
复制代码
Vue算法diff
Diff算法的作用是用来计算出Virtual DOM中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面,diff算法其实就是深度优先算法
算法策略
diff有三大策略:
Tree Diff:
是对树每一层进行遍历,找出不同
Component Diff
是数据层面的差异比较
Element Diff
真实DOM渲染,结构差异的比较
网友评论