概念理解
- 什么是Vue?
官方解释,它是构建用户界面的渐进式框架
- 框架和库的区别?
库,本身是一些函数的集合,每次调用函数,实现一个特定的功能(如:jQuery)
框架,是一个完整的解决方案,使用框架的时候,只需要按照它的规则去编写代码
个人理解,它是构建用户界面的一套完整的解决方案,有自己的一套完整流程,它的核心是只关注视图层,不需要开发人员关心数据的处理。
核心:双向绑定和diff算法。
双向绑定原理
简单的例子:
<input id="input" type="text" />
<div id="text"></div>
let input = document.getElementById("input");
let text = document.getElementById("text");
let data = { value: "" };
Object.defineProperty(data, "value", {
set: function(val) {
text.innerHTML = val;
input.value = val;
},
get: function() {
return input.value;
}
});
input.onkeyup = function(e) {
data.value = e.target.value;
};
- 原理实现:
vue是采用数据劫持结合发布者订阅者模式,使用defineproperty劫持各个属性的setter,在数据变动的时候,发布消息给订阅者,触发相应的监听回调。
实现双向绑定,需要实现以下几点:
第一,实现一个监听器observer,observe会通过递归遍历所有数据对象,然后给数据对象的属性都加上setter,getter,如果数据有变动,就会触发setter,拿到最新的值,然后通知订阅者。
第二,实现一个指令解析器compile,compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后渲染页面视图,并对每个指令对应的节点绑定更新函数,同时,在处理指令的时候,会实例化订阅者,一旦数据变动,订阅者收到通知,更新视图。
因为遍历解析的过程中会多次操作dom节点,为提高性能和效率,会先将根节点el转换成文档碎片fragment进行操作,解析完成,再将fragment添加会原来的dom节点中。
指令的声明规定是通过特定前缀的节点属性来标记。
第三,实现一个watcher订阅者,在实现watcher之前,需要先实现一个消息订阅器dep,dep是一个数组,它的作用主要是收集订阅者,当数据变化时会触发它的notify函数,在notify中调用订阅者的update更新方法,以上是实现一个简单的订阅器。
说回来订阅者,订阅者作为observer和compile之间通信的桥梁,主要做的事情有三个,一是在自身实例化的时候,往订阅器里添加自己,第二是它自身必须有一个更新函数,第三是当数据变化,收到dep的通知时,能调用自身的更新方法,并触发compile中绑定的回调。
第四,实例化一个Vue对象,vue作为数据绑定的入口,整合了observer,compile,watcher三者,
以上就是我对vue双向绑定的理解。
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta name="viewport">
<title>66666</title>
<script src='js/jquery.min.js'></script>
</head>
<body>
<div id="app">
<input id="input" type="text" v-model="name"/>
<div id="viewShow">{{name}}</div>
</div>
<script type="text/javascript">
// 实现一个观察者,递归循环将data数据劫持
function observe(data){
// 判断data是否还有子属性,只支持观测对象的子属性为object
if(!data || typeof data !== 'object'){
return;
}
for(let key in data){
defineReactive(data,key,data[key])
}
}
// 劫持数据
function defineReactive(data,key,val){
var dep = new Dep();
observe(val);
Object.defineProperty(data,key,{
enumberable:true,//可枚举
configurable:false,//不能再define
get:function(){
if(Dep.target){
// 判断当前是否有订阅者,若有,则加入订阅器
dep.addSub(Dep.target)
}
console.log("you get it")
return val;
},
set:function(newVal){
console.log("you are updating it")
if(newVal === val){
return;
}
val = newVal;
// 通知所有订阅者数据更新
dep.notify()
}
})
}
// 指令解析
function Compile(el,vm){
// 获取vm对象
this.$vm = vm
// 获取节点
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if(this.$el){
// 创建一个虚拟dom
this.$fragment = this.node2Fragment(this.$el)
// 初始化
this.init();
// 将碎片加入真实dom
this.$el.appendChild(this.$fragment)
}
}
Compile.prototype = {
init:function(){
// 解析虚拟dom
this.compileElement(this.$fragment)
},
node2Fragment:function(el){
// 创建虚拟节点对象
var fragment = document.createDocumentFragment()
var firstChild;
// 先将el.frstChild赋值给firstChild
// append方法具有可移动性,执行后,firstChild会置空
// 循环把真dom中的节点,一个一个放进去
// 直到el.firstChild = null
// 退出循环,返回虚拟dom
while(firstChild = el.firstChild){
fragment.appendChild(firstChild)
}
return fragment;
},
compileElement:function(el){
// compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,
var childNodes = el.childNodes,me = this
// [].slice.call 将参数变成伪数组,可使用数组的各种方法
for(let node of [].slice.call(childNodes)){
var text = node.textContent;// 可获取到的内容有,html元素,空行,元素内的文本
var reg = /\{\{\s*\w+\s*\}\}/
if(me.isElementNode(node)){
// 元素编译
me.compile(node)
} else if(me.isTextNode(node) && reg.test(text)){
// 文本编译且符合reg
// text.match(reg)[0] = "{{name}}"
// 这里只考虑"{{name}}" 暂不考虑多种"{{name}} {{name}}"
me.compileText(node,text.match(reg)[0])
}
if(node.childNodes && node.childNodes.length){
// 子元素继续获取
me.compileElement(node)
}
}
},
compile:function(node){
// 获取当前元素的属性,并遍历
var nodeAttrs = node.attributes,me = this
// v-text="name"
for(let attr of [].slice.call(nodeAttrs)){
var attrName = attr.name; // 如v-text
// 判断当前属性是否为指令
if(me.isDirective(attrName)){
// 属性的值
var exp = attr.value; // name
// 属性的后缀
var dir = attrName.substring(2) // text
if(me.isEventDirective(dir)){
// 是否是事件指令 v-on:click
} else {
if(compileUtil[dir]){
if(dir === 'model'){
// 为model指令
compileUtil[dir](node,me.$vm,exp,attr)
} else {
compileUtil[dir](node,me.$vm,exp)
}
}
}
}
}
},
compileText(node,matchs){
compileUtil.compileTextNode(node,this.$vm,matchs);
},
isElementNode(node){
return node.nodeType == 1;
},
isTextNode(node){
return node.nodeType == 3;
},
isDirective(attr){
return attr.indexOf('v-') == 0;
},
isEventDirective(dir){
return dir.indexOf('on') == 0;
}
}
// 指令处理
var compileUtil = {
reg: /\{\{\s*(\w+)\s*\}\}/, // 匹配 {{ key }}中的key
compileTextNode:function(node,vm,matchs){
// 当前文本内容 "{{name}}"
const rawTextContent = node.textContent;
const key = rawTextContent.match(this.reg)[1] // {{name}} 中的 name
// 首次更新
this.updateTextNode(vm, node, key,rawTextContent)
// 实例化订阅者
new Watcher(vm,key,(newVal,oldVal)=>{
// 回调更新文本
this.updateTextNode(vm, node, key, rawTextContent)
})
},
updateTextNode:function(vm,node,key,rawTextContent){
let newTextContent = rawTextContent;
// 获取 name 的值
const val = this.getModelValue(vm, key);
// 替换文本内容
node.textContent = val;
},
model:function(node,vm,exp,attr){
// model
const { value: keys, name } = attr;
node.value = this.getModelValue(vm,keys);
node.removeAttribute(name)
// input监听
node.addEventListener('input', (e) => {
this.setModelValue(vm, keys, e.target.value);
});
// 实例化订阅者
new Watcher(vm, keys, (oldVal, newVal) => {
// 更新数据
node.value = newVal;
});
},
getModelValue(vm,keys){
return vm[keys];
},
setModelValue(vm,keys,val){
vm[keys] = val
}
}
function Dep(){
this.subs = []
}
Dep.prototype = {
addSub:function(sub){
// 加入订阅器
this.subs.push(sub)
},
notify:function(){
// 通知订阅者
for(let sub of this.subs){
sub.update();
}
}
}
function Watcher(vm,exp,cb){
this.cb = cb;
this.vm = vm;
this.exp = exp;
// 实例化时,将自己加入dep
this.value = this.get();
}
Watcher.prototype = {
update:function(){
this.run();
},
run:function(){
var value = this.vm[this.exp];
var oldValue = this.value;
if(value !== oldValue){
this.value = value
// compile回调
this.cb(this.vm,value,oldValue)
}
},
get:function(){
// 将target指向自己
Dep.target = this;
var value = this.vm[this.exp];//触发data的getter
Dep.target = null;// 用完删除
return value;
}
}
function MVVM(options){
// 传入的参数
this.$options = options;
var data = this._data = this.$options.data
var me = this
for(let key of Object.keys(data)){
// 数据代理
me._proxy(key)
}
// console.log(me.name)
observe(data,this)
this.$compile = new Compile(options.el || document.body,this)
}
//从代码中可看出监听的数据对象是options.data,每次需要更新视图,则必须通过var vm = new MVVM({data:{name: 'kindeng'}}); vm._data.name = 'dmq'; 这样的方式来改变数据。
MVVM.prototype = {
_proxy:function(key){
var me = this;
Object.defineProperty(me,key,{
configurable:false,
enumberable:true,
get:function proxyGetter(){
return me._data[key];
},
set:function proxySetter(newVal){
me._data[key] = newVal
}
})
}
}
new MVVM({
el:'#app',
data:{
name:'yang'
}
})
</script>
</body>
</html>
自己画的简略图:


