热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

实现一个简单的双向绑定

接触Vue有一段时间了,但是对于其双向绑定的实现一直是似懂非懂,今天看到一篇写的比较好的文章传送门1根据原作者的指导自己也去实现了一遍简单的demo(本文的demo均基于Objec

接触Vue有一段时间了,但是对于其双向绑定的实现一直是似懂非懂,今天看到一篇写的比较好的文章 传送门1 根据原作者的指导自己也去实现了一遍简单的 demo (本文的demo均基于Object.defineProperty 实现数据劫持,利用了对Vue.js实现双向绑定的思想)

[注]本文所有图片均来自于:传送门2

前言

几种主流的双向绑定

1.发布-订阅模式
2.脏值检测

通过对比数据是否有变更,来决定是否更新视图。最简单的可以通过定时轮询去检测数据的变动。当然Google不会这么low, Angular 只有在指定事件触发时进入脏值检测:

  • DOM事件,比如用户输入文本点击按钮等(ng-click)
  • XHR响应事件
  • 浏览器 Location 变更
  • Timer事件
  • 执行 $digidt() 或 $apply()

3.数据劫持
Vue.js 采用的是 数据劫持+发布/订阅模式 的方式,通过 Object.defineProperty() 来劫持各个属性的 setter/getter, 在数据变动时发布消息给订阅者(Wacther), 触发相应的监听回调。下图展示了Vue实现双向绑定的流程
《实现一个简单的双向绑定》

实现一个简单的双向绑定














进阶版demo

DOM操作是非常耗时和好性能,所以在优化过程中先从DOM操作入手。因为遍历解析过程中有多次DOM操作,为了提高性能和效率,需要一种方法来避免对DOM元素的直接封装操作。在Vue使用 DocumentFragment 作为替代容器。
DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当做一个轻量版本的Document 使用。所以使用 DocumentFragment 代替DOM直接处理,可以提高性能和速度。

封装DOM节点为 DocumentFragment

//将传入 node 的子节点进行劫持,经过处理后重新挂载回目标节点
function convertNode(node,vm){
var fragment = document.createDocumentFragment(),
child
while(child = node.firstChild){
//将原生节点拷贝到 fragment,并删除之前的child节点
fragment.appendChild(child)
}
return fragment
}
var dom = convertNode(document.getElementById('app'))
document.getElmentById('app').appendChild(dom)

实现Complie解析模板指令

《实现一个简单的双向绑定》

Complie 主要做的事情就是解析模板指令,将模板中的变量替换为数据。所以要遍历整个DOM树,进行扫描解析编译,调用对应的指令渲染函数进行渲染,并调用对应的指令更新函数进行绑定



{{text}}

function convertNode(node,vm){
//...
while(child = node.firstChild){
Compile(child,vm)
fragment.appendChild(child)
}
return fragment
}
function Compile(node,vm){
var reg = /\{\{(.*)\}\}/
if(node.nodeType===1){
var attr = node.attributes //对所有属性进行解析
for(var i=0;i if(attr[i].nodeName=='h-model'){
//将元素与数据绑定
var bindName = attr[i].nodeValue
//为输入框添加事件监听触发
node.addEventListener('input',function(e){
vm.data[bindName] = e.target.value
node.value = vm.data[bindName]
})
node.removeAttribute('h-model');
}
if(node.nodeType===3){
if(reg.test(node.nodeValue)){
var bindName = RegExp.$1.trim()
console.log(RegExp.$1)
node.nodeValue = vm.data[bindName]
}
}
}
}

ViewModel 层向 View 层的数据绑定

接下来实现一个 Xin 构造器,通过 Compile 来解析模板指令,通过 Observer 监听属性数据的变化实现 Model 层向 View 层的数据绑定

function Xin(options){
this.data = options.data
Observer(this.data,this)
var id = options.el
var dom = convertNode(document.getElementById(id),this)
document.getElementById(id).appendChild(dom)
}

新建一个 vm 实例来测试一下 Model --> View 的绑定情况

var vm = new Xin({
el:'app',
data:{
text:'Hello MVVM'
}
})
View 层向 viewModel 层的数据绑定

实际上,在 Observer 中我们已经通过 数据劫持 实现了监听每个数据的变化,在控制台打印 console.log(val) 就可以实时看到数据的变化。所以接下来实现的关键就是怎么用监听到的数据去更新视图。
在这里,我们去实现一个 Wacther 可以将它理解为观察者 ,他的作用是能够接收从 Observer 发过来的属性变动通知, 然后根据属性的变动更新视图 update

在监听过程中,为所有的 data 属性生成一个主题对象 Dep,Dep中包含需要维护的观察者列表。每当主题对象状态发生变化时,其相关依赖都会得到通知,并且被自动更新(数据变动会触发notify,再调用订阅者的update() 方法)

function Dep(){
this.subs=[] //订阅者队列
}
Dep.prototype={
addSub:function(sub){
this.subs.push(sub)
},
notify:function(){
this.subs.forEach(function(sub){
sub.update()
})
}
}
funcion Watcher(vm,node,bindName){
//将全局Dep.target设置为当前页面元素node
Dep.target = this
//完成watcher的初始化
this.name = bindName
this.node = node
this.vm = vm
this.update() //初次绑定时进行更新
Dep.target = null //保证Dep.target唯一
}
Watcher.prototype = {
get:function(){
this.value = this.vm.data[this.name]
},
update:function(){
this.get()
this.node.nodeValue = this.value
}
}
function Observer(obj,vm){
//...
Object.defineProperty(obj,prop,{
get:function(){...},
set:function(newVal){
if(val == newVal) return
val = newVal
//data属性被修改,由dep触发view层更新
dep.notify()
}
})
}

考虑这样一个问题,什么时候会有双向绑定? viewModel --> view 可能会发生在所有类型的DOM节点上,而 view --> viewModel 只能发生在 input, select, textarea 等交互控件上。所以将文本节点包装成 Watcher , 添加相关元素的观察者列表中,Watcher 负责更新页面元素

function Compile(node,vm){
//...
if(node.nodeType ===3){ //文本节点类型
if(reg.test(node.nodeValue)){
var bindName = RegExp.$1.trim()
new Watcher = (vm,node,bindName) //为该页面元素node生产watcher
}
}
}
更新

本文中实现模板渲染的方法借鉴了 Vue 1.x 中实现模板渲染的方法。
Vue 2.x 模板渲染 方法借鉴React 中的 VirtualDOM,基于 VirtualDOM。 Vue 2.x 还支持服务端渲染SSR

资料参考

1.https://github.com/DMQ/mvvm#_2


推荐阅读
author-avatar
xuanchen
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有