最近有了点空,就想着 把 vue 给搞定了,
看了一遍之后,决定自己写一个乞丐版的 vue
index.html
{{age}}
{{name}}
{{name}}
{{name}}
{{inp}}
index.js
const props = {
el: document.querySelector('#app'),
data: {
name: 'jack',
age: 123,
inp: ''
},
methods: {
printName() {
this.name = this.name + 1
}
}
}
new PoorVue(props)
PoorVue.js
由于当时是纯手打,没有看其他的东西,所以函数的命名上、部分代码可能会有出入
class PoorVue {
constructor(props) {
// 对当前的 数据进行保存
this.props = props;
this.$data = props.data;
// 在这里对数据进行一个 双向绑定的前期工作,也就是代理工作
// 大名鼎鼎的 defineProperty 就是在 这里进行的
this.defineData(this.$data);
// 对 html 进行解析,这里只会 提取 {{}} 和 @ 事件
new Analyze(this.props.el, this)
}
defineData(data) {
// 判断当前的 数据是不是一个对象
if (Object.prototype.toString.call(data) === "[object Object]") {
Object.keys(data).map(key => {
// 这里是对 data 中的数据进行一个代理
// 这样的话,就可以直接使用 this[props] 而不是 this.$data[props]
this.proxydata(key)
// 这里真正的 对数据进行代理,并收集 watcher 了
this.defineProperty(data, key, data[key]);
})
}
}
// 这里是对 data 中的数据进行一个代理
// 这样的话,就可以直接使用 this[props] 而不是 this.$data[props]
proxydata(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(val) {
this.$data[key] = val
}
})
}
defineProperty(obj, key, value) {
// 对数据进行一个遍历
// 可以对 对象内部的 数据进行深层次的绑定
this.defineData(value)
// 建立一个 Dep ,也就是依赖收集工具
// 把所有 记录着 key 这个参数 要做的操作 都放进这个数据之中
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// 判断 Dep.target 是否存在,如果存在,就 进行依赖收集
// 这样也是为了防止多次收集
Dep.target && dep.addDep()
return value
},
set(val) {
// 当 数据一致的时候,不进行任何操作
if (value === val) return
value = val;
// 进行响应,页面开始进行变化
// 这里的 dep 实际上已经在 闭包里面了
// 所以也就是说,一个 key 对应 于一个 dep
dep.notify()
}
})
}
}
class Dep {
constructor() {
// 建立一个收集 依赖的数组
this.deps = []
}
addDep() {
// 收集 对应的 Watcher
this.deps.push(Dep.target);
}
notify() {
// 执行所有 的 Watcher
// 注意,这里的 Watcher 都是对应于 同一个 key 之下的
this.deps.map(dep => dep.update())
}
}
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm;
this.cb = cb;
// 对应了 上面数据之中 的 Dep.target && dep.addDep()
Dep.target = this;
// 执行 一下 这个函数,
// 也就是说 在这个过程之中
// 先是执行了 之前就定义好的 defineProperty get 函数
// 这样就 能够 将 当前的 Watcher 给收集到 Dep 当中
// 同时 也可以对页面进行第一步 的渲染
this.cb.call(this.vm);
// 将这个 置为 null, 也就是说 当前依赖已经收集完毕
// 为 接下来的 Watcher 腾位置的同时
Dep.target = null;
}
update() {
// 执行当前 的 收集的依赖
this.cb && this.cb.call(this.vm);
}
}
analyze.js
class Analyze {
constructor(el, vm) {
this.$el = el;
this.vm = vm;
// 开始解析 当前的 html 代码
this.resolve(this.$el)
}
createFrgment(el) {
const frg = document.createDocumentFragment();
const childNodes = el.childNodes;
Array.from(childNodes).map(child => {
frg.appendChild(child)
})
return frg
}
resolve(el) {
// 创建 Fragment,并将 当前页面上的 所有节点放到 这里来
// 实际上也是为了防止 在解析的过程中,会进行 过多的 dom 操作
// 避免 资源的浪费
const fragment = this.createFrgment(el);
// 开始解析
this.ergodic(fragment)
this.$el.appendChild(fragment)
}
ergodic(frag) {
const nodeList = frag.childNodes;
Array.from(nodeList).map(node => {
// 是节点类型的话,开始解析 当前节点的 属性
if (node.nodeType === 1) {
const attr = node.attributes
this.dealAttribute(node, attr)
}
// 是文本节点的话,看看是不是 {{}} 插值 插进去的
if (node.nodeType === 3) {
if (this.interP(node.textContent)) {
// 进行当前的依赖管理
this.update(node, RegExp.$1, 'text')
}
}
// 如果当前是节点,并且还有子节点的话,继续 解析
if (node.childNodes && node.childNodes.length > 0) {
this.ergodic(node)
}
})
}
dealAttribute(node) {
Array.from(node.attributes).map(attr => {
// 这里写的很简单,就是 以 @ 开头的属性的话,就在当前节点进行绑定
// 注意,vue 是绑定在 document 上,进行了 事件委托的
if (attr.name.startsWith('@')) {
const method = attr.value
node.addEventListener(attr.name.substr(1), this.vm.props.methods[method].bind(this.vm))
}
// 这里关于 v-model 就不做太多的校验了,明白意思就好
if (attr.name === 'v-model') {
const value = attr.value
console.log(value)
this.update(node, value, 'model')
}
})
}
update(node, exp, type) {
// 找到 当前处理的 函数 handler
const handler = this[`${type}Handler`];
const vm = this.vm
// 创建一个 Watcher ,把 渲染的函数发过去
new Watcher(this.vm, exp, function() {
handler && handler(node, exp, vm)
})
}
// 解析 {{}}
// 注意,这样解析之后 RegExp.$1 就是匹配的 结果
interP(text) {
return /\{\{(.*)\}\}/.test(text)
}
// 这个就很简单了,在 update 之中被调用 不赘述
textHandler(node, exp, vm) {
node.textContent = vm.$data[exp]
}
// v-model 的渲染函数
modelHandler(node, exp, vm) {
node.value = vm[exp];
node.addEventListener('input', (e) => {
vm[exp] = e.target.value
})
}
}
所以说,在自己实现了一遍之后,倒是对于 Dep Watcher 的理解更深刻了
每一个 data 里面的数据,包括深层次对象里面的 数据,每一个 字段都会有一个 Dep,每一个 双向绑定的地方,就会有一个 Watcher
然后每一个 页面,或者 computed ,或者 watch 每一个 都会有 一个 Watcher,
例如上面,
页面上有 两个 {{name}}, 那么每一个 name ,就会有一个 渲染Watcher,然后这些 name 的 Watcher都会被放在 同一个 Dep 之中
------------
那么问题来了,既然 每个 {{}} 都有一个 watch ,那么还要 虚拟 dom 呢?
不过 这上面其实是 vue1.0 的写法,在 vue2.0 之中,使用了虚拟dom
所以说,复杂页面一定要 拆分组件!!!!减少 dom diff 的过程!!!!
=========
2020.3.20更新
这次在 这个 乞丐版的 vue 上增加 一个 computed 属性
先看 对应的 文件配置,增加了 一个 fullname
{{fullname}}
const props = {
el: document.querySelector('#app'),
data: {
name: 'jack',
age: 123,
inp: '',
++ firstname: '',
++ lastname: ''
},
++ computed: {
++ fullname() {
++ return this.firstname + this.lastname
++ }
},
methods: {
printName() {
this.name = this.name + 1
},
reduce() {
this.name = this.name.slice(0, -1)
}
}
}
new PoorVue(props)
由于 只是 简单地实现,以及 了解其原理,就不搞那么复杂了
PoorVue.js
class PoorVue {
constructor(props) {
// 对当前的 数据进行保存
this.props = props;
this.$data = props.data;
// 在这里对数据进行一个 双向绑定的前期工作,也就是代理工作
// 大名鼎鼎的 defineProperty 就是在 这里进行的
this.defineData(this.$data);
// 对 computed 属性进行解析
this.defineComputed(this.props.computed);
// 对 html 进行解析,这里只会 提取 {{}} 和 @ 事件
new Analyze(this.props.el, this)
}
....
....
// 可以看到,我其实什么都没有改,
// 就是在这里增加了这么一段代码,就让 整个函数运行起来了
defineComputed(computed) {
const _this = this;
Object.keys(computed).map(comput => {
const dep = new Dep()
Object.defineProperty(this, comput, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addDep()
return computed[comput].call(_this)
},
})
})
}
....
....
}
所以说,在这里我们可以看到 这个 computed 属性 fullname 的 watcher 不仅仅是进入了一个 Dep 中,还被放进了 fullname 和 lastname 的 Dep 之中
然后改造一下 watcher
class Watcher {
....
update() {
this.run()
}
// 给这里加个节流
run = throttle(function () {
console.log('执行次数')
// 执行当前 的 收集的依赖
this.cb && this.cb.call(this.vm);
}, 0, this)
}
function throttle(fn, delay, ctx) {
let isAvail = true
return function () {
let args = arguments
// 开关打开时,执行任务
if (isAvail) {
isAvail = false
// delay时间之后,任务开关打开
Promise.resolve().then(function () {
fn.apply(ctx, args)
isAvail = true
}, delay)
}
}
}
这样就能做到 超简单的 异步更新了
使用场景 可以看下面,那个 run 函数里面的 console 只打印了 一次
{{name}}
methods: {
reduce() {
this.name = this.name + 'h'
this.name = this.name + 'h'
this.name = this.name + 'h'
this.name = this.name + 'h'
}
}