vue核心效果的实现
1.此次实现了vue的模板语法的编译({{}}),包括v-text, v-html, v-bind, v-model, v-on, 以及绑定时间的简写(@)和v-bind的简写(:);
2.实现了数据的响应式更新,observer数据劫持,watcher依赖收集 Dep 类管理wather
vue.js 主要实现编译效果 observer.js 实现数据的劫持及响应式更新;代码如下:
// Vue.js 2020.12.29 wangchuxian
class Vue {
constructor(options){
this.$el = options.el
this.$data = options.data
this.$options = options
this.$methods = options.methods
if(this.$el){
new Observer(this.$data) //数据劫持
new Compile(this.$el, this) //模板编译
}
this.proxyData(this.$data) // 代理this.$data数据 可以直接通过 this.obj 直接访问(this.$data.obj)
}
proxyData(data){
for(let key in data){
Object.defineProperty(this, key, { //this指向vue实例,即当访问vue下的数据时,可以从$data数据中查找
set:(newVal)=>{
data[key] = newVal
},
get(){
return data[key]
}
})
}
}
}
class Compile {
constructor(el, vm){
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 为了减少页面的回流和重绘,创建文档碎片。
let fragment = this.nodeToFragment(this.el)
this.compile(fragment)
// 在插入节点前,先要对节点进行处理,解析v-命令及{{}}语法
this.el.appendChild(fragment)
}
compile(node){
let childNodes = [...node.childNodes]
childNodes.forEach(child=>{
if(this.isElementNode(child)){
this.compileElement(child)
}else{
this.compileText(child, this.vm)
}
if(child.childNodes && child.childNodes.length) { //如果子节点里还有元素节点, 再次对子节点进行编译
this.compile(child)
}
})
}
compileElement(el){
let attributes = [ ...el.attributes ]
attributes.forEach(attr =>{
let {name, value} = attr
// 判断是否是vue的指令
if(this.isRedirect(name)){
let [, directive] = name.split('-') //获取指令 如v-text 中的text v-html 中的html v-on:click 中的on:click
let [dirName,eventName] = directive.split(':') //获取事件类型 如 click
compileUtil[dirName](el, value, this.vm, eventName)
el.removeAttribute('v-' + directive) // 删除标签中的v-属性
}else if(this.isEvent(name)){ //兼容@方法绑定事件
let [, eventName] = name.split('@')
compileUtil['on'](el, value, this.vm, eventName)
el.removeAttribute(name) // 删除标签中的v-属性
}else if(this.isBindInfo(name)){ //兼容v-bind的 :简写
let [, eventName] = name.split(':')
compileUtil['bind'](el, value, this.vm, eventName)
el.removeAttribute(name) // 删除标签中的v-属性
}
})
}
compileText(textNode,vm){ //解析{{}}语法
let reg = /\{\{(.+?)\}\}/g
let textContent = textNode.textContent
function getContentValue(expr, vm){
return expr.replace(reg, (...args)=>{
return compileUtil.getValue(vm, args[1])
})
}
if(reg.test(textContent)){
let value = textContent.replace(reg, (...args) => {
new Watcher(vm, args[1], (newValue)=>{ //创建watcher
console.log(getContentValue(args[1], vm) )
textNode.textContent = getContentValue(textContent, vm) //更新视图 newValue变化后的值
})
return compileUtil.getValue(vm, args[1])
})
textNode.textContent = value
}
}
isBindInfo(name){
return name.startsWith(':')
}
isEvent(name){
return name.startsWith('@')
}
isRedirect(name){
return name.startsWith('v-')
}
isElementNode(node){
return node.nodeType === 1
}
nodeToFragment(el){
let fragment = document.createDocumentFragment()
while( el.firstChild ){
fragment.appendChild(el.firstChild )
}
return fragment
}
}
const compileUtil = {
// 处理v-text 命令
text(node, val, vm){
let value = this.getValue(vm, val)
node.textContent = value
new Watcher(vm, val, (newValue)=>{ //创建watcher 在new watcher() 的同时 将watcher 放入 dep watcher管理中
node.textContent = newValue //更新视图 newValue变化后的值
})
},
// 处理v-html 命令
html(node, val, vm){
let value = this.getValue(vm, val)
node.innerHTML = value //value 初始化视图的数据
new Watcher(vm, val, (newValue)=>{ //创建watcher
node.innerHTML = newValue //更新视图 newValue变化后的值
})
},
// 处理v-model 命令
model(node, val, vm){
let value = this.getValue(vm, val)
node.value = value
new Watcher(vm, val, (newValue)=>{ //创建watcher
node.value = newValue //更新视图 newValue变化后的值
})
node.addEventListener('input', (e) =>{ //处理双向绑定 所以说v-model 是v-bind和input事件的语法糖
let newVal = e.target.value
this.setValue(vm, val, newVal)
})
},
bind(node, val, vm, eventName){ //处理v-bind 命令
let value = this.getValue(vm, val)
node.setAttribute(eventName, value)
new Watcher(vm, val, (newValue)=>{ //创建watcher
node.setAttribute(eventName, newValue) //更新视图 newValue变化后的值
})
},
// 处理v-on 事件处理
on(node, val, vm, eventName){
node.addEventListener(eventName, vm.$methods[val])
},
getValue(vm,val){
return val.split('.').reduce((data, current) =>{ //循环获得深层级的数据 如data.obj.val.key
return data[current]
}, vm.$data)
},
setValue(vm, attr, val){
attr.split('.').reduce((data, current) =>{
data[current] = val
}, vm.$data)
}
}
observer.js 如下
class Observer{
constructor(data){
this.observer(data)
}
observer(data){
if(data && typeof data === 'object'){
Object.keys(data).forEach(key =>{
this.defineReactive(data, key, data[key])
})
}
}
defineReactive(obj, key, val){
this.observer(val) // val 可能还是对象,递归劫持
//在数据劫持的时候创建dep
// let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable:true,
configurable: false,
get(){
return val
},
set: (newValue) =>{
this.observer(newValue)
if(newValue !== val){
val = newValue //更改数据
dep.notify() //通知更改视图
}
}
})
}
}
class Dep {
constructor(){
this.subs = [] //管理watcher 的数组
}
addSub(watcher){
this.subs.push(watcher) //添加watcher
}
notify(){ //通知观察者更新
this.subs.forEach(w => w.update())
}
}
let dep = new Dep()
class Watcher {
constructor(vm, expr, cb){
this.vm = vm
this.expr = expr
this.cb = cb
this.oldValue = this.getOldValue()
dep.addSub(this)
}
update(){
let newValue = compileUtil.getValue(this.vm, this.expr) //vue.js 中申明的工具函数得到对应的值 在调用update函数时(更新视图),数据已经更改,所以得到的是新的值
if(newValue !== this.oldValue){
this.cb(newValue) //调用更新视图函数
}
}
getOldValue(){
return compileUtil.getValue(this.vm, this.expr) //vue.js 中申明的工具函数得到对应的值
}
}
效果查看,和vue的使用方法一致,如:
<!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">
<h1>{{title}}------{{html_txt}}</h1>
<input type="text" v-model="input_text">
<ul v-bind:hello="bind">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<div v-html="html_txt"></div>
<div v-text="text_txt"></div>
<button v-on:click="handleClick">点击</button>
<button @click="handleClick">点击</button>
</div>
</body>
</html>
<script src="vue.js"></script>
<script src="observer.js"></script>
<script>
let VM = new Vue({
el:"#app",
data:{
title: '这是一个标题',
input_text:'input 输入框',
html_txt: '这是v-html的内容',
text_txt: '这是v-text指令的内容',
bind: 'bind 属性'
},
methods:{
handleClick(){
console.log(999)
}
}
})
</script>