1、Vue的工作机制

1.1、初始化
- 在 new Vue() 后会调用_init()进行初始化,包括初始化生命周期、data, props, methods, computed与watch等。其中最重要的是通过Object.defineProperty设置setter与getter,用来实现【响应式】以及【依赖收集】。
- 初始化之后调用 $mount 挂载组件,主要执行编译和首次更新。
1.2、编译
在非webpack的开发环境比如带着编译器的浏览器环境,编译要把template转化成render function,这个过程包括三个阶段:
- parse:使用正则解析template中的vue的指令(v-xxx) 变量等等,形成抽象语法树AST
- optimize:标记一些静态节点,用作后面的性能优化,在diff的时候直接略过
- generate:把第一部分成的AST 转化,生成渲染函数 render function
渲染函数最终返回虚拟DOM.
1.3、虚拟DOM
Virtual DOM是react首创,Vue2开始支持,就是用javascript对象来描述dom结构【如下所示】,数据修改的时候,我们先修改虚拟dom中的数据,然后数组做diff,最后再汇总所有的diff,力求做最少的dom操作, 毕竟js里对比很快,而真实的dom操作太慢。
<div title="虚拟DOM示例" style="colir: red;" @click="test">
<button>click me</button>
</div>
{
tag: "div",
props: {
title: "虚拟DOM示例",
style: { color: red },
onClick: test
},
children: [
{
tag: "button",
text: "click me"
}
]
}
1.4、更新
执行render function的时候,会有一个依赖收集的过程,即通过getter把data中的数据添加到watcher. 当data中的数据被修改时,就会触发setter, 然后watcher会通知进行修改。Vue通过Diff算法对比新旧vdom树,得到最小修改,就是patch,最后patch()通过批量的真实的DOM操作将有变化的数据在对应DOM节点进行修改。
2、实现简版Vue
今天,我们要手写的简版Vue,如下图所示,主要关注【响应式】这个过程,即如何劫持监听data对象的所有属性,如何进行依赖收集,订阅数据变化,在数据变化时如何更新视图以及解析一些简单的Vue指令等。

