xiaofuzi / deep-in-vue Goto Github PK
View Code? Open in Web Editor NEW从源码的角度看vue的成长历程。
从源码的角度看vue的成长历程。
为了更好的了解数据驱动更新原理,我们先来要理解如下几个组成部分:
模板使用data中的数据来实现动态渲染,如下所示:
Template
<div id='app'>
<h2 v-text='hello'></h2>
</div>
Data
new TinyVue(
data: {
hello: 'Hello World!'
}
)
模板中的v-text='hello'
会渲染为data.hello
的值,记得到如下的html.
<div id='app'>
<h2 v-text='hello'>Hello World!</h2>
</div>
那么这里两者是怎么关联的呢?就是通过binding来实现的,如下就是一个简单的binding:
Binding
var binding = {
value: '',
directives: []
}
每一个binding都会对应一个data中的key,如v-text='hello'
中的 hello 就是一个key.
在TinyVue实例中,会将所有的binding以binding对应的key为键值存储起来,这样当key对应的data数据更新的时候就能取出对应的binding,而binding又记录了关联的directive,从而就知道如何去更新UI了。
Bindings对象:
var vm = new TinyVue(
data: {
hello: 'Hello World!'
}
);
var binding = {
value: '',
directives: [textDirective]
};
vm._bindings = {
hello: binding
}
vm.hello = 'hi, girl!';
当执行 vm.hello='hi, girl!'
后,会触发hello的setter操作,然后在setter操作中取出对应的binding(已经存储在vm._bindings中),
var binding = vm._bindings['hello'];
得到binding后,就可以执行绑定的指令的更新操作来更新UI了。
binding.directies.forEach(function(directive){
direcitve.update();
})
如上所示就完成了数据驱动界面的响应。
可以简单的将指令(directive)当做一个函数即可,如 text 指令:
/**
* 对应于 v-text 指令
*/
text: function (value) {
this.el.textContent = value || '';
}
指令和dom节点的绑定是在解析模板的时候完成得,解析得到directive后,在directive中存储dom节点。
directive.el = el;
即text指令的update函数,当执行 directive.update函数的时候,就会更新关联dom节点的文本内容。
为什么要单独将v-if/v-for指令提出来单独说明,因为与v-text/v-show等指令不同,他们会改变DOM结构,改变DOM结构就会影响到vue的编译过程,同时会涉及到重新编译的过程。
以v-for为例,根据条件的不同,v-for指令会删除或是新增该DOM节点,如果是删除,那么会导致父节点的子元素的个数减少一个,那么久会影响父节点后续节点的编译,新增的过程会更麻烦,因为新增后的节点需要重新编译,为了不影响整个组件,所以需要单独进行编译处理。
因为指令的预处理会更改节点的结构,所以会对其余节点的编译,所以需要自己了解哪些节点已编译,哪些节点是未编译的,不会少编译也不会重复编译。
如下为compilerNode函数的部分代码:
function compilerNode (el) {
for (; el.index < el.childNodes.length; el.index++) {
/**
* el.index 表示当前第几个子元素,在compileNode函数中可能会更改el的子元素结构,
* 所以需要el.index来标识编译的节点索引
*/
let child = el.childNodes[el.index];
compileNode(child);
}
}
如上为一个基本的DOM递归遍历,如果递归的过程中DOM结构没有发生变化,那么是不会有什么问题的,但是当遇到v-for这样的指令时,会对v-for节点进行编译处理,结果就是可能会当前节点元素增量的增加了,那么上述的遍历就会有问题,所以通过el.index来控制编译的流程,比如v-for指令增加了5个节点,那么el.index会加5,从而跳过新增的编译处理后的节点。
为了提高编译的效率,当v-for相关的节点需要重新编译时不需要整体编译而进行局部编译,所以v-for得到的每一个子元素都会实例化一个viewModel对象,这样它们就可以进行单独编译而不影响整个组件。
这里有一个需要注意的点的,子元素实例化的vm对象应该与整个组件公用上下文(父子组件则是分别具有独立的上下文),即childVm可以访问parentVm的数据。
如下为v-for子实例的简单创建:
let vm, node = this.el.cloneNode(true);
this.parent.insertBefore(node, this.endRef);
this.parent.index++;
/**
* array item data process
*/
let data = {
$index: index
};
data[this.subKey] = item;
vm = new Vm({
el: node,
data: data
}, this.vm);
//父子实例数据共享
vm.__proto__ = this.$vm;
vm.appendTo(this.vm);
this.childElements[index] = node;
this.childVms[index] = vm;
这里巧用了原型链的功能,达到了父子实例公用上下文的目的。
一个极简版vue的实现,希望对于想了解vue内部实现原理以及学习其源码而又无从下手的人有所帮助。
因为数组属于对象类型,所以对于arr[0]
形式取值与普通对象无异,都可以通过Object.defineProperty来对其进行处理,从而达到响应式的目的。
如下所示:
var person = [{}]
var p = []
Object.defineProperty(person, '0', {
get: function () {
return p[0];
},
set: function (val) {
p[0] = val;
//directive.update();
console.log('0', person[0]);
}
})
person[0] = {};
person[0] = {a: 1}
person[0].a = 2
console.log('1', person[0])
实现了person数组第一个元素的响应式处理。
数组的操作除了数组元素的取值赋值外,还需要考虑数组本身的更改。数组的下面这八个实例方法会改变数组本身,所以需要监测其更改操作。
'pop',
'push',
'reverse',
'shift',
'unshift',
'splice',
'sort'
我们来看如下代码(为vue初期源码):
var proto = Array.prototype,
slice = proto.slice,
mutatorMethods = [
'pop',
'push',
'reverse',
'shift',
'unshift',
'splice',
'sort'
]
module.exports = function (arr, callback) {
mutatorMethods.forEach(function (method) {
arr[method] = function () {
proto[method].apply(this, arguments)
callback({
event: method,
args: slice.call(arguments),
array: arr
})
}
})
}
可以看到,这里是对需要监测得数组的这八个实例方法进行了重写,重写之后的方法除了进行原有的数组操作之外会执行一个回调函数,即只要发生了pop,posh
等方法调用,回调函数就会调用,这就达到了检测的目的,将该回调函数和对应的指令更新操作绑定在一起即实现了数组的响应式。
为什么说length属性不支持响应式,因为这个属性不能够重定义,即不能通过Object.defineProptery进行重写,所以无法实现对length属性的监测。
var length = 0;
Object.defineProperty(person, 'length', {
get: function () {
return length;
},
set: function (val) {
length = val;
}
})
这段代码会报类型错误:Uncaught TypeError: Cannot redefine property: length
.
recently , i wrote a lot of components .
but i always thinking this is trouble to me.
so i think move all component to vuex store . render all element with one component , but always fail
//home store
export default {
state: {
elements: function (h) {
return [
h('Nav', {width: '100%', height: '40px', backgroundColor: 'green', position: 'top'},
[
h('Menu',{
tag: "ul",
grid: "column",
device: "phone tables desktop tv",
width: "0 12 12 12",
direction: "row",
})
])
]
},
}
//home.js
export default {
render(create){
return create('div', {
attrs: {
id: 'app'
}
},this.$store.state.home.elements(create))
}
}
fail reason : render function or template not defined in component: anonymous
ok ! this is no good ,so i move h to render
//home store
export default {
state: {
elements: [
{
name: 'Nav',
props: {width: '100%', height: '40px', backgroundColor: 'green', position: 'top'},
parent: undefined,
children: [
{
name: 'Menu',
props: {
tag: "ul",
grid: "column",
device: "phone tables desktop tv",
width: "0 12 12 12",
direction: "row",
},
children: undefined,
parent: 'Nav'
}
]
},
]
}
}
//home.js
export default {
computed: {
home(){
return this.$store.state.home.elements
}
},
render(create){
return create('div', {
attrs: {
id: 'app'
}
}, innerDom.call(this, create))
}
}
function innerDom(create) {
let element = this.home
function loop() {
element.forEach(el=>{
if(el.children){
element = el
}
// I almost tried all the array logic
})
}
loop()
}
fail reason : vnode can not correct append parent vonde or can not render to body
how can fixed this .?
组件系统功能说明:
组件按父子关系形成一个组件树,父子组件具有自己的上下文即他们有自己的viewModel结构(可能是一个单独的vm实例,也可能是vm树形结构),父子之间的数据是不能直接访问的。
当数据发生变化时,组件单独更新。
以<hello-world></hello-world>
组件为例,当解析该组件的时候,对该组件进行编译并将编译后的结果替换<hello-world></hello-world>
自定义标签,这样就将父子组件关联起来。
说明,因为是自定义标签,所以需要声明组件名,以此和自定义标签名关联起来。因为html规范中不区分大小写,所以只能以中横线的形式命名,考虑到编码方面的便利,需要在驼峰和中横线直接进行转换。
<hello-world name='hello-world-component' :msg='message'></hello-world>`
通过props传递信息,支持静态属性和动态属性。
如上:msg
即为动态属性,message
对于父组件的一个属性,当message
发生变更时,子组件能够监听到变化并自动更新。
具体实现还是通过$watch
方法来监听即可,在子组件中watch父组件的值,当发生变化时触发自身指令的更新即可。
vue通过Object.defineProperty方法实现对数据更改的监测来实现数据的响应式,这是vue比较让人着迷的一个特性,刚接触vue的时候真的太为它着迷了,但也有它自身的问题。
function bindAccessors (vm, key, binding) {
Object.defineProperty(vm, key, {
get: function () {
return binding.value;
},
set: function (value) {
binding.value = value;
binding.directives.forEach(function (directive) {
directive.update(
directive.el,
value,
directive.argument,
directive,
vm,
key
)
})
}
})
}
如上所示,通过对vm对象的key属性set操作的拦截,实现了vm.key属性的监测和响应,当vm[key] = 1
代码片段执行的时候,会监测到该属性的赋值操作(这里不能说监测到值的更改,因为如果原有值也为1的话,那么就不能算更改,因为最后的值为1),然后执行directive.update更新操作。
(这里并不是说vue响应式特性存在的问题,因为vue在此基础上进行了不少的优化,这里讨论的是set/get监测数据存在的问题,毕竟优化问题和消灭问题区别还是很大的)
var data = {
value: 1
}
data.value = 1;
data.value = 1;
如果两次set的值是一样的,那么会触发两次作用一样的更新操作,无疑造成了资源的浪费。
如果想通过比较连续两次值是否相等来过滤掉其中一次无意义的更新操作所带来的成本是很大的,因为属性的值可能会是对象、函数、数组等复合对象,要判断此次更新对页面是否有副作用是很困难的。
这种方式的更新不是监测赋值操作而是将触发的时机交给了开发者.
因为需要开发者手动更新,所以与get/set的方式相比会显得繁琐,但是却没有上面提及的那几个问题(复合对象值比较的问题依然不能解决,但是可以将其留给开发者来决定是否需要更新)。
指令式声明以其简洁、复合html语法、易于学习等特点为其带来很大的优势,但在可编程性方面却很差。
HTML一直在web编程中占有很大的地位,HTML这种标记类型的语言的确经受住了万维网发展的考验,在文本传输和显示上如鱼得水。但现在对网页的要求已不再是内容的展示,UI交互已成为重点,而这点上,HTML显得很乏力。
从组件化的角度来考虑,HTML也有局限,复用性和正交性比较弱,而这在组件化方案中是很受重视的。
函数式编程**逐渐得到大众认可,其优点也逐渐凸显,基于函数式的UI生成方式也成为一种不错的选择.
采用函数生成dom而不是解析dom的形式,这样可以避免dom解析遍历资源浪费的问题,因为dom是通过相应的函数指令生成的,所以viewModel可以精确的追踪view中的相关节点。
待更新。。。
源码:https://github.com/xiaofuzi/re-vue/tree/9de26c017dc937e19faec6b962d28a444cea7af4
String | Node
根节点选择器或是根节点dom元素。
Object
初始化响应式数据模型
Object
计算属性,每一个元素对应一个函数
注:
* computed属性依赖于data中的响应式数据
* computed属性可依赖computed属性
* computed禁止赋值操作
methods
Type: Object
每一个元素对应一个函数,支持响应式替换
watch
Type: Object
监测对象,监测对应的响应式数据,当数据发生更改时执行回调.
Function
如:
var vm = new TinyVue({
data: {
info: {
age: 18
}
}
});
vm.$watch('info', function (info) {
});
vm.$watch('info.age', function (age) {
})
* $directive
Type: `Function`
自定义指令
如:
```js
vm.$directive('text', function (text) {
this.el.textContent = text;
});
beforeCompiler
生命周期函数,编译前执行
ready
生命周期函数,渲染完毕后执行
example:
<div id="app">
<h2 v-text='hello' v-visible='isShow'></h2>
<input type="text" v-model='counter'>
<button v-on:click='add' type="button">add</button>
<button v-on:click='toggle' type="button">toggle</button>
<p v-text='counter'></p>
<p v-text='info.age'></p>
<p v-text='wellcome.text'></p>
</div>
var mvvm;
var opts = {
el: '#app',
data: {
isShow: false,
counter: 1,
hello: 'ahahah!',
info: {
age: 18
},
person: {
weight: 20,
height: 170
}
},
computed: {
wellcome () {
return {text: this.hello + '---' + this.info.age};
}
},
methods: {
add: function () {
this.counter += 1;
this.info.age += 1;
},
toggle: function () {
this.isShow = !this.isShow;
}
},
watch: {
counter (val) {
console.log('counter: ', val);
},
info (info) {
console.log('info: ', info);
},
'info.age' () {
},
wellcome () {
console.log('wellcome: ', this.wellcome);
}
},
ready () {
let self = this;
self.hello = 'Ready, go!';
setTimeout(function () {
self.hello = 'Done!';
}, 1000)
}
}
TinyVue.$directive('visible', function (value) {
this.el.style.visibility = value ? 'visible' : 'hidden';
})
mvvm = new TinyVue(opts);
(注:这里是笔者自己尝试的一种实现方式,也许与vue的实现方式会有所区别)
这里对数据的响应式定义进行了调整,之前的tinyVue采用的是遍历 DOM 节点,获取指令对应的key然后将获得的key所对应的对象进行了getter/setter操作的处理,这种实现方式对watch功能和计算属性的实现不便,所以更改为直接将 option.data 对象转换为响应式对象,这样就不会受限于模板中声明的指令对应key的限制,从而未绑定指令的key也可以转换为可监测的。
tinyVue中是通过给每个key建立对应的binding对象来实现响应式的(setter/getter监测由binding对象内部实现)。tinyVue则维护数据模型得到的所有的binding,存储在_bindings中(关于binding的详细说明可参考这里)。
简单的说就是监测到某一属性的变化后执行相应的回调。
例如:
var vm = new TinyVue({
data: {
name: 'xiaofu',
info: {
height: 170
}
},
watch: {
name: function (newValue, oldValue) {
console.log(newValue);
},
info: function (info, oldInfo) {
console.log(info);
},
'info.height': function (height, oldHeight) {
console.log(height);
}
},
ready () {
this.name = 'xiaoyang';
this.info.height = 180;
}
});
上述例子中,分别定义了name
,info
,info.height
的watch函数,watch回调函数包含两个参数,分别为新值和旧值。
因为子属性的变化也需要触发父属性的回调,所以在这里采用冒泡的实现方式,即当一个属性监测到它发生了变化时,它会通知它的父级发生了变化,父级再往上传,从而实现了当监测info
的时候,如果info.height
发生了变化,那么info
也会知道已发生了变化。
这里完全禁止了计算属性的赋值操作,因为给计算属性赋值其实是没必要的,用了反而会影响逻辑,因为赋值操作比较分散,增加理解的难度。
这里比较难以实现的是依赖的收集,这里先将计算属性对应的求值函数定义为该计算属性的getter函数,如下所示:
defineComputedProperty () {
let key = this.key,
obj = this.vm,
self = this;
def(obj, key, {
get () {
let getter = self.vm._opts.computed[key];
if (isFunc(getter)) {
self.value = getter.call(self.vm);
return self.value;
}
},
set () {
//console.warn('computed property is readonly.');
}
});
}
当我们对计算属性取值时,会调用对应的求值函数来得到计算属性的值,依赖的收集也可以在这里进行,当求值函数执行的时候,求值的过程中会发生依赖项的getter操作,通过监测发生的getter操作即可得到依赖项。
如下所示:
def(obj, key, {
get () {
observer.isObserving && observer.emit('get', self);
return self.value;
},
set (value) {
if (value !== self.value) {
self.oldValue = self.value;
if (!isObj) {
self.value = value;
self.update(value);
} else {
for (let prop in value) {
self.value[prop] = value[prop];
}
}
observer.emit(self.key, self);
self.refresh();
}
}
});
observer.isObserving && observer.emit('get', self);
首先通过 observer.isObserving来标识一次计算属性的取值过程,然后监测 emit 的 get 事件,事件传回来的self即该计算属性的依赖项,这样就得到了计算属性的依赖项,收集完毕后我们只要监测在setter中触发的emit事件即可实现计算属性的更新(与watch的实现是有所区别的)。
在指令更新上计算属性也需要单独处理,当计算属性更新后,需要通知其所有子属性绑定的指令执行更新操作。
如下所示:
_bind (el, directive) {
el.removeAttribute(prefix + '-' + directive.name);
directive.el = el;
let key = directive.key,
binding = this._bindings[key];
if (!binding) {
/**
* computed property binding hack
* 针对计算属性子属性
*/
//get computed property key
let computedKey = key.split('.')[0];
binding = this._bindings[computedKey];
if (binding.isComputed) {
binding.directives.push(directive);
} else {
console.error(key + ' is not defined.');
}
}
binding.directives.push(directive);
}
在_bing函数中收集计算属性相关的所有指令并存储下来,然后在更新的时候动态的更新指令。
待更新。。。
对于刚接触vue的同学会经常遇到数据更新了但是模板没有更新的问题,下面将结合vue的响应式特性以及异步更新机制分析常见的错误:
异步数据的处理基本是一定会遇到的,处理不好就会遇到数据不更新的问题,但有一种情况是在未正确处理的情况下也能正常更新,这就会造成一种误解,详情如下所示:
<div id="app">
<h2>{{dataObj.text}}</h2>
</div>
new Vue({
el: '#app',
data: {
dataObj: {}
},
ready: function () {
var self = this;
/**
* 异步请求模拟
*/
setTimeout(function () {
self.dataObj = {};
self.dataObj['text'] = 'new text';
}, 3000);
}
})
上面的代码非常简单,我们都知道vue中在data里面声明的数据才具有响应式的特性,所以我们一开始在data中声明了一个dataObj空对象,然后在异步请求中执行了两行代码,如下:
self.dataObj = {};
self.dataObj['text'] = 'new text';
首先清空原始数据,然后添加一个text属性并赋值。到这里为止一切都如我们所想的,数据和模板都更新了。
模板更新了,应该具有响应式特性,如果这么想那么你就已经走入了误区,一开始我们并没有在data中声明.text
属性,所以该属性是不具有响应式的特性的。
但模板切切实实已经更新了,这又是怎么回事呢?
那是因为vue的dom更新是异步的,即当setter操作发生后,指令并不会立马更新,指令的更新操作会有一个延迟,当指令更新真正执行的时候,此时.text
属性已经赋值,所以指令更新模板时得到的是新值。
具体流程如下所示:
self.dataObj = {};
发生setter操作self.dataObj['text'] = 'new text';
赋值语句所以真正的触发更新操作是self.dataObj = {};
这一句引起的,所以单看上述例子,具有响应式特性的数据只有dataObj这一层,它的子属性是不具备的。
注:其实vue文档中已经有说明,对于新增以及删除的属性,vue是无法监测到的。
var a = {};
a.b = 0; //新增b属性
a = {
c: 0
}; //更改a属性的值
上述两种赋值方式对vue造成的影响是不同的。
对比示例:
<div id="app">
<h2>{{dataObj&&dataObj.text}}</h2>
</div>
new Vue({
el: '#app',
data: {
dataObj: {}
},
ready: function () {
var self = this;
/**
* 异步请求模拟
*/
setTimeout(function () {
self.dataObj['text'] = 'new text';
}, 3000);
}
})
上述例子的模板是不会更新的。
通过$set方法可以将添加一个具备响应式特性的属性,并且其子属性也具备响应式特性,但是必须是新属性才可以,如果是本身已有的属性该方法是不起作用的。
new Vue({
el: '#app',
data: {
dataObj: {}
},
ready: function () {
var self = this;
/**
* 异步请求模拟
*/
setTimeout(function () {
var data = {
name: 'xiaofu',
age: 18
};
var data01 = {
name: 'yangxiaofu',
age: 19
};
self.dataObj['person'] = {};
self.$set('dataObj.info', data);
self.$set('dataObj.person', data01);
}, 3000);
}
})
如上所示,.person
属性是不具备响应式特性的。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.