vue双向数据绑定原理实现

准备工作

​ 新建一个index.js文件, 一个index.html文件

​ index.js文件中, 定义Vue类, 并将Vue并称全局变量 window.Vue = Vue

​ index.html中引入index.js

index.js

class Vue({})

window.Vue = Vue

index.html

<script src="./tt.js"></script>

​ 然后就可以在index.html中 new Vue() 了

初始化data

​ 在index.html中,先 new 一个 Vue实例

<div id="app">
<input type="text" v-model="inputValue">
<div>{{inputValue}}</div>
<input type="text" v-model="obj.input">
<div>{{obj.input}}</div>
</div>
<script src="./tt.js"></script>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
inputValue: '12345',
obj: {
input: '输入狂里面的内容是:'
},
message: '通常'
}
}
})
</script>

​ 在index.js中, 初始化数据

​ 获取用户选项,并初始化数据

class Vue {
constructor(options) {
this.$options = options
const vm = this
if(this.$options.data) {
this.initData(vm)
}
if(this.$options.el) {
compile(this.$options.el, vm)
}
}
initData(vm) {
let data = vm.$options.data
data = typeof data === 'function' ? data.call(vm) : data
vm._data = data
observe(data)
// 这个是为了实现 vm.name可以直接访问的代理方法
for(let key in data) {
proxy(vm, key, data[key])
}
}
}

​ 给数据添加setter和getter

function observe(data) {
// 不是对象, 直接返回
if(data === null || typeof data !== 'object') {
return
}
return new Observer(data)
} // 这里值考虑了对象和对象嵌套, 没有考虑数组的情况
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
// 遍历对象的每一项, 添加响应式方法
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
} function defineReactive(target, key, value) {
// 如果值是对象的话, 也需要重复添加
observe(value)
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
value = newValue
// 对设置的新值也需要observe
observe(newValue)
}
})
}

​ 访问代理的方法

function proxy(target, key, value) {
Object.defineProperty(target, key, {
get() {
return target['_data'][key]
},
set(newValue) {
target['_data'][key] = newValue
}
})
}

依赖收集

​ 添加一个Dep类, 用户收集属性的watcher

