最近在跟着教学视频学习vue源码,做了一些总结,希望能对学习vue有所帮助
知识要点
- vue工作机制
- vue响应式原理
- 依赖收集与追踪
- 编译compile
通过实现的入门级源码一步一步实现vue的简单功能指令
Vue响应式原理:defineProperty
<div id="app">
<p>你好,<span id="name"></span></p>
</div>
<script>
var obj = {}
Object.defineProperty(obj,"name",{
get(){
console.log('获取name');
return document.querySelector('#name').innerHTML;
},
set(names){
console.log('设置name');
document.querySelector('#name').innerHTML=names;
}
})
obj.name='jerry';
console.log(obj.name)
</script>
//测试代码
<script src="./kvue.js"></script>
<script>
const app=new KVue({
data:{
test:'I am test',
foo:{
bar:"bar"
}
}
});
app.test='hello';
app.foo.bar='hello'
</script>
//KVue.js
class KVue{
//构造器接收option选项参数
constructor(options){
//讲选项参数赋值给当前对象的选项属性
this.$options=options;
//将选项里面的data赋值给当前对象的data
this.$data=options.data;
//监听data变化
this.observe(this.$data)
}
observe(value){
//判断传进来的参数是不是对象,如果不是就直接返回
if(!value||typeof value!=='object'){
return;
}
//参数是对象,则对对象的每一个属性进行遍历
Object.keys(value).forEach(key=>{
//响应式传参,传入当前对象,对象属性,属性值
this.defineReactive(value,key,value[key]);
//设置代理
this.proxyData(key);
})
}
proxyData(key){
//对当前的对象进行响应式数据绑定,为每一个key设置代理
Object.defineProperty(this,key,{
get(){
return this.$data[key];
},
set(newVal){
this.$data[key]=newVal;
}
})
}
defineReactive(obj,key,val){
//对传入的属性值进行深层次的遍历,判断是否是对象,如果是则再一次进行监听
this.observe(val)
Object.defineProperty(obj,key,{
get(){
return val;
},
set(newVal){
if(val!==newVal){
val=newVal;
console.log(`${key}更新`)
}
}
})
}
}
依赖收集与追踪
这是在响应式原理上建立起来的依赖收集与追踪,重复部分不再注释
class KVue{
constructor(option){
this.$options=option;
this.$data=option.data;
this.observe(this.$data)
new Compile(option.el,this)
if(option.created){
option.created.call(this)
}
}
observe(value){
if(!value||typeof value!=='object'){
return;
}
Object.keys(value).forEach(key=>{
this.defineReactive(value,key,value[key])
this.proxyData(key)
})
}
proxyData(key){
Object.defineProperty(this,key,{
get(){
return this.$data[key];
},
set(newVal){
this.$data[key]=newVal
}
})
}
defineReactive(obj,key,val){
this.observe(val)
const dep=new Dep()
Object.defineProperty(obj,key,{
get(){
Dep.target&&dep.addDep(Dep.target)
return val;
},
set(newVal){
if(val!==newVal){
val=newVal
// console.log(`${key}属性更新了`)
dep.notify()
}
}
})
}
}
//Dep管理若干Watcher,它和key存在一对一的关系
class Dep{
constructor(){
//建立deps依赖的空数组集
this.deps=[];
}
addDep(watcher){
//每监听到一个依赖,就将依赖传入deps
this.deps.push(watcher)
}
notify(){
//提示更新的方法,如果有变化发生,就将deps依赖数组进行遍历更新
this.deps.forEach(dep=>dep.updata())
}
}
//保存ui中依赖,实现update函数可以更新
class Watcher {
//构造函数中传入三个参数:实例,属性名,回调函数
constructor(vm, key,cb) {
this.vm = vm;
this.key = key
this.cb=cb
//将当前实例指向Dep.target
Dep.target = this
this.vm[this.key];
Dep.target =null;
}
updata() {
console.log(`${this.key}更新了,${this.vm}`)
//回调函数的更新
this.cb.call(this.vm,this.vm[this.key])
}
}
编译器compile
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
//1.$el中的内容都搬家到一个fragment,提高操作效率
this.$fragment = this.node2Fragment(this.$el);
// console.log(this.$fragment)
//2.编译fragment
this.compile(this.$fragment)
// console.log(this.$fragment)
//3.讲编译结果追加到宿主中
this.$el.appendChild(this.$fragment)
}
}
//遍历el,将里面的内容搬到新创建的fragment
node2Fragment(el) {
const fragment = document.createDocumentFragment();
let child;
//判断当前文档还有没有元素,有的话会一直执行
while ((child = el.firstChild)) {
//appendChild会将子元素移入fragment
fragment.appendChild(child);
}
return fragment
}
compile(el) {
//遍历el
const childNodes = el.childNodes;
console.log(childNodes)
//对当前实例的每一个子节点进行遍历
Array.from(childNodes).forEach(node => {
//判断当前元素类型
if (this.isElement(node)) {
// console.log('编译元素:'+node.nodeName)
//判断是否是带有指令k- 或者@的方法
this.compileElement(node)
} else if (this.isInterpolation(node)) {
//判断是否是带{{}}的元素,是的话进行文本编译
this.compileText(node)
}
//如果当前元素还有子节点,则对其进行再次编译
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
//判断当前元素类型的方法
isElement(node) {
return node.nodeType === 1;
}
//判断是否是带{{}}的元素的方法
isInterpolation(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
compileText(node) {
const exp = RegExp.$1
//进行更新,传入当前节点,当前实例对象,立即双括号中的内容
this.update(node, this.$vm, exp, 'text')
}
//对方法进行抽离,传入节点,实例,内容,以及类型
update(node, vm, exp, dir) {
//根据传入的类型进行组合方法名
const fn = this[dir + 'Updator']
//如果方法存在则执行方法,传入当前节点,以及内容
fn && fn(node, vm[exp])
//监听当前实力上这个节点的变化,如果改变了,重新执行更新方法
new Watcher(vm, exp,function(){
fn && fn(node, vm[exp])
})
}
//判断指令的方法并进行对应指令操作
compileElement(node){
//将当前节点的所有属性存储在nodeAttr上
const nodeAttrs=node.attributes;
Array.from(nodeAttrs).forEach(attr=>{
//对节点元素属性进行遍历,属性名称为attrName,属性值为exp
const attrName=attr.name;
const exp=attr.value;
if(attrName.indexOf('k-')===0){
const dir=attrName.substring(2)
//如果节点属性名称是以k-开头,则执行k-后面的指令名称的方法
this[dir]&&this[dir](node,this.$vm,exp)
}else if(attrName.indexOf('@'===0)){
//如果节点属性名称是以@开头,则执行@后面的指令名称的方法
const eventName=attrName.substring(1)
this.eventHandle(node,this.$vm,exp,eventName)
}
})
}
//k-text文本更新
text(node,vm,exp){
this.update(node,vm,exp,'text')
}
//双向数据绑定
model(node,vm,exp){
this.update(node,vm,exp,'model')
node.addEventListener('input',e=>{
vm[exp]=e.target.value
})
}
//k-html
html(node,vm,exp){
this.update(node,vm,exp,'html')
}
//text文本更新方法
textUpdator(node, value) {
node.textContent = value
}
//html更新方法
htmlUpdator(node, value) {
console.log(value)
node.innerHTML = value
}
//model更新方法
modelUpdator(node, value) {
console.log(value)
node.value = value
}
eventHandle(node,vm,exp,eventName){
const fn=vm.$options.methods&&vm.$options.methods[exp]
if(eventName&&fn){
node.addEventListener(eventName,fn.bind(vm))
}
}
}
写到这里我们可以通过自己编写的vue源码实现以下的案例
<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>
<div k-html="html"></div>
</div>
<script src="./compile.js"></script>
<script src="./kvue.js"></script>
<script>
const kaideba=new KVue({
el:'#app',
data:{
name:'I am test',
age:12,
html:'<button>这是一个按钮</button>'
},
created(){
console.log('开始');
setTimeout(()=>{
this.name='我是测试'
},1000)
},
methods:{
changeName(){
this.name='hello',
this.age=1
}
}
})
</script>