学习了Vue之后,根据自己的理解,手写了简版Vue,希望对正在学习Vue源码的初学者有所帮助,一起加油!
语雀访问地址:手写Vue
HTML
<!-- 声明meta,不然会乱码 -->
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<!-- HTML -->
<div id="app">
<p>{{counter}}</p>
<p>{{counter}}</p>
<p>{{name}}</p>
<p j-text='name'></p>
<p j-html='hobby'></p>
<button @click='clickFn'>点我</button>
<!-- <button j-show='show' @click='clickFn'>点我</button> -->
</div>
<!-- 引入JVue -->
<script src="./J-Vue.js"></script>
<!-- 逻辑 -->
<script>
const app = new JVue({
el: '#app',
data: {
show: false,
name: '张三',
hobby: "我喜欢<span style='color: red'>听歌</span>",
counter: 1
},
methods: {
clickFn () {
console.log(this.counter)
this.show = !this.show
this.counter++
}
}
})
// setInterval(() => {
// app.counter++
// }, 1000)
</script>
简版JVue
key、Dep、watcher之间的关系
- data中一个key对应一个Dep实例(管家)
- 界面中出现一次key绑定,就会产生一个watcher
- Dep(管家)负责管理同一个key产生的一个或多个watcher
实现步骤
- defineReactive时为data中每⼀个key创建⼀个Dep实例(管家)
- 初始化视图时,读取某个key,例如name1,创建⼀个watcher1 => Compile里创建
- 由于触发name1的getter⽅法,便将watcher1添加到name1对应的Dep(管家)中,即创建Dep和watcher之间订阅(管理)关系
- 当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
自我理解
- 初始化的时候,页面中首次渲染Compile,调用更新方法update(),同时为每一个读取的key创建(new)一个watcher,并将当前data、key、更新方法update(newVal)作为参数传递给这个watcher,将来key的值发生改变后,不会再走Compile里的update()了,而是直接触发watcher中的update()
- 此时我们创建一个Watcher类,作用:
• 获得data、key、更新函数
• 刻意读取一次当前key,要立flag做判断,即:Dep.target = this,保证是在刻意读取key的情况下,才收集wathcer依赖
• 详解:new Watcher的同时,刻意读取一次该watch对应的key,触发defineProperty中的get方法,在get方法中触发管家(Dep)的addDep方法,将此watcher添加到管家的deps中,为了判断是watcher刻意读取的,我们需要定义一个全局变量,即Dep.target = this,同时这个this也是需要添加到deps中的watcher,如果Dep.target存在,我们才调用addDep方法,key读取完成后,我们要把Dep.target设置为null,防止之后再获取key时,也会触发addDep的方法 - 同时,在初始化的时候,在defineReactive中为每一个key创建(new)一个“一对一的专属管家“——Dep
- 此时我们创建一个Dep类,作用:
• 收集依赖(watcher)——deps数组添加watcher
• 通知watcher更新——遍历触发watcher中的update - 这样,每一个管家Dep就收集到了所有使用同一个key的watcher
- 将来,data中的key发生变化时,触发defineProperty的set方法,在set中让管家(Dep)触发通知notify方法,遍历一个个watcher,调用watcher中的update(),至此就完成了视图更新。
// 可按序号阅读
class JVue {
constructor(options) {
// 1、保存配置项
this.$options = options
// 2、保存data
this.$data = options.data
// 3、保存方法
this.$methods = options.methods
// 14、获取DOM
this.$el = document.querySelector(options.el)
// 4、给data中的数据添加响应式
observe(this.$data)
// 12、添加代理,可以直接通过this + key获取
proxy(this)
// 15、调用编译
if (this.$el) {
new Complie(this, this.$el)
}
}
}
// 5、observe方法
function observe(data) {
// 🔥判断是数组或者对象
if (typeof data !== 'object' || data === null) return
new Observe(data)
}
// 6、Observe类
class Observe {
constructor(data) {
if (Array.isArray(data)) {
// 数组
this.walkArray(data)
} else {
// 对象
this.walk(data)
}
}
// 7、给对象添加响应式
walk(obj) {
// 对对象进行遍历,给对象中的每一项key添加响应式
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
// 46、给数组添加响应式
walkArray(array) {
array.__proto__ = defineArray()
// 48、对数组内部元素添加响应式
for (let i = 0; i < array.length; i++) {
observe(array[i])
}
}
}
// 47、给数组添加响应式
// (1) 找到数组原型
// (2) 覆盖那些能够修改数组的更新方法,使其可以通知更新
// (3) 将得到的新原型设置到数组的实际原型上
function defineArray() {
// 找到数组原型
const originProto = Array.prototype
// 拷贝一份原型
const arrayProto = Object.create(originProto)
// 数组的方法
const methods = ['push', 'pop', 'shift', 'unshift', 'reserve']
console.log(originProto)
methods.forEach(method => {
arrayProto[method] = function () {
// 原始操作
originProto[method].apply(this, arguments)
// 覆盖操作:通知更新
console.log('调用了数组的' + method + '方法')
}
})
return arrayProto
}
// 8、创建defineReactive方法
// (1) 注意对象嵌套的响应式
// (2) 注意直接将val赋值为对象的响应式
// (3) 注意新添加key的响应式
function defineReactive(obj, key, val) {
// 9、递归,解决对象嵌套的问题
observe(obj[key])
// 38、初始化的时候为每一个key添加管家Dep
const dep = new Dep()
Object.defineProperty(
obj,
key,
{
get() {
console.log('get:', key)
// 43、将watcher添加到deps,依赖收集
// 🔥要判断Dep.target存在才添加watcher,否则每获取一次key的值,都会触发addDep,此时target为null
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
console.log('set:', key)
if (newVal !== val) {
val = newVal
// 10、解决直接将val赋值为对象
observe(val)
// 为了测试用
// watchers.forEach(w => w.update(newVal))
// 44、key的值发生变化,通知更新
dep.notify(newVal)
}
}
}
)
}
// 11、set方法,用于对新添加的key添加响应式
function set(obj, key, val) {
defineReactive(obj, key, val)
}
// 13、添加代理,可以直接通过this + key获取
function proxy(vm) {
// 对data遍历,对每一个key代理
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(
vm,
key,
{
get() {
// 获取data中的key,其实是获取的$data中的key
return vm.$data[key]
},
set(newVal) {
// 设置data中的key,其实是设置的$data中的key
vm.$data[key] = newVal
}
}
)
})
}
// 16、Complie类
class Complie {
constructor(vm, el) {
// 17、保存vm和el,后面会使用到
this.$vm = vm
this.$el = el
// 18、编译方法
this.complie(el)
}
// 19、编译方法
complie(el) {
// 获取DOM节点
const childNodes = el.childNodes
// 循环节点,获得元素类型和文本类型
childNodes.forEach(node => {
// nodeType:1元素、2属性、3文本
if (node.nodeType === 1) {
// 元素
// 26、对元素的属性进行遍历
const attributes = node.attributes
// attributes是一个类数组,要把它转化成数组
Array.from(attributes).forEach(attr => {
const attrName = attr.name
const attrVal = attr.value
// 27、判断是不是自定义指令
if (this.isDirective(attrName)) {
const dir = attrName.slice(2)
this[dir] && this[dir](node, attrVal, dir)
}
// 29、判断是不是事件
if (this.isEvent(attrName)) {
// 事件类型
const eventType = attrName.slice(1)
this.eventHanlder(node, eventType, attrVal)
}
})
} else if (this.isInter(node)) {
// 21、文本:只判断是包含插值表达式的文本
// 23、触发update(),RegExp.$1会保存最近一个正则匹配的分组
this.text(node, RegExp.$1)
console.log(node, RegExp.$1)
}
// 20、递归,遍历到DOM每一个节点
if (node.childNodes && node.childNodes.length) {
this.complie(node)
}
})
}
// 22、是否是包含插值表达式的文本
isInter(node) {
const interRule = /\{\{(.*)\}\}/
return node.nodeType === 3 && interRule.test(node.textContent)
}
// 24、更新方法
update(node, exp, dir) {
// 拼接textUpdater、htmlUpdate方法
const fn = this[dir + 'Updater']
// (1) 只会初始化执行
fn && fn(node, this.$vm[exp])
// 32、为每个使用key创建watcher
new Watcher(this.$vm, exp, (newVal) => {
// (2) 除开初始化,之后data数据每一次更新执行
fn && fn(node, newVal)
})
}
// 27、判断是不是自定义指令
isDirective(attrName) {
return attrName.startsWith('j-')
}
// 25、text()、textUpdater(),更新文本节点
text(node, exp) {
this.update(node, exp, 'text')
}
textUpdater(node, value) {
node.textContent = value
}
// 28、html()、htmlUpdater(),更新HTML
html(node, exp) {
this.update(node, exp, 'html')
}
htmlUpdater(node, value) {
node.innerHTML = value
}
// 45、model()、modelUpdater(),更新value
//j-model本质是语法糖:value设定和事件监听
model(node, exp) {
this.update(node, exp, 'model')
// 监听input事件
node.addEventListener('input', e => {
this.$vm[exp] = e.target.value
})
}
modelUpdater(node, value) {
// 回显value
node.value = value
}
// 30、判断是不是事件
isEvent(attrName) {
return attrName.startsWith('@')
}
// 31、绑定事件
eventHanlder(node, eventType, fnName) {
// 主要修改this指向
node.addEventListener(eventType, this.$vm.$methods[fnName].bind(this.$vm))
}
}
// 为了测试用
// const watchers = []
// 33、创建Watcher类
class Watcher {
constructor(vm, key, updateFn) {
// 34、JVue实例
this.$vm = vm
// 35、依赖key
this.key = key
// 36、更新函数
this.updateFn = updateFn
// 为了测试用
// watchers.push(this)
// 42、刻意读取一次key,创建watcher时触发getter
// 立一个flag,保证是刻意读取的情况下收集的依赖watcher
Dep.target = this // 此处的this就是watcher
this.$vm[this.key] // 刻意读取
Dep.target = null // 要清空,不然下次再读取key也会变成刻意读取
}
// 37、触发更新的方法
update() {
// 将key最新的值传过去
// 🔥注意this指向和key最新的值传递方式
this.updateFn.call(this.vm, this.$vm[this.key])
}
}
// 38、创建一个Dep类
class Dep {
constructor() {
// 39、创建数组,装同一key的watcher
this.deps = []
}
// 40、添加watcher
addDep(dep) {
this.deps.push(dep)
}
// 41、通知更新
notify() {
// 循环遍历deps,触发watcher的更新
this.deps.forEach(dep => dep.update())
}
}