接着上一版继续。
如果你不知道上一版在哪?电梯直达
mini-vue 仓库源码 点击关注不迷路,哈哈。
v0.0.2 模块化与响应式依赖收集
要求:
- 模块化,即功能点分离为不同的文件
- 每个模块的功能尽量通用,说的再明白点,减少全局变量,能传参就传参,纯函数最好。
- 搞懂dep 与 watcher 之间的多对多关系映射
每个版本的要求我都会写在最前面,这里推荐大家先不要着急往下看,在上一个版本的基础上,试着自己写写看。
多啰嗦一句哈,上一个版本没搞懂之前,不要急着看下一版本,先按照简单的功能做,不要想太复杂的场景,每个版本的小功能实现了,就可以了,能自己手敲出来,才算真正掌握。
模块化改造
- 在example文件夹中,新增app.js文件,这个版本我们主要是探究响应式与依赖收集,因此其他的功能先简化,模板编译先用render函数替代,虚拟DOM先用真实DOM凑合着。
export default {
data: function () {
return {
count: 0,
test: 1001
}
},
methods: {
plus () {
this.count++
},
testNoReactive () {
// 测试修改test属性,不触发重新渲染
console.log('testNoReactive', this.test++)
}
},
render () {
/**
* this为当前实例对象,通过vm.$createElement来生成dom对象
* vm._v() 通过这个函数来收集依赖,因为vm.count将触发count属性的getter函数
* 重点注意,此处 h('h2', [vm._v(vm.count)]) 按照js的执行顺序,
* 1. 先执行vm.count, 触发get响应
* 2. 然后执行vm._v()
* 3. 最后执行h(),
* 明白了这个执行顺序,能同时在多个函数内使用的变量,就是全局变量,因此在执行render之前,
* 要先声明一个全局变量,来将这过程中产生的操作记录下来,方便后续数据响应时调用
*/
const vm = this
const h = vm.$createElement
return h('div', [
h('h2', [vm._v(vm.count)]),
h('button', { on: { click: vm.plus } }, [vm._v('+1')]),
h('button', { on: { click: vm.testNoReactive } }, [vm._v('testNoReactive')])
])
}
}
-
转到src文件夹下,重点先将vue.js中的代码分离出来。 看过源码的同学,这里可能会说先init初始化methods, props 吧啦吧啦一大堆操作,stop!停下,先忘了vue的源码,只写我们目前需要的功能,我们这个版本只做响应式与依赖收集。
-
abserver.js 模块 首先是new Vue(app), app参数传进来了,使用abserver将app.data处理成响应式的,abserver算是一个独立的功能,先把它抽离出去,我们新建一个abserver.js文件,再将abserver函数中的代码移到abserver.js文件中
-
watcher.js 模块 监听器,抽离成一个对象,方便存储依赖,以及更新时调用update
-
dep.js 模块 依赖锚点,为啥叫锚点,这个很重要。dep对象就只是打个标记,该对象并没有存储实质数据,主要就是为了在响应的数据属性处收集依赖,set时能准确触发到对应的watcher
这里重点理解下 Dep.target 作为全局对象的一个妙用
- render 模块 简易版的渲染函数,直接操作了真实DOM进行页面更新
-
具体逻辑还得看代码,一两句说不清楚。
vue.js
import { abserver } from './abserver.js'
import { createElement } from './render.js'
import { noop } from './shared.js'
import Watcher from './watcher.js'
function Vue (app) {
const data = app.data()
// 将data改造成响应式的
abserver(data)
// 将methods也合并到vm中,方便获取
const vm = Object.assign(data, app.methods)
// 定义渲染函数,app的render函数中需要用到
vm.$createElement = function (...args) {
return createElement.call(vm, vm, ...args)
}
vm._v = val => {
return val
}
function updateComponent () {
const container = vm.container
// 调用render函数,获得真实dom节点
const root = app.render ? app.render.call(vm) : ''
container.innerHTML = ''
container.append(root)
}
const $mount = selector => {
const page = document.querySelector(selector) || document.body
vm.container = page
// 声明一个全局监听对象,记录app渲染时的操作,当vm中,依赖属性发生变化时,触发updateComponent函数重新渲染
vm._watcher = new Watcher(vm, updateComponent, noop)
}
return {
$mount
}
}
// 为了更清晰的了解逻辑,会省略一些数据校验,我们都假设为正确的数据
Vue.$createElement = createElement
export default Vue
abserver.js
import Dep from "./dep.js"
export function abserver (data) {
for (let key in data) {
defineReactive(data, key, data[key])
}
}
function defineReactive (obj, key, val) {
// 创建一个依赖对象
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
// 将count添加到依赖中
dep.depend()
return val
},
set: function (newVal) {
val = newVal
// 在v0.0.1版本中,此处是写死的 button.innerText = newVal, 为了通用,我们这里用一个函数代替
// 如下notify() 将实现button.innerText = newVal操作,但是button没法获取到,因此需要在get时进行收集
dep.notify()
}
})
}
dep.js
let uid = 0
class Dep {
constructor() {
this.id = ++uid
this.subs = []
}
addSub (watcher) {
this.subs.push(watcher)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// 因为可能存在多个依赖,此处使用数组形式遍历触发
// 依赖是有先后顺序的,因此需要提前排下序
this.subs.sort((a, b) => a.id - b.id)
this.subs.forEach(sub => sub.update())
}
}
Dep.target = null
const staticTargets = []
export function pushTarget (watcher) {
staticTargets.push(watcher)
Dep.target = staticTargets[staticTargets.length - 1]
}
export function popTarget () {
staticTargets.pop()
Dep.target = staticTargets[staticTargets.length - 1]
}
export default Dep
watcher.js
import { pushTarget, popTarget } from './dep.js'
let uid = 0
class Watcher {
constructor(vm, expOrFn, cb, val) {
this.id = ++uid
this.vm = vm
this.expOrFn = expOrFn
this.cb = cb
this.deps = []
this.depIds = new Set()
this.value = this.get() || val
}
addDep (dep) {
const { id } = dep
if (!this.depIds.has(id)) {
dep.addSub(this)
this.deps.push(dep)
this.depIds.add(id)
}
}
get () {
// 将当前监听器设置成target
pushTarget(this)
this.expOrFn.call(this.vm)
popTarget()
}
update () {
let oldVal = this.value
// 触发组件更新渲染
this.value = this.get()
// 执行用户自定义的watch
this.cb.call(this.vm, this.value, oldVal)
}
}
export default Watcher
render.js
export function createElement (vm, tag, prop, children) {
const el = document.createElement(tag)
vm.el = el
if (Array.isArray(prop)) {
children = prop
} else {
if (prop.on) {
// 如果存在on属性,则进行事件绑定操作
for (let ev in prop.on) {
el.addEventListener(ev, prop.on[ev].bind(vm))
}
}
}
if (children && children.length > 0) {
children.forEach(child => el.append(child))
}
return el
}
shared.js // 工具类
export function noop (a, b, c) { }
如果上面的代码逻辑都明白了,vue的响应式依赖收集算是入门了,我们这里摒弃了复杂情况以及数据校验、边界处理等等,主要目的是凸显出 watcher与dep之间的关系,以及它是怎么通过 dep.depend()、dep.notify()两个方法,就实现了依赖收集与响应。真正读懂后,不由感叹,尤大大,妙啊!
v0.0.2版本搞懂后,就可以继续看下一个版本了。