示意图
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="app">
<input type="text" v-model="message" />
{{ message }}
<p>
姓名: <span>{{ obj.name }}</span>
年龄: <span>{{ obj.age }}</span>
</p>
</div>
<script src="Watcher.js"></script>
<script src="Observer.js"></script>
<script src="Compiler.js"></script>
<script src="Vue.js"> </script>
<script>
let vm = new Vue({
el:'#app',
data:{
message: 'hello wrold',
obj: {
name:'jack',
age:19
}
}
})
</script>
</body>
</html>
// Vue.js
class Vue {
constructor(options) {
// 将数据缓存在vue实例属性上,方便实例中的函数能够使用this.xx访问到
this.$el = options.el;
this.$data = options.data;
if (this.$el) {
// 数据劫持 监听data这个对象 监听其中的属性 映射为get 和 set
new Observer(this.$data)
// 代理数据 this.$data.message => this.message
this.proxyData(this.$data)
// 如果$el存在 就进行编译 (编译需要数据和元素)
// 这里第二个参数把this传过去 那边要什么直接通过this取。
new Compiler(this.$el, this);
}
}
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this,key,{
get() { // this.message => this.$data.message
return data[key]
},
set(newVal) {
// this.$data.message = '新值'
data[key] = newVal;
}
})
})
}
}
// Compiler.js
class Compiler {
constructor(el, vm) {
// vm是传过来的vue实例 初始化数据 方便函数中调用
// 如果el是一个元素直接赋值给this.el 如果是一个字符串'#app' 则使用dom方法自己去取。
this.el = this.isElementNode(el) ? el : document.querySelector(el);
// 这里的vm就是 Vue.js中的this 拥有 $el 和 $data 属性的。
this.vm = vm;
if (this.el) {
// 如果这个元素存在(使用dom方法能够获取到)则开始编译。
// 1.先把真实的dom即this.el 存在fragment中 (文档节点,可以使用dom的方法,但是不会影响页面)
let fragment = this.node2fragment(this.el);
// 此时的fragment 就相当于是el的副本 只不过不存在于真实dom中 存在内存中 不用担心过多操作影响性能。
// 2.编译fragment : 就是从中提取 插值表达式 {{}} 和 v-xx指令 换成数据
this.compile(fragment)
// 3.将编译好之后的fragment塞到页面中区,替换#app那个div
// 经过第二步 fragment已经编译好了 将他塞回页面
this.el.appendChild(fragment)
}
}
// 定义一个方法判断传进来的node是否是一个元素
isElementNode(node) {
return node.nodeType === 1;
}
// 定义一个方法判断是不是一个指令
isDirective(name) {
return name.includes('v-')
}
// 将真实dom取出存在fragment中
node2fragment(el) {
// 创建一个文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
// 定义个变量firstChild,每次都将元素的第一个节点赋值给firstChild
// 当调用fragment.appendChild()时,el中的第一个节点就会从el中**移除**然后添加到文档碎片中
// 当el所有的节点都移除是,el.ffirstChild就是null 那么firstChild也就是null 循环结束
// 这样就所有的节点中el中移入到了fragment中 然后将文档碎片返回。
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
// 就是从中提取 插值表达式 {{}} 和 v-xx指令 换成数据
compile(fragment) {
// 先获取所有的子节点
let childNodes = fragment.childNodes;
// 遍历节点集合,针对性编译
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) { // 如果是元素节点
// 编译元素
this.compileElement(node)
// 并且如果是元素节点则还需要递归 目的是为了拿到所有的插值表达式及指令
this.compile(node)
} else { // 文本节点
// 编译文本
this.complieText(node)
}
})
}
// 编译元素 => 取出指令即元素的v-xx 属性。
compileElement(node) {
// 取出元素身上的所有属性
let attrs = node.attributes;
// 编译属性 找到v-xx attr.name拿到属性名 attr.value拿到属性值
Array.from(attrs).forEach(attr => {
if (this.isDirective(attr.name)) {
// 如果是一个指令 取到对应指令的值渲染成数据放到节点中
// 需要 属性值、数据、节点
let val = attr.value; // val == message 表达式
// 解构赋值 v-model 取到model = type
let [,type] = attr.name.split('-');
// 通过vm就能取到实例上的data
CompileUtil[type](val, this.vm, node)
}
});
}
// 编译文本 => 取出插值表达式
complieText(node) {
// 取出节点中的文本
let txt = node.textContent;
// 定义正则取出表达式
let reg = /\{\{([^}]+)\}\}/g;
if (reg.test(txt)) {
// 如果为true则说明有插值表达式
// 取出表达式 渲染成数据 插到节点中
// 需要 表达式、数据、节点
// 通过vm就能取到实例上的data 将整个文本传过去 在函数里面进行表达式的抽取
CompileUtil['text'](txt, this.vm, node)
}
}
}
// 定义一个专门用来编译的工具
CompileUtil = {
// 定义一个方法 从vm.$data中取值
getVal(vm, expr) {
// message.a.b 转换成 vm.$data.message.a.b
// vm.$data[message.a.b] 很明显是错误的写法 取不到 所以需要借助reduce
expr = expr.split('.');
return expr.reduce((prev,next)=>{
return prev[next]
},vm.$data)
},
getTxtVal(vm, expr) {
return expr.replace(/\{\{([^}]+)\}\}/g,(...arg)=>{
// 这里的arg[1] 就是 message / message.a
return this.getVal(vm, arg[1].trim())
})
},
setVal(vm,expr,newVal) {
expr = expr.split('.')
return expr.reduce((prev,next,cIndex)=>{
if (cIndex === expr.length-1) {
// 循坏到最后的时候 message => message.a => message.a.b 赋新值
return prev[next] = newVal;
}
return prev[next]
},vm.$data)
},
text(expr, vm, node) { // 插值文本处理
// 取出更新函数
let updateFn = this.updater['txtUpdater'];
// 更新
// 通过正则取出真正的表达式 {{ message.a }} == vm.$data.message.a
// 此时的value就是 vm.$data.message / vm.$data.message.a
let value = this.getTxtVal(vm, expr)
// 这里应该加一个监控 数据变化了 重新编译模板 **数据=>视图**
expr.replace(/\{\{([^}]+)\}\}/g,(...arg)=>{
// 这里的arg[1] 就是 message / message.a
new Watcher(vm,arg[1].trim(),()=>{
// 如果数据变化了 文本节点需要重新获取新的数据 然后更新dom
// 回调会在Watcher.update()调用时执行 , 什么时候会调用呢
// 数据更新的时候 应该调用 就是在劫持数据映射set哪里
updateFn && updateFn(node, this.getTxtVal(vm,expr))
})
})
updateFn && updateFn(node, value)
},
model(expr, vm, node) { // v-model处理
// 取出更新函数
let updateFn = this.updater['modelUpdater'];
// 这里应该加一个监控 数据变化了 重新编译模板 **数据=>视图**
new Watcher(vm, expr, (newVal)=>{
// 回调会在Watcher.update()调用时执行 , 什么时候会调用呢
// 数据更新的时候 应该调用 就是在劫持数据映射set那里
updateFn && updateFn(node, newVal)
})
// 更新 : vm.$data[expr] == vm.$data.message即数据
// updateFn && updateFn(node, vm.$data[expr])
// 因为这个expr 很可能是 message.a.b 所以需要在定义一个专门取值的函数
updateFn && updateFn(node, this.getVal(vm, expr))
// 处理 v-model时 监听node的input事件 **视图改变=>数据更新**
node.addEventListener('input',(e)=>{
// 因为可能v-model 绑定的是一个深层属性 所以同样要去reduce 修改 最深层属性的值
this.setVal(vm, expr, e.target.value)
})
},
updater: {
txtUpdater(node, value) { // 编译更新插值表达式
// 传入一个节点 一个新值,在fragment中更新这个新值 最后渲染到页面上去
// 作用:即可以初始时将 message 替换成 'hellowrold' 也可以将新的message 替换 旧的message
node.textContent = value;
},
modelUpdater(node, value) { // 编译更新v-model
// v-model 即绑定的是表单元素的value属性
// 传入一个节点(表单元素) 一个新值,更新表单元素的value
node.value = value;
}
}
}
// Observer.js
// 劫持数据
class Observer {
constructor(data) {
// 监听数据
this.observer(data)
}
observer(data) {
// 将data原有的属性改成get和set
if (!data || typeof data !== 'object') {
// 如果数据不存在 或者不是一个对象 则直接return 不监听
return;
}
// 将数据一一劫持
Object.keys(data).forEach(key => {
// 定义一个劫持数据的方法
// data message 'hello wrold'
this.defineReactive(data, key, data[key])
// 递归进行深度劫持 因为data[key]有可能一个对象
// 如果不是对象 则在上面的判断就会直接return 不会走到这里进行深度劫持了
this.observer(data[key])
})
}
defineReactive(obj,key,val) {
let that = this;
let dep = new Dep();
// 每一个变化的数据 都对应一个Dep 这里面的subs数组 存放着所有更新的操作
Object.defineProperty(obj,key,{
enumerable: true,
configurable: true,
get(){
// 每次编译更新模板 new Watcher的时候 就会把watcher实例赋值给Dep.target
// 然后调用addSub订阅
Dep.target && dep.addSub(Dep.target)
return val
},
set(newVal){
if (newVal !== val) {
// 这个新值可能是一个对象 就需要再次劫持
// vm.$data.message是'helloword' => vm.$data.message = {a:1}
// 不需要去判断这个值到底是不是一个对象 因为在observer中如果不是对象 就直接return了
that.observer(newVal)
// 把新值赋值给val 则在取的时候 调用get返回的就是新值
val = newVal;
// 数据变化的时候 调用更新操作 在编译阶段new Watcher中的回调就会被执行。 编译新的dom
dep.notify();
}
}
})
}
}
// 发布-订阅
class Dep {
constructor(){
// 订阅的数组
this.subs= [];
}
addSub(watcher) { // 订阅
this.subs.push(watcher)
}
notify() { // 发布
this.subs.forEach(watcher=>{
watcher.update();
})
}
}
// Watcher.js
// 增加一个观察者,来监听数据的改变,然后更新数据
// 给需要变化的元素增加一个观察者,当数据变化之后 执行对应的方法 更新dom 或者 数据
// 数据 => 视图
// <input type="text" v-model="message" />
// message:'hello wrold' => message: '123'
// input.value = 123
// 视图 => 数据
// 当用户在input标签中进行输入的时候
// 监听oninput事件 将vm.$data.message = e.target.value
// 双向数据绑定
// 接受三个参数 vm实例 / expr表达式= message / cb 回调 当值发生变化之后执行的回调
class Watcher {
constructor(vm,expr,cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 缓存老的值 就是初始时的值
this.oldVal = this.get();
}
// 复用一下complier.js中获取值的方法
getVal(vm, expr) {
// message.a.b 转换成 vm.$data.message.a.b
// vm.$data[message.a.b] 很明显是错误的写法 取不到 所以需要借助reduce
expr = expr.split('.');
return expr.reduce((prev,next)=>{
return prev[next]
},vm.$data)
}
get() {
// 缓存初始值的时候 将这个watch实例赋值给Dep.target
Dep.target = this;
// getVal 去取的时候就会调用 数据劫持中的get方法
let value = this.getVal(this.vm, this.expr);
// 取值的时候即上面getVal的时候 将watcher传过去 取完之后 加Dep.target置空
// 因为别的watch在调用的时候
Dep.target = null;
return value
}
// 对外暴露方法,更新数据
update() {
// 拿到新值 调用这个方法时 重新去获取的值就是新值。 数据变化了
let newVal = this.getVal(this.vm, this.expr);
// 拿到老值
let oldVal = this.oldVal;
if(newVal !== oldVal) {
// 执行回调,传递新值过去
this.cb(newVal)
}
}
}
网友评论