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資料做資料回應 =====》理解Vue中通過this.$data可以取所有我們寫的data資料*/
this.$data = options.data||{};/*data不為空則取data,data為空則取一個空物件*/
/*短路或*/
/*避免$data為undefined*/
}
}
defineProperty
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app"></div>
<script>
/*
* Object.defineProperty對obj物件中的name屬性進行挾持======ES6新特性
* 函式劫持屬性
* 一旦該屬性發生變化則會觸發set函式,做出回應操作,做出視圖與資料的雙向系結
*/
var obj = {};
obj.name="hello";
/*但要回圈取*/
/*es6引擎---監聽系結 基于es6的js引擎*/
Object.defineProperty(obj,"name",{/*靜態方法*/
get(){
return document.querySelector("#app").innerHTML;
//return document.getElementById("app").innerHTML;
},
set(val){
document.querySelector("#app").innerHTML=val;
}
})
</script>
</body>
</html>
原理刨析
- Observer:資料監聽器,對資料物件所有的屬性進行監聽,有變動時拿到最新值并通知Watcher
- Compile:指令決議器,對每個元素節點的指令進行掃描和決議,根據指令模板替換資料,并系結相應的更新函式
- Watcher:連接Observer和Compile的橋梁,接收Observer發出的屬性變動通知,執行指令系結的相應回呼函式,從而更新視圖
視頻講解地址
講解1
講解2
代碼匯總
/*
* 創建QVue類,接收一個options物件
*/
class QVue{
//構造方法
constructor(options){
/*快取option物件資料 $是為了防止命名污染*/
this.$options = options;
/*
* opthins====
* {
* el:"#app",
* data:{
* name:"name",
* age:12
* }
*/
/*取出data資料做資料回應 =====》理解Vue中通過this.$data可以取所有我們寫的data資料*/
this.$data = options.data||{};/*data不為空則取data,data為空則取一個空物件*/
/*短路或*/
/*避免$data為undefined*/
//監聽資料的變化
this.observe(this.$data);
//對應一個set get方法
//決議各種指令
//this:vue物件==app物件
//決議代碼段 options.el==id=app段 ====== 里面的{{ }}文本節點,v-xxx屬性節點
new Compile(options.el,this);
}
//觀察資料變化
observe(data){
//$data不存在或不是物件型別則不予監聽
/*{}[]都是object*/
if( !data || typeof data !== "object"){
return;
}
//取所有的鍵(陣列的話取出下標) ["name","age"]
let keys = Object.keys(data);//keys==Object內置物件方法,回傳一個物件自身可列舉屬性組成陣列
//回圈每一個建 在$data app物件分別添加set get方法
keys.forEach( (key)=>{
//回圈每一個物件中的鍵值對
//資料回應 物件 屬性 屬性值
this.defineReactive(data,key,data[key]);
//代理data中的屬性到vue實體上
//model中有的view-model中也要一份 model中的真實資料映射一份到view-model中
this.proxyData(key);
});
}
//資料回應 操作$data model
// $data name "name"
// $data age "12"
//給每個屬性添加一個set get函式==》感知資料變化
defineReactive(data,key,val){
//解決資料層次嵌套,遞回系結監聽 $data中屬性下還是一個物件
/*如:
* data:{
* name:"name",
* addr:{
* pro:"湖南",
* city:"長沙"
* }
* }
* 若不回圈,則監聽的是addr物件,只有物件地址改變才監聽得到,當值改變pro="北京",物件得地址并沒有改變,所以監聽不到
*/
this.observe(val);
//管理watcher:(請求和回應與誰)
const dep= new Dep();
//此處data指$data物件/*es6引擎---監聽系結 基于es6的js引擎*/
//開始系結 開始資料監聽了 物件,屬性名
Object.defineProperty(data,key,{//第一次時并沒有執行,只是系結set和get方法
get(){//獲取當前key對應的值
//向管理watcher的物件追加watcher實體,方便管理,每一個都要系結監聽
//target監聽發給誰 必須要有目標物件,然后就是添加Watcher ============= Watcher 連接 Observer
//添加對應Watcher
//此時target 會是一個watcher物件 添加watchers陣列統計
Dep.target && dep.addWatcher(Dep.target);//完了之后target會等于null
//回傳當前屬性的值
return val;
},
set(newVal){
//值沒有變化
if(newVal===val){
return;
}
//只有值發生變化才發出更新通知
val=newVal;
//通知對應watcher起作用 將頁面重新渲染一次
dep.notify();
}
})
}
//代理data 復制一份
proxyData(key){//鍵名
//this指vue物件 == app,key指$data中的每個鍵
//view-model中也生成一套相同的model 雙向系結
Object.defineProperty(this,key,{
get(){
return this.$data[key];//前面$data中的都系結了set get 此處就相當于呼叫了$data中的get方法
//this.$data[key] ==> 相當與呼叫$data中get函式 ==> 85行
},
set(newVal){
this.$data[key] = newVal;//此處就相當于呼叫了$data中的set方法
}
})
}
//在view-model中同樣生成一套set get方法,聯系起來
}
//管理watcher 中間物件 用來存一些資料 方便A類存值 B類取值
class Dep{
constructor() {
//儲存每個屬性對應的$data屬性對應的watcher
this.watchers=[];//初始化 陣列存盤多個watcher
}
//添加
addWatcher(watcher){
this.watchers.push(watcher);
}
//通知所有watcher更新
notify(){
this.watchers.forEach((watcher)=>{
watcher.update();//通知更新
})
}
}
//觀察者,具體更新
class Watcher{
constructor(vm,key,func) {
//vue實體
this.vm = vm;//app
//需要更新的key
this.key=key;//"name"
//更新后執行的函式 回呼函式XXXUpdater
this.func=func;
//將當前watcher實體指定到Dep靜態屬性target,用于類間通信
Dep.target = this;//本來Dep原來沒有target屬性,在此處就加上了
//觸發get,添加依賴
this.vm[this.key];
//app[name] ===> 相當于呼叫物件app中中get函式 ===> 相當于呼叫$data中的get函式 ==> 85行
Dep.target=null;
}
update(){//func==updateFn && updateFn(node,value)回呼函式
// vue物件 對應屬性值 ===== 相當于呼叫textUpdater(value)...通過這個函式更新到界面上去
this.func.call(this.vm,this.vm[this.key]);
}
}
/*
* 雙向系結原理
* 編譯時可以決議出v-model在操作時,在使用v-model元素上添加了一個事件監聽(input)
* 將事件監聽的的回呼函式作為事件監聽的回呼函式
* input發生變化的時候,將最新的值設定在vue實體上,因為vue已經實作資料的回應化
* 回應化的set函式會觸發界面中所有的函式依賴模塊的更新,
* 然后通知哪些model做更新,所以所有個這個資料有關的東西就更新了
*/
class Compile{
//el就是 #app 的層 vm:view-model物件==app物件 this指Compile物件
constructor(el,vm) {
this.$el = document.querySelector(el);//el就是id名
this.$vm=vm;//app物件
if(this.$el){//此處忽略<template>====加個else就好 el和template按兩套標準決議
//決議節點內容
this.$fragment = this.node2Fragment(this.$el);
//傳入節點數的根節點
this.compile(this.$fragment);
this.$el.appendChild(this.$fragment);
}
}
//將宿主元素的代碼片段取出來,遍歷=====高效
//只考慮id="xx" 沒有考慮<template>
//此函式呼叫一次的話 只能取到第一層孩子節點
node2Fragment(el){
//創建代碼段的根節點===app節點
const frag = document.createDocumentFragment();
let child;
//直到取出的節點為undefined或null
while(child = el.firstChild){
frag.appendChild(child);
}
return frag;
}
compile(el){
//宿主節點下所有的子元素
const childNodes = el.childNodes;
//轉成陣列 迭代開始 迭代每一個節點
Array.from(childNodes).forEach((node)=>{//第一個node就是第一個子元素
//判斷是不是元素節點(標簽) 文本節點(內容)看是不是插值語法 還有一個屬性節點
if(this.isElement(node)){
//如果是元素,列印一下標簽的名字
console.log("編譯元素"+node.nodeName);
//拿元素上的所有屬性元素
const nodeAttrs = node.attributes;
//將其轉換成陣列后回圈每個屬性節點
//屬性節點
Array.from(nodeAttrs).forEach((attr)=>{
//屬性名 判斷是普通屬性 v-xxx @ :===普通屬性不需要操作
const attrName = attr.name;
//屬性值 判斷需要做什么操作
const attrValue = attr.value;
//指令以 q-(真正的vue是v-)
if(this.isDirective(attrName)){
//q-text q-html q-on q-model對應v-xxx
//獲取指令后面的內容
const dir = attrName.substring(2);
//執行更新————this:compile物件
//查找compile物件中的text() html() on() model
//this[dir]找里面自己定義的準備決議的函式或者屬性=====取到宣告部分
//這些函式都有三個引數(節點物件,vm物件(app),屬性值,型別)
this[dir] && this[dir](node,this.$vm,attrValue);//激活函式同時傳入引數
}
//是事件處理 @或v-on
if(this.isEvent(attrName)){
//取出事件名 @click
let dir = attrName.substring(1);
//事件處理 四個引數(節點物件,vm物件,屬性值,事件型別)
this.eventHandler(node,this.$vm,attrValue,dir);
}
})
}else if(this.isInterPolation(node)){
//如果是插值文本
this.compileText(node);
console.log("編譯文本"+node.textContent);
}
//看有沒有下一級 遞回子元素,解決元素嵌套問題====子下面還有標簽
//有子節點且長度不為0
if(node.childNodes && node.childNodes.length){
this.compile(node);
}
})
}
//是否為元素節點
isElement(node){
return node.nodeType===1;
}
//是否為文本節點 且有胡子語法===捕獲到陳述句中的{{內容}}
isInterPolation(node){
return node.nodeType===3 && /\{\{(.*)\}\}/.test(node.textContent);
}
//是否為指令 q-xxx (對應vue中的v-html,v-once,v-on,,,)
isDirective(attr){
return attr.indexOf("v-")==0;//判斷q-開頭
}
//是否為事件 @開頭的屬性
isEvent(attr){
return attr.indexOf("@")==0;//判斷@開頭
}
//更新函式——橋接
update(node,vm,exp,dir){
//``執行里面的占位符 ${dir}:運行運算式(eval());獲取事件型別
const updateFn = this[`${dir}Updater`];//textUpdater
//初始化 不為空且激活函式 (標簽物件,Vue物件的屬性值 )
updateFn && updateFn(node,vm[exp]);//呼叫textUpdater函式
//vm[exp] === 在app物件中尋找exp屬性
//依賴收集 回呼函式
new Watcher(vm,exp,function(value){
updateFn && updateFn(node,value);
})
}
//v-text
text(node,vm,attrValue){
this.update(node,vm,attrValue,"text");
}
textUpdater(node,value){
node.textContent = value;//修改節點上文本內容
}
//雙向系結 v-model
model(node,vm,exp){
//指定input的value屬性,模型到視圖的系結
this.update(node,vm,exp,"model");
//視圖對模型的回應 添加一個輸入事件
//input--->blur事件也可以
node.addEventListener('input',(e)=>{
vm[exp] = e.target.value;
})
}
modelUpdater(node,value){
node.value=value;
}
html(node,vm,exp){
this.update(node,vm,exp,"html");
}
htmlUpdater(node,value){
node.innerHTML=value;//讓標簽起作用
}
//更新插值文本
compileText(node){
let key = RegExp.$1;
this.update(node,this.$vm,key,"text");
}
//事件處理器 節點物件,vm物件,屬性值,事件型別
eventHandler(node,vm,exp,dir){
let fn = vm.$options.methods && vm.$options.methods[exp];
if(dir && fn){
node.addEventListener(dir,fn.bind(vm));
}
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/395372.html
標籤:其他
