vue 源码分析(尚硅谷视频学习笔记)
下面是我邊看視頻變記錄的重點難點,詳細具體有條理的看我轉載的那篇尚硅谷課件,課件有的內容我基本不重復寫到
1.類數組和數組
用document.getElementsByClassName()方法或者jQuery方法獲取的標簽集合是類數組,不是真正的數組。使用instanceof可以知道lis instanceof Object為true; lis instanceof Array為false。假如用lis接收,那么lis可以使用lis[index]的方式獲取某一個標簽對象,但是不可以使用Array(數組)原型鏈上的屬性和方法。如forEach、splice、join等。
2.類數組使用forEach遍歷所有標簽對象(類數組轉數組)
ES6: Array.from(lis)
ES5:[].slice.call(lis) 或者 Array.prototype.slice.call(lis)
3.節點類型(nodeType)
常用的:
Document
Element
Attribute
Text
node.nodeType獲取節點類型(Number)
4.定義屬性Object.defineProperty(obj, prop, descriptior)
在descriptior內定義set和get方法,可以實現屬性的監聽和獲取。
IE8不支持此語法,此方法是Vue實現數據綁定等操作的核心方法。
屬性描述符:
1.數據描述符:
configurable: true/false 是否可以重新define
enumerable: true/false 是否可以枚舉(for…in / keys())
value: 指定初始值
writable: true/false value 是否可以修改
2.訪問描述符:
get: 回調函數, 用來得到當前屬性值
set: 回調函數, 用來監視當前屬性值的變化
5.判斷是否是自身屬性(obj.hasOwnProperty(pro))
JS學過,返回布爾值。
6.DocumentFragment: 文檔碎片(高效批量更新多個節點)
內存中保存n個element的容器對象(不與界面關聯),如果更新fragment中的某個element,界面不變。
多次更新界面變成一次更新界面,減少更新界面的次數。
一個節點只能有一個父親
使用步驟:
7.數據代理
vue 數據代理: 通過vm 對象來代理data 對象中所有屬性的操作
vm._data.name === vm.name
8.set和get執行的斷點調試
打斷點后觀察可知:
1.vm對象對data中的數據實現數據代理
2.vm中觸發回調函數執行,函數中調用_propxy()方法,里面是用Object.defineProperty(obj, prop, descriptior)實現。
3.修改屬性值調用Object.defineProperty(obj, prop, descriptior)中的descriptior對象中的set方法,數據被存入data。
4.頁面中顯示數據調用get方法從data中取數據。
9.大佬仿Vue的main.js
/* 相關于Vue的構造函數*/ function MVVM(options) {// 將選項對象保存到vmthis.$options = options;// 將data對象保存到vm和datq變量中var data = this._data = this.$options.data;//將vm保存在me變量中var me = this;// 遍歷data中所有屬性Object.keys(data).forEach(function (key) { // 屬性名: name// 對指定屬性實現代理me._proxy(key);});// 對data進行監視observe(data, this);// 創建一個用來編譯模板的compile對象this.$compile = new Compile(options.el || document.body, this) }MVVM.prototype = {$watch: function (key, cb, options) {new Watcher(this, key, cb);},// 對指定屬性實現代理_proxy: function (key) {// 保存vmvar me = this;// 給vm添加指定屬性名的屬性(使用屬性描述)Object.defineProperty(me, key, {configurable: false, // 不能再重新定義enumerable: true, // 可以枚舉// 當通過vm.name讀取屬性值時自動調用get: function proxyGetter() {// 讀取data中對應屬性值返回(實現代理讀操作)return me._data[key];},// 當通過vm.name = 'xxx'時自動調用set: function proxySetter(newVal) {// 將最新的值保存到data中對應的屬性上(實現代理寫操作)me._data[key] = newVal;}});} };10.模板解析
利用到fragment。
1.將元素節點轉移到內存中
2.在內存中生成需要的元素節點(init,初始化,涉及到遞歸),遍歷fragment中所有的子元素節點。利用正則匹配將空文本節點、數據節點等區分進行操作。
3.最后一次性添加到頁面
10.1{{data}}如何變成具體數值
正則中的子匹配():
()中的可以匹配到data的內容并儲存起來。
Vue中有一套用來更新標簽的指令方法。
調用函數的層次很深。。。
11. compile.js
function Compile(el, vm) {// 保存vm到compile對象this.$vm = vm;// 保存el元素到compile對象this.$el = this.isElementNode(el) ? el : document.querySelector(el);// 如果el元素存在if (this.$el) {// 1. 取出el中所有子節點, 封裝在一個framgment對象中this.$fragment = this.node2Fragment(this.$el);// 2. 編譯fragment中所有層次子節點this.init();// 3. 將fragment添加到el中this.$el.appendChild(this.$fragment);} }Compile.prototype = {node2Fragment: function (el) {//創建空的fragmentvar fragment = document.createDocumentFragment(),child;// 將原生節點拷貝到fragment(將el中所有子節點轉移到fragment)while (child = el.firstChild) {fragment.appendChild(child);}//返回fragmentreturn fragment;},init: function () {// 編譯fragment(編譯所有層次的子節點)this.compileElement(this.$fragment);},compileElement: function (el) {// 得到所有子節點var childNodes = el.childNodes,// 保存compile對象me = this;// 遍歷所有子節點(text/element)[].slice.call(childNodes).forEach(function (node) {// 得到節點的文本內容var text = node.textContent;// 正則對象(匹配大括號表達式)var reg = /\{\{(.*)\}\}/; // {{name}}// 如果是元素節點if (me.isElementNode(node)) {// 編譯元素節點的指令屬性me.compile(node);// 如果是一個大括號表達式格式的文本節點} else if (me.isTextNode(node) && reg.test(text)) {// 編譯大括號表達式格式的文本節點me.compileText(node, RegExp.$1); // RegExp.$1: 表達式 name}// 如果子節點還有子節點if (node.childNodes && node.childNodes.length) {// 遞歸調用實現所有層次節點的編譯me.compileElement(node);}});},compile: function (node) {// 得到所有標簽屬性節點var nodeAttrs = node.attributes,me = this;// 遍歷所有屬性[].slice.call(nodeAttrs).forEach(function (attr) {// 得到屬性名: v-on:clickvar attrName = attr.name;// 判斷是否是指令屬性if (me.isDirective(attrName)) {// 得到表達式(屬性值): testvar exp = attr.value;// 得到指令名: on:clickvar dir = attrName.substring(2);// 事件指令if (me.isEventDirective(dir)) {// 解析事件指令compileUtil.eventHandler(node, me.$vm, exp, dir);// 普通指令} else {// 解析普通指令compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);}// 移除指令屬性node.removeAttribute(attrName);}});},compileText: function (node, exp) {// 調用編譯工具對象解析compileUtil.text(node, this.$vm, exp);},isDirective: function (attr) {return attr.indexOf('v-') == 0;},isEventDirective: function (dir) {return dir.indexOf('on') === 0;},isElementNode: function (node) {return node.nodeType == 1;},isTextNode: function (node) {return node.nodeType == 3;} };// 指令處理集合(包含多個解析指令的方法的工具對象) var compileUtil = {// 解析: v-text/{{}}text: function (node, vm, exp) {this.bind(node, vm, exp, 'text');},// 解析: v-htmlhtml: function (node, vm, exp) {this.bind(node, vm, exp, 'html');},// 解析: v-modelmodel: function (node, vm, exp) {//實現數據的初始化顯示和創建對應的watcherthis.bind(node, vm, exp, 'model');var me = this,//得到表達式的值val = this._getVMVal(vm, exp);//給節點綁定input事件監聽(輸入改變時)node.addEventListener('input', function (e) {//得到輸入的最新值var newValue = e.target.value;//如果沒有變化,直接結束if (val === newValue) {return;}//將最新value保存給表達式所對應的屬性me._setVMVal(vm, exp, newValue);val = newValue;});},// 解析: v-classclass: function (node, vm, exp) {this.bind(node, vm, exp, 'class');},// 真正用于解析指令的方法bind: function (node, vm, exp, dir) {/*實現初始化顯示*/// 根據指令名(text)得到對應的更新節點函數var updaterFn = updater[dir + 'Updater'];// 如果存在調用來更新節點updaterFn && updaterFn(node, this._getVMVal(vm, exp));// 創建表達式對應的watcher對象new Watcher(vm, exp, function (value, oldValue) {/*更新界面*/// 當對應的屬性值發生了變化時, 自動調用, 更新對應的節點updaterFn && updaterFn(node, value, oldValue);});},// 事件處理eventHandler: function (node, vm, exp, dir) {// 得到事件名/類型: clickvar eventType = dir.split(':')[1],// 根據表達式得到事件處理函數(從methods中): test(){}fn = vm.$options.methods && vm.$options.methods[exp];// 如果都存在if (eventType && fn) {// 綁定指定事件名和回調函數的DOM事件監聽, 將回調函數中的this強制綁定為vmnode.addEventListener(eventType, fn.bind(vm), false);}},// 從vm得到表達式對應的value_getVMVal: function (vm, exp) {var val = vm._data;exp = exp.split('.');exp.forEach(function (k) {val = val[k];});return val;},_setVMVal: function (vm, exp, value) {var val = vm._data;exp = exp.split('.');exp.forEach(function (k, i) {// 非最后一個key,更新val的值if (i < exp.length - 1) {val = val[k];} else {val[k] = value;}});} };// 包含多個用于更新節點方法的工具對象 var updater = {// 更新節點的textContenttextUpdater: function (node, value) {node.textContent = typeof value == 'undefined' ? '' : value;},// 更新節點的innerHTMLhtmlUpdater: function (node, value) {node.innerHTML = typeof value == 'undefined' ? '' : value;},// 更新節點的classNameclassUpdater: function (node, value, oldValue) {//靜態class屬性的值var className = node.className;className = className.replace(oldValue, '').replace(/\s$/, '');var space = className && String(value) ? ' ' : '';//將靜態class屬性的值與動態class值進行合并后設置為新的className屬性值node.className = className + space + value;},// 更新節點的valuemodelUpdater: function (node, value, oldValue) {node.value = typeof value == 'undefined' ? '' : value;} };12. 事件指令解析
內部實現事件處理的回調函數的this一定要用bind()方法指定為vm,因為方法都定義在methods中。
使用了原生的addEventListener(eventType, callback, boolean)方法。
13.一般指令解析
最后有移除指令屬性。
14. 數據劫持是Vue中用來實現數據綁定的一種技術
基本思想: 通過defineProperty()來監視data 中所有屬性(任意層次)數據的變化, 一旦變
化就去更新界面。
15. vm中的set和data中的set不一樣
vm中的set主要是用來實現數據代理,當this.xxx(即vm.xxx)改變時,vm中的set會被調用,去改變data中xxx的值,從而data中set被調用,更新界面,主要用來實現數據綁定。
16.實現數據劫持的函數內部重寫定義data中數據,為其添加set和get方法
Observer、Observe、walk等方法
17.編譯模板最后都會經過bind方法,除了事件指令(添加事件監聽)
一般指令和大括號表達式:bind,創建watcher,監視器。
事件指令:eventHandler,添加事件監聽。
18.回調函數三個注意
1.什么是調用
2.用來做什么
3.this指向什么
19.Dep和Watcher
Dep:
它的實例什么時候創建?
答:初始化的給data的屬性進行數據劫持時創建的。
個數?
答:與data中的屬性一一對應。
Dep的結構?
答:id:標識。
subs:[] //n個相關的watcher的容器
Watcher:
它的實例什么時候創建?
答:初始化的解析大括號表達式/一般指令時創建。
個數?
答:與模板中表達式(非事件指令)一一對應。
Watcher結構?
答:
this.cb = cb; // callback 用于更新界面的回調
this.vm = vm; //vm
this.exp = exp; //對應的表達式
this.depIds = {}; // {0: d0, 1: d1, 2: d2} 相關的n個dep的容器對象
this.value = this.get(); //當前表達式對應的value
Dep與Watcher之間的關系
什么關系?
答:多對多的關系
Dep --> n個watcher的情況:模板中寫多個{{name}}或v-text="name"等(屬性在模板中多次被使用)
Watcher --> n個Dep()的情況:a.b(一個表達式只對應一個watcher)對應兩個Dep()(多層表達式)
如何建立的?
答:
1.在數據劫持的get方法(data屬性的get方法)中建立Dep和Watcher的關系。
2.Dep先創建(在Observer數據劫持的時候創建),Watcher后創建(在模板解析的時候創建)。
3.在創建Watcher的時候建立Dep和Watcher的關系。在創建Watcher內部調用get()方法(vm的get方法)。
4.Dep.target一開始為null,當創建Watcher的時候,.Watcher中的get()方法將Dep.target設置為Watcher。并且調用getVMVal()方法,導致get()方法調用,執行dep.depend()方法,再執行Dep.target.addDep(this)(進入watcher.js文件中)首先給Dep的subs添加Watcher,再為Watcher的depIds添加Dep的id,并將dep保存到depIds中。
5.當屬性改變的時候,會通過subs通知所有關聯的watcher。
6.關系只在初始化的時候建立。
20.改變變量時代碼內部引起的變化
v.name = ‘abc’ --> data中的name屬性值變化 --> name的set()調用 --> dep(set方法中有一句dep.notify()語句) --> 通知所有相關的watcher(subs使用forEach方法,sub.update())–>cb()(調用回調函數更新界面this.cb.call(this, value, oldVal))–> updater
21. observer.js
function Observer(data) {// 保存data對象this.data = data;// 走起this.walk(data); }Observer.prototype = {walk: function(data) {//保存observer對象var me = this;// 遍歷data中所有屬性Object.keys(data).forEach(function(key) {// 針對指定屬性進行處理me.convert(key, data[key]);});},convert: function(key, val) {// 對指定屬性實現響應式數據綁定this.defineReactive(this.data, key, val);},defineReactive: function(data, key, val) {// 創建與當前屬性對應的dep對象(dependency,依賴)var dep = new Dep();// 間接遞歸調用實現對data中所有層次屬性的劫持var childObj = observe(val);// 給data重新定義屬性(添加set/get)Object.defineProperty(data, key, {enumerable: true, // 可枚舉configurable: false, // 不能再defineget: function() {// 建立dep與watcher的關系if (Dep.target) {dep.depend();}// 返回屬性值return val;},set: function(newVal) {if (newVal === val) {return;}val = newVal;// 新的值是object的話,進行監聽childObj = observe(newVal);// 通過depdep.notify();}});} };function observe(value, vm) {// value必須是對象, 因為監視的是對象內部的屬性if (!value || typeof value !== 'object') {return;}// 創建一個對應的觀察都對象return new Observer(value); };var uid = 0;function Dep() {// 標識屬性this.id = uid++;// 相關的所有watcher的數組this.subs = []; }Dep.prototype = {addSub: function(sub) {this.subs.push(sub);},depend: function() {Dep.target.addDep(this);},removeSub: function(sub) {var index = this.subs.indexOf(sub);if (index != -1) {this.subs.splice(index, 1);}},notify: function() {// 通知所有相關的watcher(一個訂閱者)this.subs.forEach(function(sub) {sub.update();});} };Dep.target = null;22. watcher.js
function Watcher(vm, exp, cb) {this.cb = cb; // callback //更新界面的回調函數this.vm = vm;this.exp = exp; //表達式this.depIds = {}; // {0: d0, 1: d1, 2: d2} 包含所有相關的dep的容器對象this.value = this.get(); //得到表達式的初始值,保存 }Watcher.prototype = {update: function () {this.run();},run: function () {// 得到最新的值var value = this.get();// 得到舊值var oldVal = this.value;// 如果不相同if (value !== oldVal) {this.value = value;// 調用回調函數更新對應的界面this.cb.call(this.vm, value, oldVal);}},addDep: function (dep) {//判斷dep與watcher的關系是否已經建立if (!this.depIds.hasOwnProperty(dep.id)) {// 建立dep到watcherdep.addSub(this);// 建立watcher到dep的關系this.depIds[dep.id] = dep;}}, //得到表達式的值,建立dep與watcher的關系get: function () {//給dep指定當前的watcherDep.target = this;// 獲取當前表達式的值, 內部會導致屬性的get()調用,建立dep與watcher的關系var value = this.getVMVal();//去除dep中指定的當前的watcher,實現只要初始化建立關系。Dep.target = null;return value;},//得到表達式對應的值getVMVal: function () {var exp = this.exp.split('.');var val = this.vm._data;exp.forEach(function (k) {val = val[k];});return val;} }; /*const obj1 = {id: 1} const obj12 = {id: 2} const obj13 = {id: 3} const obj14 = {id: 4}const obj2 = {} const obj22 = {} const obj23 = {} // 雙向1對1 // obj1.o2 = obj2 // obj2.o1 = obj1// obj1: 1:n obj1.o2s = [obj2, obj22, obj23]// obj2: 1:n obj2.o1s = {1: obj1,2: obj12,3: obj13 } */23. MVVM 原理圖
23.1 初始化階段
23.2更新階段
24. 雙向數據綁定(v-model)
使用Dom監聽實現。
24.1 v-model指令的實現
1.實現頁面初始化顯示,并對當前表達式添加watcher對象。
2.保存當前的compile。
3.添加事件監聽,addEventListener。
4.改變data的值,更新界面。
總結
以上是生活随笔為你收集整理的vue 源码分析(尚硅谷视频学习笔记)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: fataexception matlab
- 下一篇: html5倒计时秒杀怎么做,vue 设