人生当自勉,学习需坚持
代码地址
https://gitee.com/xiaozhidayu/vue-study-zvue
https://gitee.com/xiaozhidayu/vue-study-zvue.git
前面看了一篇关于Vue响应式的原理分析文章收益匪浅,进行了转载,今天看了一节课成,敲了自己的Vue,整体思想等都收获很多,只是小demo 中没有涉及到虚拟DOM,因此类似是1.0 Vue。
Vue 的设计思想
MVVM 框架的三要素 :数据响应式,模板引擎及其数据渲染
数据响应式原理
所谓数据响应式 ,就是数据变更能够响应在视图中,vue2.0 中使用 Object.defineProperty()实现变更监测,实例代码不再赘述,完整代码中有各种实验。
Vue数据响应化实现
1. new Vue() 首先执行初始化,对data执行响应化处理,这个过程发生在Observer中
2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在
Compile中
3. 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
4. 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个
Watcher
5. 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
超啰嗦的图解:
MVVM 代表我们要创建的Vue 即 ZVue
当进行创建的时候 要执行两个操作:
- 数据响应式 Observer 数据劫持
- 编译 compile 主要作用是解析模板当中非HTML 的部分 ,比如差值表达式 以及 一些特殊的指令 v-if v-model v-on @click 等指令,编译会做两件事情① 初始化视图 ,就是从劫持的数据中心get 到数据,设置为初始值。② 设置数据变化的时候可以更新视图 怎么做呢?引入了几个新的角色,观察者 Watcher ,她的作用是在界面中如果出现一个绑定就创建一个观察者Watcher ,每个watcher 会保存一个更新函数,更新函数做对应的dom 元素的更新操作。以上Observer 和 Watcher 之间还没有出现闭环 ,闭环的关键是 角色Dep(依赖)的主要作用是管理多个watcher,因为很多数据在页面中不止出现一次,因此对应的会有多个watcher,那么watcher 和 拦截器Observer 之间就产生了一个一对多的关系,此时就需要一个管家把多个watcher 管理起来,就是Dep,但是在拦截器Observer 中每一个key 和 Dep 之间是一定有一个一对一的关系。这样整体形成了一个闭环关系。
代码中的类型介绍:
1、ZVue:框架构造函数
2、Observer:执行数据响应化(需要分析是对象还是数组)
3、Compile:编译模板,初始化视图,收集依赖(更新函数、watcher)
4、Watcher:执行更新函数(更新都没)
5、Dep:管理多个Watcher ,批量更新
附上代码
zvue.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>zvue</title>
</head>
<body>
<div id="app">
<!-- 验证差值 -->
<p>{{counter}}</p>
<!-- 验证指令 -->
<p z-text="counter"></p>
<p z-html="desc"></p>
</div>
<script src="zvue.js"></script>
<script src="compile.js"></script>
<script>
const app = new ZVue({
el: '#app',
data: {
counter: 1,
desc: '<span style="color:red">zvue 可还行? </span>'
},
})
setInterval(() => {
// app.$data.counter++
app.counter++
// app.$data 这种访问数据的方式在实际中不应用,我们在vue中都是直接访问的,因此要做数据代理 即直接访问实例的某个属性,就像访问$data 中的数据一样
}, 1000)
</script>
</body>
</html>
compile.js
// 编译器
// 递归遍历dom树
//判断节点类型 如果是文本则判断是否是差值绑定
//如果是元素,则遍历其属性判断是否是指令或事件,然后递归子元素
class Compiler {
// el是宿主元素
// vm是 ZVue 实例
constructor(el, vm) {
this.$vm = vm
this.$el = document.querySelector(el)
if (this.$el) {
// 执行编译
this.compile(this.$el)
}
}
compile(el) {
//遍历el 树
const childNodes = el.childNodes
//childNodes 本身不是一个数组,所以用Array转换一下
Array.from(childNodes).forEach(node => {
//判断是否是元素
if (this.isElement(node)) {
console.log('编译元素' + node.nodeName)
this.compileElement(node)
} else if (this.isInter(node)) {
console.log("编译差值的绑定" + node.textContent)
this.compileText(node)
}
//递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
isElement(node) {
return node.nodeType === 1
}
isInter(node) {
//首先是文本标签,其次内容是{{xxx}}
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
compileText(node) {
// console.log(RegExp.$1)
// node.textContent = this.$vm[RegExp.$1]
this.update(node, RegExp.$1, 'text')
}
compileElement(node) {
// 节点是元素
// 遍历其属性列表
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr => {
//规定:指令以z-xx = 'oo'定义
const attrName = attr.name // z-xx
const exp = attr.value // oo
if (this.isDirective(attrName)) {
const dir = attrName.substring(2) //xx
// 执行指令
this[dir] && this[dir](node, exp)
}
})
}
isDirective(attr) {
return attr.indexOf('z-') === 0
}
textUpdater(node, value) {
node.textContent = value
}
update(node, exp, dir) {
// 初始化
// 指令对应的更新函数 xxUpdater
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
// 更新处理 封装一个更新函数可以更新对应的dom 元素
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val)
})
}
//z-text
text(node, exp) {
// node.textContent = this.$vm[exp]
this.update(node, exp, 'text')
}
// z-html
html(node, exp) {
// node.innerHTML = this.$vm[exp]
this.update(node, exp, 'html')
}
htmlUpdater(node, value) {
node.innerHTML = value
}
}
zvue.js
function defineReactive(obj, key, val) {
// 递归 如果val 还是对象的话 进行遍历响应式
observe(val)
// 创建一个Dep 和 当前的可以 一一对应
const dep = new Dep()
//对传入obj 进行访问拦截
// 在每次执行defineReactive 时候,其实形成了一个闭包,因为在内部保留了一个内部作用域的变量 就是value
Object.defineProperty(obj, key, {
get() {
console.log('get', key)
// 核心的依赖手机过程 发生在get 中
Dep.target && dep.addDep(Dep.target)
return val;
},
set(newVal) {
if (newVal !== val) {
console.log('set::' + key + ":" + newVal)
// 如果newVal 是对象,应该做响应化的处理
observe(newVal)
val = newVal
// 执行更新函数遍历watchers
// watchers.forEach(w => {
// w.update()
// })
dep.notify()
}
}
})
}
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
// 希望传入的是obj
return
}
// 遍历 相当于所有的可以 都定义了响应式
// Object.keys(obj).forEach(key => {
// defineReactive(obj, key, obj[key])
// })
//此处不再是直接循环遍历,而是要引入一个新的角色Observer 一个主要作用是分析数据是对象还是数组。
// 创建Observer 的实例
new Observer(obj)
}
// 代理函数,方便用户直接访问$data中的数据
function proxy(vm, sourceKey) {
//框架实例 以及要代理的属性可以
Object.keys(vm[sourceKey]).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm[sourceKey][key]
},
set(newVal) {
vm[sourceKey][key] = newVal
}
})
})
}
// 创建zvue的构造函数
class ZVue {
constructor(options) {
// 保存选项
this.$options = options
this.$data = options.data
// 响应化处理
observe(this.$data)
// 代理
proxy(this, '$data')
// 创建编译器的实例
new Compiler(options.el, this)
}
}
// 根据对象类型决定如何做响应化
class Observer {
constructor(value) {
this.value = value
// 判断其类型
if (typeof value === 'object') {
this.walk(value)
}
}
//对象数据的响应化
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
// 数组数据的响应化 待补充
}
// 创建观察者:保存更新函数,值发生变化 调用更新函数
// 在没有Dep 的情况下 先自己把每一个watcher 保存起来
// const watchers = []
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm
this.key = key
this.updateFn = updateFn
// watchers.push(this)
// Dep.target 静态属性上设置为当前watcher 实例
Dep.target = this
//
this.vm[this.key] // 读取触发get 因此可以在get 中 将其target 追加进去
Dep.target = null // 收集完后置空
}
update() {
this.updateFn.call(this.vm, this.vm[this.key])
}
}
//Dep:依赖,管理某个key 相关所有Watcher实例
class Dep {
constructor() {
this.deps = []
}
addDep(dep) {
// dep 就是watcher 实例
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => {
dep.update()
})
}
}
代码中有数据代理的实现 核心就是 把data 中的每个数据拿出来 放在组件实例上 。
学习过程很烧脑,每天收获很开心!