Vue 源码解析
Vue 的工作机制
在 new vue()
之后,Vue 会调用进行初始化,会初始化生命周期、事件、props、methods、data、computed和watch等。其中最重要的是通过Object.defineProperty
设置setter
和getter
,用来实现响应式
和依赖收集
。
初始化之后,调用 $mount
挂载组件。
启动编译器compile()
,对template进行扫描,parse、optimize、generate,在这个阶段会生成渲染函数或更新函数,render function
,生成虚拟节点数,将来我们改变的数据,并不是真的DOM操作,而是虚拟DOM上的数值。
在更新前,会做一个diff算法的比较,通过新值和老值的比较,计算出最小的DOM更新。执行到patch()
来打补丁,做界面更新,目的是用JS计算的时间换DOM操作的时间。因为页面渲染很耗时间,所以vue的目的就是减少页面渲染的次数和数量。
render function
除了编译渲染函数以外,还做了一个依赖搜集(界面中做了很多绑定,如何知道和数据模型之间的关系)。当数据变化时,该去界面中更新哪个数据节点。通过观察者watcher()
来调用更新函数patch()
编译
编译模块分为三个阶段
- parse 使用正则解析template中vue的指令变量等,形成语法树AST
- optimize 标记一些静态节点,用作后面的性能优化,在diff的时候直接略过
- generate 把第一步生成的AST转化为渲染函数render function
响应式
vue 核心内容
初始化的时候通过defineProverty进行绑定,设置通知机制,当编译生成的渲染函数被实际渲染时,会触发getter进行依赖收集,在数据变化时,通过setter进行更新。
虚拟DOM
virtual DOM 是react首创,Vue2开始支持,用js对象来描述DOM结构,数据修改的时候,先修改虚拟DOM中的数据,然后数组做diff,最后再汇总所有的diff,力求做最少的dom操作,毕竟js里对比很快,而真实的dom操作太慢。
{
tag: 'div',
props: {
name: 'xx',
style: {color: red},
onClick: xx
},
children: [{
tag: 'a',
text: 'click me'
}]
}
<div name="xx" style="color: red" @click="xx">
<a>click me</a>
</div>
更新视图
数据修改触发setter,然后监听器会通知进行修改,通过对比两个DOM树,得到改变的地方,就是patch,只需要把这些差异修改即可。
Vue2响应式的原理: defineProperty
<div id="app"><div id="name"></div></div>
<script>
let obj = {}
Object.defineProperty(obj, 'name', {
get: function() {
return document.querySelector('#name').innerHTML
},
set: function(val) {
document.querySelector('#name').innerHTML = val
}
})
obj.name='adela'
</script>
描述vue数据绑定的原理
利用了Object.defineProperty这个属性,将data中的每一个属性,都定义了getter和setter,去监听这些属性的变化,当某些属性变化时,我们可以通知需要更新的地方去更新。[数据劫持]
实现数据响应式
监听Observe
增加了一个Dep类,用来搜集Watcher对象。
读数据的时候,会触发getter函数把当前的Watcher对象(存放在Dep.target中)搜集到Dep类中去。
写数据的时候,则会触发setter方法,通知Dep类调用notify来触发所有watcher对象的update方法更新对应视图。
编译Compile
核心逻辑获取DOM,遍历DOM,获取{{}}格式的变量,以及每个DOM的属性,截获k-和@开头的设置响应式。
检查点
- vue编译过程是怎么样的 vue写的模板语句,HTML不识别,通过编译的过程,进行依赖搜集,data中的数据模型和视图进行了绑定,如果模型发生变化,会通知依赖的地方进行更新,这就是执行编译的目的。模型驱动视图。
- 双向绑定的原理是什么 v-model 的指令放在input上,在编译时,可以解析出v-model。操作时做了两件事情,一,在当前v-model所属的元素上加了一个事件监听,v-model指定的事件回调函数当做input事件回调函数去监听,当input发生变化时,就将值更新到vue实例上。二、vue实例已经实现了数据的响应化,setter函数会触发界面中所有依赖的更新。
知识点
let fragment = document.createDocumentFragment();
fragment 是一个指向空DocumentFragment对象的引用。
DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。
因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。
代码
kvue.js文件
class KVue {
constructor(options) {
this.$options = options
this.$data = options.data
this.observe(this.$data)
new Compile(options.el, this)
if (options.created) {
options.created.call(this)
}
}
observe(value) {
if (!value || typeof value !== 'object') {
return
}
Object.keys(value).forEach(key => {
this.defineReactive(value, key, value[key])
// 代理data中的属性到vue实例上
this.proxyData(key)
})
}
defineReactive(obj, key, val) {
this.observe(val) // 递归解决数据嵌套
const dep = new Dep() // 初始化dependence
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if (newVal == val)
return
val = newVal
console.log(`${key}属性更新了:${val}`)
dep.notify()
}
})
}
proxyData(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
}
}
// Dep: 用来管理watcher对象。
// 读数据的时候,会触发getter函数,把当前的Watcher对象(存放在Dep.target中)搜集到Dep类中去。
// 写数据的时候,会触发setter方法,通知Dep类调用notify来触发所有watcher对象的update方法更新对应视图。
class Dep {
constructor() {
// 这里存放若干依赖(watcheer)
this.deps = []
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
// 通知所有的依赖去做更新
this.deps.forEach(dep => dep.update())
}
}
// Wathcer
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
// 将当前watcher实例制定到Dep静态属性target
Dep.target = this
this.vm[this.key] // 触发getter, 添加依赖
Dep.target = null
}
update() {
console.log(`属性更新了`)
this.cb.call(this.vm, this.vm[this.key])
}
}
compile.js文件
class Compile {
constructor(el, vm) {
// 要遍历的宿主节点
this.$el = document.querySelector(el)
this.$vm = vm
// 编译
if (this.$el) {
// 转换内部内容为片段fragment
this.$fragment = this.node2fragment(this.$el)
// 执行编译
this.compile(this.$fragment)
// 将编译完的HTML结果追加至$el
this.$el.appendChild(this.$fragment)
}
}
// 将宿主元素中代码片段拿出来遍历,比较高效
node2fragment(el) {
const frag = document.createDocumentFragment()
// 将el中的所有子元素搬家至frag中
let child
while ((child = el.firstChild)) {
frag.appendChild(child)
}
return frag
}
// 编译过程
compile(el) {
const childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
// 判断类型
if (this.isElement(node)) {
// 元素
// console.log('编译元素' + node.nodeName)
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr => {
const attrName = attr.name // 属性名
const exp = attr.value // 属性值
if (this.isDirective(attrName)) {
// k-text
const dir = attrName .substring(2)
this[dir] && this[dir](node, this.$vm, exp)
} else if (this.isEvent(attrName)) {
let dir = attrName.substring(1)
this.eventHandler(node, this.$vm, exp, dir)
}
})
} else if (this.isInterpolation(node)) {
// 文本
// console.log('编译文本' + node.textContent)
this.compileText(node)
}
// 递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
compileText(node) {
// console.log(RegExp.$1)
this.update(node, this.$vm, RegExp.$1, 'text')
}
// 更新函数
update(node, vm, exp, dir) {
const updaterFn = this[dir + 'Updater']
// 初始化
updaterFn && updaterFn(node, vm[exp])
// 依赖收集
new Watcher(vm, exp, function(value) {
updaterFn && updaterFn(node, value)
})
}
text(node, vm, exp) {
this.update(node, vm, exp, 'text')
}
// 事件处理器
eventHandler(node, vm, exp, dir) {
let fn = vm.$options.methods && vm.$options.methods[exp]
if (dir && fn) {
node.addEventListener(dir, fn.bind(vm))
}
}
html(node, vm, exp) {
this.update(node, vm, exp, 'html')
}
// 双向绑定
model(node, vm, exp) {
// 指定input的value属性
this.update(node, vm, exp, 'model')
// 视图对模型响应
node.addEventListener('input', e => {
vm[exp] = e.target.value
})
}
modelUpdater(node, value) {
node.value = value
}
textUpdater(node, value) {
node.textContent = value
}
htmlUpdater(node, value) {
node.innerHTML = value
}
isDirective(attr) {
return attr.indexOf('k-') == 0
}
isEvent(attr) {
return attr.indexOf('@') == 0
}
isElement(node) {
return node.nodeType === 1
}
isInterpolation(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
}
index.html文件
<body>
<div id="app">
<p>{{name}}</p>
<p k-text="name"></p>
<p>{{age}}</p>
<p> {{doubleAge}} </p>
<input type="text" k-model="name">
<button @click="changeName">click me</button>
<div k-html="html"></div>
</div>
<script src='./compile.js'></script>
<script src='./kvue.js'></script>
<script>
let xx = new KVue({
el: '#app',
data: {
name: "I am test.",
age: 12,
html: '<button>这是一个按钮</button>'
},
created() {
console.log('开始啦')
setTimeout(() => {
this.name = '我是测试'
}, 1500)
}, methods: {
changeName() {
this.name = '哈喽,嘻嘻嘻'
this.age = 1
this.id = 'xx'
console.log(1, this)
}
}
})
</script>
</body>
【VUE】Vue 源码解析的更多相关文章
-
Vue.js源码解析-从scripts脚本看vue构建
目录 1. scripts 脚本构建 1.1 dev 开发环境构建过程 1.1.1 配置文件代码 1.1.2 如何进行代码调试? 1.2 build 生产环境构建过程 1.2.1 scripts/bu ...
-
Vue.js源码解析-Vue初始化流程
目录 前言 1. 初始化流程概述图.代码流程图 1.1 初始化流程概述 1.2 初始化代码执行流程图 2. 初始化相关代码分析 2.1 initGlobalAPI(Vue) 初始化Vue的全局静态AP ...
-
Vue.js源码解析-Vue初始化流程之动态创建DOM
目录 前言 一._update 如何判断是初始化还是更新操作? 二.patch 2.1 patch 定义 2.2 初始化的 patch 三.createElm 动态创建DOM 3.1 创建组件节点 3 ...
-
【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器
写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. vuejs是一个优秀的前端mvvm框架,它的易用性和渐进式的理念可以使每一个前端开发人员感到舒服,感到easy.它内 ...
-
【vuejs深入三】vue源码解析之二 htmlParse解析器的实现
写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. 昨天博主分析了一下在vue中,最为基础核心的api,parse函数,它的作用是将vue的模板字符串转换成ast,从而 ...
-
vue UI库iview源码解析(2)
上篇问题 在上篇<iview源码解析(1)>中的index.js 入口文件的源码中有一段代码有点疑惑: /** * 在浏览器环境下默认加载组件 */ // auto install if ...
-
Vue源码解析(一):入口文件
在学习Vue源码之前,首先要做的一件事情,就是去GitHub上将Vue源码clone下来,目前我这里分析的Vue版本是V2.5.21,下面开始分析: 一.源码的目录结构: Vue的源码都在src目录下 ...
-
Vue源码解析---数据的双向绑定
本文主要抽离Vue源码中数据双向绑定的核心代码,解析Vue是如何实现数据的双向绑定 核心思想是ES5的Object.defineProperty()和发布-订阅模式 整体结构 改造Vue实例中的dat ...
-
Vue源码解析之数组变异
力有不逮的对象 众所周知,在 Vue 中,直接修改对象属性的值无法触发响应式.当你直接修改了对象属性的值,你会发现,只有数据改了,但是页面内容并没有改变. 这是什么原因? 原因在于: Vue 的响应式 ...
-
Vue源码解析之nextTick
Vue源码解析之nextTick 前言 nextTick是Vue的一个核心功能,在Vue内部实现中也经常用到nextTick.但是,很多新手不理解nextTick的原理,甚至不清楚nextTick的作 ...
随机推荐
-
MongoDB的安装和配置成服务的三种方法和一些难点
1. Hotfix KB2731284 or later update is not installed的问题: If you are running any edition of Windows S ...
-
Redis在Linux下的安装和启动和配置
第一步:下载Redis安装包,下载版本:3.0.5 在所在目录右键打开终端输入命令: wget http://download.redis.io/releases/redis-3.0.5.tar.gz ...
-
rails再体验(第一个程序)
掌握redmine plugin开发的目标在2016年未实现,2017年继续. 选择<Ruby on Rails Tutorial>教程,windows安装railsinstaller,该 ...
-
strtok和strtok_r
1.strtok()函数的用法 函数原型:char *strtok(char *s, const char *delim); Function:分解字符串为一组字符串.s为要分解的字符串,delim为 ...
-
shell语法基础
一.变量 1.linux大小写敏感,变量取名要注意大小写.可以通过变量名前面加$来访问变量的内容.可以通过使用read命令来将用户输入的值赋给一个变量. 2.给变量赋值时,如果字符串中包含空格,就必须 ...
-
配置Apache+Mysql+Php
以下操作均在Debian 6.0 64bit 环境root权限下进行,如果提示权限不足请切换至root用户或者sudo,本人比较喜欢自行安装,因为安装的过程中能最小化安装而且能够知道安装了什么,然后可 ...
-
Html 中select标签的边框与右侧倒三角的去除
首先是边框的去除:可以设置属性border:none;或border:0px; 不过这还是有一个bug,不同浏览器会在选中select标签时,加上一个边框: 之后是右侧倒三角的去除:设置属性 appe ...
-
Java中的Math类的简单实用
System.out.println(Math.PI);//获取PI的值 System.out.println(Math.E);//常量E int min = Math.min(5, 4);//求最小 ...
-
Hyperledger Fabric 1.0 从零开始(十二)——fabric-sdk-java应用
Hyperledger Fabric 1.0 从零开始(十)--智能合约 Hyperledger Fabric 1.0 从零开始(十一)--CouchDB 上述两章,最近网上各路大神文章云集,方案多多 ...
-
中兴F660光猫改桥接
家里使用的电信宽带,电信给配的是中兴的F660光猫.光猫内置路由和WIFI功能,但是无线有些稳定,希望把光猫改成桥接模式,使用自己的路由拨号. 所需工具 offzip.exe(下载地址http://a ...