class Vue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
let computed = options.computed;
let methods = options.methods
// 这个跟元素是否存在
if (this.$el) {
// 把数据全部转化成用Object.defineProperty来定义
new Observer(this.$data)
for(let key in computed) {
Object.defineProperty(this.$data,key,{
get: () => {
return computed[key].call(this)
}
})
}
for(let key in methods) {
Object.defineProperty(this,key, {
get() {
return methods[key]
}
})
}
// 把数据获取操作,vm上的取值操作都代理到vm.$data
this.proxyVm(this.$data)
new Compiler(this.$el, this)
}
}
proxyVm(data) {
for(let key in data) {
Object.defineProperty(this,key, {
get() {
return data[key] //进行了转换操作
},
set(newVal) {
data[key] = newVal
}
})
}
}
}
class Observer { //实现数据劫持功能
constructor(data) {
this.observer(data);
}
observer(data) {
// 如果是对象才观察
if (data && typeof data == 'object') {
// 如果是对象
for (let key in data) {
this.defineReactive(data, key, data[key]);
}
}
}
defineReactive(obj, key, value) {
// 对象里面还是一个对象就要继续监听
this.observer(value)
let dep = new Dep() //给每一个属性都加上一个具有发布订阅的功能
Object.defineProperty(obj, key, {
get() {
// 创建watcher时,会取到对应的内容,并且把watcher放到了全局上
Dep.target && dep.addSub(Dep.target)
return value;
},
set: (newVal) => { //{school: {name: 'LM'}} school = {}
// 新值跟旧值不一样才去更新
if (newVal != value) {
// 当给对象赋值一个新对象时也要监听
this.observer(newVal)
value = newVal
dep.notify()
}
}
})
}
}
class Compiler {
constructor(el, vm) {
// 判断el属性是不是一个元素,不是元素,就获取
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm
// 把当前节点中的元素获取到,放到内存中
let fragment = this.node2fragement(this.el)
// 把节点中的内容进行替换
// 编译模板,用数据编译
this.compile(fragment)
// 把内容再塞到页面中
this.el.appendChild(fragment)
}
isElementNode(node) {
return node.nodeType === 1;
}
// 判断是不是指令
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 编译元素
compileElement(node) {
let attributes = node.attributes;
[...attributes].forEach(attr => {
let { name, value: expr } = attr; //v-model= school.msg
// 判断是不是指令
if (this.isDirective(name)) { //v-model v-html v-bild
let [, directive] = name.split('-')
let [directveName,eventName] = directive.split(':')
// 需要调用不同的指令来处理
CompileUtil[directveName](node, expr, this.vm, eventName);
}
})
}
// 编译文本的
compileText(node) { //判断当前文本节点中的内容是否包含 {{}}
let content = node.textContent
if (/\{\{(.+?)\}\}/.test(content)) {
// 文本节点
CompileUtil['text'](node, content, this.vm); //{{a}}
}
}
compile(node) { //用来编译内存中的dom节点
let childNodes = node.childNodes; //类数组
[...childNodes].forEach(child => {
if (this.isElementNode(child)) {
this.compileElement(child)
// 如果是元素的话,需要把自己传进去,在去遍历子节点
this.compile(child)
} else {
this.compileText(child)
}
})
}
// 把节点移动到内存中
node2fragement(node) {
// 创建一个文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = node.firstChild) {
// appendChild具有移动性
fragment.appendChild(firstChild);
}
return fragment
}
}
// 观察者(发布订阅) 观察者 被观察者
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 默认先放一个老值
this.oldValue = this.get();
}
get() {
Dep.target = this //先把自己放在this上
// 取值把这个观察者和数据关联起来
let value = CompileUtil.getVal(this.vm, this.expr)
Dep.target = null
return value
}
// 更新操作,数据变化后,会调用观察者update方法
update() {
let newVal = CompileUtil.getVal(this.vm, this.expr)
if (newVal !== this.oldValue) {
this.cb(newVal)
}
}
}
class Dep {
constructor() {
this.subs = [] //存放所有的Watcher
}
// 订阅
addSub(watcher) {
this.subs.push(watcher)
}
// 发布
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
CompileUtil = {
getVal(vm, expr) { //vm.$data 'school.name' [school,name]
return expr.split('.').reduce((data, current) => {
return data[current]
}, vm.$data)
},
setValue(vm, expr, value) {
console.log(value);
expr.split('.').reduce((data, current, index, arr) => {
if (index == arr.length - 1) {
return data[current] = value
}
return data[current]
}, vm.$data)
},
model(node, expr, vm) {
// node是节点,expr是表达式,vm是当前实例;给输入框value属性 node.value=xxx
let fn = this.updater['modelUpdater']
// 给输入框加一个观察者,如果数据更新了会触发次方法,会拿新值给输入框赋予值
new Watcher(vm, expr, (newVal) => {
fn(node, newVal)
})
// 给input添加一个事件
node.addEventListener('input', (e) => {
let value = e.target.value //获取用户输入的值
this.setValue(vm, expr, value)
})
let value = this.getVal(vm, expr);
fn(node, value)
},
html(node,expr,vm) {
let fn = this.updater['htmlUpdater']
// 给输入框加一个观察者,如果数据更新了会触发次方法,会拿新值给输入框赋予值
new Watcher(vm, expr, (newVal) => {
fn(node, newVal)
})
let value = this.getVal(vm, expr);
fn(node, value)
},
// 遍历表达式 将内容重新替换成一个完整的内容,返还回去
getContentValue() {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1])
})
},
on(node,expr,vm,eventName) { //v-on:click = "change"
node.addEventListener(eventName, (e) => {
vm[expr].call(vm,e)
})
},
text(node, expr, vm) { //expr ==={{a}}
let fn = this.updater['textUpdater'];
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 给表达式{{}}都加上观察者
new Watcher(vm, args[1], () => {
fn(node, this.getContentValue(vm, expr)) //返回一个全的字符串
})
return this.getVal(vm, args[1])
});
fn(node, content)
},
updater: {
//把数据插入到节点中
modelUpdater(node, value) {
node.value = value;
},
htmlUpdater(node,value) {
node.innerHtml = value
},
// 处理文本节点
textUpdater(node, value) {
node.textContent = value
}
}
}
手写vue双向数据绑定原理
最新推荐文章于 2024-02-16 21:02:36 发布