对于vue.js中的属性值我们要格外关注:
$attrs 获取当前传递的参数
$listeners 获取当前组件的自定义事件
$children 获取当前组件所有子组件
$parent 获取当前组件所有父组件
$options 获取当前vue实例参数信息
$refs 获取ref所有的引用节点
设计原则:单一职责,一个类或一个函数,只做一件事。
数据定义后页面更新
页面数据切换操作data数据
添加数据劫持方法
// Vue.js
//Observer专门用于数据劫持
class Observer {
data;
constructor(data) {
this.data = data
this.walk()
}
defineProperty(data, key, value) {
Object.defineProperty(data, key, {
get() {
console.log(`使用了${key}这个属性`);
return value
},
set(val) {
console.log(`修改了${key}属性`, val);
value = val
}
})
}
walk() {
Object.keys(this.data).forEach(el => {
this.defineProperty(user, el, this.data[el])
})
}
}
const user = {
username:'xiaowang',
age:11
}
new Observer(user)
console.log(user.username);
对于data来说,我们自己数据劫持存了一份在$data,并且还将$data中每个数据都再次存在了vue实例上,创建Vue类。
class Vue {
constructor(options) {
this.$options = options
this.$data = options.data()
this.$el = options.el
//$data上的所有数据都要数据劫持
new Observer(this.$data)
//$data存放所有的数据
//会将$data的数据挂并挂载到this身上
this.proxy()
}
proxy() {
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(val) {
this.$data[key] = val
}
})
})
}
}
进行模版渲染,这里仅仅渲染一层,如果多层可以执行递归。
//模版渲染
class Compile{
constructor(el,data){
this.$el = document.querySelector(el)
this.$data = data
this.compiler()
}
compiler(){
[...this.$el.children].forEach(item=>{
if(/\{\{([a-zA-z0-9]+)\}\}/.test(item.innerHTML)){
const key =RegExp.$1.trim()
item.innerHTML=this.$data[key]
}
})
}
}
观察者模式
观察者模式是一种设计模式,就是一种代码规范。有两个非常重要的元素:发布者和订阅者。一个发布者可能对应多个订阅者。
对应到我们的代码,使用{{}}的标签,意味着是我们需要标记的标签,作为订阅者,在项目中提供一个发布者,一旦数据发生变化,发布者通知订阅者更新页面。
//订阅者
class Watcher {
constructor(callback) {
Dep.target = this
this.callback = callback
this.update()
Dep.target = null
}
update() {
//这一步并不是直接修改,而是更新虚拟dom,这里只是简化了
this.callback()
}
}
//发布者
class Dep {
constructor() {
this.subs = []
}
notify() {
this.subs.forEach(item => {
item.update()
})
}
}
于此同时,在set get中需要收集wacher以及通知watcher更新数据,那么也要修改,这里有个难点,就是在数据劫持的时候怎么获取对应的watcher,这里我们采用在watcher生成实例时,将watcher挂载到发布者身上,在获取属性的劫持过程中,使用target属性,那么就生成一个watcher,并将他进行收集,在修改属性的时候,可以使用notify的方法,通知watcher使用update方法更新对应的render函数进行渲染。
Object.defineProperty(data, key, {
get() {
if (Dep.target) { //只收集编译时的watcher
//依赖收集,使用这个属性就生成一个watcher
dep.subs.push(Dep.target)
}
return value
},
set(val) {
//一旦数据更新,Dep通知watcher更新
value = val
dep.notify()
}
})
此刻,我们简易的响应式就算完成了。
这里的执行顺序比较绕,先调用数据劫持,然后调用compile,每个有{{}}的标签生成watcher,并传入render函数,在watcher的constructor中挂到发布者dep的身上,执行update,render函数得以执行,然后会触发数据劫持get方法,dep的subs进行收集watcher,同时update结束后,清除挂载在dep上的watcher。修改数据时,使用notify通知watcher然后调用update方法,触发render方法,同时继续触发get方法获取数据,但是这个时候,就不会再继续获取wacher,因为整个watcher的生成只有一次,就是在第一次编译的时候。
数据嵌套模式:
如果涉及到多层数据嵌套,我们需要递归对象属性数据,在render方法中我们要对数据进行处理,获取到对应的值。
思路:
1.渲染函数需要渲染对应的值
2.对象数据内部的数据进行响应式处理
//模版渲染
class Compile {
constructor(el, data) {
this.$el = document.querySelector(el)
this.$data = data
this.compiler()
}
compiler() {
[...this.$el.children].forEach(item => {
//修改正则表达式匹配任何内容
if (/\{\{(\S+)\}\}/.test(item.innerHTML)) {
const key = RegExp.$1.trim()
//根据.获取层级内容arr
let arr = key.split('.')
//实际上vue底层不是直接innerHTML
const render = () => {
let value = this.$data
arr.forEach((el, index) => {
if (arr.length == 1) {
value = this.$data[el]
} else {
//大于一层的要调递归方法一层一层取数据
value = this.getData(value, el)
}
})
item.innerHTML = value
}
new Watcher(render)
}
})
}
getData(obj, key) {
return obj[key]
}
}
defineProperty(data, key, value) {
const dep = new Dep()
//如果属性值为对象,那么对象属性进行遍历做响应式
if (typeof data[key] == 'object') {
Object.keys(data[key]).forEach(el => {
this.defineProperty(data[key], el, data[key][el])
})
}
Object.defineProperty(data, key, {
get() {
if (Dep.target) { //只收集编译时的watcher
//依赖收集,使用这个属性就生成一个watcher
dep.subs.push(Dep.target)
}
return value
},
set(val) {
//一旦数据更新,Dep通知watcher更新
value = val
dep.notify()
}
})
}
附完整代码:
// Observer专门用于数据劫持
class Observer {
data;
constructor(data) {
this.data = data
this.walk()
}
defineProperty(data, key, value) {
const dep = new Dep()
//如果属性值为对象,那么对象属性进行遍历做响应式
if (typeof data[key] == 'object') {
Object.keys(data[key]).forEach(el => {
this.defineProperty(data[key], el, data[key][el])
})
}
Object.defineProperty(data, key, {
get() {
if (Dep.target) { //只收集编译时的watcher
//依赖收集,使用这个属性就生成一个watcher
dep.subs.push(Dep.target)
}
return value
},
set(val) {
//一旦数据更新,Dep通知watcher更新
value = val
dep.notify()
}
})
}
walk() {
Object.keys(this.data).forEach(el => {
this.defineProperty(this.data, el, this.data[el])
})
}
}
class Vue {
constructor(options) {
this.$options = options
this.$data = options.data()
this.$el = options.el
//$data上的所有数据都要数据劫持
new Observer(this.$data)
//$data存放所有的数据
//会将$data的数据挂并挂载到this身上
this.proxy(this.$data)
new Compile(this.$el, this.$data)
console.log(this);
}
proxy(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(val) {
data[key] = val
}
})
})
}
}
//模版渲染
class Compile {
constructor(el, data) {
this.$el = document.querySelector(el)
this.$data = data
this.compiler()
}
compiler() {
[...this.$el.children].forEach(item => {
//修改正则表达式匹配任何内容
if (/\{\{(\S+)\}\}/.test(item.innerHTML)) {
const key = RegExp.$1.trim()
//获取层级内容arr
let arr = key.split('.')
const render = () => {
let value = this.$data
arr.forEach((el, index) => {
if (arr.length == 1) {
value = this.$data[el]
} else {
//大于一层的要循环一层一层取数据
value = this.getData(value, el)
}
})
//实际上vue底层不是直接innerHTML
item.innerHTML = value
}
new Watcher(render)
}
})
}
getData(obj, key) {
return obj[key]
}
}
//订阅者
class Watcher {
constructor(callback) {
Dep.target = this
this.callback = callback
this.update()
Dep.target = null
}
update() {
//这一步并不是直接修改,而是更新虚拟dom,这里只是简化了
this.callback()
}
}
//发布者
class Dep {
constructor() {
this.subs = []
}
notify() {
this.subs.forEach(item => {
item.update()
})
}
}
<!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">
<p>{{username}}</p>
<p>{{age}}</p>
<p>{{obj.c.x}}</p>
<p>{{obj.a}}</p>
</div>
</body>
<script src="./vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data() {
return {
username: 'xiaoming',
age: 10,
obj:{
a:123,
b:456,
c:{
x:0
}
}
}
}
})
</script>
</html>
另一个版本(包含指令处理):
function defineReactive(obj, key, value) {
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.add(Dep.target)
return value
},
set(val) {
observe(val)
value = val
dep.notify()
}
})
}
//动态添加响应式属性
function set(obj, key, val) {
defineReactive(obj, key, val)
}
function observe(obj) {
if (typeof obj != 'object' || obj == null) {
return obj
}
//没出现一个对象,就创建一个Ob实例
new Observer(obj)
}
//判断传入obj类型,做对应的响应式处理
class Observer {
constructor(obj) {
this.value = obj
if (Array.isArray()) {
} else {
this.walk(obj)
}
}
//对象响应式
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key]
},
set(val) {
vm.$data[key] = val
}
})
})
}
class KVue {
constructor(options) {
//保存选项
this.$options = options
this.$data = options.data
this.data = this.$data
//响应式处理
observe(this.$data)
//代理data到kVue实例
proxy(this)
//编译
new Compile(options.el, this)
}
}
class Compile {
//el宿主 vm kvue实例
constructor(el, vm) {
this.$vm = vm
this.$el = document.querySelector(el)
this.compile(this.$el)
}
compile(el) {
//遍历 dom树
el.childNodes.forEach(node => {
if (this.isElement(node)) {
this.compileElement(node)
//需要处理属性和子节点,递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
} else if (this.isInter(node)) {
this.compileText(node)
}
})
}
isElement(node) {
return node.nodeType === 1
}
//满足{{}}的标签
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
isDir(attr) {
console.log(attr);
return attr.startsWith("k-")
}
update(node, exp, dir) {
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val)
})
}
//渲染对应的值到页面
compileText(node) {
this.update(node, RegExp.$1, 'text')
}
textUpdater(node, val) {
node.textContent = val
}
//处理元素所有动态属性
compileElement(node) {
Array.from(node.attributes).forEach(attr => {
//有name和value
const attrName = attr.name
const exp = attr.value
//判断是否是一个指令
if (this.isDir(attrName)) {
//执行指令的处理函数
//k-text,关心text
const dir = attrName.substring(2)
this[dir] && this[dir](node, exp)
}
})
}
// k-text的处理函数
text(node, exp) {
this.update(node, exp, 'text')
}
html(node, exp) {
this.update(node, exp, 'html')
}
htmlUpdater(node, val) {
node.innerHTML = val
}
}
//做dom更新
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm
this.key = key
this.updateFn = updateFn
//读取一下key的值触发get,从而收集依赖
Dep.target = this
this.vm[this.key]
Dep.target = null
}
update() {
this.updateFn.call(this.vm, this.vm[this.key])
}
}
//依赖:和响应式对象的key一一对应
class Dep {
constructor() {
this.deps = []
}
add(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => dep.update())
}
}