引申提问
- 为什么要进行依赖收集?
如果模板中没有使用到数据,但该数据在data中定义了,当该数据改变的时候,会触发setter事件,重新渲染页面,这样显然会影响性能,所以呢,需要进行依赖收集,数据在模板中用到就收集,没有用到,就不收集了。
- 依赖是怎么收集的?
在解析模板指令的时候,会实例化订阅者watcher,watcher实例化的时候会触发defineprotype的getter方法,同时会标记一个target,然后在defineprotype的getter方法中,会去做一个判断,如果target存在,就会把当前target添加到订阅器中。
- 基于vue数据绑定,如果data中的数据进行了一秒1000次的改变,每次改变会全部显示在页面中吗?
不会。因为dom异步更新。
- 在new vue()中,data可以直接是一个对象,为什么在vue组件中,data必须是一个函数呢?
因为,每一个vue组件通过new Vue()实例化,引用的是同一个对象,如果组件中的data直接是一个对象的话,那么一旦修改组件的data,其他组件相同的数据就会被改变,而如果data是一个函数,那么组件的data就会有一个自己的作用域,做到互不干扰。
v-model实现原理
v-model是vue的一个指令,主要是在input,textarea,select,radio,checkbox,以及子组件上实现数据双向绑定。
v-model在元素中实现:以input元素为列子,
1.指令解析:模板在编译的时候,会将元素中的指令进行解析。
2.v-model会被拆分成两段代码,第一个是,将元素的value值绑定到data数据比如message中,同时给元素添加input事件,触发input事件,动态的把当前input的value赋值给message。
-
text 和 textarea 元素使用 value 属性和 input 事件;
-
checkbox 和 radio 使用 checked 属性和 change 事件;
-
select 字段将 value 作为 prop 并将 change 作为事件。
<input
v-bind:value="message"
v-on:input="message=$event.target.value">
v-model在组件中实现:它会默认传入一个prop,并且prop的名称为value,再定义一个触发事件。
v-model修饰符
.lazy
在触发 change 事件的时候进行数据同步
.number
将输入的值自动转成数字类型
使用 parseFloat() 函数对输入的值进行处理,如果输入的值是 parseFloat() 函数不能解析的,如以非数字开头的字符串,就会返回原始值。
.trim
去掉收尾空白字符。
v-bind分析
v-bind可绑定的类型:
-
v-bind:key
-
v-bind:title
-
v-bind:class
-
v-bind:style
vue-cli
首先我们要知道,vue-cli生成的项目,帮我们配置好了哪些功能?
- ES6代码转换成ES5代码
- scss/sass/less/stylus转css
- .vue文件转换成js文件
- 使用 jpg、png,font等资源文件
- 自动添加css各浏览器产商的前缀
- 代码热更新
- 资源预加载
- 每次构建代码清除之前生成的代码
- 定义环境变量
- 区分开发环境打包跟生产环境打包