index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"> </div>
<script type="module">
import Vue from './vue.js'
new Vue({
// el: '#app',
data() {
return {
title: '这里是标题',
li2: [{ tag: 'span', children: 'li2的初始值' }],
li3: 'li3'
}
},
render(h) {
// return h('div', { 'class': 'title' }, this.title)
return h('ul', { 'class': 'list' }, [
{ tag: 'li', children: 'li1' },
{ tag: 'li', children: this.li2 },
{ tag: 'li', children: this.li3 }
])
},
mounted() {
setTimeout(() => {
// this.title = '哈哈哈,标题改变了'
// console.log('title的值变为:',this.title);
// this.li3 = 'li3的值改变了!'
this.li2 = 'li2的值改变了'
this.li3 = [{ tag: 'span', children: 'li3的值改变了!' }]
}, 2000)
}
}).$mount('#app')
</script>
</body>
</html>
vue.js
export default class Vue {
constructor(options) {
this.$options = options
this.$data = options.data()
this.proxy(this.$data)
this.observe(this.$data)
if (options.el) {
this.$mount(options.el)
}
return this
}
// {title: '标题'} => this.$data.title => this.title
proxy(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(v) {
this.$data[key] = v
},
})
})
}
// 劫持、响应化
observe(data) {
if (typeof data !== 'object' || data === null) {
return
}
if (Array.isArray(data)) {
data.forEach(d => {
this.observe(d)
})
} else {
Object.keys(data).forEach(key => {
// data:{form: {user:'', certNo: ''}} => data[key]是对象
// 遍历所有层次
this.observe(data[key])
new Observe(this, data, key)
})
}
}
// 挂载:
// $mount 函数,返回一个mountComponent函数, 该函数中,主要:
// ①: 定义updateComponent函数,该函数主要用来①初始化(首次渲染), ② 更新渲染
// updateComponent函数,主要执行_update()函数,这个函数的参数,就是render函数返回的虚拟dom, _update函数的作用:将虚拟dom渲染到dom上
// render函数获取虚拟dom,将虚拟dom传入_update函数,在_update函数中调用_patch_函数,渲染至dom
// ②: 实例化Watcher, 将updateComponent函数传入
$mount(el) {
this.$el = document.querySelector(el)
// 优先级: render>template>el
// 如果 options中传入render函数,则执行mountComponent()
// 否则,判断是否传入el,如果传入el,则将el => 转换为template,再用 compileToFunction函数来生成render函数,再赋给options中,然后再调用mountComponent函数
const mountComponent = () => {
const updateComponent = () => {
/*
if(!this.$options.render){
if(el){
// todo: 将el转为template
}
// todo: 将template传入 compileToFunction函数,生成返回render函数。再讲render函数加入 options中。
}
*/
// 假定 在options中传入了render函数
const { render } = this.$options
// render函数有2个参数,第一个参数:h => $createElement(args),$createElement将传入的js对象转为虚拟dom,返回虚拟dom树
// render函数实际就是返回$createElement(args)的结果
const vnode = render.call(this, this.$createElement)
console.log(vnode)
// _update(vnode)将虚拟节点转为真实dom : 函数内部调用_path_(diff算法)找出不同,再定点生成dom,定点打补丁
this._update(vnode)
}
new Watcher(this, updateComponent)
}
return mountComponent()
}
// vnode => dom
// _update函数主要是将虚拟node转为真实dom,主要是执行 _patch_函数,将虚拟dom转为真实dom,_patch_也是执行diff算法的函数
// 获取上一次更新vnode树,① 如果不存在,则为初始化; ② 如果存在,则为视图更新操作
_update(vnode) {
// 获取上一次更新的vnode树
const prevVnode = this._vnode // this._vnode,在_patch_执行时,会将生成的vnode树存储到this._vnode上
if (!prevVnode) {
this._patch_(this.$el, vnode) // 初始化
} else {
this._patch_(prevVnode, vnode) // 更新操作
}
}
// 源码中,调用_patch_函数,实际就是调用 createPatchFunction函数返回的patch函数。这里就简化了。
// patch函数其实就是diff的过程。
// ① 旧节点存在, 新节点不存在。(组件销毁时), 调用旧节点的destroy生命周期函数
// ② 旧节点不存在, 新节点存在。增加新节点创建dom
// ③ 新旧节点都存在
// => 1. 旧节点是真实dom => 执行 ☆ createElm(vnode),进行初始化
// => 2. 旧节点不是真实dom,新旧节点相同 => 执行 ☆ patchVnode,节点更新打补丁
// => 3. 旧节点不是真实dom是vnode,但是新旧节点不相同 => 执行 ☆ createElm(vnode), 销毁旧节点以及dom
_patch_(oldVnode, vnode) {
// 真实dom节点存在nodeType: 1:元素节点 3:文本节点
if (oldVnode.nodeType) {
const parent = oldVnode.parentNode
const nextSibling = oldVnode.nextSibling
const el = this.createElm(vnode)
parent.insertBefore(el, nextSibling)
parent.removeChild(oldVnode)
// mounted钩子函数执行
if (this.$options.mounted) {
this.$options.mounted.call(this)
}
this._vnode = vnode // 是为了在_update()函数中获取上一次更新的vnode树
} else {
// 判断新旧节点是否相同: tag、key 相同就是同一个节点。 这里就不考虑key了。
// 新旧节点相同 => patchVnode,进行diff
if (oldVnode.tag === vnode.tag) {
this.patchVnode(oldVnode, vnode)
} else {
// todo: el.parentNode.replaceChild(this.createElm(newVnode), el)
const oldElm = oldVnode.elm
const parentElm = oldElm.parent
const el = this.createElm(vnode)
vnode.elm = el
parentElm.replaceChild(el, oldElm)
}
this._vnode = vnode // 是为了在_update()函数中获取上一次更新的vnode树
}
}
// 新旧节点是同一个节点,进行patchVnode操作
// ① 如果新旧节点完全相同,引用地址相同 , return;
// ② 新节点不是文本节点:
// 1. 新旧节点都存在子节点,且新旧节点的子节点数组不相同 ch !== oldCh => updateChildren
// 2. 新节点有子节点, 旧节点没有子节点 => 旧节点是文本节点,则清空文本,并创建新节点的子节点,并新增
// 3. 新节点没有子节点, 旧节点有子节点 => 移除旧节点的子节点及dom
// 4. 新旧节点都没有子节点 => 清除文本
patchVnode(oldVnode, vnode) {
if (oldVnode === vnode) return
// 新旧节点是同一个节点: 将旧节点的elm赋值给新节点的elm,新旧节点保持一致
// 获取oldVnode对应的真实dom, 用于做真实的dom操作, 并将这个真实dom,存储到新节点的el变量上,以方便下次更新是使用
const el = (vnode.elm = oldVnode.elm)
const oldCh = oldVnode.children
const ch = vnode.children
if (oldCh && ch) {
if (Array.isArray(oldCh) && Array.isArray(ch)) {
// 新旧节点都存在子节点 => 更新子节点 updateChildren
this.updateChildren(el, oldCh, ch)
} else if (typeof ch === 'string') {
el.textContent = ch
} else {
el.textContent = ''
this.addVnodes(el, ch, 0, ch.length - 1)
}
} else if (ch) {
// 新节点存在子节点,旧节点不存在子节点 => 清空旧节点文本,创建新的子节点,并添加
if (typeof ch === 'string') {
el.textContent = ch
} else {
this.addVnodes(el, ch, 0, ch.length - 1)
}
} else if (oldCh) {
// 旧节点存在子节点,新节点不存在子节点 => 移除旧节点子节点及dom
this.removeVnodes(oldCh, 0, oldVnode.length - 1)
} else {
el.textContent = ''
}
}
updateChildren(parentElm, oldCh, newCh) {
// oldCh、ch都是子节点数组 => diff
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let newEndIdx = newCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldStartVnode, newStartVnode)) {
this.patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
this.patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
this.patchVnode(oldStartVnode, newEndVnode)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
this.patchVnode(oldEndVnode, newStartVnode)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 新旧节点的首位都没有相同节点,则新数组头节点的key去旧数组剩余节点中进行匹配,如果存在相同key的节点,就patchVnode(),并将旧数组中该位置的值设为undefined。
// 否则,就createElm()
// 这里就简写成全部创建了。
parentElm.appendChild(this.createElm(newStartVnode))
newStartVnode = newCh[++newStartIdx]
}
}
// 如果旧数组遍历完成,即oldStartIdx > oldStartIdx,此时newStartIdx、newEndIdx之间的节点为新增节点
if (oldStartIdx > oldEndIdx) {
this.addVnodes(parentElm, newCh, newStartIdx, newEndIdx)
} else if (newStartIdx > newEndIdx) {
// 如果新数组遍历完成,即newStartIdx > newEndIdx, 此时oldStartIdx、oldEndIdx之间的节点为需要删除的节点
this.removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
addVnodes(parent, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
parent.appendChild(this.createElm(vnodes[startIdx]))
}
}
// ul 删除 第1个-第3个 li, 还剩第4个li
removeVnodes(vnodes, startIdx, endIdx) {
const parent = vnodes[0].elm.parentNode
parent.textContent = ''
}
// 将ast语法转为虚拟节点
// @returns { vnode }
$createElement(tag, props, children) {
return { tag, props, children }
}
// 将虚拟node转为真实dom
// vnode : { tag, props, children } => { tag: 'div', props: { class:'list'}, children: '这里是个列表'}
/* vnode : { tag: 'ul', props: { class:'list'}, children: [
{ tag: 'li', props: { class:'item'}, children: '1'},
{ tag: 'li', props: { class:'item'}, children: '2'},
]}
*/
createElm(vnode) {
const el = document.createElement(vnode.tag)
if (vnode.props) {
Object.keys(vnode.props).forEach(propName => {
el.setAttribute(propName, vnode.props[propName])
})
}
if (vnode.children) {
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach(child => {
const childEl = this.createElm(child)
el.appendChild(childEl)
})
}
}
// vnode.elm 存在: 说明该vnode已经被渲染过了
vnode.elm = el
return el
}
}
// 数据响应式处理
class Observe {
constructor(vm, data, key) {
this.$vm = vm
this.defineReactive(data, key, data[key])
}
defineReactive(data, key, val) {
const vm = this.$vm
// 一个key对应一个dep
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
// Dep.target里面存储的是watcher实例, 只有实例化Watcher时,Dep.target才有值
// Watcher,在初始化时会进行实例化,在vue使用过程中使用watch监听时,也会产生Watcher实例
// initState先执行,执行之后才会进行$mount挂载,在挂载时才会实例化Watcher,所以在首次定义响应式时,是不存在Dep.target值的。
// 在$mount挂载之后,使用key值时,Dep.target才存在
if (Dep.target) {
dep.addDep(Dep.target)
}
return val
},
set(v) {
if (v !== val) {
// 考虑到用户在使用this.title时,以前为string字符串,后进行重新赋值为this.title = {name: '我是标题'}
vm.observe(v) // 重新对新值v做响应式处理
val = v
// 通知依赖更新
dep.notify()
}
},
})
}
}
// 依赖收集器
class Dep {
constructor() {
// Set数据容器,存放 无重复有序列表 set.add(),set.delete(),set.has();set.size ; set.forEach()
this.deps = new Set()
}
addDep(watcher) {
this.deps.add(watcher)
}
notify() {
this.deps.forEach(watcher => {
watcher.update()
})
}
}
class Watcher {
constructor(vm, callback) {
this.$vm = vm
// callback: updateComponent
this.$cb = callback
// 在watcher实例化的时候,进行收集依赖,执行渲染
this.getter()
}
// getter()方法: ① 初始化首次渲染 ② 更新组件再次渲染
// 收集依赖,执行渲染
getter() {
// 将watcher实例赋值给Dep.target, 方便data数据,在get访问器中进行收集
Dep.target = this
// 执行updateComponent渲染函数,这个函数是在$mount挂载的时候,对Watcher进行实例化时传入的参数
this.$cb.call(this.$vm)
console.log('updateComponent组件更新方法执行!')
Dep.target = null
}
update() {
// 组件更新时,执行组件渲染函数
this.getter()
}
}
function sameVnode(a, b) {
return a.tag === b.tag
}