浅谈Vue2.0的双向数据绑定与模板渲染原理
一:浅谈Vue2.0的双向数据绑定与模板渲染原理1. 基本原理(大概)MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/definePropertyvue2.0 采用 ES5 的 Object 的 defineProperty() 方法来进行数据劫持,元...
·
一:浅谈Vue2.0的双向数据绑定与模板渲染原理
1. 基本原理(大概)
MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
vue2.0 采用 ES5 的 Object 的 defineProperty() 方法来进行数据劫持,元素节点来进行绑定判断
- 先通过js来获取页面上的vue实例(#app),然后通过 node.childNodes 属性来获取他的子元素(包含文本节点和属性节点)列表,循环遍历子元素列表,通过 node.nodeType 属性来判断他的节点类型(1为元素节点,2为文本节点),判断完后需再次判断该节点是否还有子元素,如有需递归调用(为了防止元素多层嵌套)
- 当节点类型为文本节点时可以用 node.nodeValue 属性来获取文本内容,然后再用正则来进行查找是否包含{{}},即检查是否含有插值表达式,如果有,取出data里面的数据进行替换
- 当节点类型为元素节点时,则需要通过 node.attribute 属性来获取该元素的所有属性节点,然后遍历,通过attr.nodeName来获取属性节点名(每一个属性节点都是一个对象),再来判断该节点名是否是 v-text, v-mode,
v-on, v-html等vue指令,如果是,则分别进行处理 - 数据劫持:先获取vue的data对象,然后通过 Object.keys(data) 来获取属性名。再通过Object.defineProperty(data, key, descriptor) 进行数据劫持(参数一:目标对象,参数二:对象的属性名,参数三:对劫持属性的一些列操作,是个对象{})
- 当解析每一个文本节点或属性节点时,会new一个定义好的Watcher类(俗名观察者),他里面有一个方法,会获取data里面的值来触发 defineProperty 里面的 get 方法,在第一次遍历data里面的属性时,会在每次遍历时new 一个 Dep 类(俗名发布者),Dep类里面有个方法,里面有一个数组,会存放所有使用到当前属性的 Watcher 对象,
- 当设修改属性值时,会触发 defineProperty 里面的 set 方法,通过判断属性值来是否遍历Dep里面的数组进行数据改变,即页面的数据修改
上代码:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge,chrome=1">
<title>Document</title>
</head>
<body>
<div id="app">
<p>----{{msg1}}----</p>
<div>
<p>{{index}}</p>
</div>
<p>----{{md.value}}----</p>
<p>---点击了: {{index}} 次</p>
<p v-html="html" title="123"></p>
<p v-text="obj.value"></p>
<input type="text" v-model="md.value">
<p>obj.value----{{obj.value}}</p>
<button v-on:click="onClick">测试</button>
</div>
<script src="./src/watcher.js"></script>
<script src="./src/observer.js"></script>
<script src="./src/compile.js"></script>
<script src="./src/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg1: '测试1',
msg: 'v-text',
html: '<h2>123</h2>',
md: {
value: 'v-model测试',
},
index: 0,
obj:{
value: '对象数据测试'
}
},
methods:{
onClick(){
this.index++
}
}
})
</script>
</body>
</html>
vue.js
class Vue {
constructor (options = {}){
this.$el = options.el
this.$data = options.data
this.$methods = options.methods
// 绑定属性劫持
new ObServer(this.$data)
// 数据代理---把data和methods里面的熟悉都添加到Vue实例上
this.porxy(this.$data,this.$methods)
// 编译模板
if (this.$el) {
new Compile(this.$el, this)
}
}
porxy(data){
// 因为最开始就把data里面的所有数据(包含深层数据)都进行了监听,所以只需要监听绑定后的最外层数据就行,不需要再次递归
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get(){
return data[key]
},
set(newValue) {
console.log(newValue)
if(data[key] === newValue) return
data[key] = newValue
}
})
})
}
}
obServer.js
用于进行数据劫持
class ObServer {
constructor(data) {
this.data = data
// 编辑数据,添加setter,getter
this.walk(data)
}
walk(data) {
// 如果data为空或者不是一个对象
if (!data || typeof data != 'object') {
return
}
Object.keys(data).forEach(key => {
// 进行数据劫持
this.defineReactive(data, key, data[key])
// 递归调用---防止data[key]值为对象
this.walk(data[key])
})
}
defineReactive(data, key, value) {
let that = this
let dep = new Dep()
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// Dep.target 存在时,即设置(修改)数据时才添加
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue) {
if (value === newValue) return
// 设置值
value = newValue
// 设置完后调用walk进行绑定劫持,为了防止设置的是一个对象而没有监听到
that.walk(newValue)
dep.notify()
}
})
}
}
compile.js
用于进行模板编译
class Compile {
constructor(el, vm) {
this.el = typeof el === "string" ? document.querySelector(el) : el
this.vm = vm
// 把所有的子节点放入fragment中
let fragment = this.node2fragment(this.el)
// 编辑fragment
this.compile(fragment)
// 把内存节点添加到页面上
this.el.appendChild(fragment)
}
/**
* 核心方法
* node2fragment ------------------ 创建文档碎片,并把文档的子节点添加到文档碎片中
* compile ------------------------ 编译内存碎片
* compileElementNode ------------- 解析元素节点
* compileTextNode ---------------- 解析文本节点
*/
node2fragment(node) {
// 创建文档碎片
let fragment = document.createDocumentFragment()
// 获取所有的子节点,包含文本节点
let childNodes = node.childNodes
// 遍历往fragment中添加子节点
this.toArray(childNodes).forEach(node => {
fragment.appendChild(node)
})
return fragment
}
compile(node) {
let childNodes = node.childNodes
this.toArray(childNodes).forEach(node => {
let nodeType = this.isNodeType(node)
// 元素节点
if (nodeType == 1) {
this.compileElementNode(node)
}
// 文本节点
if (nodeType == 3) {
this.compileTextNode(node)
}
// 还有子节点时递归调用
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
compileElementNode(node) {
// 获取该节点的所有属性---伪数组
let attributes = node.attributes
this.toArray(attributes).forEach(attr => {
// attr为一个属性节点
console.dir(attr)
let attrName = attr.nodeName
if (this.isDirective(attrName)) {
// 去除 v-
let type = attrName.slice(2)
// 获取指令值
let expr = attr.nodeValue
/*
// text节点
if(type === "text") {
node.innerText = this.vm.$data[expr]
}
// html节点
if(type === "html") {
node.innerHTML = this.vm.$data[expr]
}
// model节点
if(type === "model") {
node.value = this.vm.$data[expr]
}
// v-on
if(this.isEventDirective(type)) {
// 获取v-on后面的事件源
let eventType = type.split(":")[1]
// 为节点绑定事件并改变事件体中的this指向
node.addEventListener(eventType, this.vm.$methods[expr].bind(this.vm))
}
*/
// 代码优化
if (this.isEventDirective(type)) {
CompileUtil['eventHandler'](type, node, this.vm, expr)
} else {
CompileUtil[type] && CompileUtil[type](node, this.vm, expr)
}
}
})
}
compileTextNode(node) {
CompileUtil.mustache(node, this.vm)
}
/**
* 工具方法
* toArray -------------------------- 将类数组变成数组
* isNodeType ----------------------- 判断节点类型 1-元素节点 2-属性节点 3-文本节点
* isDirective ---------------------- 是否为vue的指令
*/
toArray(likeArray) {
return Array.from(likeArray)
}
isNodeType(node) {
return node.nodeType
}
isDirective(attrName) {
return attrName.startsWith('v-')
}
isEventDirective(type) {
return type.split(':')[0] === "on"
}
}
/**
* 解析文本节点的工具对象
* mustache ------------------------- 解析插值表达式
* text ----------------------------- 解析 v-text
* html ----------------------------- 解析 v-html
* model ---------------------------- 解析 v-model
* eventHandler --------------------- 解析 v-on
* getVmValue ----------------------- 数据处理,用获得到的属性值去data里面寻找数据
* setVmValue ----------------------- 设置data值,用于双向数据绑定
*/
let CompileUtil = {
mustache(node, vm) {
let attrText = node.nodeValue
let reg = /\{\{(.+)\}\}/g
if (reg.test(attrText)) {
let expr = RegExp.$1
// node.textContent = attrText.replace(reg, vm.$data[expr])
node.textContent = attrText.replace(reg, this.getVmValue(vm, expr))
new Watcher(vm, expr, (newValue) => {
node.textContent = attrText.replace(reg, newValue)
})
}
},
text(node, vm, expr) {
node.innerText = this.getVmValue(vm, expr)
new Watcher(vm, expr, (newValue) => {
node.innerText = newValue
})
},
html(node, vm, expr) {
node.innerHTML = this.getVmValue(vm, expr)
new Watcher(vm, expr, (newValue) => {
node.innerHTML = newValue
})
},
model(node, vm, expr) {
let that = this
node.value = this.getVmValue(vm, expr)
node.addEventListener('input', function () {
that.setVmValue(vm, expr, this.value)
})
new Watcher(vm, expr, (newValue) => {
node.value = newValue
})
},
eventHandler(type, node, vm, expr) {
let eventType = type.split(":")[1]
let fn = vm.$methods && vm.$methods[expr]
if (eventType && fn) node.addEventListener(eventType, fn.bind(vm))
},
// 数据data中的值
getVmValue(vm, expr) {
let data = vm.$data
expr.split('.').forEach(key => {
data = data[key]
})
return data
},
// 设置data中的值
setVmValue(vm, expr, value) {
let data = vm.$data
let arr = expr.split('.')
let length = arr.length
// 遍历找到对应的键并赋值
arr.forEach((item, index) => {
if (index === length - 1) {
data[item] = value
} else {
data = data[item]
}
})
}
}
watcher.js
用于进行数据监听-即观察者
// 用于数据更新
class Watcher{
constructor(vm, expr, cb) {
/**
* vm: vue实例
* expr: 属性名
* cb: 调用更新的回调函数
*/
this.vm = vm
this.expr = expr
this.cb = cb
// 保存每一个订阅者
Dep.target = this
console.log(this)
// this.getVmValue 调用时触发数据监听,添加订阅者
this.oldValue = this.getVmValue(this.vm, this.expr)
// 订阅者添加后置空
Dep.target = null
}
// 判断是否更新
updata(){
let oldValue = this.oldValue
let newValue = this.getVmValue(this.vm, this.expr)
if(oldValue != newValue){
this.cb(newValue)
}
}
// 数据处理
getVmValue(vm, expr){
let data = vm.$data
expr.split('.').forEach( key => {
data = data[key]
} )
return data
}
}
// 发布者
class Dep{
constructor(){
this.subs = []
}
// 多个订阅者放在一起
// 订阅者是指页面的节点,即:多个节点订阅data里面的一个变量
addSub(watcher){
this.subs.push(watcher)
}
// 发布,所有的订阅者一起更新
notify(){
this.subs.forEach( item => {
item.updata()
} )
}
}
运行过程
/* 友情提示:最好结合上面的代码看比较好
首先,index.html文件运行到 new Vue() 时会触发 Vue 类,传递了一个对象,对象里面的属性
分别是页面上的Vue容器对象:el,数据:data,方法:methods
然后通过 new ObServer(this.$data)来把数据传递过去进行数据劫持
此时进入 ObServer类里面
先调用 walk 方法进行一个基础的兼容性处理,然后通过 Object.keys(data).forEach 循环调用
this.defineReactive方法进行数据劫持
每次循环时都会为一个属性创建 Dep类的对象,defineProperty里面的configurable属性大致意思是是否
可以修改值,enumerable是是否乐意被for...in遍历到,即:是否可以枚举,get方法里面的 Dep.target &&
dep.addSub(Dep.target) 先不说明,后面会说明,set方法里面 value = newValue 就是把设置的值替换掉
以前的值,that.walk(newValue) 是为了防止新设置的值是一个对象,所以保险起见需要再次遍历进行绑定劫持
dep.notify() 也先不说明
ObServer类执行完后又返回到Vue类里面,继续下面代码,调用 this.porxy()方法
porxy方法的作用就是把 this.$data.msg 简写变成 this.msg,methods同理,所以再次调用
Object.defineProperty方法进行数据劫持,与ObServer中的不同的是,一个是监听的属性绑定在this.$data
上,一个直接绑定到this上,即直接挂载到vue实例上,所以当this.msg调用时,先触发porxy里面监听的get(),
由于return data[key],即从data里面获取了数据,所以会再次触发 ObServer类 里面的数据劫持,然后进行
数据处理一系列操作,set方法同理
porxy里面执行完了后又返回到vue类里面,然后执行 new Complie 指令,进入到Complie类里面
Complie 类主要用于模板编译
首先,先获取Vue容器对象(el),然后调用node2fragment()方法把el里面的所有子节点添加到文档碎片里
面去,这样可以全部编译完成后一次性添加到页面上去,性能优化
fragment创建好后就可以进行模板解析,先通过node.childNodes属性获取他的子节点,然后进行遍历判断
节点类型是否是元素节点还是文本节点,如果是元素节点时先通过 node.attributes 属性获取它所有的属性节点
,然后遍历判断属性名书否是vue的指令,是的话进入到对应的方法里面进行处理
例:一个v-text指令时,此时进入到text方法里面,getVmValue()方法是用于获取data里面的值,然后给
对应的节点赋值,然后执行
new Watcher(vm, expr, (newValue) => {
// 参数一:vue实例,参数二:属性名,参数三:更新时的回调函数
node.innerText = newValue
})
此时进入到 Watcher 类里面
Dep.target = this 的作用是每次调用解析指令的方法时,都会保存这个节点,方便更新的时候直接找到
该节点(因为有个回调函数,回调函数里面有个节点)
然后执行下一行代码,因为调用了this.getVmValue()方法用于获取data里面的值,所以会触发ObServer
类里面监听该属性的get()方法,此时 Dep.target 的第二个用处在于是知道此时是设置(修改)数据,而不是第
一次的数据监听,然后调用dep对象(最开始进行属性监听的时候为每个属性常见的对象)的 addSub(Dep.target)
方法,用于保存所有使用了这个属性的Watcher对象,当这个属性值修改时会统一遍历这个数组,调用这个对象一开
始保存的回调函数来更新页面上的数据
v-model 与 v-text 的区别在于解析v-model1时候为当前节点绑定了input事件,事件触发后会设置当前
data里面的值,然后会触发ObServer里面的set方法,然后会触发dep对象的notify()方法,用于通知所有订阅
了该属性的节点进行数据更新,然后编辑这个属性上存放Watcher对象的数组-subs,来调用每个对象的回调函数
进行页面的数据更新
事件绑定原理:当解析到v-on时调用对应的处理函数,获取v-on后面的函数名,并在methods对象里面找到对
应的函数,然后再为该节点绑定对应的事件即可
插值表达式的解析:因为本人正则不熟,所以插值表达式里面的正则只能处理一对{{}},匹配到{{}}里面值后
就在data里面遍历查找对应的值,然后进行替换
*/
更多推荐
已为社区贡献2条内容
所有评论(0)