// my-vue.js
class XVue {
constructor(opts) {
this.$opts = opts
// 处理data选项
this.$data = opts.data
// 1 拦截并监听data中的所有属性
this.observe(this.$data)
// 在vue实例创建后就要开始执行首次编译
new Compile(opts.el, this)
// 执行created
if (opts.created) {
opts.created.call(this)
}
}
// 1 拦截并监听data中的所有属性
observe(dataObj) {
// this.$data必须为对象,不考虑函数形式
if (!dataObj || typeof dataObj !== 'object') return;
// 1.1 遍历data中的所有属性
Object.keys(dataObj).forEach(key => {
// 1.2 为data中的所有属性设立响应式
this.defineReactive(dataObj, key, dataObj[key])
// 4.1 把this.$data.key代理到this.key上
this.provyData(key)
})
}
// 1.2 为data中的所有属性设立响应式
defineReactive(obj, key, val) {
// 递归
this.observe(val)
// 2.1 一个data中的key对应一个Dep
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// 2.2 一个页面中对data的key的引用对应一个Watcher
// 将Dep.target添加到dep中
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
// 2.3 执行每一个Watcher的update方法
dep.notify()
}
}
})
}
// 4. 代理data对象
provyData(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
}
}
// 2. 收集依赖: 一个data中的key对应一个Dep
class Dep {
constructor() {
this.deps = []
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => dep.update())
}
}
// 3. 通知更新:一个页面中对data的key的引用对应一个Watcher[一个Dep数组可能有多个同名的Watcher]
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
// 将当前watcher实例指定到Dep静态属性target
Dep.target = this
// 触发getter,添加依赖
this.vm[this.key]
// 回收
Dep.target = null
}
update() {
this.cb.call(this.vm, this.vm[this.key])
}
}
//compile.js
class Compile {
constructor(el, vm) {
// 1. 获取DOM
this.$el = document.querySelector(el)
this.$vm = vm
if (this.$el) {
// 提取宿主中模板内容到Fragment标签,dom操作会提⾼效率
this.$fragment = this.node2Fragment(this.$el)
// 编译模板内容,同时进⾏依赖收集
this.compile(this.$fragment)
// 把编译好的内容插入到根节点元素中
this.$el.appendChild(this.$fragment)
}
}
node2Fragment(el) {
const fragment = document.createDocumentFragment()
let child
while ((child = el.firstChild)) {
fragment.appendChild(child)
}
return fragment
}
compile(el) {
// 2. 遍历子元素
const childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
// 3. 判断节点类型并执行响应的编译方法
if(this.isElement(node)) {
// 3.1 元素节点,查找x-, @
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr => {
const attrName = attr.name //属性名
const attrVal = attr.value //属性值
if (this.isDirective(attrName)) {
// 3.3 如果是指令: x-text,x-html,x-model
const dir = attrName.substring(2)
this['compile' + dir] && this['compile' + dir](node, this.$vm, attrVal)
}
if (this.isEvent(attrName)) {
// 3.4 如果是事件@
const dir = attrName.substring(1)
this.compileevent(node, this.$vm, attrVal, dir)
}
})
} else if (this.isInterpolation(node)) {
// 3.2 插值文本{{xxx}}节点
this.compileInterpolation(node)
}
// 递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
// 3. 判断节点类型
// 3.1 元素节点
isElement(node) {
return node.nodeType === 1
}
// 3.2 插值文本{{xxx}}节点
isInterpolation(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
// 3.3 指令
isDirective(attr) {
return attr.indexOf('x-') === 0
}
// 3.4 事件
isEvent(attr) {
return attr.indexOf('@') === 0
}
// 4. 不同类型节点的编译方法
// 4.1 编译插值文本{{}}
compileInterpolation(node) {
const exp = RegExp.$1
this.update(node, this.$vm, exp, 'text')
}
// 4.2 编译指令x-text
compiletext(node, vm ,dataKey) {
this.update(node, vm, dataKey, 'text')
}
// 4.3 编译指令x-html
compilehtml(node, vm, dataKey) {
this.update(node, vm, dataKey, 'html')
}
// 4.3 编译指令x-model
compilemodel(node, vm, dataKey) {
// 指定input的value属性
this.update(node, vm, dataKey, 'model')
// 视图对模型响应:双向绑定
node.addEventListener("input", e => {
vm[dataKey] = e.target.value
})
}
// 4.4 编译事件@
compileevent(node, vm, dataKey, dir) {
let fn = vm.$opts.methods && vm.$opts.methods[dataKey]
if (dir && fn) {
node.addEventListener(dir, fn.bind(vm))
}
}
// 5. 公共的更新方法
update(node, vm, dataKey, dir) {
const updateFn = this[dir + 'Update']
// 初始化
updateFn && updateFn(node, vm[dataKey])
// 依赖收集,即当data变化,更新视图
new Watcher(vm, dataKey, function(newVal) {
updateFn && updateFn(node, newVal)
})
}
// 6. 不同类型节点的渲染方法
// 6.1 文本节点
textUpdate(node, val) {
node.textContent = val
}
// 6.2 html节点x-html
htmlUpdate(node, val) {
node.innerHTML = val
}
// 6.3 双向数据绑定
modelUpdate(node, val) {
node.value = val
}
}
<!--TestMyVue.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>测试自己的简版Vue</title>
</head>
<body>
<div id="app">
<!-- 测试插值 -->
<h1>{{name}}</h1>
<p>{{name}}</p>
<!-- 测试x-text -->
<h2 x-text="age"></h2>
<!-- 测试x-html -->
<h3 x-html="html"></h3>
<!-- 测试双向数据绑定x-model -->
<input type="text" x-model="name" />
<!-- 测试事件 -->
<button @click="changeName">呵呵</button>
</div>
<script src='./compile.js'></script>
<script src='./my-vue.js'></script>
<script>
const app = new XVue({
el: '#app',
data: {
name: "我是name属性",
age: 12,
html: '<button @click="changeAge">这是一个按钮</button>'
},
created() {
setTimeout(() => {
this.name = 'name属性发生变化了,视图更新'
}, 1500)
},
methods: {
changeName() {
this.name = 'name属性又变化了'
},
changeAge() {
this.age ++
}
}
})
</script>
</body>
</html>