目标
初步掌握Vue2框架的底层原理
要点
不想啰嗦,直接盘它
先来个HTML文件
<div id="app">
<p>{{counter}}</p>
<p v-text="counter"></p>
</div>
<script src="./MyVue.js"></script>
<script>
const app = new XuSirVue({
el: '#app',
data: {
counter: 1
},
})
setInterval(() => {
app.counter++
}, 1000)
</script>
重点来了,我里乖乖,先看下MVVM经典原理图😎😎😎
先来一个响应式,由浅入深,继续向下看⬇
function defineReactive(obj, key, val) {
// 递归处理,给对象深层次拦截
observe(val)
// 创建Dep实例
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// 判断一下Dep.target是否存在,若存在进行依赖收集
// 这里的Dep.target其实就是watcher
Dep.target && dep.addDep(Dep.target);
return val
},
set(v) {
if (v !== val) {
val = v
}
// 检测到值变化通知对应的dep管理的watchers进行更新
// this.deps.forEach(dep => dep.update());
// 也就是watcher执行更新函数 this.updateFn.call(this.vm, this.vm[this.key]);对应下面⬇⬇⬇
dep.notify()
}
})
}
// 对象响应式,执行defineReactive
function observe(obj) {
// 首先判断obj是否是对象,忽略我的有问题的写法😂
if (typeof obj !== 'object' || obj === null) {
return
}
new Observer(obj) // 观察者⬇⬇⬇
}
搞一个观察者去监听数据的变化并使其变成响应式⬇
// 观察者:监听传过来的数据 ---> 变成响应式数据
class Observer {
constructor(obj) {
this.value = obj
if (Array.isArray(obj)) {
// todo 数组的重写7个方法暂不处理 push/shift/pop/unshift...
} else {
this.work(obj)
}
}
// 给对象加上拦截实现响应式
work(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
插播一个代理--把data的key代理到vm上,我们可以直接写this.xxx拿到data中变量而不必写this.data.xxx⬇
// 异常处理忽略--比如vm已经有了某个$data[key],我们不能覆盖原属性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;
}
});
})
}
Vue主入口来了⬇
class XuSirVue {
constructor(options) {
// 1.响应式
this.$options = options
// 把数据绑定到$data
this.$data = options.data
observe(this.$data) // 开始观察所有的data属性
// 代理- 把vm传给proxy方法做处理
proxy(this)
// 2.compile编译
// 就是把{{}}这些变量、 v-xx 指令编译成对应的值和绑定上对应的方法
new Compile(options.el, this)
}
}
搞一个编译器--有点长但是注释很清晰,哈哈😂⬇
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
// 遍历el
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
// childNodes是伪数组
Array.from(childNodes).forEach(node => {
if (this.isElement(node)) {
console.log("编译元素" + node.nodeName);
this.compileElement(node)
} else if (this.isInterpolation(node)) {
console.log("编译插值⽂本v-text" + node.textContent);
this.compileText(node);
}
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
});
}
// 元素
isElement(node) {
return node.nodeType == 1;
}
// 是否是{{}}插值表达式
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
// 使用RegExp.$1拿到{{xxx}}中的插值属性 xxx
compileText(node) {
console.log(RegExp.$1);
// node.textContent = this.$vm[RegExp.$1];
this.update(node, RegExp.$1, 'text')
}
// 获取指令表达式 v-xxx
isDirective(attr) {
return attr.indexOf("v-") == 0;
}
// 编译元素
compileElement(node) {
let nodeAttrs = node.attributes;
// 编译元素上属性 v-if v-xxx
Array.from(nodeAttrs).forEach(attr => {
let attrName = attr.name;
let exp = attr.value;
if (this.isDirective(attrName)) {
let dir = attrName.substring(2);
this[dir] && this[dir](node, exp);
}
});
}
// v-text
text(node, exp) {
this.update(node, exp, 'text')
}
// v-html
html(node, exp) {
this.update(node, exp, 'html')
}
// 统一处理 指令v-
// dir: text exp: {{xxx}}中的xxx node: 使用指令的元素节点
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)
})
}
// 处理v-text的节点赋值
textUpdater(node, val) {
node.textContent = val;
}
// 处理v-html的节点赋值
htmlUpdater(node, val) {
node.innerHTML = val
}
}
看累了?来瓶红牛继续盘它⬇
dep 用于依赖收集 主要监听data中属性, 一个属性对应一个dep负责监听变化并通过watcher观察者进行更新⬇
class Dep {
constructor() {
// 依赖集合
this.deps = []
}
// 收集所有的依赖,一个data对应一个dep管家,上面的拦截方法中的getter触发的时候进行收集
addDep(dep) {
this.deps.push(dep)
}
// data属性的值改变会触发setter,通知视图进行更新
notify() {
this.deps.forEach(dep => dep.update());
}
}
创建watcher时触发getter,初始化时把对应的data[key]渲染到视图,后续管家dep拦截到值改变批量进行更新,为什么是批量,因为一个data[key]可能不止一个地方使用,比如页面中出现了插值表达式{{initStatus}} 也出现了指令 v-text="initStatus",那initStatus 就对应一个dep管家和两个watcher,initStatus改变触发dep的notify方法是watcher执行update最终使页面视图更新💘⬇
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
// 依赖key
this.key = key;
// 更新函数
this.updateFn = updateFn;
// 获取一下key的值触发get,并创建当前watcher实例和dep的映射关系
Dep.target = this;
// 这一步的目的是初始化取值触发getter
this.vm[this.key];
Dep.target = null;
}
// 更新
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
来看下执行效果😇🤓🤡
补充
如果觉得上述代码太乱,可以到我的github看完整版✔