vue 1.0源代码重点难点分析
本文分析vue1.0源代碼從入口開始到編譯輸出到網頁顯示生效的整個過程,并重點分析一些vue源代碼設計原理。
vue初始化根組件的入口代碼:
對于沒有路由的單頁應用來說,入口就是new Vue(options),options就是根組件代碼,對于有路由的項目來說,入口就是router.start(app),其中app是根組件。
function Router() {} // router構造函數
var router = new Router(); // new router實例
router.start(app); // 項目真正的入口,app是根組件代碼(對象)
Router.prototype.start = function start(App, container, cb) {
this._appContainer = container; //把app和占位元素container保存在router實例
var Ctor = this._appConstructor = typeof App === 'function' ? App : Vue.extend(App); // 根組件App繼承Vue類(js用構造函數和prototype屬性模仿類)
this.history.start();
HashHistory.prototype.start = function start() {
self.onChange(path.replace(/^#!?/, '') + query); //當瀏覽器地址中hash部分變化時觸發執行listener(),listener會格式化瀏覽器地址中hash部分(hash如果沒有/則加/前綴),再執行onChange,傳入path(url地址的hash部分)
onChange: function onChange(path, state, anchor) { // url地址中hask部分變化觸發執行此方法執行進行路由組件切換,類似input元素的輸入觸發onchange
_this._match(path, state, anchor);
Router.prototype._match = function _match(path, state, anchor) {
var _this3 = this; // this是router實例
var route = new Route(path, this);? // 這是route實例,含路由切換信息from/to,不是router實例。
var transition = new RouteTransition(this, route, currentRoute); //new transition實例時傳入router實例,transition是路由切換from/to相關處理,還有一個transition是路由組件切換時的過渡效果,別搞混了
if (!this.app) { // 如果是第一次執行,還沒有new根組件實例
_this3.app = new _this3._appConstructor({ //new根組件實例,構造函數是function VueComponent (options) { this._init(options) },new根組件實例保存在router實例中,router.app就是根組件實例,router實例會保存在根組件實例中。
在new Vuecomponent()實例化時要執行Vuecomponent構造函數,Vuecomponent是繼承Vue的,就是要執行Vue的_init初始化函數,就開始初始化根組件,編譯根組件
template。
vue的鉤子函數created()就是指new組件實例之后,而ready()就是指編譯組件template之后,先new組件實例,再編譯組件template,因為是在創建組件實例之后才
執行構造函數Vue才執行_init()開始初始化編譯,編譯完成之后插入網頁生效,是異步過程,所以執行ready()時可能插入網頁生效還沒有真正完成,如果ready()有
代碼需要在網頁找組件元素,比如初始化輪播的代碼,可能就需要延遲執行等插入網頁生效完成,否則就會出現無法理解的異常現象。
router實例會保存在根組件實例中,傳遞給各個子組件,因此vue的各個組件訪問router實例非常方便。
Vue.prototype._init就是vue組件入口/初始化函數,這是組件通用的初始化函數,是處理組件的核心函數,因為vue是從根組件開始遞歸編譯所有的子組件,
所有的子組件都要跑一遍這個方法,所以這個方法就是vue的頂層核心方法,在底層有幾十幾百個方法進行各種處理。
先從_init開始把組件初始化過程從頭到尾大概走一遍如下(只列舉主要語句):
Vue.prototype._init = function (options) {
this._initState(); //之后el從id變為網頁元素DOM(引用網頁元素)
if (options.el) {
this.$mount(options.el); //如果根組件寫了template,el就是根組件占位元素,否則el就是online template元素,從根組件開始遞歸編譯所有的子組件子節點
}
Vue.prototype._initState = function () {
this._initProps();
this._initMeta();
this._initMethods();
this._initData();
this._initComputed();
};
Vue.prototype._initProps = function () {
// make sure to convert string selectors into element now
el = options.el = query(el);
}
Vue.prototype.$mount = function (el) {
this._compile(el); //從根組件開始遞歸編譯所有的子組件子節點,然后插入網頁生效
return this;
};
Vue.prototype._compile = function (el) {
var original = el;
el = transclude(el, options);
var rootLinker = compileRoot(el, options, contextOptions); // compile產生link函數
var rootUnlinkFn = rootLinker(this, el, this._scope); // 執行link函數,插入網頁生效
var contentUnlinkFn = contentLinkFn ? contentLinkFn(this, el) : compile(el, options)(this, el); //編譯產生link()再執行link()
if (options.replace) {
replace(original, el); //可能需要替換更新
}
function compileRoot(el, options, contextOptions) {
replacerLinkFn = compileDirectives(el.attributes, options); //root元素<div id=? 沒什么要編譯的
return function rootLinkFn(vm, el, scope) {
}
function compileDirectives(attrs, options) {}
function compile(el, options, partial) {? // 編譯組件的template,el是組件占位元素,options是組件代碼
var nodeLinkFn = compileNode(el, options) //編譯根節點
var childLinkFn = compileNodeList(el.childNodes, options) // 遞歸編譯子節點
return function compositeLinkFn(vm, el, host, scope, frag) {? // 編譯返回的通用link函數
function makeChildLinkFn(linkFns) { // 產生子節點link函數
return function childLinkFn(vm, nodes, host, scope, frag) { // 子節點link函數返回給var childLinkFn
function compileNodeList(nodeList, options) {? // 會遞歸調用自己,層層遞歸所有子節點
nodeLinkFn = compileNode(node, options); //編譯一個節點
childLinkFn = compileNodeList(node.childNodes, options) //編譯一個節點的子節點,遞歸編譯子節點
return linkFns.length ? makeChildLinkFn(linkFns) : null; //nodeLinkFn和childLinkFn在linkFns中
function compileNode(node, options) {? // 編譯一個節點
var type = node.nodeType;
if (type === 1 && !isScript(node)) {
return compileElement(node, options);
} else if (type === 3 && node.data.trim()) {
return compileTextNode(node, options);
function compileElement(el, options) { // 到這里要解析處理頁面元素表達式中的指令屬性包括標簽組件,細節本文忽略
// check terminal directives (for & if)
if (hasAttrs) {
linkFn = checkTerminalDirectives(el, attrs, options);
}
// check element directives
if (!linkFn) {
linkFn = checkElementDirectives(el, options);
}
// check component
if (!linkFn) {
linkFn = checkComponent(el, options);
}
// normal directives
if (!linkFn && hasAttrs) {
linkFn = compileDirectives(attrs, options);
}
return linkFn;
function checkTerminalDirectives(el, attrs, options) {? // 編譯處理指令
def = resolveAsset(options, 'directives', matched[1]); //每種每個屬性指令都有一套方法找出來
return makeTerminalNodeLinkFn(el, dirName, value, options, termDef, rawName, arg, modifiers);
function makeTerminalNodeLinkFn(el, dirName, value, options, def, rawName, arg, modifiers) {
var parsed = parseDirective(value); //指令表達式字符串如果沒有|,就無需解析,直接返回指令表達式字符串
var fn = function terminalNodeLinkFn(vm, el, host, scope, frag) {
if (descriptor.ref) {
defineReactive((scope || vm).$refs, descriptor.ref, null);
}
vm._bindDir(descriptor, el, host, scope, frag);
};
return fn;
Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) {
this._directives.push(new Directive(descriptor, this, node, host, scope, frag));
};
至此解析編譯節點的指令表達式結束,然后執行link函數:
function compositeLinkFn(vm, el, host, scope, frag) {
var dirs = linkAndCapture(function compositeLinkCapturer() {
if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag);
if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag);
}, vm);
function linkAndCapture(linker, vm) {
linker(); //增加新解析的directive實例存儲到vm._directives中
var dirs = vm._directives.slice(originalDirCount); //取最新增加的directive實例
sortDirectives(dirs);
for (var i = 0, l = dirs.length; i < l; i++) {
dirs[i]._bind(); //執行每個directive實例的_bind方法,比如v-for是一個directive實例,每個循環項又是一個directive實例,組件標簽也是一個directive實例
}
return dirs;
}
linker()就是要執行編譯階段產生的link函數:
function childLinkFn(vm, nodes, host, scope, frag) {
nodeLinkFn(vm, node, host, scope, frag);
childrenLinkFn(vm, childNodes, host, scope, frag); //childrenLinkFn就是childLinkFn,子節點遞歸調用
nodeLinkFn()就是編譯階段產生的那個fn:
var fn = function terminalNodeLinkFn(vm, el, host, scope, frag) {
if (descriptor.ref) {
defineReactive((scope || vm).$refs, descriptor.ref, null);? // 如果有v-ref="xxx" 引用組件對象,這也是一種指令屬性
}
vm._bindDir(descriptor, el, host, scope, frag);? // bind就是初始化方法,bindDir就是初始化指令的方法
};
要執行new Directive(descriptor, this, node, host, scope, frag),然后把directive實例保存到vm._directives中,
new Directive時只是把之前compile/link階段產生的數據方法保存到directive實例中,并無其它處理,因此只是準備數據,保存到實例,后面再執行實例中的初始化方法初始化指令。
Directive.prototype._bind = function () { // 這就是dirs[i]._bind()方法代碼,元素節點每個屬性指令都有一個directive實例都有一個通用的bind初始化方法
// setup directive params
this._setupParams();
this.bind(); //每個指令實例中的bind方法是specific bind方法,通用_bind方法調用specific bind方法,每個指令都不一樣。
var watcher = this._watcher = new Watcher(this.vm, this.expression, this._update, // 凡是要獲取表達式/變量值都要創建watcher,會把watcher加入到在求值過程中依賴的變量屬性的dep中。
this.update(watcher.value); //更新屬性指令網頁數據,比如id="$index"要把變量值插入,比如{{item.name}}要把變量值插入,網頁數據更新之后就處理完了,更新網頁是最后一步,每種屬性指令的update方法都不一樣。
可見vue處理指令的關鍵成分是watcher和指令實例中的bind/update初始化函數方法,在編譯階段處理所有的指令屬性,new directive實例,保存起來,在link階段只要把所有保存的指令實例都執行一遍bind方法進行初始化就完成了所有的指令初始化工作,包括標簽組件也是要走指令流程的,只不過標簽組件還要走一遍component處理流程,要new Vuecomponent()實例執行構造函數從_init()開始把compile()流程再走一遍完成組件template的編譯,組件template中可能又有指令屬性,就又要把上述流程走一遍完成對指令屬性的編譯處理,比如v-for="item in items"。
所以vue的通用指令處理程序和組件處理程序會從編譯根組件開始被反復執行多次,遞歸所有的子組件,所有的子節點,層層嵌套,每個指令每個子組件都要執行一遍核心編譯處理程序,如果在核心編譯程序debug看,會有幾十幾百次輸出,能看到所有的指令/組件的數據,還是非常復雜的。
以v-for指令為例來看一段指令的源代碼:
var vFor = { //
bind: function bind() {
if ('development' !== 'production' && this.el.hasAttribute('v-if')) {
warn('<' + this.el.tagName.toLowerCase() + ' v-for="' + this.expression + '" v-if="' + this.el.getAttribute('v-if') + '">: ' + 'Using v-if and v-for on the same element is not recommended - ' + 'consider filtering the source Array instead.', this.vm);
} //不推薦同時使用v-for和v-if是在這兒檢查告警的
this.start = createAnchor('v-for-start'); //text節點
this.end = createAnchor('v-for-end');
replace(this.el, this.end); //先用占位元素替換掉v-for元素,等編譯好v-for元素之后再插入網頁到占位元素位置生效
before(this.start, this.end);
this.factory = new FragmentFactory(this.vm, this.el); //此過程會compile(this.el)生成linker保存在fragment實例中
function FragmentFactory(vm, el) {
linker = compile(template, vm.$options, true); // 編譯指令template,v-for指令是一個數組循環產生一個列表
router切換路由組件時涉及transition過渡效果,所以相關代碼是封裝在transition中:
var startTransition = function startTransition() {
transition.start(function () {
_this3._postTransition(route, state, anchor);
RouteTransition.prototype.start = function start(cb) {
var view = this.router._rootView; //_rootView是<router-view>指令實例,里面有app組件構造器
activate(_view, transition, depth, cb); //transition里面有要切換的子頁面路由和component構造函數對象,里面有options(組件代碼)
function activate(view, transition, depth, cb, reuse) { //depth對應子路由
var handler = transition.activateQueue[depth];
if (!handler) {
view.setComponent(null);
component = view.build({ //view就是占位標簽router-view指令實例,build方法就是var component={}中的build方法
build: function build(extraOptions) {
var child = new this.Component(options); //options就是之前準備好的組件構造器含組件代碼,這就是new 路由組件實例化
return child;
Vue[type] = function (id, definition) { //這就是vue component的構造函數的構造函數
definition = Vue.extend(definition);
this.options[type + 's'][id] = definition;
return definition;
view.transition(component);
component.$before(view.anchor, null, false);
//Actually swap the components
transition: function transition(target, cb) { //var component={}中的transition方法,target是組件實例,實例中el是已經編譯產生的DOM對象,可以直接插入網頁生效。
self.remove(current);
target.$before(self.anchor, cb);
Vue.prototype.$before = function (target, cb, withTransition) { //此時target變為anchor占位元素(text節點)
return insert(this, target, cb, withTransition, beforeWithCb, beforeWithTransition);
function insert(vm, target, cb, withTransition, op1, op2) {
function beforeWithCb(el, target, vm, cb) {
before(el, target);
if (cb) cb();
}
function beforeWithTransition(el, target, vm, cb) {
applyTransition(el, 1, function () {
before(el, target);
}, vm, cb);
有兩個細節提一下:
function View (Vue) {
Vue.elementDirective('router-view', viewDef);
在View構造函數中定義了router-view這個指令/組件。
下面是vue的on方法,是用底層js事件方法:
function on(el, event, cb, useCapture) {
el.addEventListener(event, cb, useCapture);
}
vue沒有再定義一套事件機制,就是從上層封裝直接調用底層js方法來定義事件綁定,比肩簡單。
要注意,cb中已經引用組件實例,所有的組件實例都是保存在根組件實例中,按id索引。當點擊頁面執行方法時,頁面并沒有作用域指引,方法也不在全局空間,
就是在js的events[]中找handler執行,關鍵關鍵關鍵是handler方法代碼中已經引用組件實例,這就是作用域。
它如何引用組件實例呢?估計很少有人想過這個問題。
先new router實例,要么在全局空間創建router實例,要么把router實例保存在全局,再new 根組件實例,保存在router實例,再new 組件實例,保存在根組件,組件實例的方法用this引用組件實例,因此引用組件實例就是引用保存在全局的router實例中的屬性,js對象引用機制會導致能引用
到組件實例,這個可是很關鍵的,否則就全亂套了。
其它的框架大都自己又定義了一套事件機制,就復雜了。
組件構造函數Vuecomponent用Vue.extend繼承vue構造函數,每個組件只是options不同,源代碼中組件的初始化方法:
var component = {
bind: function bind() {
this.setComponent(this.expression);
build: function build(extraOptions) { //入口參數就是組件的代碼
extend(options, extraOptions);
var child = new this.Component(options); //就是new VueComponent(),就是new Vue()實例,只不過是子實例。
setComponent: function setComponent(value, cb) {
self.mountComponent(cb);
mountComponent: function mountComponent(cb) {
var newComponent = this.build();
路由組件與頁面組件不同,頁面組件在編譯template時會創建一個directve指令實例,里面有組件的id和descriptor,而所有的組件定義代碼都在components[]中可以找到。
路由組件是new Vuecomponent()創建組件實例時執行_init() -> $mount(el)開始編譯template,指令組件是在編譯template過程中層層遞歸掃描html元素節點找到指令標簽再編譯處理指令標簽。
?
根組件實例只在執行入口初始化時創建一次,每次路由變化時,會執行一次_match,this.app指根組件/主模塊,根組件實例創建之后一直存在,不會再執行new _appConstructor()初始化根組件。
vue處理的對象主要是組件和指令,主要用vue構造函數和directive構造函數來處理組件和指令,new實例時就是初始化組件和指令,編譯組件時從根節點開始遞歸循環
掃描所有的子節點,只要有組件/指令都是用同樣的通用代碼處理,所以只要上述兩個構造函數對象寫ok了,把編譯循環遞歸寫ok了,就基本成功了,不管template有
多復雜有多少層節點嵌套,有多少層組件嵌套,都是用通用方法和循環遞歸處理。
還有watcher構造函數,是為了實現數據更新同步。
Vue內置指令router-view,slot,v-for,v-if,v-model,是重要指令,每個指令有一套方法比如bind,update,都已經設計好,如果自定義指令,需要自己寫方法,最起碼
要寫bind方法(初始化方法)。
從初始化根組件開始執行vue._init,如果根元素頁面有調用組件,則每個組件都再執行一遍vue._init,從根組件開始每個組件
都初始化一個組件實例,從根元素開始每個節點都要編譯處理一遍,編譯之后產生link函數再執行link函數。對于每個屬性指令
則執行一遍new Directive()創建一個directive實例,相關數據方法都保存在實例中,主要是el和update方法,
如果屬性指令寫在組件標簽元素內,則el是編譯之后插入網頁的<div>元素,執行update方法更新網頁時是針對網頁中這個<div>
進行屬性修改,指令實例保存在所屬的組件實例中,路由組件中定義/調用的子組件都保存在組件實例的options.components下。
頁面每個表達式都都會創建watcher,組件中被頁面綁定的每個屬性變量都會創建set/get方法,包含所有依賴自己的watcher,
watcher中的update方法會調用指令的update方法更新頁面,比如:class指令的update方法就是修改所在元素的class屬性,
class="{{}}"也是指令,更新時也是要修改所在元素的class屬性。
vue自帶指令保存在options.directives中,對于<test>這樣的自定義標簽,是已經用component定義的組件,它是指令組件,
既是指令也是組件,在當前頁面組件實例的options.components下有,在_directives下也有,指令組件要把component流程
和directive流程都走一遍。
編譯一個節點/屬性,只是準備數據,產生link函數,執行link函數才初始化生效,比如執行指令的初始化方法,
對于組件指令,初始化方法最后要執行mountComponent才把template元素插入網頁生效,之前的所有處理都是在做數據準備
每個指令都是事先定義好的,有bind/update方法,自定義指令也有方法,在頁面調用指令時,
編譯時查指令集合,獲取指令的相關數據,然后new directive創建一個指令實例,保存在當前子頁面組件實例中,編譯就
大功告成了,然后執行link函數,把當前頁面組件中調用的所有指令_directives[]都執行一遍初始化方法即可,指令的初始化
方法也是更新方法,就是是設置/修改指令所在的元素的屬性,對于組件指令,就更復雜,要new component()創建組件實例,
再執行組件指令的初始化方法,執行mountComponent完成最后一個環節,把組件template元素插入網頁生效,所以組件指令
既是組件又是指令,component和directive兩種流程都要走一遍,非常復雜。在組件指令寫屬性,處理起來是最復雜的,
要把<test>變成<div>,標簽原始屬性復制到<div>,但標簽原始屬性的作用域是調用標簽的頁面組件,也就是標簽組件的父組件,
而<div>中replacer屬性,也就是寫在template中的<div>中的屬性,作用域是標簽組件本身,除此之外,所有組件/指令處理
流程都是一樣的,只是不同的組件/指令,它們的數據方法不同。
再小結一下:
組件走Vue構造器流程,指令走Directive構造器流程,組件指令兩個流程都走,看Vue和Directive代碼,要清楚每個組件,每個指令,都要執行‘
一遍同樣的代碼,代碼會被多次執行,每次執行都是針對一個不同的組件/指令。編譯方法中compileNodesList要遞歸所有的子節點,包括看不見的文本節點,
compileNode編譯每個節點時要遍歷所有的屬性,html屬性,指令屬性,props屬性,處理方法是不同的。組件指令既是組件又是指令,兩種流程都要走一遍。
組件/指令都是事先定義好的,數據方法都已經有了,都保存在根組件Vue實例中,再繼承到各個子組件實例中,編譯時就是要判斷標簽/屬性是不是已知指令,
如果是,就把數據準備一下,然后new directive實例保存在當前組件實例中,再執行link函數,link函數會執行指令的初始化函數,初始化函數會最后完成
網頁更新,對于組件指令就是要把template元素插入網頁生效。
為了實現數據同步,還要針對組件的屬性和頁面綁定的變量包括方法創建watcher,在初始化指令時要進行watcher相關處理,這是另外一種主要的流程,貫穿在
所有主要流程中。
vuex/store是vue作者參考flux/mutation實現原理自己寫了一套代碼實現了緩存數據方法,并不是第三方插件,調用
vuex/store初始化緩存的方式就是:
new Vuex.Store({
modules: {
userInfo: {
state: {
userInfo : {
username: ''
}
}
},
mutations: {
['SETUSERINFO'] (state, data) {
state.userInfo.username = data.username;
}
}
}
},
組件調用vuex/store數據方法的方式是:
import {getMenus} from '../vuex/getters/user_getters'
import {setUserInfo} from '../vuex/actions/user_actions'
store : store,
vuex : {
getters : {
getMenus : (state) => {
return state.userInfo.globle.menus;
}
},
actions : {
setUserInfo : ({dispatch},data) => {
dispatch('SETUSERINFO',data);
}
}
}
vuex安裝到vue的相關源代碼:
function vuexInit() {
var actions = vuex.actions;
options.methods[_key] = makeBoundAction(this.$store, actions[_key], _key); // 給action方法增加一層封裝,所以你debug看組件實例里面的action方法并不是你自己寫的原始方法,而是一個封裝方法,任何框架只要訪問全局store數據本質上都是加一層封裝,由封裝方法再調用真正的action方法,由于封裝方法在store相關的程序空間,可以訪問store/dispatch,可以傳遞給原始action方法,因此你寫的原始action方法無需涉及store/state,可以寫形參dispatch,實際調用時會自動傳遞dispatch,這就是action方法的奧秘,react的reducer/action方法本質原理上也是一樣的。
store.js:
Vue.use(Vuex); //初始化時會安裝vuex插件到vue,也就是執行vuex的install程序。
vuex.js:
function install(_Vue) {
override(Vue); //改寫vue
}
function override (Vue) {
var _init = Vue.prototype._init;
Vue.prototype._init = function () { //改寫vue原來的_init方法,其實還是調用原來的_init,但給options增加init=vuexinit方法
var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
options.init = options.init ? [vuexInit].concat(options.init) : vuexInit;
_init.call(this, options);
Vue.prototype._init = function (options) {
this._callHook('init'); //會調用options.init,也就是調用vuexinit方法
Vue.prototype._callHook = function (hook) {
this.$emit('pre-hook:' + hook);
var handlers = this.$options[hook];
if (handlers) {
for (var i = 0, j = handlers.length; i < j; i++) {
handlers[i].call(this);
因此vuex擴展到vue之后,在new Vue(options)創建組件實例時,會執行vuexinit方法處理options中的vuex:{}表達式,會在組件中創建setUserInfo方法,
創建的方法代碼是在原始action方法加了一層封裝。
vue用set/get方法實現數據響應,其關鍵代碼在于:
function defineReactive (obj, key, val) {
var dep = new Dep()
var childOb = observe(val) //如果屬性有層次,要遞歸處理每一層屬性
Object.defineProperty(obj, key, { //這是針對數據對象和里面的屬性創建set/get,是為了實現同步功能,
//組件定義的數據屬性在new Vue()時復制到this._data。
enumerable: true,
configurable: true,
get: function(){
if(Dep.target){ // 在針對頁面表達式創建new watcher()時會調用get方法取值,并把new watcher實例保存在Dep.target,Dep.target就是當前正在創建的watcher實例,這個地方挺不容易看懂的,因為要懂整體設計邏輯才能看懂,
dep.addSub(Dep.target); //把watcher實例保存到屬性對應的dep中,因此依賴obj的屬性的watcher都會保存到屬性中
Dep.target = null;
}
return val
},
set: function(newVal){
var value = val
if (newVal === value) {
return
}
val = newVal
childOb = observe(newVal)
dep.notify() // 執行屬性中保存的watcher的update方法更新組件/指令的頁面
}
})
}
其中dep的方法層層封裝非常繞,詳細代碼本文忽略。
vuex/store也是用get/set方法實現數據響應,是通過加了一層computed方法實現的,computed方法再調用getter方法訪問store的屬性數據,computed方法會創建
watcher,當調用vuex getter方法獲取屬性的值時會把自身watcher實例保存在屬性中,當set屬性時就會執行watcher的update方法更新data屬性,頁面可以綁定
data屬性,當data屬性變化時,頁面會更新,因為vue已經針對頁面綁定表達式創建了watcher,針對data屬性創建了get/set方法,已經建立了數據響應機制。
所以當使用computed方法時,還是很復雜很繞的,當數據變化時,從store到頁面是通過好幾層watcher實現數據響應的,頁面表達式有一層watcher,cb是指令的
update方法,比如對于{{title}}這樣最簡單的表達式其實就是一個文本指令,其update方法就是要取title的值插入做為文本內容,computed方法又有一層watcher,
cb=null,因為computed方法只是執行方法代碼獲取值,不同于指令,沒有cb,無需處理網頁元素。
下面是vue構造comptedGetter方法的代碼:
function makeComputedGetter(store, getter) {
var id = store._getterCacheId;
if (getter[id]) {
return getter[id];
}
var vm = store._vm;
var Watcher = getWatcher(vm);
var Dep = getDep(vm);
var watcher = new Watcher(vm, function (vm) { // 創建watcher,在執行getter方法取值時把自身保存到屬性中
return getter(vm.state);
}, null, { lazy: true });
var computedGetter = function computedGetter() { //computedGetter
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
};
getter[id] = computedGetter;
return computedGetter;
}
watcher實例里面,proto里面有一個update方法,這是watcher通用update方法,還有一個cb,這是指令通用update方法,
指令通用update方法會調用指令specific update方法(如果有的話)。
下面是關于在組件標簽用v-on:綁定事件的代碼細節:
//Register v-on events on a child component
function registerComponentEvents(vm, el) {
name = attrs[i].name;
if (eventRE.test(name)) {
handler = (vm._scope || vm._context).$eval(value, true); //v-on=后面的寫法需要解析處理一下
handler._fromParent = true; //事件在當前初始化的組件,方法在父組件
vm.$on(name.replace(eventRE), handler); // 綁定邏輯事件,邏輯事件用$emit觸發
事件用addeventlistener和$on兩種方法綁定,如果是物理事件,比如物理click事件,前者有效,如果是邏輯事件,前者
無效,后者有效,后者是vue自己建立的_events[]數據,handler方法在父組件。
在標簽寫v-on只能綁定組件里面的邏輯事件,組件里面用$emit觸發邏輯事件,執行父組件里面的handler方法,不能監聽子組件里面的物理click事件,
在子組件里面才能寫v-on綁定物理click事件。
下面是關于checkbox元素用v-model雙向綁定的源代碼分析,checkbox handler代碼:
var checkbox = {
bind: function bind() {
this.listener = function () {
var model = self._watcher.get(); //獲取v-model=表達式的值,也就是變量屬性值option.checked,涉及v-for循環變量/scope
if (isArray(model)) {
} else {
self.set(getBooleanValue()); //獲取元素checked屬性值,傳遞給set方法
}
Directive.prototype.set = function (value) {
if (this.twoWay) {
this._withLock(function () {
this._watcher.set(value);
Watcher.prototype.set = function (value) {
this.setter.call(scope, scope, value); //調用變量屬性的setter方法設置值
function (scope, val) { //watcher的setter
setPath(scope, path, val);
function setPath(obj, path, val) {
obj[key] = val;
set(obj, key, val);//如果屬性不存在,就調Vue.set添加屬性?
this.getValue = function () {
return el.hasOwnProperty('_value') ? el._value : self.params.number ? toNumber(el.value) : el.value;
};
function getBooleanValue() {
var val = el.checked;
if (val && el.hasOwnProperty('_trueValue')) {
return el._trueValue;
}
if (!val && el.hasOwnProperty('_falseValue')) {
return el._falseValue;
}
return val;
}
this.on('change', this.listener);
可見v-model底層就是綁定'change'事件,就是獲取元素checked屬性值,可以重新定義checked屬性值,那就取定義的值,一般不。
當點擊checkbox元素時,其checked屬性會變化,v-model獲取元素checked屬性值同步到變量,反之則是根據變量值設置元素
的checked屬性值,從而實現兩個方向的數據同步。
vue初始化組件時處理template的代碼:
function transcludeTemplate(el, options) {
var template = options.template;
var frag = parseTemplate(template, true);
function parseTemplate(template, shouldClone, raw) {
if (typeof template === 'string') {
//template如果寫#id,則去網頁按id找元素,這已經是DOM對象了
//如果template是html字符串,則用innerHTML編譯為DOM對象再插入到frag
frag = stringToFragment(template, raw);
function stringToFragment(templateString, raw) {
node.innerHTML = prefix + templateString + suffix; //前后綴是有可能包裹一層標簽
while (child = node.firstChild) {
frag.appendChild(child);
//這是移動子元素,循環結果把所有子元素都移動插入到frag中去,template可以寫多個<div>
function nodeToFragment(node) {
// script template,template可以寫在網頁的<script>標簽中
if (node.tagName === 'SCRIPT') {
return stringToFragment(node.textContent);
}
因此在組件中寫template=只能是html代碼字符串,或者#id指向網頁中的元素,沒有其它寫法,不能寫js表達式,
也不能用js代碼修改template,按id找元素可以把template寫在網頁的<script>中,類似angular,也可以寫在網頁中的
<template>標簽元素中:
<template id="t1">
<div>{{context}}</div>
</template>
如果構造<template>插入網頁,<template>類似frag,本身是虛擬節點,在網頁不占節點。
?
本文到此就差不多結束了,本文寫的比較粗糙,因為vue 1.0源碼是以前研究的,當時沒有很好地整理出來,后來復制了幾段源代碼分析記錄,大概編輯了一下就此發表,總比爛在肚子里強,本人對vue 1.0和2.0進行了深入的研究,進行了無數次debug看數據,因為本人認為vue非常優秀實用,值得學習,而angular和react太高大上,一般人沒法學習,文中錯誤歡迎批評指正交流。
?
轉載于:https://www.cnblogs.com/pzhu1/p/9007441.html
新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!總結
以上是生活随笔為你收集整理的vue 1.0源代码重点难点分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: RBAC权限模型及数据权限扩展的实践
- 下一篇: Sanic 连接postgresql数据