前言
- 前面实现了简化版的Vue,juejin.cn/post/701995…。
- 简化版的Vue是需要真实DOM的替换。
- 一个Dep管理多个Watcher,在Watcher过多的情况下就会依赖过多导致卡顿,这就是Vue之前不支持大型项目的原因。
- 今天来实现Vue2.x版本中是如何优化上面的问题的。下面大部分是模拟源码实现。
实现模版
<!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>Vue2.x</title>
</head>
<body>
<div id="app"></div>
<script src="./vue2.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
counter: 1,
},
// 编写render函数
render(h) {
return h('div', { id: 'app' }, [
h('p', { class: 'title' }, this.counter + ''),
])
},
})
setInterval(() => {
app.counter++
}, 1000)
</script>
</body>
</html>
Vue2的实现
-
去除编译函数
class Vue { constructor(options) { this.$options = options this.$el = options.el this.$data = options.data // 1. 将data数据进行响应式数据 observer(this.$data) // 1.1 数据代理,将响应式数据data代理到Vue实例上可以使外部直接访问 proxy(this.$data, this) // 2. 这次要实现render函数编译,去除Vue1.x版本的编译函数 // new Compiler(this.$el, this) } } -
加入$mount挂载方法
$mount(el) { // 获取宿主 this.$el = document.querySelector(el) const updateComponent = () => { // 执行render const { render } = this.$options const el = render.call(this) // 获取指定节点的父级 const parent = this.$el.parentElement // 插入到父级的参考元素边,参考元素this.$el.nextSibling parent.insertBefore(el, this.$el.nextSibling) // 删除原来的节点 parent.removeChild(this.$el) // 更新最新的节点 this.$el = el } new Watcher(this, updateComponent) } -
Watcher函数更改
// 实现Watcher监听 class Watcher { constructor(vm, fn) { this.vm = vm // 接收更新函数 this.getter = fn // 执行getter方法 this.get() } get() { // 创建watcher时执行getter Dep.target = this this.getter.call(this.vm) Dep.target = null } update() { this.get() } } -
Dep函数更改
// 实现Dep,多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。 class Dep { constructor() { // 不能重复添加 this.deps = new Set() } addDep(dep) { this.deps.add(dep) } notify() { this.deps.forEach((dep) => dep.update()) } } -
执行mount方法,学习Vue源码中,如果有el选项,手动触发$mount函数,也可以在new Vue的时候进行挂载。
class Vue { constructor(options) { this.$options = options this.$el = options.el this.$data = options.data // 1. 将data数据进行响应式数据 observer(this.$data) // 1.1 数据代理,将响应式数据data代理到Vue实例上可以使外部直接访问 proxy(this.$data, this) // 2. 编译 // new Compiler(this.$el, this) // 挂载 if (options.el) { this.$mount(options.el) } } }-
方式一
<script> const app = new Vue({ el: '#app', data: { counter: 1, }, // 编写render函数 render(h) { return h('div', { id: 'app' }, [ h('p', { class: 'title' }, this.counter + ''), ]) }, }) </script> -
方式二
<script> const app = new Vue({ data: { counter: 1, }, // 编写render函数 render(h) { return h('div', { id: 'app' }, [ h('p', { class: 'title' }, this.counter + ''), ]) }, }).$mount('#app') </script>
-
-
虚拟DOM的加入
-
修改$mount中的updateComponent方法
$mount(el) { // 获取宿主 this.$el = document.querySelector(el) const updateComponent = () => { // 真实节点操作 // 执行render // const { render } = this.$options // const el = render.call(this) // // 获取指定节点的父级 // const parent = this.$el.parentElement // // 插入到父级的参考元素边,参考元素this.$el.nextSibling // parent.insertBefore(el, this.$el.nextSibling) // // 删除原来的节点 // parent.removeChild(this.$el) // // 更新最新的节点 // this.$el = el // 虚拟节点操作 const { render } = this.$options const vnode = render.call(this, this.$createElement) this._update(vnode) } new Watcher(this, updateComponent) } -
$createElement转换为虚拟DOM
// 输出一个对象,不考虑边界情况 $createElement(tag, props, children) { return { tag, props, children } } -
_update更新
_update(vnode) { // 获取上次执行的vnode const prevVnode = this._vnode if (!prevVnode) { // init this.__patch__(this.$el, vnode) } else { // update this.__patch__(prevVnode, vnode) } } -
patch对比
__patch__(oldVnode, vnode) { if (oldVnode.nodeType) { //init const parent = oldVnode.parentElement const refElm = oldVnode.nextSibling const el = this.createElm(vnode) parent.insertBefore(el, refElm) parent.removeChild(oldVnode) } else { // update // 获取el const el = (vnode.el = oldVnode.el) // props const oldProps = oldVnode.props || {} const newProps = vnode.props || {} for (const key in newProps) { el.setAttribute(key, newProps[key]) } for (const key in oldProps) { if (!(key in newProps)) { el.removeAttribute(key) } } // children const oldCh = oldVnode.children const newCh = vnode.children if (typeof newCh === 'string') { if (typeof oldCh === 'string') { if (newCh !== oldCh) { el.textContent = newCh } } else { // 以前没文本 el.textContent = newCh } } else { if (typeof oldCh === 'string') { // 清空 el.innerHTML = '' newCh.forEach((child) => { el.appendChild(this.createElm(child)) }) } else { // 重排 this.updateChildren(el, oldCh, newCh) } } } // 保存vnode this._vnode = vnode }-
createElm虚拟DOM转换真实DOM
// vnode => dom createElm(vnode) { const el = document.createElement(vnode.tag) //props if (vnode.props) { for (const key in vnode.props) { const value = vnode.props[key] el.setAttribute(key, value) } } //children if (vnode.children) { if (typeof vnode.children === 'string') { // text el.textContent = vnode.children } else { vnode.children.forEach((v) => { const child = this.createElm(v) el.appendChild(child) }) } } // 保存创建真实的dom vnode.el = el return el } -
updateChildren比较孩子的重排操作
// 重排 updateChildren(parentElm, oldCh, newCh) { const len = Math.min(oldCh.length, newCh.length) for (let i = 0; i < len; i++) { this.__patch__(oldCh[i], newCh[i]) } if (newCh.length > oldCh.length) { newCh.slide(len).forEach((child) => { const el = this.createElm(child) parentElm.appendChild(el) }) } else if (newCh.length < oldCh.length) { oldCh.slide(len).forEach((child) => { parentElm.removeChild(child.el) }) } }
-
-
最终实现
function definReactive(obj, key, val) {
// 对象递归处理
observer(val)
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// 有Watcher就收集依赖
Dep.target && dep.addDep(Dep.target)
console.log(`get ${key}`)
return val
},
set(newVal) {
if (newVal !== val) {
console.log(`set ${newVal}`)
observer(val)
val = newVal
// 修改执行通知更新
dep.notify()
}
},
})
}
// 数组的响应式处理
// 1. 替换数组中的方法
const orginalProto = Array.prototype
// 拷贝一份,进行修改
const arrayProto = Object.create(orginalProto)
// 举例常用的4个方法
;[('push', 'pop', 'shift', 'unshift')].forEach((method) => {
arrayProto[method] = function() {
// 执行原始操作
orginalProto[method].apply(this, arguments)
const dep = new Dep()
// 执行新的操作更新
dep.notify()
}
})
function observer(obj) {
// 判断是否是对象
if (typeof obj !== 'object' || obj === null) return
new Observer(obj)
}
function proxy(data, vm) {
Object.keys(data).forEach((key) => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key]
},
set(newVal) {
vm.$data[key] = newVal
},
})
})
}
class Vue {
constructor(options) {
this.$options = options
this.$el = options.el
this.$data = options.data
// 1. 将data数据进行响应式数据
observer(this.$data)
// 1.1 数据代理,将响应式数据data代理到Vue实例上可以使外部直接访问
proxy(this.$data, this)
// 2. 编译
// new Compiler(this.$el, this)
// 挂载
if (options.el) {
this.$mount(options.el)
}
}
$mount(el) {
// 获取宿主
this.$el = document.querySelector(el)
const updateComponent = () => {
// 真实节点操作
// 执行render
const { render } = this.$options
// const el = render.call(this)
// // 获取指定节点的父级
// const parent = this.$el.parentElement
// // 插入到父级的参考元素边,参考元素this.$el.nextSibling
// parent.insertBefore(el, this.$el.nextSibling)
// // 删除原来的节点
// parent.removeChild(this.$el)
// // 更新最新的节点
// this.$el = el
// 虚拟节点操作
const vnode = render.call(this, this.$createElement)
this._update(vnode)
}
new Watcher(this, updateComponent)
}
$createElement(tag, props, children) {
return { tag, props, children }
}
_update(vnode) {
// 获取上次执行的vnode
const prevVnode = this._vnode
if (!prevVnode) {
// init
this.__patch__(this.$el, vnode)
} else {
// update
this.__patch__(prevVnode, vnode)
}
}
__patch__(oldVnode, vnode) {
if (oldVnode.nodeType) {
//init
const parent = oldVnode.parentElement
const refElm = oldVnode.nextSibling
const el = this.createElm(vnode)
parent.insertBefore(el, refElm)
parent.removeChild(oldVnode)
} else {
// update
// 获取el
const el = (vnode.el = oldVnode.el)
// props
const oldProps = oldVnode.props || {}
const newProps = vnode.props || {}
for (const key in newProps) {
el.setAttribute(key, newProps[key])
}
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// children
const oldCh = oldVnode.children
const newCh = vnode.children
if (typeof newCh === 'string') {
if (typeof oldCh === 'string') {
if (newCh !== oldCh) {
el.textContent = newCh
}
} else {
// 以前没文本
el.textContent = newCh
}
} else {
if (typeof oldCh === 'string') {
// 清空
el.innerHTML = ''
newCh.forEach((child) => {
el.appendChild(this.createElm(child))
})
} else {
// 重排
this.updateChildren(el, oldCh, newCh)
}
}
}
// 保存vnode
this._vnode = vnode
}
// 重排
updateChildren(parentElm, oldCh, newCh) {
const len = Math.min(oldCh.length, newCh.length)
for (let i = 0; i < len; i++) {
this.__patch__(oldCh[i], newCh[i])
}
if (newCh.length > oldCh.length) {
newCh.slide(len).forEach((child) => {
const el = this.createElm(child)
parentElm.appendChild(el)
})
} else if (newCh.length < oldCh.length) {
oldCh.slide(len).forEach((child) => {
parentElm.removeChild(child.el)
})
}
}
// vnode => dom
createElm(vnode) {
const el = document.createElement(vnode.tag)
//props
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
el.setAttribute(key, value)
}
}
//children
if (vnode.children) {
if (typeof vnode.children === 'string') {
// text
el.textContent = vnode.children
} else {
vnode.children.forEach((v) => {
const child = this.createElm(v)
el.appendChild(child)
})
}
}
// 保存创建真实的dom
vnode.el = el
return el
}
}
// 实现编译
class Compiler {
constructor(el, vm) {
this.$vm = vm
this.$el = document.querySelector(el)
if (this.$el) {
this.compile(this.$el)
}
}
compile(el) {
const childNodes = el.childNodes
childNodes.forEach((node) => {
if (this.isElement(node)) {
// 元素节点编译
if (node.childNodes.length > 0) {
this.compile(node)
}
this.compileElement(node)
} else if (this.isInertText(node)) {
// 文本节点编译
this.compileText(node)
}
})
}
// 判断节点是不是元素节点
isElement(node) {
return node.nodeType === 1
}
// 判断节点是不是文本节点并且是插值文本{{}}
isInertText(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
// 判断是不是h-开头的指令
isDir(attr) {
return attr.startsWith('h-')
}
// 判断是不是@开头的方法
isEvent(attr) {
return attr.startsWith('@')
}
// 编译文本
compileText(node) {
// node.textContent = this.$vm[RegExp.$1]
this.update(node, RegExp.$1, 'text')
}
// 编译元素
compileElement(node) {
const attributes = node.attributes
Array.from(attributes).forEach((attr) => {
// h-text="count"
const attrName = attr.name // h-text
const exp = attr.value // count
if (this.isDir(attrName)) {
const dir = attrName.substring(2)
this[dir] && this[dir](node, exp)
} else if (this.isEvent(attrName)) {
const dir = attrName.substring(1)
this.eventHandler(node, exp, dir)
}
})
}
// 方法
eventHandler(node, exp, dir) {
const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
node.addEventListener(dir, fn.bind(this.$vm))
}
text(node, exp) {
// node.textContent = this.$vm[exp]
this.update(node, exp, 'text')
}
textUpdater(node, val) {
node.textContent = val
}
html(node, exp) {
// node.innerHTML = this.$vm[exp]
this.update(node, exp, 'html')
}
htmlUpdater(node, val) {
node.innerHTML = val
}
model(node, exp) {
this.update(node, exp, 'model')
node.addEventListener('input', (e) => {
this.$vm[exp] = e.target.value
})
}
modelUpdater(node, val) {
node.value = val
}
// 一个key对应一个Watcher实例
update(node, exp, dir) {
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
new Watcher(this.$vm, exp, function(val) {
fn && fn(node, val)
})
}
}
// 实现Dep,多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。
class Dep {
constructor() {
// 不能重复添加
this.deps = new Set()
}
addDep(dep) {
this.deps.add(dep)
}
notify() {
this.deps.forEach((dep) => dep.update())
}
}
// 实现Watcher监听
class Watcher {
constructor(vm, fn) {
this.vm = vm
// 接收更新函数
this.getter = fn
// 执行getter方法
this.get()
}
get() {
// 创建watcher时执行getter
Dep.target = this
this.getter.call(this.vm)
Dep.target = null
}
update() {
this.get()
}
}
// 实现响应式
class Observer {
constructor(value) {
this.value = value
if (typeof value === 'object') {
this.walk(this.value)
} else if (Array.isArray(value)) {
this.arrayWalk(value)
}
}
// 数组执行方法
arrayWalk(obj) {
obj.__proto__ = arrayProto
const keys = Object.keys()
for (let i = 0; i < keys.length; i++) {
observer(obj[i])
}
}
walk(obj) {
Object.keys(obj).forEach((key) => {
definReactive(obj, key, obj[key])
})
}
}
结语
- Vue2.x版本中加入了虚拟DOM,使的在编译的时候减少了很多真实DOM的操作大大提高了性能。
- Vue2.x版本中把Dep和Watcher的依赖关系进行变化,一个组件对应的一个Dep,一个key值对应一个Wacther,从原来的1对多的关系进化成多对多的关系,并依赖updateComponet的函数使其紧密的关联在一起,提高了性能。