文章标题

vue源码学习
有关vue
先对Vue 2.0的新特性做一个简单的介绍:
• 大小 & 性能。Vue 2.0的线上包gzip后只有12Kb,而1.0需要22Kb,react需要44Kb。而且,Vue 2.0的性能在react等几个框架中,性能是最快的。
• VDOM。实现了Virtual DOM, 并且将静态子树进行了提取,减少界面重绘时的对比。与1.0对比性能有明显提升。
• template & JSX。众所周知,Vue 1.0使用的是template来实现模板,而React使用了JSX实现模板。关于template和JSX的争论也很多,很多人不使用React就是因为没有支持template写法。Vue 2.0对template和JSX写法都做了支持。使用时,可以根据具体业务细节进行选择,可以很好的发挥两者的优势。就这一点,Vue已经超过React了。
• Server Render。2.0还对了Server Render做了支持。这一点并没有在业务中使用,不做评价。
本次主要讲解一些vue启动的流程,如何实现一个MVVM,还有虚拟DOM的解析。
项目启动的bug
Vue的最新源码可以去 https://github.com/vuejs/vue 获得。本文讲的是 2.0.3版本,2.0.3可以去 https://github.com/vuejs/vue/ 这里获得。
这里提一下有关,window10 里面有个bug有关 rollup插件的bug,无法将一些斜杠统一,官方的解决方案如下https://github.com/vuejs/vue/issues/2771
vue\src\core/index
生命周期

