vue Realizes Simple Bidirectional Data Binding
- 2021-10-16 00:39:53
- OfStack
It is mainly realized through data hijacking and publishing and subscribing
When bidirectional data binding data updates, the data updates that can update the view view are that you can reverse update the model
Composition description
The Observe listener hijacks data, senses data changes, sends notifications to subscribers, and adds subscribers to subscribers in get
The Dep message subscriber stores the subscriber and informs the subscriber to call the update function
Subscriber Wather fetches the model value and updates the view
Parser Compile parses instructions, updates template data, initializes views, instantiates one subscriber, binds update functions to subscribers, updates views twice after receiving notifications, and monitors input events for v-model to realize data flow from views to models
Basic structure
HTML template
<div id="app">
<form>
<input type="text" v-model="username">
</form>
<p v-bind="username"></p>
</div>
1 root node # app
Form element, which contains input, uses v-model instructions to bind data username
v-bind binding number username on p element
Class MyVue
Simple Simulated Vue Class
Save the option options and the data options. data during instantiation. In addition, get the dom element through options. el and store it on $el
class MyVue {
constructor(options) {
this.$options = options
this.$el = document.querySelector(this.$options.el)
this.$data = options.data
}
}
Instantiate MyVue
Instantiate 1 MyVue and pass in the option, which specifies the bound element el and data object data
const myVm = new MyVue({
el: '#app',
data: {
username: 'LastStarDust'
}
})
Implementation of Observe Listener
Hijacking data is to perceive, notify and update the view when modifying data
class MyVue {
constructor(options) {
// ...
// Attributes of Monitoring Data
this.observable(this.$data)
}
// Recursively traverses all properties of a data object , Hijacking data attributes { username: 'LastStarDust' }
observable(obj) {
// obj Is empty or not an object , Do nothing
const isEmpty = !obj || typeof obj !== 'object'
if(isEmpty) {
return
}
// ['username']
const keys = Object.keys(obj)
keys.forEach(key => {
// If the property value is an object , Recursive invocation
let val = obj[key]
if(typeof val === 'object') {
this.observable(val)
}
// this.defineReactive(this.$data, 'username', 'LastStarDust')
this.defineReactive(obj, key, val)
})
return obj
}
// Data hijacking , To modify the property get And set Method
defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(` Take out ${key} Attribute value : Value is ${val}`)
return val
},
set(newVal) {
// There has been no change , Do not update
if(newVal === val) {
return
}
console.log(` Update properties ${key} The value of is : ${newVal}`)
val = newVal
}
})
}
}
Dep Message Subscriber
The subscriber is stored, and when the notification is received, the subscriber is taken out and the subscriber's update method is called
// Defining a message subscriber
class Dep {
// Static attribute Dep.target This is 1 Individual global only 1 Adj. Watcher , because in the same 1 There can only be time 1 Global Watcher
static target = null
constructor() {
// Storage subscriber
this.subs = []
}
// Add Subscriber
add(sub) {
this.subs.push(sub)
}
// Notice
notify() {
this.subs.forEach(sub => {
// Call the subscriber's update Method
sub.update()
})
}
}
Adding a message subscriber to the data hijacking process
Add subscribers for every 1 attribute
defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// Will be initialized , Trigger attribute get() Method , Come here Dep.target Valuable , Store it as a subscriber , Object that triggers the property set() Method , Call notify Method
if(Dep.target) {
dep.add(Dep.target)
}
console.log(` Take out ${key} Attribute value : Value is ${val}`)
return val
},
set(newVal) {
// There has been no change , Do not update
if(newVal === val) {
return
}
console.log(` Update properties ${key} The value of is : ${newVal}`)
val = newVal
dep.notify()
}
})
}
Subscriber Wather
Fetch data from the model and update the view
// Defining Subscriber Classes
class Wather {
constructor(vm, exp, cb) {
this.vm = vm // vm Instances
this.exp = exp // The string value corresponding to the instruction , Such as v-model="username", exp Equivalent to "username"
this.cb = cb // Back to Function Called when the view is updated
this.value = this.get() // Add yourself to the message subscriber Dep Medium
}
get() {
// Regard the current subscriber as a global only 1 Adj. Wather, Add to Dep.target Upper
Dep.target = this
// Get data , Object that triggers the property getter Method
const value = this.vm.$data[this.exp]
// Add to Message Subscription in Execution Dep Posterior , Reset Dep.target
Dep.target = null
return value
}
// Perform an update
update() {
this.run()
}
run() {
// From Model Take out attribute values from the model
const newVal = this.vm.$data[this.exp]
const oldVal = this.value
if(newVal === oldVal) {
return false
}
// Execute callback function , Will vm Instances , New value , Pass the old value past
this.cb.call(this.vm, newVal, oldVal)
}
}
Parser Compile
Parsing template instructions, replacing template data and initializing views;
Binding the node corresponding to the template instruction to the corresponding update function and initializing the corresponding subscriber;
Initialize the compiler, store the dom element corresponding to el, store the vm instance, and call the initialization method
In the initialization method, starting from the root node, all the child nodes of the root node are taken out, and the nodes are parsed one by one
In the process of parsing nodes,
The parsing instruction exists, takes out the binding value, replaces the template data, and completes the initialization of the first view
The node corresponding to the instruction is bound with an update function, and a subscriber Wather is instantiated
For the v-model instruction, listen to the 'input' event, and realize the view update is to update the data of the model
// Defining a parser
// Parsing instruction , Replace template data , Initial view
// Instruction binding update function of template , When data is updated , Update view
class Compile {
constructor(el, vm) {
this.el = el
this.vm = vm
this.init(this.el)
}
init(el) {
this.compileEle(el)
}
compileEle(ele) {
const nodes = ele.children
// Traverse the node for resolution
for(const node of nodes) {
// If there are child nodes , Recursive invocation
if(node.children && node.children.length !== 0) {
this.compileEle(node)
}
// Instruction time v-model And the label is the input label
const hasVmodel = node.hasAttribute('v-model')
const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1
if(hasVmodel && isInputTag) {
const exp = node.getAttribute('v-model')
const val = this.vm.$data[exp]
const attr = 'value'
// Push the initial model value to the view layer , Initialize view
this.modelToView(node, val, attr)
// Instantiation 1 Subscribers , Bind the update function to the subscriber , Future data updates , You can update the view
new Wather(this.vm, exp, (newVal)=> {
this.modelToView(node, newVal, attr)
})
// Listen for changes in view
node.addEventListener('input', (e) => {
this.viewToModel(exp, e.target.value)
})
}
// Instruction time v-bind
if(node.hasAttribute('v-bind')) {
const exp = node.getAttribute('v-bind')
const val = this.vm.$data[exp]
const attr = 'innerHTML'
// Push the initial model value to the view layer , Initialize view
this.modelToView(node, val, attr)
// Instantiation 1 Subscribers , Bind the update function to the subscriber , Future data updates , You can update the view
new Wather(this.vm, exp, (newVal)=> {
this.modelToView(node, newVal, attr)
})
}
}
}
// Update model values to views
modelToView(node, val, attr) {
node[attr] = val
}
// Update view values to the model
viewToModel(exp, val) {
this.vm.$data[exp] = val
}
}
Complete code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<form>
<input type="text" v-model="username">
</form>
<div>
<span v-bind="username"></span>
</div>
<p v-bind="username"></p>
</div>
<script>
class MyVue {
constructor(options) {
this.$options = options
this.$el = document.querySelector(this.$options.el)
this.$data = options.data
// 监视数据的属性
this.observable(this.$data)
// 编译节点
new Compile(this.$el, this)
}
// 递归遍历数据对象的所有属性, 进行数据属性的劫持 { username: 'LastStarDust' }
observable(obj) {
// obj为空或者不是对象, 不做任何操作
const isEmpty = !obj || typeof obj !== 'object'
if(isEmpty) {
return
}
// ['username']
const keys = Object.keys(obj)
keys.forEach(key => {
// 如果属性值是对象,递归调用
let val = obj[key]
if(typeof val === 'object') {
this.observable(val)
}
// this.defineReactive(this.$data, 'username', 'LastStarDust')
this.defineReactive(obj, key, val)
})
return obj
}
// 数据劫持,修改属性的get和set方法
defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 会在初始化时, 触发属性get()方法,来到这里Dep.target有值,将其作为订阅者存储起来,在触发属性的set()方法时,调用notify方法
if(Dep.target) {
dep.add(Dep.target)
}
console.log(`取出${key}属性值: 值为${val}`)
return val
},
set(newVal) {
// 没有发生变化, 不做更新
if(newVal === val) {
return
}
console.log(`更新属性${key}的值为: ${newVal}`)
val = newVal
dep.notify()
}
})
}
}
// 定义消息订阅器
class Dep {
// 静态属性 Dep.target,这是1个全局唯1 的Watcher,因为在同1时间只能有1个全局的 Watcher
static target = null
constructor() {
// 存储订阅者
this.subs = []
}
// 添加订阅者
add(sub) {
this.subs.push(sub)
}
// 通知
notify() {
this.subs.forEach(sub => {
// 调用订阅者的update方法
sub.update()
})
}
}
// 定义订阅者类
class Wather {
constructor(vm, exp, cb) {
this.vm = vm // vm实例
this.exp = exp // 指令对应的字符串值, 如v-model="username", exp相当于"username"
this.cb = cb // 回到函数 更新视图时调用
this.value = this.get() // 将自己添加到消息订阅器Dep中
}
get() {
// 将当前订阅者作为全局唯1的Wather,添加到Dep.target上
Dep.target = this
// 获取数据,触发属性的getter方法
const value = this.vm.$data[this.exp]
// 在执行添加到消息订阅Dep后, 重置Dep.target
Dep.target = null
return value
}
// 执行更新
update() {
this.run()
}
run() {
// 从Model模型中取出属性值
const newVal = this.vm.$data[this.exp]
const oldVal = this.value
if(newVal === oldVal) {
return false
}
// 执行回调函数, 将vm实例,新值,旧值传递过去
this.cb.call(this.vm, newVal, oldVal)
}
}
// 定义解析器
// 解析指令,替换模板数据,初始视图
// 模板的指令绑定更新函数, 数据更新时, 更新视图
class Compile {
constructor(el, vm) {
this.el = el
this.vm = vm
this.init(this.el)
}
init(el) {
this.compileEle(el)
}
compileEle(ele) {
const nodes = ele.children
for(const node of nodes) {
if(node.children && node.children.length !== 0) {
// 递归调用, 编译子节点
this.compileEle(node)
}
// 指令时v-model并且是标签是输入标签
const hasVmodel = node.hasAttribute('v-model')
const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1
if(hasVmodel && isInputTag) {
const exp = node.getAttribute('v-model')
const val = this.vm.$data[exp]
const attr = 'value'
// 初次模型值推到视图层,初始化视图
this.modelToView(node, val, attr)
// 实例化1个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图
new Wather(this.vm, exp, (newVal)=> {
this.modelToView(node, newVal, attr)
})
// 监听视图的改变
node.addEventListener('input', (e) => {
this.viewToModel(exp, e.target.value)
})
}
if(node.hasAttribute('v-bind')) {
const exp = node.getAttribute('v-bind')
const val = this.vm.$data[exp]
const attr = 'innerHTML'
// 初次模型值推到视图层,初始化视图
this.modelToView(node, val, attr)
// 实例化1个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图
new Wather(this.vm, exp, (newVal)=> {
this.modelToView(node, newVal, attr)
})
}
}
}
// 将模型值更新到视图
modelToView(node, val, attr) {
node[attr] = val
}
// 将视图值更新到模型上
viewToModel(exp, val) {
this.vm.$data[exp] = val
}
}
const myVm = new MyVue({
el: '#app',
data: {
username: 'LastStarDust'
}
})
// console.log(Dep.target)
</script>
</body>
</html>
The above is the vue implementation of simple bi-directional data binding details, more about vue implementation of bi-directional data binding information please pay attention to other related articles on this site!