简易vue.js框架(MVVM)源码解析
学习准备
数据代理
数据代理:通过一个对象代理另一个 对象中属性的操作(读/写) vue数据代理:通过vm对象代理vm.data对象中所有属性的操作 好处:方便操作data中的所有数据 基本实现流程
a. 通过object.defineproperty给vm添加与data对象的属性对应的属性描述符
b. 所有添加的属性都包含getter/setter
c. getter/setter内部去操作data中对应的属性数据function Vue ( options ) {
this . $options = options
var data = this . _data = this . $options. data
var me = this ;
observe ( this . _data)
function observe ( value ) {
if ( ! value || typeof value !== 'object' ) {
return
}
Object. keys ( value) . forEach ( key => {
defineReactive ( value, key, value[ key] )
proxyData ( key)
} )
}
function defineReactive ( obj, key, val ) {
if ( typeof obj === 'object' ) {
observe ( val) ;
}
Object. defineProperty ( obj, key, {
get ( ) {
return val
} ,
set ( newVal) {
if ( newVal=== val) {
return
} else {
val = newVal;
}
}
} )
}
function proxyData ( key ) {
Object. defineProperty ( me, key, {
get ( ) {
return me. _data[ key]
} ,
set ( newVal) {
me. _data[ key] = newVal
}
} )
}
}
const vm = new Vue ( {
el: '#app' ,
data: {
name: "name值" ,
age: 18
}
} )
console. log ( vm. _data. name, vm. age)
模板解析
创建一个fragment,把所有的原生节点拷贝到fragment中 然后使用init方法编译 把编译过后的fragment添加到el中function Compile ( el ) {
this . $el = this . isElementNode ( el) ? el : document. querySelector ( el) ;
if ( this . $el) {
this . $fragment = this . node2Fragment ( this . $el) ;
this . init ( ) ;
this . $el. appendChild ( this . $fragment) ;
}
}
Compile . prototype = {
init : function ( ) { this . compileElement ( this . $fragment) ; } ,
node2Fragment : function ( el ) {
var fragment = document. createDocumentFragment ( ) , child;
while ( child = el. firstChild) {
fragment. appendChild ( child) ;
}
return fragment;
}
} ;
Compile . prototype = {
compileElement : function ( el ) {
var childNodes = el. childNodes, me = this ;
[ ] . slice . call ( childNodes) . forEach ( function ( node ) {
var text = node. textContent;
var reg = / \{\{(.*)\}\} / ;
if ( me. isElementNode ( node) ) {
me. compile ( node) ;
} else if ( me. isTextNode ( node) && reg. test ( text) ) {
me. compileText ( node, RegExp. $1 ) ;
}
if ( node. childNodes && node. childNodes. length) {
me. compileElement ( node) ;
}
} ) ;
} ,
compile : function ( node ) {
var nodeAttrs = node. attributes, me = this ;
[ ] . slice . call ( nodeAttrs) . forEach ( function ( attr ) {
var attrName = attr. name;
if ( me. isDirective ( attrName) ) {
var exp = attr. value;
var dir = attrName. substring ( 2 ) ;
if ( me. isEventDirective ( dir) ) {
compileUtil. eventHandler ( node, me. $vm, exp, dir) ;
} else {
compileUtil[ dir] && compileUtil[ dir] ( node, me. $vm, exp) ;
}
}
} ) ;
}
} ;
var compileUtil = {
text : function ( node, vm, exp ) {
this . bind ( node, vm, exp, 'text' ) ;
} ,
bind : function ( node, vm, exp, dir ) {
var updaterFn = updater[ dir + 'Updater' ] ;
updaterFn && updaterFn ( node, vm[ exp] ) ;
new Watcher ( vm, exp, function ( value, oldValue ) {
updaterFn && updaterFn ( node, value, oldValue) ;
} ) ;
}
} ;
var updater = {
textUpdater : function ( node, value ) {
node. textContent = typeof value == 'undefined' ? '' : value;
}
} ;
下面是简易MVVM的实现
class Compiler {
constructor ( el, vm ) {
this . el = this . isElementNode ( el) ? el: document. querySelector ( el) ;
this . vm = vm;
let fragment = this . node2fragment ( this . el) ;
this . compile ( fragment) ;
this . el. appendChild ( fragment) ;
}
isElementNode ( node ) {
return node. nodeType === 1 ;
}
node2fragment ( node ) {
let fragment = document. createDocumentFragment ( ) ;
let firstChild;
while ( firstChild = node. firstChild) {
fragment. appendChild ( firstChild)
}
return fragment;
}
compile ( node ) {
let childNodes = node. childNodes;
[ ... childNodes] . forEach ( ( child ) => {
if ( this . isElementNode ( child) ) {
this . compileElement ( child)
this . compile ( child) ;
} else {
this . compileText ( child)
}
} )
}
compileElement ( node ) {
let attributes = node. attributes;
[ ... attributes] . forEach ( attr => {
let { name, value: expr} = attr
if ( this . isDirective ( name) ) {
let [ , directive] = name. split ( '-' )
CompileUtil[ directive] ( node, expr, this . vm)
}
} ) ;
}
compileText ( node ) {
let content = node. textContent;
if ( / \{\{(.+?)\}\} / . test ( content) ) {
CompileUtil[ 'text' ] ( node, content, this . vm)
}
}
isDirective ( attrName ) {
return attrName. startsWith ( 'v-' )
}
}
CompileUtil = {
getVal ( vm, expr ) {
return expr. split ( '.' ) . reduce ( ( data, current ) => {
return data[ current]
} , vm. $data)
} ,
setVal ( vm, expr, value ) {
expr. split ( '.' ) . reduce ( ( data, current, index, arr ) => {
if ( index == arr. length- 1 ) {
return data[ current] = value
}
return data[ current]
} , vm. $data)
} ,
model ( node, expr, vm ) {
let fn = this . updater[ 'modelUpdater' ] ;
new Watcher ( vm, expr, ( newVal ) => {
fn ( node, newVal)
} )
node. addEventListener ( 'input' , ( e ) => {
let value = e. target. value;
this . setVal ( vm, expr, value) ;
} )
let value = this . getVal ( vm, expr) ;
fn ( node, value)
} ,
html ( ) {
} ,
getContentValue ( vm, expr ) {
return expr. replace ( / \{\{(.+?)\}\} / g , ( ... args) => {
return this . getVal ( vm, args[ 1 ] )
} )
} ,
text ( node, expr, vm ) {
let fn = this . updater[ 'textUpdater' ] ;
let content = expr. replace ( / \{\{(.+?)\}\} / g , ( ... args) => {
new Watcher ( vm, args[ 1 ] , ( ) => {
fn ( node, this . getContentValue ( vm, expr) )
} )
return this . getVal ( vm, args[ 1 ] )
} )
fn ( node, content)
} ,
updater: {
modelUpdater ( node, value ) {
node. value = value
} ,
htmlUpdater ( ) {
} ,
textUpdater ( node, value ) {
console. log ( value) ;
node. textContent = value
}
}
}
class Observer {
constructor ( data ) {
this . observer ( data) ;
}
observer ( data ) {
if ( data && typeof data == 'object' ) {
for ( let key in data) {
this . defineReactive ( data, key, data[ key] ) ;
}
}
}
defineReactive ( obj, key, value ) {
this . observer ( value) ;
let dep = new Dep ( )
Object. defineProperty ( obj, key, {
get ( ) {
Dep. target && dep. addSub ( Dep. target)
return value;
} ,
set : ( newVal ) => {
if ( newVal != value) {
this . observer ( newVal)
value = newVal
dep. notify ( ) ;
}
}
} )
}
}
class Dep {
constructor ( ) {
this . subs = [ ] ;
}
addSub ( watcher ) {
this . subs. push ( watcher)
}
notify ( ) {
this . subs. forEach ( watcher => watcher. update ( ) ) ;
}
}
class Watcher {
constructor ( vm, expr, cb ) {
this . vm = vm;
this . expr = expr;
this . cb = cb;
this . oldValue = this . get ( )
}
get ( ) {
Dep. target = this ;
let value = CompileUtil. getVal ( this . vm, this . expr) ;
Dep. target = null ;
return value;
}
update ( ) {
let newVal = CompileUtil. getVal ( this . vm, this . expr) ;
if ( newVal !== this . oldValue) {
this . cb ( newVal)
}
}
}
class Vue {
constructor ( options ) {
this . $el = options. el;
this . $data = options. data;
if ( this . $el) {
new Observer ( this . $data)
this . proxyVm ( this . $data)
new Compiler ( this . $el, this )
}
}
proxyVm ( data ) {
for ( let key in data) {
Object. defineProperty ( this , key, {
get ( ) {
return data[ key]
}
} )
}
}
}
<! DOCTYPE html >
< html lang = " en" >
< head>
< meta charset = " UTF-8" >
< meta http-equiv = " X-UA-Compatible" content = " IE=edge" >
< meta name = " viewport" content = " width=device-width, initial-scale=1.0" >
< title> Document</ title>
</ head>
< body>
< div id = " app" >
< input type = " text" v-model = " school.name" />
< div> {{school.name}}</ div>
< div> {{school.age}}</ div>
< ul>
< li> 1</ li>
< li> 1</ li>
</ ul>
</ div>
< script src = " mvvm.js" > </ script>
< script>
let vm = new Vue ( {
el: '#app' ,
data: {
school: {
name: "名字" ,
age: 18
}
}
} )
</ script>
</ body>
</ html>