《 详解 Vue 2.X 核心源码,手撸一个简易版Vue框架(上篇)》记录了Vue响应式系统的实现、虚拟DOM的生成,DOM的定向更新等。本篇文章对源码实现流程的总结
Vue源码实现流程
Vue实例初始化
这个阶段主要是完成数据的挂载($options、_data)、Vue选项的初始化 譬如data初始化、computed初始化、watch初始化等。
数据的代理
遍历所有data,将data数据通过Object.defineProperty代理到Vue实例上
数据的劫持
为每个data数据对象注册一个侦听器Observer,并为其声明事件中心Dep实例,遍历data并传入到defineReactive(data[key])。在defineReactive中进行数据劫持,通过getter收集watcher依赖,通过setter通过Dep更新。Dep会通知所有watcher更新。
渲染函数与虚拟DOM
Vue初始化会调用mount中声明render watcher并对其求值。render watcher求值函数会先调用渲染函数生成 虚拟DOM,将虚拟DOM传入_update方法,进行虚拟DOM的Diff对比 定向更新DOM节点。 当Dep通知render watcher更新时,它会重复这个逻辑。
全部代码及注释讲解
Vue响应式系统及VDOM部分
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
this.initData()
// 8.4 无论是计算属性的初始化还是data的初始化都必须放到watch初始化之前,因为计算属性和data的初始化完成 watch才能侦测到它们。
this.initComputed()
this.initWatch()
// 10.0 使用解析器和代码生成器 生成渲染函数
if (this.$options.el) {
// 10.1 获取模板字符串
let html = document.querySelector("div").outerHTML
// 10.2 生成抽象语法树
let ast = parser(html)
// 10.3 生成渲染函数函数体
let funCode = codegen(ast).render
// 10.4 生成渲染函数并挂载到Vue实例上
this.$options.render = new Function(funCode)
// 16.0 调用$mount 更新视图
this.$mount(this.$options.el)
}
}
$mount(el) {
// 16.1 将容器根节点挂载到Vue实例上
this.$el = document.querySelector(el)
// 16.2 新建render watcher
this._watcher = new Watcher(this, () => {
// 16.3 生成虚拟DOM
const vnode = this.$options.render.call(this)
// 16.4 调用_update,更新视图
this._update(vnode)
}, () => { })
}
_update(vnode) {
//17.0 有上次vnode时
if (this._vnode) {
// 17.1 调用patch 并传入上次vnode和此次vnode
patch(this._vnode, vnode)
} else {
// 17.2 第一次挂载Vue实例时 传入真实DOM节点
patch(this.$el, vnode)
}
// 17.3 保存此次vnode
this._vnode = vnode
}
// 11.0 生成元素节点
_c(tag, attrs, children, text) {
return new VNode(tag, attrs, children, text)
}
// 12.0 生成纯文本节点
_v(text) {
return new VNode(null, null, null, text)
}
// 13.0 获取变量内容
_s(val) {
// 13.1 如果值为空就返回空字符串
if (val === null || val === undefined) {
return ''
// 13.2 如果为对象
} else if (typeof val === 'object') {
return JSON.stringify(val)
// 13.3 如果为数字或字符串
} else {
return val
}
}
initData() {
let data = this._data
let keys = Object.keys(data)
// 数据代理
for (let i = 0; i < keys.length; i++) {
Object.defineProperty(this, keys[i], {
enumerable: true,
configurable: true,
set: function proxySetter(newVal) {
data[keys[i]] = newVal
},
get: function proxyGetter() {
return data[keys[i]]
},
})
}
// 数据劫持
observe(data)
}
initWatch() {
const watches = this.$options.watch
// 存在watch选项
if (watches) {
const keys = Object.keys(watches)
for (let index = 0; index < keys.length; index++) {
new Watcher(this, keys[index], watches[keys[index]])
}
}
}
// 8.3 对计算属性单独初始化
initComputed() {
const computeds = this.$options.computed
if (computeds) {
const keys = Object.keys(computeds)
for (let index = 0; index < keys.length; index++) {
// 8.5 第二个参数传入计算属性函数
// 8.15 计算属性初始化的watcher 需要将其标记为惰性的
const watcher = new Watcher(this, computeds[keys[index]], function () { }, { lazy: true })
// 8.6 将该watcher挂载到Vue实例上
Object.defineProperty(this, keys[index], {
enumerable: true,
configurable: true,
// 8.7 不允许用户修改计算属性
set: function computedSetter() {
console.warn("请不要修改计算属性")
},
// 8.8 通过watcher的get方法求值,并将求值结果返回出去
get: function computedGetter() {
// 8.9 只有watcher为脏数据时,再重新求值
if (watcher.dirty) {
watcher.get()
// 8.10 求出新值 更新dirty状态
watcher.dirty = false
}
// 9.12 在计算属性的getter中判断 是否还有watcher需要收集
if (Dep.target) {
for (let i = 0; i < watcher.deps.length; i++) {
// 9.13 将watcher的dep 拿出来继续收集剩余的watcher
watcher.deps[i].depend()
}
}
return watcher.value
}
})
}
}
}
$watch(key, cb) {
new Watcher(this, key, cb)
}
// 6.6 __ob__的挂载,依赖的收集工作已做完
$set(targt, key, value) {
const oldValue = { ...targt }
// 6.7 将传入的新属性也变为响应式
defineReactive(targt, key, value)
// 6.8 手动派发依赖更新
targt.__ob__.dep.notify(oldValue, targt)
}
}
// 1、 observe函数:判断数据类型,声明并返回Observer实例
function observe(data) {
const type = Object.prototype.toString.call(data)
// 1.1 如果被观测的data为基本数据类型 就返回
if (type !== '[object Object]' && (type !== '[object Array]')) return
// 1.2 观测数据涉及一些复杂的逻辑 将这个过程封装为一个Observer类
// 1.2 new Observer(data)
// 6.3 将Observer实例 return出去,并在defineReactive中接收。
if (data.__ob__) return data.__ob__
return new Observer(data)
}
// 2、Observer类:观察者/侦听器,用来观测数据、生成负责处理依赖的Dep实例等复杂逻辑
class Observer {
constructor(data) {
// 6.1 为observer实例挂一个Dep实例(事件中心)
this.dep = new Dep()
// 7.5 数组不能调用walk,因为walk会通过defineProperty劫持下标会出现依赖回调错乱等问题
if (Array.isArray(data)) {
// 7.6 用我们改造好的数组原型覆盖 自身的原型对象
data.__proto__ = ArrayMethods
// 7.7 将数组所有子元素变为响应式
this.observeArray(data)
} else {
// 2.1 将data所有属性 变为响应式
this.walk(data)
}
// 6.2 将observer实例挂在到不可枚举的属性__ob__上,供外部$set使用
Object.defineProperty(data, "__ob__", {
value: this,
enumerable: false,
configurable: true,
writable: true
})
}
walk(data) {
let keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
// 7.8 将传入的数组的所有子元素 变为响应式
observeArray(arr) {
for (let i = 0; i < arr.length; i++) {
observe(arr[i])
}
}
}
// 3、defineReactive工具函数:用来递归劫持data,将data数据变为响应式数据
function defineReactive(obj, key, value) {
// 3.1 递归调用defineReactive来递归劫持深层次data数据 defineReactive--observe--Observer--defineReactive
// 3.1 observe(obj[key])
// 6.4 接收Observer实例,为属性Dep收集依赖 Watcher
let childOb = observe(obj[key])
// 4.0、为每个data数据新建一个Dep实例,并通过闭包维护
let dep = new Dep()
// 3.2 对当前data对象的 key 进行数据劫持
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
set: function reactiveSetter(newVal) {
if (newVal === value) return
// 4.4、Dep派发依赖更新
dep.notify(newVal, value)
value = newVal
},
get: function reactiveGetter() {
// 4.5、闭包Dep收集依赖 Watcher
dep.depend()
// 6.5 observe函数 如果传入数据为简单数据类型 就不会返回Observer实例 所以需要判断一下是否有Observer实例,如果有就为Observer实例的Dep也收集一份 依赖
if (childOb) childOb.dep.depend()
return value
}
})
}
// 9.1 新增保存depTarget的栈
let targetStack = []
// 4、Dep类:事件中心,负责收集依赖、通知依赖更新等
class Dep {
constructor(option) {
// 4.1、subs用来保存所有订阅者
this.subs = []
}
// 9.7 watcher收集完dep后,调用dep.addSub来收集watcher
addSub(watcher) {
this.subs.push(watcher)
}
// 4.2、depend方法用来收集订阅者依赖
depend() {
// 5.5、如果为Watcher实例初始化
if (Dep.target) {
// 5.6 每个data数据Watcher实例化,都会先设置Dep.target并触发data数据得getter,完成依赖得收集
// this.subs.push(Dep.target)
// 9.6 watcher收集dep
Dep.target.addDep(this)
}
}
// 4.3、notify方法用来派发订阅者更新
notify(newVal, value) {
// 5.7、 执行每个订阅者Watcher的run方法完成 更新
// 8.12 依赖更新派发更新时 先走update判断是否要更新
this.subs.forEach(watcher => watcher.update(newVal, value))
}
}
let watcherId = 0
// watcher任务队列
let watcherQueue = []
// 5、Watcher类:订阅者,触发依赖收集、处理回调
class Watcher {
constructor(vm, exp, cb, option = {}) {
// 8.13 watcher增加新参数 option ,对watcher进行默认配置
this.lazy = this.dirty = !!option.lazy
// 5.1、将Vue实例、data属性名和处理回调 挂载到watcher实例上
this.vm = vm
this.exp = exp
this.cb = cb
this.id = ++watcherId
// 9.8 watcher用来保存收集到的dep
this.deps = []
// 8.14 惰性watcher 初始化时不需要收集依赖
if (!option.lazy) {
// 5.2、触发data数据的getter 完成依赖收集
this.get()
}
}
addDep(dep) {
// 9.9 由于每次9.0求值 watcher可能会收集多次dep 如果已经收集过就终止
if (this.deps.indexOf(dep) !== -1) return
// 9.10 收集dep
this.deps.push(dep)
// 9.11 让dep收集watcher
dep.addSub(this)
}
get() {
// 9.2 在dep收集依赖watcehr时,先添加进栈中
targetStack.push(this)
// 5.3、将Watcher实例设为 Dep依赖收集的目标对象
Dep.target = this
// 8.1 收集依赖之前先判断是否为函数 计算属性求值时会传入函数
if (typeof this.exp === 'function') {
// 8.2 执行函数 并求出值
this.value = this.exp.call(this.vm)
} else {
// 5.4、触发data数据getter拦截器 对其进行求值
this.value = this.vm[this.exp]
}
// 9.3 求值 收集依赖结束后 让watcher出栈
targetStack.pop()
// 9.4 判断栈中 是否有未被收集的watcher
if (targetStack.length) {
// 9.5 获取到栈顶的watcher
Dep.target = targetStack[targetStack.length - 1]
} else {
// 清空依赖目标对象
Dep.target = null
}
}
// 8.11 在调用run之前先调用update,判断是否要直接run
update(newVal, value) {
// 8.12 依赖更新当前watcher为惰性时,不要直接run。而是将watcher标记为脏数据,等到用户主动获取结果再去run
if (this.lazy) {
this.dirty = true
} else {
this.run(newVal, value)
}
}
run(newVal, value) {
// 5.8 如果该任务已存在与任务队列中 则终止
if (watcherQueue.indexOf(this.id) !== -1) return
// 5.9 将当前watcher添加到 队列中
watcherQueue.push(this.id)
const index = watcherQueue.length - 1
Promise.resolve().then(() => {
// 9.0 依赖更新,对watcher进行求值 解决计算属性watcher 不被触发的问题
this.get()
this.cb.call(this.vm, newVal, value)
// 5.10 任务执行结束 将其从任务队列中删除
watcherQueue.splice(index, 1)
})
}
}
// 7.0 获取数组原型对象
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype
// 7.1 声明需要被改造的数组方法 这里举两个例子
const methods = ['push', 'pop']
// 7.2 对数组方法进行改造
methods.forEach(method => {
ArrayMethods[method] = function (...args) {
const oldValue = [...this]
// 7.9 将新插入的数据也变为响应式
if (method === 'push') {
this.__ob__.observeArray(args)
}
// 7.3 传入参数执行原本方法
const result = Array.prototype[method].apply(this, args)
// 7.4 派发依赖更新
this.__ob__.dep.notify(oldValue, this)
return result
}
})
// 14.0 VNode抽象类实现虚拟DOM节点
class VNode {
constructor(tag, attrs, children, text) {
this.tag = tag
this.attrs = attrs
this.children = children
this.text = text
}
}
// 15.0 生成真实DOM
function createEle(vnode) {
// 15.1 为文字节点时
if (!vnode.tag) {
const el = document.createTextNode(vnode.text)
// 15.2 将节点保存起来
vnode.ele = el
return el
}
// 15.3 为元素节点时
const el = document.createElement(vnode.tag)
vnode.ele = el
// 15.4 将子节点也转换成真实DOM 并插入到父节点中
vnode.children.map(createEle).forEach(e => {
el.appendChild(e)
})
return el
}
// 18.6 判断新旧节点是否发生变化
function changed(oldNode, newNode) {
return oldNode.tag !== newNode.tag || oldNode.text !== newNode.text
}
function patch(oldNode, newNode) {
const isRealyElement = oldNode.nodeType
// 18.0 当oldNode=this.$el 为元素节点 页面第一次挂载时
if (isRealyElement) {
let parent = oldNode.parentNode
// 18.1 将vue容器节点替换为 vdom生成的新节点
parent.replaceChild(createEle(newNode), oldNode)
return
}
// 18.2 获取当前vdom的真实dom 上次patch 会在newNode上挂载ele
let el = oldNode.ele
// 18.3 新vdom节点存在 将DOM挂载到vdom.ele上,下次patch 会使用ele
if (newNode) {
newNode.ele = el
}
let parent = el.parentNode
// 18.4 新vdom节点不存在,就删除掉DOM中对应的节点
if (!newNode) {
parent.removeChild(el)
// 18.5 新旧节点标签类型或文本不一致时
} else if (changed(oldNode, newNode)) {
// 18.7 调用createEle生成新DOM节点替换旧DOM节点
parent.replaceChild(createEle(newNode), el)
// 18.8 对比子节点
} else if (newNode.children) {
let newLength = newNode.children.length
let oldLength = oldNode.children.length
// 18.9 遍历新旧vdom节点的所有子节点
for (let index = 0; index < newLength || index < oldLength; index++) {
// 18.10 子节点旧vdom不存在,调用createEle生成DOM插入到父节点el中
if (index > oldLength) {
el.appendChild(createEle(newNode.children[index]))
} else {
// 18.11 其余情况的子节点对比 通过调用 patch实现
patch(oldNode.children[index], newNode.children[index])
}
}
}
}
解析器部分
// 对HTML模板字符串进行解析 最终得到元素树抽象语法树(ElementASTs)
/**
* {
* children: [{…}],
* parent: {},
* tag: "div",
* type: 1, //1-元素节点 2-带变量的文本节点 3-纯文本节点,
* expression:'_s(name)', //type如果是2,则返回_s(变量)
* text:'{{name}}' //文本节点编译前的字符串
* }
*/
function parser(html) {
// 层级栈:记录当前元素的层级
let stack = []
// 根元素节点
let root = null
// 当前元素的父元素节点
let currentParent = null
// 1.0 不断对模板字符串解析
while (html) {
let index = html.indexOf("<")
// 2.1 如果元素之前有文本节点 例: html = "{{name}}<div>1</div></root>"
if (index > 0) {
// 2.2 截取标签前文字部分
let text = html.slice(0, index)
// 5.4 调用parseText工具函数解析文本
let element = parseText(text)
// 5.5 文本节点增加 父节点属性
element.parent = currentParent
// 2.3 将文字节点推进父元素的children中
currentParent.children.push(element)
// 2.4 截掉已经处理完的部分
html = html.slice(index)
// 1.0 如果为开始标签 例: html = "<root>{{name}}<div>1</div></root>"
} else if (html[index + 1] !== '/') {
// 1.1 获取元素类型
let gtIndex = html.indexOf(">")
let eleType = html.slice(index + 1, gtIndex).trim()
// 1.2 如果标签内存在属性 截掉标签属性部分 例: eleType = 'div id="app"' 处理后:eleType = 'div'
let emptyIndex = eleType.indexOf(" ")
let attrs = {}
if (emptyIndex !== -1) {
// 1.3 获取元素标签属性
attrs = parseAttr(eleType.slice(emptyIndex + 1))
eleType = eleType.slice(0, emptyIndex)
}
// 1.4 新建AST节点
const element = {
children: [],
attrs,
parent: currentParent,
tag: eleType,
type: 1
}
// 1.5 没有根元素节点
if (!root) {
root = element
} else {
// 1.6 将当前元素节点推进父元素的children中
currentParent.children.push(element)
}
// 1.7 解析到元素开始标签 推元素进层级栈
stack.push(element)
// 1.8 更新当前父级元素
currentParent = element
// 1.9 截掉已经处理完的部分
html = html.slice(gtIndex + 1)
// 3.0 为结束标签 例: html = "</div></root>"
} else {
let gtIndex = html.indexOf(">")
// 3.1 解析到元素的结束标签 层级栈退一个
stack.pop()
// 3.2 更新当前父级元素
currentParent = stack[stack.length - 1]
// 3.3 截掉已经处理完的部分
html = html.slice(gtIndex + 1)
}
}
return root
}
//解析文本节点
function parseText(text) {
// 未解析的文本
let originText = text
// 有可能是纯文本或者带变量的文本 默认:纯文本
let type = 3
// 节点碎片 元素节点的文本节点可能是多段组成的
// 例:<p>我的 {{name}},我的 {{age}}</p> token=['我的',{{name}},',我的',{{age}}]
let token = []
while (text) {
let start = text.indexOf("{{")
let end = text.indexOf("}}")
//4.0 如果存在插值表达式
if (start !== -1 && end !== -1) {
// 4.1 将文本节点类型标记为 带变量的文本
type = 2
// 4.2 插值表达式前存在纯文本
if (start > 0) {
// 4.3 将插值表达式 前纯文本 推进token
token.push(JSON.stringify(text.slice(0, start)))
}
// 4.4 获取插值表达式内的 表达式
let exp = text.slice(start + 2, end)
// 4.5 解析表达式 并推进token
token.push(`_s(${exp})`)
// 4.6 截掉已经处理完的部分
text = text.slice(end + 2)
// 5.0 不存在插值表达式
} else {
// 5.1 终止解析text 直接推进token
token.push(JSON.stringify(text))
text = ''
}
}
let element = {
text: originText,
type
}
// 5.3 如果type为2带有变量 文本节点需要expression
if (type === 2) {
element.expression = token.join("+")
}
return element
}
// 解析标签属性
function parseAttr(eleAttrs) {
let attrs = {}
attrString = eleAttrs.split(" ")
attrString.forEach(e => {
if (e && e.indexOf("=") !== -1) {
const attrsArr = e.split("=")
attrs[attrsArr[0]] = attrsArr[1]
} else {
attrs[e] = true
}
});
return attrs
}
代码生成器部分
// 将AST转换为渲染函数函数体
/**{
children: [{ … }],
parent: { },
tag: "div",
type: 1, //1-元素节点 2-带变量的文本节点 3-纯文本节点,
expression: '_s(name)', //type如果是2,则返回_s(变量)
text: '{{name}}' //文本节点编译前的字符串
} */
function codegen(ast) {
// 1.0 ast第一层一定是个元素节点
let code = genElement(ast)
return {
// 1.1 渲染函数执行时,传入this改变函数体内this指向。
render: `with(this){return ${code}}`
}
}
// 转换元素节点
function genElement(el) {
// 2.1 获取子节点
let children = genChildren(el)
// 2.0 返回_c(标签名,标签属性对象,标签子节点数组),将标签名变为JSON字符串
return `_c(${JSON.stringify(el.tag)}, ${JSON.stringify(el.attrs)}, ${children})`
}
// 转换文本节点
function genText(node) {
// 5.0 带有变量的文本节点
if (node.type === 2) {
// node.expression 任何变量都会通过this.[node.expression] 进行求值 !!!!
return `_v(${node.expression})`
}
// 5.1 纯文本节点 要变成JSON字符串 要不然会被当成变量处理
return `_v(${JSON.stringify(node.text)})`
}
// 判断类型 转移对应节点
function genNode(node) {
// 4.0 判断节点类型
if (node.type === 1) {
return genElement(node)
} else {
return genText(node)
}
}
// 转换子节点
function genChildren(node) {
// 3.0 判断是否存在子节点
if (node.children && node.children.length > 0) {
// 3.1 转换所有子节点 [ 子节点1,子节点2,...],递归转换所有子节点 genNode--genElement--genChildren--genNode
return `[${node.children.map(node => genNode(node))}]`
}
}