从以下几个方面,深入理解vue底层原理
- Vue工作机制
- Vue响应式的原理
- 依赖收集与追踪
- 编译compile
Vue工作机制
在new Vue()之后。Vue会进行初始化,初始化生命周期、事件、props、methods、data、computed与watch等。其中最重要的是通过Object.defineProperty设置setter与getter,用来实现响应式和依赖收集
初始化之后调用$mount 指定挂载组件,启动编译器,编译器再生成渲染函数即更新函数与依赖收集,生成虚拟DOM(一个js对象),当数据修改时,先修改虚拟DOM中的数据,然后数组做diff,最后汇总所有的diff,做真实DOM更新。
Vue响应式的原理defineProperty
class Vue {
constructor(options) {
this._data = options.data;
this.observer(this._data);
}
observer(value) {
if (!value || typeof value !== "object") {
return;
}
Object.keys(value).forEach(key => {
this.defineReactive(value, key, value[key]);
});
}
defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true /* 属性可枚举 */,
configurable: true /* 属性可被修改或删除 */,
get() {
return val;
},
set(newVal) {
if (newVal === val) return;
this.cb(newVal);
}
});
}
cb(val) {
console.log("更新数据了", val);
}
}
let o = new Vue({
data: {
test: "I am test."
}
});
o._data.test = "hello";
依赖收集与追踪
new Vue({
template:
`<div>
<span>{{text1}}</span>
<span>{{text2}}</span>
<div>`,
data: {
text1: 'name1'
},
created(){
this.text1="hello"
}
})
text1被修改,所以视图更新,但是text2视图没用到,所以不需要更新,如何实现呢,就需要我们的依赖收集
// 依赖收集
class Dep {
constructor () {
// 存数所有的依赖
this.deps = []
}
// 在deps中添加一个监听器对象
addDep (dep) {
this.deps.push(dep)
}
// 通知所有监听器去更新视图
notify () {
this.deps.forEach((dep) => {
dep.update()
})
}
}
class Watcher {
constructor () {
// 在new一个监听器对象时将该对象赋值给Dep.target,在get中会用到
Dep.target = this
}
// 更新视图的方法
update () {
console.log("视图更新啦~")
}
}
我们在增加了一个 Dep 类的对象,用来收集 Watcher 对象。读数据的时候,会触发 reactiveGetter 函数把当前的Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。
写数据的时候,则会触发 reactiveSetter 方法,通知Dep 类调用 notify 来触发所有 watcher 对象的update 方法更新对应视图
constructor(options) {
this._data = options.data
this.observer(this._data)
// 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象
new Watcher();
// 在这里模拟render的过程,为了触发test属性的get函数
console.log('模拟render,触发test的getter', this._data.test);
}
defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 将Dep.target(即当前的Watcher对象存入Dep的deps中)
dep.addDep(Dep.target)
return val
},
set: function reactiveSetter(newVal) {
if (newVal === val) return
// 在set的时候触发dep的notify来通知所有的Watcher对象更新视图
dep.notify()
}
})
}
编译compile
核心逻辑 获取dom,遍历dom,获取{{}}、k-和@开头的 ,设置响应式
目标功能
<body>
<div id="app">
<p>{{name}}</p>
<p k-text="name"></p>
<p>{{age}}</p>
<p>
{{doubleAge}}
</p>
<input type="text" k-model="name">
<button @click="changeName">呵呵</button>
compile.js
<div k-html="html"></div>
</div>
<script src='./compile.js'></script>
<script src='./kaikeba-vue.js'></script>
<script>
let kaikeba = new Vue({
el:'#app',
data: {
name: "I am test.",
age:12,
html:'<button>这是一个按钮</button>'
},
created(){
console.log('开始啦')
setTimeout(()=>{
this.name='我是蜗牛'
}, 1500)
},
methods:{
changeName(){
this.name = '哈喽,世界'
this.age=1
this.id = 'xx'
console.log(1,this)
}
}
})
</script>
</body>
compile.js
// el: 元素
// vm: vue的实例
class Compile {
constructor(el,vm) {
this.$vm = vm
// 拿到需要遍历的宿主
this.$el = document.querySelector(el)
if (this.$el) {
// 将$el内部的东西转化为文档碎片Fragment,不能直接大量操作DOM
this.$fragment = this.node2Fragment(this.$el)
// 开始编译
this.compileElement(this.$fragment)
// 将编译好的html追加至$el(追加)
this.$el.appendChild(this.$fragment)
}
}
node2Fragment(el) {
// 新建文档碎片 dom接口
let fragment = document.createDocumentFragment()
let child
// 将原生节点拷贝到fragment
// 此处做的是移动操作,循环一次,el中的元素就少一个
while (child = el.firstChild) {
fragment.appendChild(child)
}
return fragment
}
compileElement(el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach((node) => {
let text = node.textContent
// 表达式文本
// 就是识别{{}}中的数据
let reg = /\{\{(.*)\}\}/
// 按元素节点方式编译
if (this.isElementNode(node)) {
this.compile(node)
} else if (this.isInterpolation(node) && reg.test(text)) {
// 文本 并且有{{}}
this.compileText(node, RegExp.$1)
}
// 遍历编译子节点,递归
if (node.childNodes && node.childNodes.length) {
this.compileElement(node)
}
})
}
compile(node) {
let nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach( (attr)=>{
// 规定:指令以 v-xxx 命名
// 如 <span v-text="content"></span> 中指令为 v-text
let attrName = attr.name // v-text
let exp = attr.value // content
if (this.isDirective(attrName)) {
let dir = attrName.substring(2) // text
// 普通指令
this[dir] && this[dir](node, this.$vm, exp)
}
// @
if(this.isEventDirective(attrName)){
let dir = attrName.substring(1) // text
this.eventHandler(node, this.$vm, exp, dir)
}
})
}
compileText(node, exp) {
// node.textContent = this.$vm.$data[RegExp.$1]
// RegExp.$1 为{{name}}中的name,数据只会初始化一次,
// 如果数据发生变化是没用的,需要更新函数
this.text(node, this.$vm, exp)
}
isDirective(attr) {
// k-
return attr.indexOf('k-') == 0
}
isEventDirective(dir) {
// @
return dir.indexOf('@') === 0
}
isElementNode(node) {
return node.nodeType == 1
}
// 插值文本
isInterpolation(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
text(node, vm, exp) {
this.update(node, vm, exp, 'text')
}
html(node, vm, exp) {
this.update(node, vm, exp, 'html')
}
// 双向绑定
model(node, vm, exp) {
this.update(node, vm, exp, 'model')
let val = vm.exp
node.addEventListener('input', (e)=>{
let newValue = e.target.value
vm[exp] = newValue
val = newValue
})
}
// 核心函数:根据指令dir,来确定更新函数
update(node, vm, exp, dir) {
let updaterFn = this[dir + 'Updater']
// 初始化数据,只走一次
updaterFn && updaterFn(node, vm[exp])
// 需要依赖收集
new Watcher(vm, exp, function(value) {
updaterFn && updaterFn(node, value)
})
}
// 事件处理
eventHandler(node, vm, exp, dir) {
let fn = vm.$options.methods && vm.$options.methods[exp]
if (dir && fn) {
node.addEventListener(dir, fn.bind(vm), false)
}
}
textUpdater(node, value) {
node.textContent = value
}
// 动态插入html
htmlUpdater(node, value) {
node.innerHTML = value
}
// 将文本的值映射到input框中
modelUpdater(node, value) {
node.value = value
}
}
入口文件
class Vue {
constructor(options) {
// 数据响应化
this.$data = options.data
this.$options = options
this.observer(this.$data)
// 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象
// 将实例挂在Dep的静态属性target上,静态属性会覆盖
// new Watcher()
// 在这里模拟render的过程,为了触发test属性的get函数
console.log('模拟render,触发test的getter', this.$data)
// created执行
if(options.created){
options.created.call(this)
}
this.$compile = new Compile(options.el, this)
}
observer(value) {
if (!value || (typeof value !== 'object')) {
return
}
//给每个$data上的属性,数据响应化
Object.keys(value).forEach((key) => {
// 代理data中的属性到vue实例上
this.proxyData(key)
// 数据响应化
this.defineReactive(value, key, value[key])
})
}
defineReactive(obj, key, val) {
this.observe(val) // 为对象时,需要深层次数据响应
// 每个key都有一个独立的依赖管理器,在适时将watcher放入(obj有几个key就有多少个Dep)
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// dep.addDep(new Watcher()) // 在读取属性时,将watcher添加到依赖管理器
// 将Dep.target(即当前的Watcher对象存入Dep的deps中)
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
// console.log(key + '属性更新了' + newVal + '可以编译模板,更新页面数据')
// 在set的时候触发dep的notify来通知所有的Watcher对象更新视图
// 依赖收集与追踪,通知依赖管家,告诉所有watcher可以更新了
dep.notify()
}
})
}
proxyData(key) {
Object.defineProperty(this, key, {
configurable: false,
enumerable: true,
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
}
}
依赖收集 Dep
class Dep {
constructor() {
// 存数所有的依赖(watcher)
this.deps = []
}
// 在deps中添加一个监听器对象
addDep(dep) {
this.deps.push(dep)
}
depend() {
Dep.target.addDep(this)
}
// 通知所有监听器去更新视图
notify() {
this.deps.forEach((dep) => {
dep.update()
})
}
}
监听器
// 监听器
class Watcher {
constructor(vm, key, cb) {
// 在new一个监听器对象时将该对象赋值给Dep.target,在get中会用到
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
this.cb = cb
this.vm = vm
this.key = key
this.value = this.get()
}
get() {
// Dep.target指向实例,静态属性target,会覆盖之前的new watcher
Dep.target = this
// 触发geeter,添加依赖
let value = this.vm[this.key]
Dep.target = null
return value
}
// 更新视图的方法
update() {
this.cb.call(this.vm, this.value)
}
}