vue双向数据绑定v2版本
代码如下:
html
<div id="app">
{{age}}
标题:<p v-html="html">123</p>
<input type="text" v-model="name">
<input type="text" v-model="age">
<input type="text" v-model="obj.b">
{{name}}
{{name}}
<p>我是{{name}}一名前端工程师{{name}}{{name}}</p>
<p>我是{{obj.a}}一名前端工程师{{obj.b}}{{obj.b}}</p>
<hr>
--{{jl}}--
<button v-on:click="mychange">按钮,点我让age+1</button>
<ul>
<li v-for="item in ul"></li>
</ul>
</div>
js
// 1. 定义Vue基础类
// 2. 定义编译器类---找到模板中的内容编译为具体数据 把v-model='name'、{{name}}的内容 => data.name => 前端
// 2.1 创建文档碎片 提高性能
// 2.2 找到文档碎片中的v-model和{{}}
// 2.3 处理v-model
// 2.3 处理{{}}
// 2.4 定义具体处理的工具函数
// 3. 定义劫持数据
// 3.1 定义数据劫持类
// 3.2 递归保证对象嵌套
// 3.3 劫持数据
// 4. 定义观察者模式
// 4.1 定义观察者类
// 4.2 准备一个获取观察的数据的方法
// 4.2 准备一个更新观察者数据的方法,等待(发布订阅那边)被通知才执行
// 5. 定义发布订阅
// 5.1 定义发布订阅模式
// 5.1 准备一个空数组,专门收集所有观察者
// 5.2 准备一个添加观察者的方法--订阅
// 5.3 准备一个发布通知的方法,通知所有订阅过的观察者--发布
// 6 完成单向数据绑定
// 6.1 在指令处添加观察者
// 6.2 在文本渲染处添加观察者
// 6.3 在首次触动get方法处创建new Dep(),收集所有相关观察者
// 6.4 只能首次触发收集 Dep.target = this
// 7. 完成双向数据绑定
// 7.1 给v-model所在表单添加监听事件
// 7.2 获取表单数据,赋值到data中
// 8. 简化数据、代理数据
// 8.1 代理 this的属性
// 9. 处理computed
// 10. 处理methods
// 11. 处理v-html
// 抓住核心:
// 数据改变--视图改变
// 一个数据可能同时在影响多个视图(一个name 对应多个{{name}}、多个v-model='name'等) 一对多的关系
// 如何做到改变一个数据,同时影响到多个视图的改变呢?
// 发布、订阅,先订阅,再发布
// 先视图关联渲染的name全部订阅dep、再数据有改变时dep发布
// 订阅的时候因为数据劫持里面的get并不会触发,只有在触发的时候才会订阅
// 但订阅的时候可能导致重复订阅(比如:obj.a.b.c在触发complieUitls.getValue时,因为存在reduce,逐级调用获取value,就执行了4次get)
// 只有在首次才可以加入dep队列,防止后续取值get的时候也重复添加观察者
class Vue {
constructor(options) {
// 把参数定义到实例身上,方便调用
this.$el = options.el
this.$data = options.data
let computed = options.computed
let methods = options.methods
if (this.$el) {
// 劫持数据
new Observer(this.$data)
// 处理computed
for (let key in computed) {
// 有计算属性就会被代理到this.$data上(所以data和computed不能同名)
// 一旦computed被读取,就会被劫持返回执行computed函数中的内容(调用computed函数)注意this指向
// 在被computed函数执行的时候,又会触发data里面的数据依赖,触发到data的数据劫持
// 对该数据形成发布订阅和观察者(之前的Observer),创建渲染节点处创建观察者,添加到dep订阅集合中,在触发set方法的时候,发布已经订阅的观察者更新
Object.defineProperty(this.$data, key, {
get: () => {
return computed[key].call(this)
}
})
}
// 处理methods/同理computed
for (let key in methods) {
Object.defineProperty(this.$data, key, {
get() {
return methods[key]
},
enumerable:true
})
}
// 代理数据,让用户少写一层 this.name 而不是this.$data.name
// 先把methods、computed绑定到$data中,再代理数据
this.porxyVm(this.$data)
// 把模板编译成对应数据渲染
new Compiler(this, this.$el, this.$data)
}
}
porxyVm(data) {
for (let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newValue) {
data[key] = newValue
}
})
}
}
}
// 一个属性数据对应一个dep订阅器
// 一个dep订阅器数组中收集了N个wather观察者
// 一旦属性数据在set中被触发
// 就会通知该dep订阅器中的所有观察者
// 让每一个观察者都触发自己的更新功能,从而做到数据改变影响所有视图
class Dep {
constructor() {
// 用数组专门收集观察者
this.subs = []
}
// 订阅
addSub(watcher) {
this.subs.push(watcher)
}
// 发布
notify() {
console.log(this.subs);
this.subs.forEach(watcher => watcher.update())
}
}
class Wather {
constructor(vm, expr, cb) {
// expr 表达式
this.vm = vm;
this.expr = expr;
this.cb = cb;
this.oldValue = this.get()
}
// new 观察者,但是没有添加到dep订阅器中,所以手动获取一次数据就会自动触发数据劫持中的get
get() {
// 核心套路:第一次get的时候,把this(new出来的观察者实例)放到当前Dep类身上,证明是首次(第一次添加完毕之后会置空)
Dep.target = this
// 手动读取数据,相当于触发了数据劫持中的get,会把当前this添加到dep队列身上(dep订阅器完成收集观察者的任务)
let oldValue = complieUitls.getValue(this.expr, this.vm)
// 置空,防止观察者重复加入队列(更新数据时,又会读取数据劫持的get)
Dep.target = null
return oldValue
}
update() {
let newValue = complieUitls.getValue(this.expr, this.vm)
if (newValue != this.oldValue) {
this.cb && this.cb(newValue)
}
}
}
// 数据劫持
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
// 是对象才遍历
if (data?.constructor === Object) {
for (let key in data) {
this.defineDeactive(data, key, data[key])
}
}
}
defineDeactive(data, key, value) {
// 防止对象中存在对象,所以调自己一次
this.observer(value)
// 每一个属性都会进入一次,创建一个新的dep订阅器的数据集合
let dep = new Dep()
Object.defineProperty(data, key, {
get() {
// 只有第一次在 new 观察者的时候Dep.target才会有值(防止反复添加观察者入dep订阅器队列)
if (Dep.target) {
dep.addSub(Dep.target)
}
return value
},
set: newValue => {
if (value != newValue) {
// 防止属性被替换为对象,get、set失效,所以重新劫持新对象
this.observer(newValue)
value = newValue
dep.notify()
}
}
})
}
}
class Compiler {
constructor(vm, el, data) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
this.$data = data;
// 获取文档碎片
let fragment = this.getFragment();
// 把文档碎片去编译
this.complie(fragment)
// 把编译完的文档碎片放入html
this.$el.appendChild(fragment)
}
// 判断是否为元素节点
isElementNode(node) {
return node.nodeType === 1
}
// 判断是否为vue指令
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 获取文档碎片提高性能
getFragment() {
let fragment = document.createDocumentFragment();
let child
while (child = this.$el.firstChild) {
fragment.appendChild(child)
}
return fragment
}
// 编译指令
complieElement(node) {
[...node.attributes].forEach(attr => {
// 检测元素身上的所有属性
const { name, value: expr } = attr
// name 属性名 v-model
// expr 属性值 name、age
if (this.isDirective(name)) {
// 如果是指令(存在v-),继续结构拿到 - 后面的内容
const [, directive] = name.split('-')
// directive 指令 model、html、for、on 等
// 防止出现注册事件(除了指令还要获取click) v-on:click 要分别拿到on和click
let [directiveName, eventName] = directive.split(':')
// directiveName 指令 on/if/html/for
// eventName 事件类型 click/mouseenter
// 分别调用各自的指令
if (complieUitls.directive[directiveName]) {
complieUitls.directive[directiveName](node, expr, this.$vm, eventName)
}
}
})
}
// 编译文本内容
complieText(node) {
if (/\{\{(.+?)\}\}/.test(node.textContent)) {
complieUitls.txt(node, this)
}
}
// 编译
complie(fragment) {
[...fragment.childNodes].forEach(node => {
if (node.nodeType === 1) {
// 如果是元素节点
// 检测是否存在指令,有指令就编译 <p v-model="name"></p> 没有就放弃
this.complieElement(node)
// 防止元素本身有儿子,儿子可能有指令、差值表达式 没有就放弃
this.complie(node)
}
if (node.nodeType === 3) {
// 如果是内容节点
// 检测是否存在差值表达式,有就编译 {{}},没有就放弃
this.complieText(node)
}
})
return fragment
}
}
const complieUitls = {
// 获取对象的值(可多层嵌套) obj.a.b.c = 5 取值
getValue(objStr, vm) {
return objStr.split('.').reduce((data, current) => data[current], vm.$data)
},
// 设置对象的值(可多层嵌套) obj.a.b.c = 5 赋值(相当于已经更新了数据)
setValue(objStr, vm, newValue) {
// newValue 用户操作的最新数据
objStr.split('.').reduce((data, current, index, arr) => {
// 找到最后的obj.a.b.c的时候(根据长度来判断),赋值
if (index === arr.length - 1) {
data[current] = newValue
}
return data[current]
}, vm.$data)
},
// 插值表达式内容的替换 {{name}} => vm.$data.name => 前端
txt(node, vm) {
// node.textContent = xxx
let template = node.textContent // 带{{}}的原始模板,后续才用到,这里保存起来
template.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Wather(vm, args[1], () => {
// 这里不能直接用新值替换 node.textContent = newValue 会出问题,比如:
// 原始 js name=前端 ------ html 我是一名{{name}}{{name}}工程师 => 我是一名前端前端工程师
// 错误 js name=java ------ html 我是一名{{name}}{{name}}工程师 => java
// 正确 js name=java ------ html 我是一名{{name}}{{name}}工程师 => 我是一名java工程师
complieUitls.update.txt(node, template, vm)
})
})
// 初始化
complieUitls.update.txt(node, template, vm)
},
// 更新
update: {
// 更新插值表达式
txt(node, template, vm) {
// template 原始插值表达式模板
//{{name}}--{{name}} 前端--前端
node.textContent = template.replace(/\{\{(.+?)\}\}/g, (...args) => complieUitls.getValue(args[1], vm))
},
// 更新指令内容
model(node, expr, vm) {
// complieUitls.setValue(value,vm)
// expr 表达式
// node.value = xxx
node.value = complieUitls.getValue(expr, vm)
},
html(node, expr, vm) {
// complieUitls.setValue(value,vm)
// expr 表达式
// node.innerHTML = xxx
node.innerHTML = complieUitls.getValue(expr, vm)
},
},
// 各种v-指令
directive: {
model(node, expr, vm) {
// 给v-model指令 添加一个观察者,如果有改变,触发后面的回调函数
// new Wather(vm,value,newValue => node.value = newValue)
// node.value = complieUitls.getValue(node,newValue, vm)
// 提取了一个公共方法(具体操作),显得正规一些
// 初始化,编译模板中的内容 ---- v-model="name" => 前端
complieUitls.update.model(node, expr, vm)
// 添加观察者,观察该位置的指令,一旦发现数据更新后,收到dep的通知,更新视图
new Wather(vm, expr, () => complieUitls.update.model(node, expr, vm))
// v-model 是双向数据绑定的,所以实时监听事件
node.addEventListener('input', function (e) {
complieUitls.setValue(expr, vm, e.target.value)
})
},
on(node, expr, vm, eventName) {
// v-on:click='mychange'
// expr 指令中的表达式 mychange
// eventName 绑定事件的名字 是 click
node.addEventListener(eventName, function (e) {
vm[expr](e)
})
},
html(node, expr, vm) {
// node.innerHTML = xxx
// 添加观察者,让数据改变的时候,v-html也可以改变
new Wather(vm, expr, newValue => complieUitls.update.html(node, expr, vm))
complieUitls.update.html(node, expr, vm)
},
for(node, expr, vm) {
const [item, , list] = expr.split(' ')
let nodeName = node.nodeName
vm.$data[list].forEach(li => {
let n = document.createElement(nodeName);
n.innerHTML = li
node.appendChild(n)
})
}
}
}
let vm = new Vue({
el: '#app',
data: {
name: '前端',
age: 3,
obj: {
a: 'obj1',
b: 'obj2',
c: {
cc: {
ccc: 'obj4'
},
}
},
ul: ['yes', 'ok', 'go', 'ya'],
html: '<h1>h1</h1>'
},
computed: {
jl() {
return '我的简历' + this.$data.name + this.$data.age
}
},
methods: {
mychange(e) {
// console.log('e',e);
this.$data.age += 1
}
}
})