上图就是官方给出的Vue 2.0的生命周期图,其中包含了Vue对象生命周期过程中的几个核心步骤。了解了这几个过程,可以很好的帮助我们理解Vue的创建与销毁过程。 从图中我们可以看出,生命周期主要分为4个过程:
• create。new Vue时,会先进行create,创建出Vue对象。
• mount。根据el, template, render方法等属性,会生成DOM,并添加到对应位置。
• update。当数据发生变化后,会重新渲染DOM,并进行替换。
• destory。销毁时运行。
那么这4个过程在源码中是怎么实现的呢?我们从new Vue开始。
new Vue()
下面来分析下具体的过程和代码:
首先,运行new Vue()的时候,会进入代码src/core/instance/index.js的Vue构造方法中,并执行this._init()方法。在_init中,会对各个功能进行初始化,并执行beforeCreate和created两个生命周期方法。核心代码如下:
initRender(vm)
callHook(vm, ‘beforeCreate’)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, ‘created’)
beforeCreate和created之间只有initState,和官方给出的生命周期图并不完全一样。这里的 initState是用于初始化data,props等的监听的。 其实在beforeCreate中,虚拟的DOM已经生成了。
在_init的最后,会运行initRender方法。在该方法中,会运行vm. mountif(vm. options.el) {
vm. mount(vm. options.el)
}
这里的$mount在src/platforms/web-runtime-with-compiler.js中,主要逻辑是根据el, template, render三个属性来获得AST render方法。代码如下:
if (!options.render) { // 如果有render方法,直接运行mount
let template = options.template
if (template) { // 如果有template, 获取template参数对于的HTML作为模板
if (typeof template === ‘string’) {
if (template.charAt(0) === ‘#’) {
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== ‘production’) {
warn(‘invalid template option:’ + template, this)
}
return this
}
} else if (el) { // 如果没有template, 且存在el,则获取el的outerHTML作为模板
template = getOuterHTML(el)
}
if (template) { // 如果获取到了模板,则将模板转化为render方法
const { render, staticRenderFns } = compileToFunctions(template, {
warn,
shouldDecodeNewlines,
delimiters: options.delimiters
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
这个过程有三点需要注意: compile时,将最大静态子树提取出来作为单独的AST渲染方法,以提升后面vNode对比时的性> 能。所以,当存在多个连续的静态标签时,可以在外边添加一个静态父节点,这样,staticRenderFns数目可以减少,从而提升性能。 Vue 2.0中的模板有三种引用写法:el, template, render(JSX)。其中的优先级是render > template > el。 el, template两种写法,最后都会通过compiler转化为render(JSX)来运行,也就是说,直接写成render(JSX)是性能最优的。当然,如果使用了构建工具,最终生成的包就是使用的render(JSX)。这样子,在源码上就可以不用过多考虑这一块的性能了,直接用可维护性最好的方式就行。
将模板转化为render,用到了compileToFunctions方法,该方法最后会通过src/compiler/index.js文件中的compile方法,将模板转化为AST语法结构的render方法,并对静态子树进行分离。
完成render方法的生成后,会进入_mount(src/core/instance.lifecycle.js)中进行DOM更新。该方法的核心逻辑如下:
vm._watcher = new Watcher(vm, () => {
vm._update(vm._render(), hydrating)
}, noop)
首先会new一个watcher对象,在watcher对象创建后,会运行传入的方法vm._update(vm._render(), hydrating)(watcher的逻辑在下面的watcher小节中细讲)。其中的vm._render()主要作用就是运行前面compiler生成的render方法,并返回一个vNode对象。这里的vNode就是一个虚拟的DOM节点。
拿到vNode后,传入vm._update()方法,进行DOM更新。
用简单的demo来实现一个MVVM
目的很简单,创建一个对象,根据数据的变化来直接影响到绑定的DOM节点
// 数据转化的方法,命名一个对象
const xxObj = {
a: 1,
b: 2
}
//这里对数据进行一个绑定的操作

document.querySelector(“#xxtest”).innerHTML = xxObj.a

//最后对值进行修改
xxObj.a = 2
vue是引入了 observer,watch,dep来处理这一系列操作的
observer
第一步:观察者引入一个方法Object.definProperty,来对数据进行get set操作,来监听数据是否被绑定
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
// 遍历 data 对象属性,调用 defineReactive 方法
let keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
}

// defineReactive方法仅仅将data的属性转换为访问器属性
function defineReactive(data, key, val) {
// 递归观测子属性
observer(val)

Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
        return val
    },
    set: function (newVal) {
        if (val === newVal) {
            return
        }
        val = newVal

        observer(newVal)
    }
})

}
当我们对数据赋值时,数据中的set方法会判断数据是否被修改了,如果被修改,那么就修改数据,并对修改以后的数据再次附上观察者。这里仅仅只做了观察,每次赋值后都会进入到,对象的set方法。
第二步,添加watch,并绑定我们的数据,如果数据绑定了watch,并有修改,则通过观察者通知到watch。由于watch只是一个事件的处理,所以这里还引入了一个dep,dep内部是一个数组,用来储存所有的watcher。一但有数据的变化,则通知到相应的watcher。
class Dep {
constructor() {
this.subs = []
}
depend() {
if (Dep.target && this.subs.indexOf(Dep.target) === -1) {
this.subs.push(Dep.target)
}
}
notify() {
for (let i = 0; i < this.subs.length; i++) {
this.subs[i].fn()
}
}
}
class Watch {
constructor(exp, fn) {
this.exp = exp
this.fn = fn
pushTarget(this)
xxObj[exp]
}
}

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) { // 这里我们防止循环调用,则只启用一个Dep
dep.depend() // 处理好依赖watcher

}
return value
},
set: function reactiveSetter (newVal) {

childOb = observe(newVal) // 对新数据重新observe
dep.notify() // 通知到dep进行数据更新
}
})
在vue源码中的代码不只这些,它是将里面的dep和props的绑定起来。但总体的逻辑还是逃不开这个思路,主要的关键点是要理解Object.defineProperty(),这里可以参考文献Object.defineProperty的作用 深度数据解析和数组的绑定我暂时没实现,这里有相关的数据代码,但还是存在一些bug,就是对象的数据第二层无法是数组。
用简单的demo来实现一个虚拟dom
首先必须明确一点DOM是很慢的
如果我们把一个简单的div元素的属性都打印出来,你会看到:

