关于源码的部分总结
-
编译的重要性:首先vue模板中的很多语法html是不能识别的,例如插值表达式、指令等,其次我们通过编译的过程可以进行依赖收集,依赖收集后
data
中的数据模型就跟数据产生了绑定关系,当数据模型发生变化就可以通知依赖做更新,最终实现模型驱动视图变化
-
双向绑定的原理:双向绑定是指在
input
元素上使用 v-model
指令,在编译时解析 v-model
然后给当前元素加上事件监听,将 v-model
的回调函数作为 input
的回调函数,如果input
发生变化就可以更新对应值,值又是绑定在 vue
实例上的,实例同时又做了数据响应化也就是数据劫持,会触发他的setter
函数,然后通知对应依赖进行更新
相关方法的理解
-
Object.defineProperty()
直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
-
Document.createDocumentFragment
创建一个新的空白的文档片段
1. vue
整体执行流程
1. new Vue()
1.1 通常new Vue()之后,Vue会调用进行初始化,初始化生命周期、事件、props、methods、data、computed、watch等
1.2 在beforeCreate之后、create之前,data数据的劫持、属性代理
- 主要是通过Object.defineProperty给每个属性添加Dep实例,负责收集与该属性有依赖关系的Watcher(这个Dep实例可以存储当前属性的依赖、添加依赖、通知属性更新)
- 将属性定义到当前创建的vue实例上,同时创建属性的setter、getter函数,实时获取与修改实例中的data数据
2. 调用$mount挂载组件
3. compile编译(对template模板进行扫描)
3.1 转换根节点内容为DocumentFragment对象
- 通过document.createDocumentFragment创建空白文档片段,将根节点中的所有子元素添加至文档片段中,然后编译DocumentFragment对象
3.2 正则解析vue的指令,形成语法树(AST)
- 遍历所有子节点,分别匹配元素节点(v-、@、:开头的)、文本节点(插值表达式)
3.3 标记静态节点,用于性能优化
- 匹配对应节点后,调用对应的更新函数,更新node节点的值,同时创建依赖(也就是watcher),一个节点属性对应一个watcher
- 创建依赖时,会将依赖存入每个属性的Dep实例中(同一个变量在模板中使用几次,对应属性的Dep实例数组中就保存几个watcher)
- 同时创建属性的setter、getter函数,在访问和修改对象属性时,更新对应的依赖
3.4 将编译后的结果追加至根节点
3.5 generate 将AST转为渲染函数render function
4. render function
4.1 生成虚拟DOM树,后期改变数据改变的其实都是虚拟DOM上的数据,在更新前会做diff算法的比较,通过计算后执行最小更新,这个流程是用js计算时间换来了更少的dom操作,核心目的就是减少浏览器的页面渲染次数和数量
4.2 依赖收集,通过数据劫持、观察者模式,发现数据发生变化,然后找到对应的DOM节点进行更新
2. 实现数据响应(数据劫持)部分,利用defineProperty
对属性进行读取、改变操作时,会执行属性对应的 getter
、setter
函数
// 写个小例子感受下defineProperty,在obj对象上定义一个新属性
// 设置属性的getter、setter函数
// defineProperty.html
<body>
<div id="app">
<p id="name"></p>
</div>
<script>
let obj = {}
Object.defineProperty(obj,'name', {
get () {
return document.querySelector('#name').innerHTML
},
set (val) {
document.querySelector('#name').innerHTML = val
}
})
obj.name = 'Asher'
</script>
</body>
下面实现数据响应部分(数据劫持)
// 先约定好使用方法,和vue一样
new Vue({
el: '',
data: {},
methods: {}
})
// svue.js
class SVue {
constructor (options) {
this.$options = options
// 数据响应
this.$data = options.data
this.observe(this.$data)
}
observe (val) {
if(!val || typeof val !== 'object') return
// 遍历data的属性
Object.keys(val).forEach(key => {
this.defineReactive(val, key, val[key])
})
}
// 数据响应化函数(数据劫持)
defineReactive (obj, key, val) {
Object.defineProperty (obj, key, {
get () {
return val
},
set (newVal) {
if(val === newVal) return
val = newVal
console.log(`${key}属性更新了:${val}`)
}
})
}
}
// index.html
// 执行当前文件后,发现只打印了一个'属性更新了'
// 因为data属性中,有一个foo属性的值是一个对象,因此需要考虑深度遍历的问题
<script src="svue.js"></script>
<script>
const app = new SVue({
data: {
test: 'I am test',
foo: {
bar: 'bar'
},
name: 'Asher'
}
})
app.$data.test = 'hello, SVue' // test属性更新了:hello, SVue
app.$data.foo.bar = 'oh bar'
</script>
// svue.js
// 修改数据响应化函数,添加递归
defineReactive (obj, key, val) {
// 添加递归 解决数据嵌套
this.observe(val)
Object.defineProperty (obj, key, {
get () {
return val
},
set (newVal) {
if(val === newVal) return
val = newVal
console.log(`${key}属性更新了:${val}`)
}
})
}
}
4. 依赖收集:遍历模板( template
)进行依赖收集,在更新数据时,只有能匹配到有对应依赖时才会做数据更新
// 通过一个简单例子理解下依赖收集
// 在下面这个例子中,data数据中有name1、name2和name3,但实际在元素中使用并展示的数据只有name1、name2,而且name1使用2次,但是在created函数中又更新了name1和name3
// 这时的内部执行机制是先进行视图依赖收集,保存视图中有哪些地方对数据有依赖,这样当数据变化时,直通知对应依赖即可,没有依赖的则不做更新
new Vue({
template:
`
<div>
<span>{{name1}}</span>
<span>{{name2}}</span>
<span>{{name1}}</span>
</div>
`,
data: {
name1: 'name1',
name2: 'name2',
name3: 'name3'
},
created () {
this.name1 = 'Asher',
this.name3 = 'Andy'
}
})
实现依赖收集、观察者部分,针对 data
中的每个属性创建每个对应的依赖,当属性在页面中出现几次,那么就有几个 watcher
,这个过程大概就是每个依赖有自己各自的 watcher
,依赖对自己的 watcher
进行管理
// svue.js
// 用来管理watcher
class Dep {
constructor () {
// 存放若干依赖(watcher,一个watcher对应一个属性)
this.deps = []
}
// 添加依赖
addDep (dep) {
this.deps.push(dep)
}
// 通知watcher,更新依赖
notify () {
this.deps.forEach(dep => dep.update())
}
}
// watcher
class Watcher {
constructor () {
// 将当前watcher实例指向Dep静态属性target
Dep.target = this
}
update () {
console.log('属性更新了')
}
}
模拟依赖创建(注意这里只是模拟!模拟!new Watcher()
并不在这里调用)
// 在构造函数中模拟watcher创建,修改数据劫持部分
constructor (options) {
this.$options = options
// 数据响应
this.$data = options.data
this.observe(this.$data)
// 模拟watcher创建,每改变一个属性就要创建一个watcher
new Watcher()
this.$data.test;
new Watcher()
this.$data.foo.bar;
}
// data属性响应式
defineReactive (obj, key, val) {
// 递归 解决数据嵌套
this.observe(val)
// 初始化 Dep
const dep = new Dep()
Object.defineProperty(obj, key, {
get () {
Dep.target && dep.addDep(Dep.target)
return val
},
set (newVal) {
if(val === newVal) return
val = newVal
dep.notify()
}
})
}
5. 编译:在vue中使用的插值表达式、v-指令
、@事件
等等,这些标识,html
都不认识
使用 document.createDocumentFragment()
创建空白文档片段,主要用于将元素附加到该文档上,然后将文档片段附加到DOM树,这样不会引起页面回流,避免频繁操作DOM更新页面
// compile.js
class Compile {
// 当前遍历元素 当前vue实例
constructor (el, vm) {
// 要遍历的节点
this.$el = document.querySelector(el)
this.$vm = vm
if(this.$el) {
// 转换内部内容为Fragment
this.fragment = this.node2Fragment(this.$el)
// 执行编译
this.compile(this.fragment)
// 将编译后的结果追加至$el
this.$el.appendChild(this.fragment)
}
}
// 编译
compile (el) {
const childsNodes = el.childNodes
Array.from(childsNodes).forEach(node => {
if(this.isElement(node)) {
// 元素节点
console.log('编译元素' + node.nodeName)
}else if(this.isInterpolation(node)) {
// 文本节点 插值格式
console.log('编译文本' + node.textContent)
}
// 递归子节点
if(node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
node2Fragment (el) {
const frag = document.createDocumentFragment()
// 将el中所有子元素移至frag中
let child;
while(child = el.firstChild) {
frag.appendChild(child)
}
return frag
}
// 元素子节点
isElement (node) {
return node.nodeType === 1
}
// 文本子节点 插值表达式{{}}
isInterpolation (node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
}
修改之前的文件,测试一下编译部分的逻辑
// index.html
<div id="app">
<p>{{name}}</p>
<p s-text="name"></p>
<p @click="handleClick">点击更换文案</p>
<input type="text" s-model="name">
</div>
<script src="svue.js"></script>
<script src="compile.js"></script>
// svue.js
constructor (options) {
this.$options = options
// 数据响应
this.$data = options.data
this.observe(this.$data)
// 编译
new Compile(options.el, this)
}
根据 index.html
结构可以得到打印结果,以及页面展示
完成元素节点的匹配,下面就是针对各种类型的节点做不同处理,例如文本节点插值表达式,我们要根据使用的属性在 data
中获取到对应的值,然后才能正确的展示在页面上
// compile.js
// 编译文本节点
compileText (node) {
// RegExp.$1 获取正则表达式中第一个()分组匹配到的值
// 将变量值复制给DOM并展示
node.textContent = this.$vm.$data[RegExp.$1]
}
完成部分编译之后,每编译一部分内容就要绑定对应的依赖,要不然我们是没办法做到依赖监听和及时更新的,数据也只会初始化一次,我们来测试下现在没有绑定依赖的情况(参照如下文件的修改)
// index.html
<div id="app">
<p>{{name}}</p>
<p s-text="name"></p>
<p @click="handleClick">点击更换文案</p>
<input type="text" s-model="name">
</div>
<script>
const app = new SVue({
el: 'app',
data: {
test: 'I am test',
foo: {
bar: 'bar'
},
name: 'Asher'
},
// 设置生命周期以及延时执行修改name
created () {
console.log('created执行了')
setTimeout(() => {
console.log('created中的setTimeout执行了')
this.name = 'created中修改name属性'
}, 1500)
},
})
</script>
// svue.js
constructor (options) {
this.$options = options
// 数据响应
this.$data = options.data
this.observe(this.$data)
// 编译
new Compile(options.el, this)
// 匹配created 修改this绑定
if(options.created) {
options.created.call(this)
}
}
从下面打印的结果可以看出,created
和延时函数都执行了,但页面展示的结果并没有改变,说明数据并未更新
下一步,处理属性代理,添加更新逻辑,属性代理是将属性绑定到当前vue实例上,为了方便在访问属性时可以直接通过 this.data
访问
// svue.js
class SVue {
// 观察者
observe (val) {
if(!val || typeof val !== 'object') return
// 遍历data的属性
Object.keys(val).forEach(key => {
this.defineReactive(val, key, val[key])
// 代理data中的属性到vue实例上
this.proxyData(key)
})
}
proxyData (key) {
// this 指当前vue实例
Object.defineProperty(this, key, {
get () {
return this.$data[key]
},
set (newVal) {
this.$data[key] = newVal
}
})
}
}
// compile.js
// 通用更新函数 参数:节点、实例、表达式、指令(区分文本、事件等,方法名前缀)
update (node, vm, exp, dir) {
const updaterFn = this[dir + 'Updater']
// 初始化
updaterFn && updaterFn(node, vm[exp])
// 依赖收集
new Watcher(vm, exp, function(val){
updaterFn && updaterFn(node, val)
})
}
textUpdater (node, val) {
node.textContent = val
}
// 修改 compileText 方法
compileText (node) {
this.update(node, this.$vm, RegExp.$1, 'text')
}
// svue.js
// 修改 Watcher 类
// watcher
class Watcher {
constructor (vm, key, callback) {
this.vm = vm
this.key = key
this.callback = callback
// 将当前watcher实例指向Dep静态属性target
Dep.target = this
// 触发getter 添加依赖
this.vm[this.key]
// 置空 避免重复添加
Dep.target = null
}
update () {
// console.log('属性更新了')
this.callback.call(this.vm, this.vm[this.key])
}
}
接着补充编译部分对指令、事件等的识别和处理
// compile.js
// 编译
compile (el) {
const childsNodes = el.childNodes
Array.from(childsNodes).forEach(node => {
if(this.isElement(node)) {
// 元素节点
console.log('编译元素' + node.nodeName)
// 查找s-、@、:开头的
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr => {
// 属性名
const attrName = attr.name
// 属性值
const exp = attr.value
if(this.isDirective(attrName)) {
const dir = attrName.substring(2)
this[dir] && this[dir](node, this.$vm, exp)
}
if(this.isEvent(attrName)) {
const dir = attrName.substring(1)
this.eventHandler(node, this.$vm, exp ,dir)
}
})
}else if(this.isInterpolation(node)) {
// 文本节点 插值格式
console.log('编译文本' + node.textContent)
this.compileText(node)
}
// 递归子节点
if(node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
// 指令
isDirective (attr) {
return attr.indexOf('s-') == 0
}
// 事件
isEvent (attr) {
return attr.indexOf('@') == 0
}
// 事件处理 从methods找出对应方法
eventHandler (node, vm, exp, dir) {
let fn = vm.$options.methods && vm.$options.methods[exp]
if(dir && fn) {
node.addEventListener(dir, fn.bind(vm))
}
}
text (node, vm, exp) {
this.update(node, vm, exp, 'text')
}
下面可以添加一个事件测试一下
// index.html
<div id="app">
<p>{{name}}</p>
<p s-text="name"></p>
<p @click="handleClick">点击更换文案</p>
<input type="text" s-model="name">
</div>
methods: {
handleClick () {
this.name = 'Click'
}
}
最后,补充双向绑定的部分,通过指令 s-modle
使用
// compile.js
module (node, vm, exp) {
// 指定input的value属性
this.update(node, vm, exp, 'model')
node.addEventListener('input', e => {
vm[exp] = e.target.value
})
}
moduleUpdater (node, value) {
node.value = value
}
// 编译
compile (el) {
const childsNodes = el.childNodes
Array.from(childsNodes).forEach(node => {
if(this.isElement(node)) {
// 元素节点
console.log('编译元素' + node.nodeName)
// 查找s-、@、:开头的
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr => {
// 属性名
const attrName = attr.name
// 属性值
const exp = attr.value
if(this.isDirective(attrName)) {
const dir = attrName.substring(2)
this[dir] && this[dir](node, this.$vm, exp)
}
if(this.isEvent(attrName)) {
const dir = attrName.substring(1)
this.eventHandler(node, this.$vm, exp ,dir)
}
})
}else if(this.isInterpolation(node)) {
// 文本节点 插值格式
console.log('编译文本' + node.textContent)
this.compileText(node)
}
// 递归子节点
if(node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}