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

vue双向数据绑定原理简单实现的更多相关文章

  1. vue双向数据绑定的简单实现

    vue双向数据绑定的简单实现 参考教程:链接 <!DOCTYPE html> <html lang="en"> <head> <meta ...

  2. vue双向数据绑定原理探究(附demo)

    昨天被导师叫去研究了一下vue的双向数据绑定原理...本来以为原理的东西都非常高深,没想到vue的双向绑定真的很好理解啊...自己动手写了一个. 传送门 双向绑定的思想 双向数据绑定的思想就是数据层与 ...

  3. Vue双向数据绑定原理分析(转)

    add by zhj: 目前组里使用的是前端技术是jQuery + Bootstrap,后端使用的Django,Flask等,模板是在后端渲染的.前后端没有分离,这种做法有几个缺点 1. 模板一般是由 ...

  4. Vue双向数据绑定原理深度解析

    首先,什么是双向数据绑定?Vue是三大MVVM框架之一,数据绑定简单来说,就是当数据发生变化时,相应的视图会进行更新,当视图更新时,数据也会跟着变化. 在分析其原理和代码的时候,大家首先了解如下几个j ...

  5. 手写MVVM框架 之vue双向数据绑定原理剖析

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  6. Vue双向数据绑定原理解析

    基本原理 Vue.采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,数据变动时发布消息给订阅者,触发相应函数的回调 ...

  7. Vue 双向数据绑定原理分析 以及 Object.defineproperty语法

    第三方精简版实现 https://github.com/luobotang/simply-vue Object.defineProperty 学习,打开控制台分别输入以下内容调试结果 userInfo ...

  8. vue 双向数据绑定原理

    博客地址: https://ainyi.com/8 采用defineProperty的两个方法get.set 示例 <!-- 表单 --> <input type="tex ...

  9. Vue双向数据绑定原理

    https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension

  10. Vue双向绑定原理梳理

    简介 vue数据双向绑定主要是指:数据变化更新视图,视图变化更新数据. 实现方式:数据劫持 结合 发布者-订阅者 模式. 数据劫持通过 Object.defineProperty()方法. 对对象的劫 ...

随机推荐

  1. vue中 beforeRouteLeave 生命周期函数

    beforeRouteLeave需求描述在使用 element-UI的table 的时候,有这么一个需求.从一个页面切换到另一个页面,再切回来的时候,滚动条的位置不变. 需求:滚动浏览列表页,出现滚动 ...

  2. Unity_UIWidgets - 组件Scaffold

    UIWidgets - 组件Scaffold 各位兄弟姐妹,想通过Unity来开发UIWidgets的么,想通过UIWi的gets..来开发手机APP么??想么想么,哈哈哈哈哈哈哈哈. 好了,小黑不唠 ...

  3. 读C#代码整洁之道笔记07_代码评审和集成测试

    1. 代码评审注意事项 1.1. 始终保持代码评审的意识 1.2. 保证代码构建成功 1.3. 确保所有的测试都是通过的 1.4. 注意YAGNI原则 1.5. 检查重复代码 1.6. 使用静态分析器 ...

  4. Performance API不完全使用指北

    本教程解释了如何使用Performance API来记录真实用户访问你的应用程序的统计数据. 使用浏览器的DevTools来评估web应用性能是很有用的,但要复现现实世界的使用情况并不容易.因为人们在 ...

  5. 1月3日内容总结——bbs项目登陆页面和主页、个人站点页的搭建

    目录 一.登陆功能完善 验证码功能实现 单机验证码实现验证码刷新(局部刷新) 点击登陆提交数据进行校验 二.主页搭建 html代码 views.py代码 主页内容部分 后台添加数据 分页器 前端获取头 ...

  6. Python实用代码片段(1)-rot13加密

    Python之禅:THIS.PY 你安装了python之后,能在Lib目录下找到一个this.py的文件,就是此处的内容. s = """Gur Mra bs Clgub ...

  7. Swagger2多包扫描

    package com.xf.config; import org.springframework.context.annotation.Bean; import org.springframewor ...

  8. 浅析 SeaweedFS 与 JuiceFS 架构异同

    SeaweedFS 是一款高效的分布式文件存储系统,最早的设计原型参考了 Facebook 的 Haystack,具有快速读写小数据块的能力.本文将通过对比 SeaweedFS 与 JuiceFS 在 ...

  9. 接水问题(NOIP 2010 PJT2)

    这个的思路就是让各个水龙头所用的时间尽可能地接近,可以先向优先队列中推入前m个数,由于开的是小根堆最小的数在前面我们把它拿出来,加上下一个人所需的时间.如此反复,直到都接完水,最大值就是答案. #in ...

  10. 注释、input()、运算符、组织结构(顺序、选择、循环)

    注释 单行注释 # 多行注释 将三对引号之间的为多行注释 ''' ''' 中文编码的声明注释 #coding:gbk input()函数 接受来自用户的输入 返回值是str 值的存储 使用=对输入的值 ...