defineProperty
1.概述
Object.defineProperty(objName,"attributeName",{
get(){return ..},
set(newVal){..}
})
-->挟持objName对象中的attributeName属性(ES6新特性:函数劫持属性)
一旦attributeName属性发成改变则会触发set函数,做出响应
-->数据与视图的双向绑定
2.案例
<div id="app"></div>
<script>
var obj = {};
obj.name="hello";
Object.defineProperty(obj,"name",{
get(){
return document.querySelector("#app").innerHTML;
//return document.getElementById("app").innerHTML;
},
set(val){
document.querySelector("#app").innerHTML=val;
}
})
</script>
data的取值原理
- html
<div id="app">
<p>{{name}}</p>
</div>
<script type="text/javascript" src="1.data的取值.js" ></script>
<script>
var app = new QVue({
el:"#app",
data:{
name:"name",
age:12
}
})
</script>
- js
//创建QVue类,接收一个options对象
class QVue{
//构造方法
constructor(options){
//缓存option对象数据, $是为了防止命名污染
this.$options = options;
/* opthins====
* {
* el:"#app",
* data:{
* name:"name",
* age:12
* }
*/
//取出data数据做数据响应
//data==null?{}:data
this.$data = options.data||{};
//短路或,避免$data为undefined
}
}
原理刨析
1.Observer
- 数据监听器
- 对数据对象所有的属性进行监听,有变动时拿到最新值并通知Watcher
2.Compile
- 指令解析器
- 对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,并绑定相应的更新函数
3.Watcher
- 连接Observer和Compile的桥梁
- 接收Observer发出的属性变动通知,执行指令绑定的相应回调函数,从而更新视图
视频讲解地址
代码汇总
/*
*双向绑定原理
* 页面元素的值发生变化时,将最新的值设置在vue实例中,因为vue已经实现数据的响应化
* 响应化的set函数会触发界面中所有函数依赖模块的更新,所以所有有该数据模块就都更新了
*/
//创建QVue类,接收一个options对象
class QVue{
//构造方法
constructor(options){
//缓存option对象数据, $是为了防止命名污染
this.$options = options;
//取出data数据做数据响应
this.$data = options.data||{};
//短路或,避免$data为undefined*/
//监听数据的变化,搭建observer与watcher的桥梁
this.observe(this.$data);
//解析vue语法对应的指令,并渲染到页面上,搭建compile与watcher的桥梁
//解析el对应id的容器中的文档树(文本节点{{}},属性节点v-xx)
new Compile(this,options.el);
//此处this已经绑定好数据监听(app的set/get,即data的set/get)
}
//观察数据变化
observe(data){
//$data不存在或不是对象类型则不予监听,{}[]是object
if( !data || typeof data !== "object"){
return;
}
//取data中所有的属性名 (数组的话取出下标) ["name","age"]
let keys = Object.keys(data);
//keys:Object内置对象方法,返回对象自身的属性组成的数组
//循环data的每一个属性名 -->$data,app对象分别添加set get方法
keys.forEach( (key)=>{
//数据响应-defineProperty(data中的每个属性设置set/get)----(对象,属性名,属性值)
this.defineReactive(data,key,data[key]);
//代理data中的属性到vue实例-defineProperty(app的data中的每个属性设置set/get)
this.proxyData(key);
//model中有的映射一份到view-model(view-model=Watcher桥梁)--双向绑定
});
}
//数据响应defineProperty(给每个属性添加set/get方法,感知数据变化)
defineReactive(data,key,val){
//解决数据层次嵌套,递归绑定监听
this.observe(val);
//-->data中属性还是一个对象 data:{name:"name",addr:{pro:"湖南",city:"长沙"}}
//若不循环,则监听的是addr对象,只有对象地址改变才监听得到,当值改变监听不到
//管理watcher(请求和响应与谁)
const dep= new Dep();
//开始数据监听--data对象
Object.defineProperty(data,key,{//第1次时,只是绑定set/get,并没有执行(不是因为值改变)
get(){
//update中new Watcher时会调用Watcher构造函数,构造函数中Dep.target=watcher的对象,然后会调用get函数就到了此处
Dep.target && dep.addWatcher(Dep.target);
//addWatcher会向watchers数组中保存target对象,也就是watcher对象
//返回之后Watcher构造函数继续执行Dep.target=null
//返回当前属性的值
return val;
},
set(newVal){
if(newVal===val){//值没有变
return;
}
//值发生变化-->更新
val=newVal;
//值在更新,通知所有的watcher起作用,将页面中vue重新渲染一次
dep.notify();
}
})
}
//代理data中的属性到vue实例上--app对象
//model中有的映射一份到view-model(view-model=Watcher桥梁)--双向绑定
proxyData(key){//key=data中的属性名,this=app
Object.defineProperty(this,key,{
get(){
return this.$data[key];
//$data中的所有属性都绑定过set/get,此处就相当于调用了$data中的get方法
},
set(newVal){
this.$data[key] = newVal;
//此处就相当于调用了$data中的set方法
}
})
}
}
//解析vue语法对应的指令,并渲染到页面上,搭建compile与watcher的桥梁
class Compile{
//vm:app对象 -- el:层的id名 -- this:Compile对象
constructor(vm,el) {
this.$vm=vm;
this.$el=document.querySelector(el);
//此处忽略<template>-->解决:加个else,el和template按两套标准解析
if(this.$el){
//解析节点内容,将宿主元素的代码片段取出
this.$fragment = this.nodeFragment(this.$el);
//将vue语法对应的内容渲染上$fragment代码片段
this.compile(this.$fragment);
//将$fragment渲染到页面上
this.$el.appendChild(this.$fragment);
}
}
//解析节点内容,将宿主元素的代码片段取出
nodeFragment(el){
//创建根节点
const frag = document.createDocumentFragment();
let child;
while(child = el.firstChild){//直到取出的节点为undefined或null
frag.appendChild(child);
}
return frag;
}
//分析容器内的vue语法,渲染$fragment代码片段
compile(el){
//取宿主节点下所有的子元素
const childNodes = el.childNodes;
//转成数组,迭代每个子元素
Array.from(childNodes).forEach((childNode)=>{
//判断是不是元素节点(标签)
if(this.isElement(childNode)){
console.log("编译元素节点的name:"+childNode.nodeName);
//取出元素节点上所有的属性
const nodeAttrs = childNode.attributes;
//转成数组,迭代每个属性节点
Array.from(nodeAttrs).forEach((nodeAttr)=>{
//取属性名 ---> 判断是普通属性(不需要操作),还是vue语法中的属性(v-xxx/:/@)
const attrName = nodeAttr.name;
//取属性值 ---> 如果不是普通属性,判断需要做的操作
const attrValue = nodeAttr.value;
//判断是不是指令 v-开头
if(this.isDirective(attrName)){
//取指令v-后面的内容
const dir = attrName.substring(2);
//执行更新 --> 不同的dir操作在compile中有对应的,以dir命名的函数来实现(text()/html()/on()..)
this[dir] && this[dir](this.$vm,childNode,attrValue);
//this[dir]:寻找compile中的dir属性 ---> 取到声明部分
//&&:有对应的方法则继续
//this[dir](this.$vm,childNode,attrValue):激活dir函数同时传入参数(vue实例(app),子元素,属性值)
}
//判断是不是事件处理 @
if(this.isEvent(attrName)){
//取出事件名 @click--click
let dir = attrName.substring(1);
//事件处理 (vue实例(app),子元素,属性值,事件类型)
this.eventHandler(this.$vm,childNode,attrValue,dir);
}
})
}else if(this.isInterPolation(childNode)){
//判断是不是文本节点(内容是不是插值语法)
//更新插值文本
this.compileText(childNode);
console.log("插值文本:"+childNode.textContent);
}
//递归子元素,解决元素嵌套问题 --> 有子节点且长度不为0
if(childNode.childNodes && childNode.childNodes.length){
this.compile(childNode);
}
})
}
//是否为元素节点
isElement(node){
return node.nodeType===1;
}
//是否为文本节点(内容是不是插值语法{{内容}})
isInterPolation(node){
return node.nodeType===3 && /\{\{(.*)\}\}/.test(node.textContent);
}
//是否为指令(v-xx)
isDirective(attr){
//indexOf:返回字符串第一次的索引
return attr.indexOf("v-")==0;
}
//是否为事件(@xx)
isEvent(attr){
return attr.indexOf("@")==0;
}
//更新函数——桥接
update(vm,node,exp,dir){
//获取dir对应的函数名
const updateFn = this[`${dir}Updater`];//``执行里面的占位符${dir}
//函数不为空则激活函数 (标签对象,vue对象的属性值 )
updateFn && updateFn(node,vm[exp]);//vm[exp]:在app对象中寻找exp属性
//依赖收集 --> 桥梁——设置target,触发get,添加依赖
new Watcher(vm,exp,function(value){
updateFn && updateFn(node,value);
})
}
//v-text
text(vm,node,exp){
this.update(vm,node,exp,"text");
}
textUpdater(node,value){
//修改节点上文本内容
node.textContent = value;
}
//v-model
model(vm,node,exp){
this.update(vm,node,exp,"model");
//对input添加监听 -- 当事件被触发时,调用回调函数
node.addEventListener('input',(e)=>{
vm[exp] = e.target.value;
})
}
modelUpdater(node,value){
node.value=value;
}
//v-html
html(vm,node,exp){
this.update(vm,node,exp,"html");
}
htmlUpdater(node,value){
node.innerHTML=value;
}
//更新插值文本
compileText(node){
let key = RegExp.$1;
this.update(this.$vm,node,key,"text");
}
//事件处理器 --> exp:回调函数名
eventHandler(vm,node,exp,dir){
//判断是否存在事件中调用的回调函数
let fn = vm.$options.methods && vm.$options.methods[exp];
if(dir && fn){
//给事件添加监听
node.addEventListener(dir,fn.bind(vm));
//bind创建新函数(第一个参数指定新函数中的this)
}
}
}
//桥梁——设置target,触发get,添加依赖
class Watcher{
constructor(vm,key,func) {
//vue实例
this.vm = vm;
//vue实例中需要更新的属性值
this.key=key;
//更新执行回调函数XXXUpdater
this.func=func;
//给Dep新添加属性target(目标对象),target=当前Watcher实例,用于类间通信
Dep.target = this;//本来Dep原来没有target属性,在此处才加上的
//触发get,添加依赖
this.vm[this.key];//app[name] --> 相当于调用对象app中get --> 相当于调用$data中get
Dep.target=null;
}
//激活func回调函数---具体的更新操作
update(){
//通过回调函数更新页面 (vue实例,实例对应属性值)
this.func.call(this.vm,this.vm[this.key]);
//this --> watcher对象,每个对象的更新操作(即回调函数func的内容)可能不同
//func --> updateFn && updateFn(node,value)调用对应类型的函数更新
}
}
//管理Watcher的中间对象,用来存数据---方便A类存值,B类取值
class Dep{
constructor() {
//页面中每个vue语法对应一个watcher
this.watchers=[];//初始化
}
//添加
addWatcher(watcher){
this.watchers.push(watcher);
}
//通知所有watcher更新
notify(){
this.watchers.forEach((watcher)=>{
//通知更新
watcher.update();
})
}
}