本系列文章会以实现Vue的各个核心功能为标杆(初始化、相应式、编译、虚拟dom、更新、组件原理等等), 不会去纠结于非重点或者非本次学习目标的细节, 从头开始实现简化版Vue, 但是, 即使是简化, 也需要投入一定的时间和精力去学习, 而不可能毫不费力地学习到相对复杂的知识; 所有简化代码都会附上原版源码的路径, 简化版仅仅实现了基本功能, 如需了解更多细节, 可以去根据源码路径去阅读对应的原版源码;
响应式基本原理
# 手写简化版Vue(一) 初始化里我们简单介绍了Vue的初始化过程, 还有data访问的实现, 那么接下来, 我们就要实现一下响应式的功能, 也就是this.xx = xx的时候, 能够触发相应的更新逻辑;
既然是要在修改data数据的时候触发更新, 那就必然要监听data, 那么如何监听呢, 在什么地方开始执行这个监听呢? 之前我们介绍了initdata方法, 我们知道了data就是在这里被初始化的, 那么是否应该从此处入手呢? 来看看源码:
// 源码路径src/core/instance/state.ts
function initData (vm) {
let data = vm.$options.data
data = vm._data = isFunction(data) ? data.call(vm) : data
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, '_data', key)
}
// 相比于之前初始化, 这里增加了observe方法
observe(data)
}
// 源码路径src/core/observer/index.ts
export function observe (value) {
let ob = null
// 判断该对象是否已经被纳入监听
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
// 对传入的对象/数组进行遍历监听
// 注意, 此处目前只考虑了对象的情况, 数组的基本原理与之本质上是相似的
// 有兴趣可以自己去实现下
export class Observer {
constructor (value) {
def(value, '__ob__', this)
// 如果value是一个对象
if (isPlainObject(value)) {
let keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
defineReactive(value, keys[i])
}
}
}
}
// 工具方法
// 源码路径src/shared/util.ts
const hasOwnProperty = Object.prototype.hasOwnProperty
// 判断对象上是否有特定的自身的属性
export function hasOwn (value, key) {
return hasOwnProperty.call(value, key)
}
const _toString = Object.prototype.toString
// 判断是否是一个对象
export function isPlainObject (value) {
return _toString.call(value) === '[object Object]'
}
// 判断是否为一个函数
export function isFunction(value: any): value is (...args: any[]) => any {
return typeof value === 'function'
}
// 源码路径src/core/util/lang.ts
export function def (obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value: value,
configurable: true,
writable: true,
enumerable: !!enumerable
})
}
小节: 目前为止, 我们可以看到监听的大体逻辑是:
- 通过initData中执行的observe方法判断传入的data是否有被监听过, 有则返回__ob__属性, 没有, 就进行new Observer实例化;
- Observer中, 遍历对象中的属性, 针对每一个属性, 实现一个defineReactive, 即, 将其变为响应式;
我们继续往下看其响应式实现的具体逻辑, 下面介绍了三个内容: defineReactive, Dep, Watcher, 建议先熟悉下defineReactive和Dep, 然后重点关注Watcher逻辑
// 源码路径src/core/observer/index.ts
// 监听具体的属性, 在这里利用Object.defineProperty
// 定义了一个属性存/取的相关逻辑
export function defineReactive (obj, key, val) {
const dep = new Dep()
val = obj[key]
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// 注意此处的Dep.Target是一个全局的变量, 当其有值的时候, 说明
// 正在执行监听依赖操作, 这个后续会继续解析, 现在注意下就行了
if (Dep.Target) {
dep.depend() // 依赖收集
}
return val
},
set (newVal) { // 数据发生改变后, 需要通知依赖于该数据的watcher, 并执行对应的方法
val = newVal
dep.notify()
}
})
}
// 下面是具体实现
// 源码路径src/core/observer/dep.ts
let uid = 0
export default class Dep {
static Target = null
constructor () {
this.id = uid++
// 存储watcher
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
depend () {
if (Dep.Target) {
Dep.Target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0; i < subs.length; i++) {
subs[i].update()
}
}
}
Dep.Target = null
export function pushTarget (target) {
Dep.Target = target
}
// 源码路径src/core/observer/watcher.ts
export default class Watcher {
// vm为当前vue实例, expOrFn现在可以只将其看作是一个方法
constructor(vm, expOrFn) {
this.vm = vm
this.depIds = new Set()
if (isFunction(expOrFn)) {
this.getter = expOrFn
}
this.get()
}
get () {
const vm = this.vm
// 注意这个pushTarget, 这里就是前面defineReactive中get中
// Dep.Target的由来, 此时全局的Dep.Target 变为了这个watcher实例!
pushTarget(this)
let value = ''
try {
// 此处执行的getter方法中, 如果有对data进行取值的逻辑(this.xx), 那么就会触发
// Object.defineProperty中的get方法最终执行到dep.depend(), 从而进行依赖的收集
value = this.getter.call(vm, vm)
} catch (e) {
console.log(e.message)
}
return value
}
// 通过addDep方法可以将当前Watcher实例加入到对应的dep
addDep (dep) {
const id = dep.id
if (!this.depIds.has(id)) {
this.depIds.add(id)
dep.addSub(this)
}
}
update () {
this.run()
}
run () {
this.get()
}
}
代码小节:
- 依赖收集:
通过defineReactive, Dep, Watcher三者的操作, 我们可以梳理下
通过一系列处理, 可以将watcher存入对应属性的dep的subs属性当中, 如果未来某个属性发生改变, Vue就会依次执行subs中的watcher, 而watcher中监听的方法就会被执行, 下面来看看事件触发后如何进行更新的;
- 依赖更新
已经收集到了依赖之后, 更新就很简单了
- Object.defineProperty的setter方法执行
- 触发dep.notify方法, 从而遍历dep.subs数组
- 执行subs中的watcher.run(), 从而将依赖的方法都更新了
验证
好了, 现在来验证下我们响应式的效果
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>响应式测试页面</title>
</head>
<body>
<input type="text" id="input">
<script type="module">
import Vue from '/lib/Vue/index.js'
import Watcher from './lib/Vue/core/observer/watcher.js'
const vm = new Vue({
data () {
return {
name: 'york'
}
}
})
// 由于我们还没有实现v-model, 所以此处暂时使用原生方法绑定事件
document.querySelector('#input').addEventListener('input', (e) => {
vm.name = e.target.value
})
// 注意, 这里我们监听了
new Watcher(vm, (this) => {
let name = this.name
console.log('🚀 被监听的name的最新值:', name)
})
</script>
</body>
</html>
效果如下:
挂载简述
上面的案例中, 我们使用 new Watcher成功监听到了data属性的改变, 但是实际使用中, 我们肯定不可能把watcher拉出来和new Vue放在都一个层级使用吧, 那它应该在哪里呢? 不错, 就隐藏在挂载逻辑中, 注意我们刚才的案例中, new Vue是没有el属性的, 而这在正常的开发中, 是不可能存在的, 接下来, 就来简单过下挂载的逻辑, 进一步验证我们之前的成果, 也为后续进一步的开发做准备;
// 源码路径 /src/platforms/web/runtime/index.ts
/**
注意, 这里的Vue, 是已经经过了初始化, 源码中此处引入Vue的代码为:
import Vue from 'core/index', 而core/index中
Vue又引入自src/core/instance/index.ts, 也就是我们第一节最开始初始化Vue的那个文件
*/
Vue.prototype.$mount = function (el) {
// 入传入字符传选择器, 则将其转换为节点
if (typeof el === 'string') {
el = document.querySelector(el)
}
mountComponent(this, el)
}
export default Vue
// 源码路径 /src/core/instance/lifecycle.ts
export function mountComponent (vm, el) {
const updateComponent = function () {
// 由于我们还没有学习到模板部分, 所以此处简单使用原生方法进行插值
el.innerHTML = vm.name
}
// 注意, watcher出现了
new Watcher(vm, updateComponent)
}
// 源码路径 /src/core/instance/init.ts
import { initState } from './state'
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
const vm = this
vm.$options = options
initState(vm)
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
小节: 以上代码中, $mount方法传入一个节点/字符串, 然后执行mountComponent方法, 将updateComponent方法纳入其中, 根据上一节的案例, 我们知道, 只要在这方法中执行this.xx取值操作, 就会被监听, 并在后续的更新中被执行;
现在我们可以像真的一样执行了
<input type="text" id="input">
<div id="app"></div>
<script type="module">
import Vue from '/lib/Vue/index.js'
const vm = new Vue({
data () {
return {
name: 'york'
}
},
el: '#app' // 增加了挂载
})
document.querySelector('#input').addEventListener('input', (e) => {
vm.name = e.target.value
})
// new Watcher(vm, (_vm) => {
// let name = _vm.name
// console.log('🚀 被监听的name的最新值:', name)
// })
</script>
总结:
- 本节我们主要是在上一节data访问的基础上, 增加了对data数据的监听;
- 响应式依赖收集的基本原理: 我们使用Object.defineProperty, 的get方法, 来监听属性在哪里被使用, 从而将该watcher存入属性专属的dep内, 又用了Object.defineProperty的set方法, 从该属性的dep中获取依赖, 并依次执行
- 简单使用了挂载方法, 将watcher内置到挂载逻辑之内, 使其可以进行自动监听, 并更新页面数据