记一次vue原理学习记录,涉及了如何进行数据劫持、数据代理、观察者模式(发布订阅)、数据双向绑定功能等知识点,也简单实现了vue中一些经常使用功能,代码中作了比较详细的注释,能够直接复制执行查看结果javascript
index.htmlhtml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MVVM原理</title>
</head>
<body>
<div id="app">
{{school.name}}{{age}}
<div>{{age}}</div>
<input type="text" v-model="school.name">
<div>{{school.name}}</div>
<div>compute: {{getNewName}}</div>
<ul>
<li>li_1</li>
<li>li_2</li>
</ul>
<button v-on:click="changeName">methods</button>
<div v-html="htmlName"></div>
</div>
<!-- <script src="./node_modules/vue/dist/vue.js"></script> -->
<script src="mvvm.js"></script>
<script> let vm = new Vue({ // el: document.querySelector('#app'), el: '#app', data: { school: { name: '学校名' }, age: 20, htmlName: '<h1>inner html</h1>' }, methods: { changeName () { this.school.name = '我是methods方法修改的' } }, computed: { getNewName () { return this.school.name + 'new' } } }) </script>
</body>
</html>
复制代码
mvvm.jsvue
// 基础类 负责调度
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)
// computed存在依赖关系,执行computed方法会取值触发get
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)
}
}
// 将vm操做代理到vm.$data上取值
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) {
// 给每一个数据都添加一个具备发布订阅的功能
let dep = new Dep()
Object.defineProperty(obj, key, {
get () {
// 当建立watcher时候会取到值,会触发get,而且将watcher放到全局
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newVal) => {
if (value !== newVal) {
this.observer(newVal) // 对新赋值的进行劫持
value = newVal
dep.notify() // 触发发布方法,通知watcher修改了数据
}
}
})
this.observer(value) // 进行递归劫持数据
}
}
// 观察者模式 (发布订阅模式)观察者 被观察者
class Watcher { // 观察者
constructor (vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
this.oldVal = this.getValue() // 先存放一个老值
}
getValue () {
Dep.target = this // 先将本身放到Dep.target
let value = CompilerUtils.getValue(this.vm, this.expr)
Dep.target = null // 触发get后清除掉
return value
}
update () { // 更新操做据发生变化了会执行观察者的update方法
let newVal = CompilerUtils.getValue(this.vm, this.expr)
if (newVal !== this.oldVal) {
this.cb(newVal)
}
}
}
// 发布订阅
class Dep {
constructor () {
this.subs = [] // 存放全部的watcher
}
// 订阅
addSub (watcher) {
this.subs.push(watcher)
}
// 发布
notify () { // 执行全部watcher的update方法
this.subs.forEach(watcher => watcher.update())
}
}
// 编译类
class Compiler {
constructor (el, vm) { // 传入 '#app' || document.querySelector('#app')
this.vm = vm
// 判断el是否为元素节点
this.el = this.isElementNode(el) ? el : document.querySelector(el)
// console.log(this.el)
// 将元素使用文档碎片存到内存中
this.fragment = this.el2fragment(this.el)
// console.log('fragment:', this.fragment)
// 编译模版 将fragment中的变量进行替换
this.compile(this.fragment)
// 从新将fragment塞回el
this.el.appendChild(this.fragment)
}
isElementNode (node) { // 判断是否元素节点
return node.nodeType === 1
}
// 把节点存到内存中
el2fragment (node) {
// 建立一个文档碎片
let fragment = document.createDocumentFragment()
let firstChild
while (firstChild = node.firstChild) {
// console.log(firstChild)
// fragment.appendChild 具备移动性,会删除相应的节点
fragment.appendChild(firstChild)
}
return fragment
}
compile (fragment) { // 用来编译内存中的dom节点
let childNodes = [...fragment.childNodes]// 类数组
childNodes.forEach(child => {
if (this.isElementNode(child)) {
this.compileElement(child)
// 递归编译子节点
this.compile(child)
} else {
// console.log('text: ', child)
this.complieText(child)
}
})
}
// 编译节点
compileElement (node) {
let attrs = [...node.attributes]
// console.log('attrs:', [...attrs])
attrs.forEach(attr => {
let { name, value:expr } = attr
// 判断是否节点
if (this.isDirecttive(name)) {
let [, directive] = name.split('-')
let arr
if (directive.split(':').length > 0) {
arr = directive.split(':')
} else {
arr = [directive]
}
let [directiveName, eventName] = arr
CompilerUtils[directiveName](node, expr, this.vm, eventName)
}
})
}
// 编译文本节点
complieText (node) {
let content = node.textContent
if (/\{\{(.+?)\}\}/.test(content)) {
// console.log(content)
CompilerUtils['text'](node, content, this.vm)
}
}
// 判断是否指令
isDirecttive (attrName) {
return attrName.startsWith('v-')
}
}
// 编译工具方法
CompilerUtils = {
// 根据表达式取值
getValue (vm, expr) {
return expr.split('.').reduce((data, curKey) => {
return data[curKey]
}, vm.$data)
},
setValue (vm, expr, value) {
expr.split('.').reduce((data, curKey, index, arr) => {
if (index === arr.length -1) { // 给表达式最后一个赋值
return data[curKey] = value
}
return data[curKey]
}, vm.$data)
},
updater: { // 更新值的方法
modelUpdater (node, value) {
node.value = value
},
htmlUpdater (node, value) {
node.innerHTML = value
},
textUpdater (node, value) { // 更新文本节点
node.textContent = value
}
},
model (node, expr, vm) { // node 节点 expr 表达式 vm 当前实例
// 给输入框赋值value
let value = this.getValue(vm, expr)
let fn = this.updater['modelUpdater']
// 放入观察者
new Watcher(vm, expr, (newVal) => {
fn(node, newVal) // 将新值给输入框赋值
})
fn(node, value)
// 视图驱动数据 给v-model绑定input事件
node.addEventListener('input', evt => {
let val = evt.target.value
this.setValue(vm, expr, val)
})
},
on (node, expr, vm, eventName) {
node.addEventListener(eventName, (evt) => {
// console.log(vm, expr)
vm[expr].call(vm, evt) // 执行相应方法methods
})
},
getContentVal (vm, expr) {
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(vm, args[1])
})
return content
},
text (node, expr, vm) { // 处理文本节点
let fn = this.updater.textUpdater
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 为文本插入观察者 给{{}}的都加上观察者
new Watcher(vm, args[1], () => {
fn(node, this.getContentVal(vm, expr)) // 返回一个全字符串
})
return this.getValue(vm, args[1])
})
fn(node, content) // 更新值
},
html (node, expr, vm) {
let fn = this.updater['htmlUpdater']
new Watcher(vm, expr, (newVal) => {
fn(node, newVal) // 将新值给v-html赋值
})
fn(node, this.getValue(vm, expr))
}
}
复制代码