手写简版Vue,实现双向绑定和模板编译
Vue的设计思想
-
MVVM模式

MVVM框架的三要素:
数据响应式:监听数据变化并在视图中更新
-
Object.defineProperty()
-
Proxy
模板引擎:提供描述视图的模板语法
-
差值:{{}}
-
指令:v-bind、v-on、v-model、v-for、v-if
渲染:如何将模板转换为html
-
模板 => vdom => dom
数据响应式原理
数据变更能够响应在视图中,就是数据响应式。vue2中利用Object.defineProperty()实现变更监测。
实现原理
const obj = {}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
console.log(`set ${key}:${newVal}`);
val = newVal
} }
}) }
defineReactive(obj, 'foo', 'foo')
obj.foo
obj.foo = 'foooooooooooo'
遍历需要响应化的对象
// 对象响应化:遍历每个key,定义getter、setter function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
const obj = {foo:'foo',bar:'bar',baz:{a:1}}
observe(obj)
obj.foo
obj.foo = 'foooooooooooo' obj.bar
obj.bar = 'barrrrrrrrrrr' obj.baz.a = 10 // 嵌套对象no ok
解决嵌套对象问题
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
//...
解决赋的值是对象的情况,在赋值是判断:如果是对象,进行响应式处理
obj.baz = {a:1}
obj.baz.a = 10 // no ok
set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是对象的情况 notifyUpdate()
如果添加/删除了新属性无法检测
obj.dong = 'dong' obj.dong // 并没有get信息
解决方法:增加set Api
function set(obj, key, val) {
defineReactive(obj, key, val)
}
-
同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
-
同时定义一个更新函数和watcher,将来对应数据变化时Watcher会调用更新函数
-
由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
-
涉及类型介绍
-
Vue:框架构造函数
-
Observer:执行数据响应式(分辨数据对象还是数组)
-
Compile:编译模板,初始化视图,手机依赖(更新函数、watcher创建)
-
Watcher:执行更新函数(更新dom)
-
Dep:管理多个Watcher,批量更新
完整代码
// 利用Object.defineProperty(obj,key,val)
// Vue.util.defineReactive() 设置响应式属性
// 设置响应式属性
function defineReactive(obj, key, val) {
// 如果val是对象,递归遍历
observer(val)
// 每个key对应一个Dep实例
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// 关系映射:dep和watcher
Dep.target && dep.addDep(Dep.target)
return val
},
set(newval) {
if (newval !== val) {
// 新值如果是对象,则要对它进行响应式处理
observer(newval)
// 进行赋值
val = newval
// 通知更新
dep.notify()
}
}
})
}
// 遍历指定数据对象每个key,拦截他们
function observer(obj) {
// obj必须是对象
if(typeof obj !== 'object' || obj === null) {
return obj
}
// 每遇到一个对象,就创建一个Observe实例
// 创建一个Observe实例去拦截
new Observer(obj)
}
// 根据传入不同类型值,做不同操作
class Observer {
constructor(value) {
this.value = value
if(Array.isArray(value)) { // Array
// to do
} else { // obj
this.walk(value)
}
}
// 对象的响应式处理
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
}
}
// proxy代理函数:让用户可以直接访问data中的key
function proxy(vm) {
Object.keys(vm.$data).forEach((key) => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key]
},
set(newVal) {
vm.$data[key] = newVal
}
})
})
}
// 遍历dom树,找到动态的表达式或者指令等
class Compile {
constructor(el, vm) {
this.$vm = vm
this.$el = document.querySelector(el)
// 解析模板
if(this.$el) {
// 编译
this.compile(this.$el)
}
}
// 递归传入节点,根据节点类型不同做不同操作
compile(el) {
// 获取所有孩子节点(包括文本节点,元素节点,注释节点)
const childNodes = el.childNodes
childNodes.forEach((node) => {
if(node.nodeType === 1) {
// 编译元素节点
this.compilElement(node)
// 递归
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
} else if (this.isInter(node)) { // 文本节点并且为差值表达式 形如:{{xxx}}
// 编译文本节点
this.compileText(node)
}
})
}
// 判断文本节点并且为差值表达式 形如:{{xxx}}
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
// 编译文本节点
compileText(node) {
this.update(node, RegExp.$1, 'text')
}
// 编译元素节点
compilElement(node) {
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach((attr) => {
// v-text = 'xxx'
const attrName = attr.name // v-text
const exp = attr.value // 'xxx'
if (attrName.indexOf('v-') === 0) { // 判断以k-开头的指令
const dir = attrName.substring(2)
this[dir] && this[dir](node, exp)
}
})
}
// 遇到绑定表达式或者指令
// 1.首先初始化
// 2.创建watcher实例,管理node它的更新
update(node, exp, dir) {
// 初始化编译
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
// 创建watcher实例,添加更新函数
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val)
})
}
// v-text
text(node, exp) {
this.update(node, exp, 'text')
}
textUpdater(node, value) {
node.textContent = value
}
// v-html
html(node, exp) {
this.update(node, exp, 'html')
}
htmlUpdater(node, value) {
node.innerHTML = value
}
}
// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下,触发get
this.vm[this.key]
// 置空
Dep.target = null
}
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
// 依赖:defineReactive中每一个key,对应一个Dep实例
class Dep {
constructor() {
this.deps = []
}
// 添加依赖
addDep(watcher) {
this.deps.push(watcher)
}
// 通知更新
notify() {
this.deps.forEach(watcher => watcher.update())
}
}
class Vue {
constructor(options) {
this.$options = options
this.$data = options.data
// 对data进行响应式处理
new Observe(this.$data)
// 代理data到vm上 -- 用户使用不需要加$data
proxy(this)
// 执行编译
new Compile(this.$options.el, this)
}
}
总结:
1.在new Vue()过程中执行了3件事。
2.第一件事进行data响应式处理,在Observer中进行判断对象还是数组,分别执行不同的方式,并且递归处理。对象采用的是Object.defineProperty()方式,对象中每个key对应一个Dep。数组是改写7中原型方法,进行拦截。
3.第二件事进行模板编译,获取根节点,递归遍历字元素。判断是否为文本节点、元素节点、注释节点。进行不同的编译,在编译过程中发现动态的指令、差值,会new 一个 Watcher(),在new Watcher()的过程中会保存更新方法,并将该watcher实例添加到该data中的dep中,将来数据变化时,在set中执行dep中所有watcher实例的更新方法。
4.第三件是则将data中的key代理到vm实例上,这么每次访问直接使用this,而不需要this.$data。


浙公网安备 33010602011771号