class Dep {
constructor() {
// 里面装的是属性收集的watcher
this.subs = []
}
// 添加watcher的方法
addSub(watcher) {
this.subs.push(watcher)
}
// 更新的时候,找到属性里面的所有watcher, 触发watcher的update方法实现更新
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
// 添加一个静态属性. 相当于全局变量, 指向当前的watcher, 初始值是null
Dep.target = null

​ 添加观察者watcher, 在取值的时候让Dep.target指向当前的watcher, 取值结束之后,让Dep.target为null, 这样就可以通过属性的get方法里面将当前的watcher添加到属性里面的dep中, dep也需要在定义响应式的时候添加

// 订阅者
class Watcher {
constructor(vm, key, callback) {
// vm: 实例, key: 需要更新的属性. callback: 更新时执行的回调
this.vm = vm
this.key = key
this.callback = callback
// 让Dep.target属性指向当前watcher
Dep.target = this
// 通过获取操作, 触发属性里面的get方法
key.split('.').reduce((total, current) => total[current], vm._data)
Dep.target = null
}
update() {
const value = this.key.split('.').reduce((total, current) => total[current], this.vm._data)
this.callback(value)
}
}

​ 给属性收集依赖

function defineReactive(target, key, value) {
observe(value)
// 给属性添加dep实例, 因为是闭包,这个空间不会被销毁
let dep = new Dep()
Object.defineProperty(target, key, {
get() {
// Dep.target指向当前watcher, 如果当前watcher有值, 当前属性收集这个watcher
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue) {
value = newValue
observe(newValue)
// 赋值时,触发该方法更新视图
dep.notify()
}
})
}

解析模板,更新模板

​ 添加解析模板方法

if(this.$options.el) {
compile(this.$options.el, vm)
}

​ 使用documentFragment创建模板, 注意fragment.append 时会一处页面上的元素, while循环结束后, 页面就没了,

​ 然后对模板里面的每一项进行解析, 先实例 node.nodeType === 3 的元素, 表示文本节点, 看文本节点里面有没有匹配到{{name}}模板表达式的, 如果有, 如vm._data里面去除对应的值, 替换文本的值, 最后vm.$el.appendChild(fragment)就可以将替换后的结果显示在页面上

​ 对nodeType === 1 的元素, 即标签解析, 这里我们处理的是input, 获取节点的所有属性, 一个伪数组, 变成真数组, 里面有个nodeName = v-model 和 nodeValue = name 的, 同样获取vm._data里面name的值, 然后让节点的 node.value = 这个值, 就能显示在输入框里面了, 这就是数据改变视图.接下来是视图改变数据, 添加input方法, 为node 添加 addEventListener方法, input, 然后让vm._data里面对应属性的值等于e.target.value, 这样就实现了视图改变数据

​ 重点: 上面的两种情况, nodeType == 3 的时候更新方法是 node.nodeValue = newValue, nodeType == 1 的时候更新方法是 node.value = value, 需要将这两个方法封装到 watcher中, 在更新之后 new 一个 Watcher, 并将对应的参数传入, 后面在获取值的时候就会自动收集依赖, set值的时候就会触发更新, ojbk

function compile(el, vm) {
vm.$el = el = document.querySelector(el) const fragment = document.createDocumentFragment()
let child
while(child = el.firstChild) {
fragment.append(child)
} fragment_compile(fragment) function fragment_compile(node) {
const parttern = /\{\{\s*(\S+)\s*\}\}/
// 文本节点
if(node.nodeType === 3) {
// 匹配{{}}, 第一项为匹配的内容, 第二项为匹配的变量名称
const match = parttern.exec(node.nodeValue)
if(match) {
const needChangeValue = node.nodeValue
// 获取到匹配的内容, 可能是 msg, 也可能是 mmm.msg,
// 注意通过 vm[mmm.msg]是拿不到数据的, 要 vm[mmm][msg]
// 获取真实的值, 替换掉模板里面的 {{name}}, 真实的值从vm.$options.data里面取
let arr = match[1].split('.')
let value = arr.reduce(
(total, current) => total[current], vm._data
)
// 将真实的值替换掉模板字符串, 这个就是更新模板的方法, 将这个方法封装到watcher里面
node.nodeValue = needChangeValue.replace(parttern, value)
const updateFn = value => {
node.nodeValue = needChangeValue.replace(parttern, value)
}
// 有个问题, node.nodeValue在执行过一次之后, 值就变了, 不是 {{name}}, 而是 12345, 要救{{name}}里面的name暂存起来
new Watcher(vm, match[1], updateFn)
}
return }
// 元素节点
if(node.nodeType === 1 && node.nodeName === 'INPUT') {
// 伪数组
const attrs = node.attributes
let attr = Array.prototype.slice.call(attrs)
// 里面有个nodeName -< v-model, 有个nodeValue 对应 name
attr.forEach(item => {
if(item.nodeName === 'v-model') {
let value = getVmValue(item.nodeValue, vm)
// input标签是修改node.value
node.value = value
// 也需要添加watcher
new Watcher(vm, item.nodeValue, newValue => node.value = newValue)
// 添加input事件
node.addEventListener('input', e => {
const name = item.nodeValue
// 给vm上的属性赋值
// 不能直接 vm._data[name] = e.target.value , 因为那么可能是 a.b
// 也不能直接获取b的值, 然后赋新值, 因为这个值是一个值类型, 需要先获取前面的引用类型
// 如: let tem = vm._data.a 然后 tem[b] = 新值, 这样就可以
const arr1 = name.split('.')
const arr2 = arr1.slice(0, arr1.length - 1)
const head = arr2.reduce((total, current) => total[current], vm._data)
head[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
node.childNodes.forEach(child => fragment_compile(child))
} vm.$el.appendChild(fragment)
}

完整代码

​ index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实现简单的双向数据绑定</title>
</head>
<body>
<div id="app">
<input type="text" v-model="inputValue">
<div>{{inputValue}}</div>
<input type="text" v-model="obj.input">
<div>{{obj.input}}</div>
</div>
<script src="./index.js"></script>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
inputValue: '12345',
obj: {
input: '输入狂里面的内容是:'
},
message: '通常'
}
}
}) </script>
</body>
</html>

​ index.js


class Vue {
constructor(options) {
this.$options = options
const vm = this
if(this.$options.data) {
this.initData(vm)
}
if(this.$options.el) {
compile(this.$options.el, vm)
}
}
initData(vm) {
let data = vm.$options.data
data = typeof data === 'function' ? data.call(vm) : data
vm._data = data
observe(data)
for(let key in data) {
proxy(vm, key, data[key])
}
}
} function proxy(target, key, value) {
Object.defineProperty(target, key, {
get() {
return target['_data'][key]
},
set(newValue) {
target['_data'][key] = newValue
}
})
} function observe(data) {
if(data === null || typeof data !== 'object') {
return
}
return new Observer(data)
} class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
} function defineReactive(target, key, value) {
observe(value) let dep = new Dep()
Object.defineProperty(target, key, {
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue) {
value = newValue
observe(newValue)
// debugger
dep.notify()
}
})
} function compile(el, vm) {
vm.$el = el = document.querySelector(el) const fragment = document.createDocumentFragment()
let child
while(child = el.firstChild) {
fragment.append(child)
} fragment_compile(fragment) function fragment_compile(node) {
const parttern = /\{\{\s*(\S+)\s*\}\}/
// 文本节点
if(node.nodeType === 3) {
// 匹配{{}}, 第一项为匹配的内容, 第二项为匹配的变量名称
const match = parttern.exec(node.nodeValue)
if(match) {
const needChangeValue = node.nodeValue
// 获取到匹配的内容, 可能是 msg, 也可能是 mmm.msg,
// 注意通过 vm[mmm.msg]是拿不到数据的, 要 vm[mmm][msg]
// 获取真实的值, 替换掉模板里面的 {{name}}, 真实的值从vm.$options.data里面取
let arr = match[1].split('.')
let value = arr.reduce(
(total, current) => total[current], vm._data
)
// 将真实的值替换掉模板字符串, 这个就是更新模板的方法, 将这个方法封装到watcher里面
node.nodeValue = needChangeValue.replace(parttern, value)
const updateFn = value => {
node.nodeValue = needChangeValue.replace(parttern, value)
}
// 有个问题, node.nodeValue在执行过一次之后, 值就变了, 不是 {{name}}, 而是 12345, 要救{{name}}里面的name暂存起来
new Watcher(vm, match[1], updateFn)
}
return }
// 元素节点
if(node.nodeType === 1 && node.nodeName === 'INPUT') {
// 伪数组
const attrs = node.attributes
let attr = Array.prototype.slice.call(attrs)
// 里面有个nodeName -< v-model, 有个nodeValue 对应 name
attr.forEach(item => {
if(item.nodeName === 'v-model') {
let value = getVmValue(item.nodeValue, vm)
// input标签是修改node.value
node.value = value
// 也需要添加watcher
new Watcher(vm, item.nodeValue, newValue => node.value = newValue)
// 添加input事件
node.addEventListener('input', e => {
const name = item.nodeValue
// 给vm上的属性赋值
// 不能直接 vm._data[name] = e.target.value , 因为那么可能是 a.b
// 也不能直接获取b的值, 然后赋新值, 因为这个值是一个值类型, 需要先获取前面的引用类型
// 如: let tem = vm._data.a 然后 tem[b] = 新值, 这样就可以
const arr1 = name.split('.')
const arr2 = arr1.slice(0, arr1.length - 1)
const head = arr2.reduce((total, current) => total[current], vm._data)
head[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
node.childNodes.forEach(child => fragment_compile(child))
} vm.$el.appendChild(fragment)
} // 依赖收集
class Dep {
constructor() {
// 里面装的是属性收集的watcher
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
} // 订阅者
class Watcher {
constructor(vm, key, callback) {
this.vm = vm
this.key = key
this.callback = callback
// 让Dep.target属性指向当前watcher
Dep.target = this
// 通过获取操作, 触发属性里面的get方法
key.split('.').reduce((total, current) => total[current], vm._data)
Dep.target = null
}
update() {
const value = this.key.split('.').reduce((total, current) => total[current], this.vm._data)
this.callback(value)
}
} function getVmValue(key, vm) {
return key.split('.').reduce((total, current) => total[current], vm._data)
} function setVmValue(key, vm) {
let tem = key.split('.')
let fin = tem.reduce((total, current) => total[current], vm._data)
return fin
} window.Vue = Vue

最新文章

  1. iOS-----dSYM 文件分析工具
  2. Android 手机卫士--选中SettingItemView条目状态切换
  3. NServiceBus 更换服务名及队列名称
  4. Windows 64位 安装Oracle instantclient 官方绿色版和PL/SQL Developer 总结
  5. swith 语句详解
  6. jar包有嵌套的jar的打包成jar的方法
  7. [BZOJ1058][ZJOJ2007]报表统计
  8. Java面试18|关于进程、线程与协程
  9. 安装Kali的小问题
  10. [Android源码]Android源码之高仿飞鸽传书WIFI热点搜索与创建(一)
  11. The Topo to Raster tool returns errors 010235 and 010067转
  12. 微信公众号网页授权获取用户openid
  13. android开发(25) - 推送的实现,使用百度云推送
  14. mysql免安装版本(用批处理安装和启动)
  15. AngularJS标准Web业务流程开发框架—1.AngularJS模块以及启动分析
  16. css !important的作用
  17. 1021 Deepest Root (25)(25 point(s))
  18. URLConnection格式与用法
  19. LOJ3044. 「ZJOI2019」Minimax 搜索
  20. POJ 1011 Sticks 【DFS 剪枝】

热门文章

  1. 【力扣】nSum问题模板
  2. Caddy-用Go写的新一代可扩展WebServer
  3. Blazor如何实现类似于微信的Tab切换?
  4. 使用iframe引入文件后设置响应式宽高以及其他问题解决;
  5. Vue3源码阅读梳理
  6. Python实用代码片段(1)-rot13加密
  7. 【学习日志】MongoDB为什么选择B树,而MySQL选择B+树实现索引
  8. 主题样式选择效果代码及css样式
  9. 这是一篇乖巧的草稿——vscode上传代码到代码托管平台GitHub
  10. C#小知识之中英文转换、去空格