而这仅仅是第一层。真正的 DOM 元素非常庞大,这是因为标准就是这么设计的。而且操作它们的时候你要小心翼翼,轻微的触碰可能就会导致页面重排,这可是杀死性能的罪魁祸首。相对于 DOM 对象,原生的 JavaScript 对象处理起来更快,而且更简单。DOM 树上的结构、属性信息我们都可以很容易地用 JavaScript 对象表示出来:
var tree = el(‘ul’, {
id: ‘list’
}, [
el(‘li’, {
class: ‘item’
}, [‘Item 1’]),
el(‘li’, {
class: ‘item’
}, [‘Item 2’]),
el(‘li’, {
class: ‘item’
}, [‘Item 3’])
])
上面对应的HTML写法是:

  • Item 1
  • Item 2
  • Item 3

实现算法
function Element (tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}

Element.prototype.render = function () {
var el = document.createElement(this.tagName) // 根据tagName构建
var props = this.props

for (var propName in props) { // 设置节点的DOM属性
var propValue = props[propName]
el.setAttribute(propName, propValue)
}

var children = this.children || []

children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
: document.createTextNode(child) // 如果字符串,只构建文本节点
el.appendChild(childEl)
})

return el
}

module.exports = function (tagName, props, children) {
return new Element(tagName, props, children)
}
这里的render算法是根据虚拟dom数据便利并生成dom节点
对比2个虚拟dom的数据,只修改相应的节点数据
实现的对比算法有很多,我偷工减料参考了list-diff2模块,对数据进行对比,这块可以深入研究;在vue中还加了很多节点类型,先对类型进行判断,在判断节点的数据,可以参考patch里面的sameVnode方法
最终实现的内容都是相同的,参考了vue的patch方法,最后生成一个对比完的数组patch,将不同的数据存到数组内
代码过长就不贴了
将变更的dom节点重绘
function patch (node, patches) {
var walker = {index: 0}
dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
var currentPatches = patches[walker.index]

var len = node.childNodes
? node.childNodes.length
: 0
for (var i = 0; i < len; i++) {
var child = node.childNodes[i]
walker.index++
dfsWalk(child, walker, patches)
}

if (currentPatches) {
applyPatches(node, currentPatches)
}
}
表现层代码展现
Virtual DOM 算法主要是实现上面步骤的三个函数:element,diff,patch。然后就可以实际的进行使用:
import el from ‘./element’
import diff from ‘./diff’
import patch from ‘./patch’

// 1. 构建虚拟DOM
var tree = el(‘ul’, {
id: ‘list’
}, [
el(‘li’, {
class: ‘item’
}, [‘Item 1’]),
el(‘li’, {
class: ‘item’
}, [‘Item 2’]),
el(‘li’, {
class: ‘item’
}, [‘Item 3’])
])

console.log(tree)

var root = tree.render()
document.body.appendChild(root)

// 3. 生成新的虚拟DOM
var newTree = el(‘ul’, {
id: ‘list’
}, [
el(‘li’, {
class: ‘item’
}, [‘Item 1’]),
el(‘li’, {
class: ‘item’
}, [‘Item 22’]),
el(‘li’, {
class: ‘item’
}, [‘Item 3’])
])

// 4. 比较两棵虚拟DOM树的不同
var patches = diff(tree, newTree)

console.log(patches)

patch(root, patches)
在vue实现中,可以参考里面的initRender方法,里面还是引入了defineReactive对dom里面的值做了事件监听,也监听了data里面的参数,如果何vnode有绑定会同步更新节点或者数据。
这里代码还有些缺陷,只能改html内容,无法修改attr的属性 在patch和值绑定的方面,一笔带过,无法定位到vue是如何实现的 用一些vue的模板和jsx的模板上面还没有研究很深

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值