本项目将在GitHub上维护更新。
https://github.com/dangjingtao/FeRemarks
工作比的是内力,而不是一味的模仿。如果你深入弄懂了造飞机的流程,给坦克造个拧螺丝钉还是绰绰有余的。靠卖关键零件赚钱也够了。但如果你说飞机坦克都能组装,就是不会自主研发生产任何东西。就形成不了自身的技术竞争力。在解决关键问题时还得靠大神同事。这就是技术壁垒。
本节将尝试手撸一个核心的vue代码。不妨称之为Due
,因为这是同事的口头禅。也是我火大时唾上最狠的一句。
如果之前对vue的机制一无所知,很有必要先研究下这么一张图:
$mount:挂载:执行编译函数,做三件事
template是一个js。 render渲染出一个虚拟dom树
render(createElement){
return createElement('标签名',attrs:{},[])
}
vue核心在于:响应式机制。而响应式的核心在于defineProperty
初始化通过defineProperty
定义对象getter
和setter
,设置通知机制。
当编译生成的函数被实际渲染的时候,会触发getter
进行依赖收集,数据变化时,触发setter
进行更新。
看一下原理。 在浏览器环境下运行下列代码:
<div id="name"></div>
<script>
var obj={};
Object.defineProperty(obj,'name',{
get(){
console.log('获取name');
return document.querySelector('#name').innerHTML;
},
set(nick){
console.log('设置name')
document.querySelector('#name').innerHTML='nick';
},
})
obj.name='djtao'
console.log(obj.name)
</script>
当你运行obj.name='djtao
时,执行set
,还顺带夹杂了“私货”:把div#name
的内容设置为djtao。
当你打印出obj.name
时,执行get
。因为我们的设置,get还顺便返回了div#name
内的值。
那么,一个响应式雏形就有了。我就不用去做dom操作了。
这里在做的实际上就是observer
做的事情。
对比,更新,
由react首创,就是用JavaScript对象来描述dom结构。数据修改时,先修改虚拟dom中的数据,然后数组做diff。最后再汇总所有的diff。力求把dom操作减少到最少。
对于手写核心来说,需要做到以下内容:
创建一个编译器(complie),编译html。由watcher负责更新。
依赖管理器dep:(管理watcher),每个watcher创建后都跟新到dep中
做之前必须分析来自Due
开发者的需求:
new Due({
data:{
msg:'helloworld'
}
})
在这个Due里面,核心需求就是对data运用观察者模式。然而和上节的obj不同,这个observe可能拥有多个。因此还需要做遍历:
class Due{
constructor(options){
this.$options=options;
//处理data
this.$data=options.data;
// 响应式监听
this.observe(this.$data)
}
在observe
方法中,接收的是this.$data,拿到之后先别急着动,,做个数据类型检测:
observe(data){
// 数据类型检测
if(!data||typeof(data)!=='object'){
return ;
}
// this.$data可能存在多个键值对
// 因此需要遍历对象
Object.keys(data).forEach((key)=>{
return this.defineReactive(data,key,data[key])
})
//等效于以下代码
// for(let attr in data){
// this.defineReactive(data,key,data[key])
// }
}
然后呢,遍历每个对象,执行defineProperty
,在此处我把它放到了this.defineReactive
中。注意:this.$data
可能包含深层次的数据对象,因此需要递归调用this.observe
// 执行深层次的数据劫持
defineReactive(obj,key,val){
Object.defineProperty(obj,key,{
get(){
//直接获取
return val;
},
set(newVal){
// 判断是否更新
if(newVal!==val){
val=newVal;
console.log(`${key}更新了:${newVal}`)
}
}
})
//递归,允许多层嵌套
this.observe(val)
}
一口气写了那么多代码,是时候测试一下了:在js文件中声明我们想要实现的需求:
const app =new Due({
data:{text:'djtao'}
})
console.log(app)
app.$data.text='dangjingtao'
发现我们每一步都能走得通:
目前,我们只是实现了数据的响应式跟踪,还没有真正劫持数据去干点事情。
设想使用Due
的开发者有个需求是:
new Due({
tamplate:`
<div>
<span>{{name1}}</span>
<span>{{name2}}</span>
<span>{{name1}}</span>
</div>
`,
data:{
name1:'',
name2:'',
name3:''
},
created(){
this.name1='djtao';
this.name2='dangjingtao';
}
})
你认为要完成上述需求,我们的数据劫持需要哪些依赖呢?
template
内的插值绑定created
方法;
要做依赖收集,我们要新建一个工厂函数Dep来收集各种依赖,并对其具体位置进行监控(Watcher)。
有一个key,就有一个dependency,template一个地方出现了几次,就有几个watcher。
在上述的需求中,name1出现了两次,它的dep中就有两个watcher。name3压根没出现,它的dep中就没有watcher。class Dep{
constructor(){
// 这个数组用来放依赖
this.deps=[]
}
// 增加watcher的方法
addDep(watcher){
this.deps.push(watcher)
}
// 通知视图更新
notify(){
this.deps.forEach(watcher=>watcher.update())
}
}
class Watcher{
constructor(){
//hack
Dep.target=this;
}
// 更新(做dom操作)
update(){
console.log('属性已更新')
}
}
有了Dep这个类,在Due
内的set就不用去直接操作数据了。完全可以丢给dep。
在类Due
中,每set一次,都应该去通知Dep
。每次get一次,都去把watcher的this扔进Dep中!因此需要重构defineReactive:
// Due
// 执行数据劫持
defineReactive(obj,key,val){
const dep=new Dep()// 新建实例
Object.defineProperty(obj,key,{
get(){
if(Dep.target){
// 把watcher扔进dep!
// 注意函数作用域:
// 这个dep和key是一对一的关系
dep.addDep(Dep.target)
}
return val;
},
set(newVal){
// 判断是否更新
if(newVal!==val){
val=newVal;
dep.notify();//通知deps更新
//console.log(`${key}更新了:${newVal}`)
}
}
})
//递归,允许多层嵌套
this.observe(val)
}
在Due的构造函数中添加下列代码,让程序读一次text属性:
new watcher();
this.$data.text;
成功打印出了watcher.update
的执行过程。依赖通知成功
来点轻松功能吧。开发者出于书写方便,更愿意使用app.text
而非app.$data.text
。接到这个需求应如何实现呢?
proxyData(key){
Object.defineProperty(this,key,{
get(){
return this.$data[key];
},
set(newVal){
// 判断是否更新
if(newVal){
this.$data[key]=newVal;
}
}
})
}
注意,第一个参数由this.$data
(data)变成了this
。
功能实现。
下节将阐述Due另外一个核心——编译器的实现