不要在翻译插件里跑demo, 会出不来的
html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="school.name">
<div>{{school.name}}</div>
<div>{{school.age}}</div>
<ul>
<li>1</li>
<li>1</li>
</ul>
</div>
<script src="./mvvm.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
school: {
name: '珠峰',
age: 10
}
}
})
</script>
</body>
</html>
mvvm.js
// 观察者模式 发布订阅
class Dep {
constructor () {
this.subs = []; // 存放所有watcher
}
// 订阅 添加
addSub(watcher) {
this.subs.push(watcher)
}
// 发布
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm; // 观察者绑定vm实例
this.expr = expr; // 观察者绑定对应表达式 school.name
this.cb = cb; // 观察者绑定cb函数,被观察者通知更新时 执行
this.oldValue = this.get(); // 获取 表达式 school.name旧值
}
get() {
Dep.target = this; // 挂载(当前表达式school.name)观察者实例到全局Dep.target
let value = compileUnit.getVal(this.vm, this.expr); // 获取初始化旧值
Dep.target = null; // 清空缓存
return value
}
update() { // 数据更新,被观察者通知观察者update
let newVal = compileUnit.getVal(this.vm, this.expr); // 重新获取表达式值
if (newVal !== this.oldValue) { // 如果数据改变,观察者执行回调
this.cb(newVal)
}
}
}
/* vm.$watch(vm, 'school.name', (newVal => {
})) */
// 数据劫持
class Observer {
constructor (data) {
this.observer(data)
}
observer(data) {
// 判断传入的vm.$data是否是一个对象
if (data && typeof data == 'object') {
for (var key in data) { // 对vm.$data所有属性进行遍历
this.defineRective(data, key, data[key]) // 进行属性劫持
}
}
}
defineRective(data, key, value) {
this.observer(value); // 子递归 {school: {name: '珠峰'}}, 对所有属性值添加get,set方法
let dep = new Dep(); // 给每一个属性,添加发布订阅的功能, 声明一个被观察者(被观察数据),一旦属性数据改变,通知观察者
Object.defineProperty(data, key, { // 重写对象的属性 Object.defineProperty(对象,属性,属性特性)
get () { // 获取属性值 vm.data.school vm.data.school.name都要挂载
// 创建watcher时 会取到对应的实例,并且把watcher放到全局上
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newVal) => { // 设置属性值
if (newVal != value) { // vm.school.age=18,newVal为设置新值,与原先不同时才进入
this.observer(newVal) // 递归赋值的元素,vm.school.age = {a:123}可能赋值的是个对象,里面属性也要劫持
value = newVal; // 设置新值
dep.notify(); // 数据被重设,通知观察者
}
}
})
}
}
class Complier {
constructor (el, vm) {
// 判断el是不是元素节点
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm;
// 获取view层el节点内所有元素,放到内存中进行编译
let fragment = this.node2fragment(this.el);
// 编译模板,fragment文档节点的数据绑定
this.compile(fragment);
// 把内容塞到页面中
this.el.appendChild(fragment)
}
isDirective(attrName) {
return attrName.startsWith('v-')
}
compileElement (node) {
// {type="text", v-model="school.name"}
let attributes = node.attributes;
// console.log(attributes);
[...attributes].forEach(attr => { // 遍历节点内所有属性
let {name, value: expr} = attr; // v-model="school.name", 解构属性名,属性值
if (this.isDirective(name)) { // 判断当前属性名是否是以'v-'开头的指令
let [ ,directive] = name.split('-'); // directive 是自定义的指令
compileUnit[directive](node, expr, this.vm) //定义compileUnit对象对不同指令数据进行编译, (传入节点,传入表达式, vm实例)对节点进行数据绑定
}
})
}
compileText(node) {
let content = node.textContent;
if (/\{\{(.+?)\}\}/g.test(content)) { // 如果文本值是插值表达式
compileUnit['text'](node, content, this.vm) //content => {{a}} {{b}} 可能多个插值表达式
}
}
compile (node) {
let childNodes = node.childNodes; //获取fragment第一级子节点,相当于el内的第一级,包括元素节点,与文本节点
[...childNodes].forEach(child => { // 节点转数组遍历
if (this.isElementNode(child)) { // 判断当前节点是否是元素节点
this.compileElement(child) // 对元素节点进行编译,数据绑定
this.compile(child)
} else { // 当前节点是文本节点
this.compileText(child)
}
})
}
isElementNode(node) {
return node.nodeType === 1 // 元素节点nodeType类型是1
}
node2fragment (node) {
let fragment = document.createDocumentFragment(); //创建一个文档碎片
let firstChild;
// node.firstChild为el绑定区域的第一个孩子节点,不断获取,赋值给firstChild变量
while (firstChild = node.firstChild) {
//appendChild具有移动性,追加到文档碎片中,el绑定区元素就不存在了,相当于不断移动el内第一个孩子节点到fragment
fragment.appendChild(firstChild)
}
// 此时,el区域内没有任何节点
return fragment
}
}
// 定义的编译对象, 对不同指令执行不同方法 , model,html
compileUnit = {
getVal(vm,expr) { // vm.$data, expr=> school.name
return expr.split('.').reduce((prev,next) => {
return prev[next]
}, vm.$data)
},
setVal(vm, expr, value) {
expr.split('.').reduce((prev,next, index, arr) => { // $data.school.name = 18
if (index == arr.length - 1) { // 当循环到最后一项,直接赋值
prev[next] = value
}
return prev[next]
}, vm.$data)
},
model (node,expr,vm) { // node是节点,expr是表达式(school.name),vm是实例
let fn = this.updater['modelUpdater']; // 更新函数
// 声明观察者实例,
new Watcher(vm, expr, (newVal) => { // 给输入框添加观察者,如果数据更新了,此方法会给输入框重新赋值
fn(node, newVal) // 给input节点赋值
})
node.addEventListener('input', (e) => { // 给input节点添加input事件,输入时不断获取input值,在vm.$data中重新赋值
let value = e.target.value;
this.setVal(vm, expr, value)
})
let value = this.getVal(vm,expr);
fn(node, value) // input节点赋值
},
html () {
},
getContentValue(vm, expr) { // 一旦一个插值表达式改变,获取文本节点所有值
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1]);
})
},
text(node,expr,vm) { // expr => {{a}} {{b}}
let fn = this.updater['textUpdater']; // 更新函数
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => { // 检测每个{{}}
// 给表达式每个{{}} 都加上观察者, 一旦一个改变,渲染所有
new Watcher(vm, args[1], () => { //args[1],表达式,获取()元组中内容
fn(node, this.getContentValue(vm, expr)) // 返回一个全的字符串
})
// 每一次返回 将插值表达式替换为实际值,{{school.name}} => ‘珠峰‘
return this.getVal(vm, args[1]) //args[1], 获取()元组中内容
})
fn(node,content) // 文本节点赋值
},
// 定义更新方法,可实现部分复用
updater: {
modelUpdater(node,value) {
node.value = value
},
htmlUpdater() {
},
textUpdater(node,value) {
node.textContent = value
}
}
}
class Vue {
constructor (options) {
this.$el = options.el; // 获取view层绑定元素
this.$data = options.data; // 获取model数据
if (this.$el) { // 如果view层存在,开始数据劫持,和模板编译
new Observer(this.$data); // 数据劫持
new Complier(this.$el, this); // 模板编译
// 添加数据代理
this.proxyVm(this.$data);
}
}
proxyVm(data) {
for (var key in data) {
// 获取vm中key值,会指向vm.$data中key值
Object.defineProperty(this, key, {
get () {
return this.$data[key]
}
})
}
}
}
主要参考: https://www.bilibili.com/video/BV1o4411T7ib?p=2