vue3.0Beta 版本已经上线, 听了 Evan 在 bilibili 上的最新的介绍, 特性不多 (高频用法 Proxy,Reflect), 但想和 Vue2.x 版本做个对比, 决定再读一下 2.x 源码, 本文主要用代码截图和自己的理解图介绍.
高频用法
一些高频用法及技术点: 类, 函数柯里化, 递归, Object.create,Object.defineProperty,macrotask,microtask,AST,vnode, 相关知识点请自行查阅, 本文主要从源码角度分析各个关键点的实现
主要从以下关键点入手
vue 源码地址: https://github.com/vuejs/vue.git
1 调试环境
1.1 添加 sourcemap
- # package.JSON->scripts->dev
- "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap",
添加 sourcemap
1.2 安装依赖
NPM i
1.3 查看编译入口
script/config.JS
别名查找
1.4 启动调试
将 dist 文件夹删除
运行 NPM run dev
修改 / vue/examples/commits/index.html 文件 vue.min.JS 改为 vue.js
index.HTML 直接用浏览器打开, 我的是放在 D/workspace
file:///D:/workspace/vue/examples/commits/index.HTML
浏览器短点调试
2 初始化过程
2.1 从调用栈看执行过程
好了, 你可以按 F11 逐步跟进查看源码, 下图是我的调用栈跟进信息
根据下图, 你可以查看文件对应的执行函数
调用栈信息
根据以上调用栈我将 vue 视图渲染分为几个阶段来查看源代码
几个阶段
2.2 初始化阶段
其实这些都是比较容易看懂, 我们只看关键点做了那些事情, 和一些不容易发现的细节
从下图看到各个阶段都做了什么事情, 一张图能够搞明白加载顺序了吧
vue._init
2.3 数据劫持, 依赖收集
将为模版每个读取到的属性创建一个 watcher, 例如有两处 {{title}}, 则将会为 title 属性创建一个依赖, 两个 watcher, 这点明白基本上数据劫持就通了.
数据劫持相关调用
3 渲染过程
3.1 模版编译
这里基本上都是通用的思想了
核心代码
- const ast = parse(template.trim(), options)
- console.log('ast:',ast)
- if (options.optimize !== false) {
- optimize(ast, options)
- }
- const code = generate(ast, options)
- return {
- ast,
- render: code.render,// 创建虚拟 vdom 的字符串
- staticRenderFns: code.staticRenderFns
- }
看看如下返回的 render 是什么?
_c 函数是什么呢?
- const { render, staticRenderFns } = compileToFunctions(template, {
- outputSourceRange: process.env.NODE_ENV !== 'production',
- shouldDecodeNewlines,
- shouldDecodeNewlinesForHref,
- delimiters: options.delimiters,
- comments: options.comments
- }, this)
- options.render = render
- options.staticRenderFns = staticRenderFns
- (function anonymous(
- ) {
- with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("Latest Vue.js Commits")]),_v(""),_l((branches),function(branch){return [_c('input',{directives:[{name:"model",rawName:"v-model",value:(currentBranch),expression:"currentBranch"}],attrs:{"type":"radio","id":branch,"name":"branch"},domProps:{"value":branch,"checked":_q(currentBranch,branch)},on:{"change":function($event){currentBranch=branch}}}),_v(" "),_c('label',{attrs:{"for":branch}},[_v(_s(branch))])]}),_v(" "),_c('p',[_v("vuejs/vue@"+_s(currentBranch))]),_v(" "),_c('ul',_l((commits),function(record){return _c('li',[_c('a',{staticClass:"commit",attrs:{"href":record.html_url,"target":"_blank"}},[_v(_s(record.sha.slice(0, 7)))]),_v("\n - "),_c('span',{staticClass:"message"},[_v(_s(_f("truncate")(record.commit.message)))]),_c('br'),_v("\n by "),_c('span',{staticClass:"author"},[_c('a',{attrs:{"href":record.author.html_url,"target":"_blank"}},[_v(_s(record.commit.author.name))])]),_v("\n at "),_c('span',{staticClass:"date"},[_v(_s(_f("formatDate")(record.commit.author.date)))])])}),0)],2)}
- })
模版编译调用关系图
3.2 初次渲染
如 2.3 图, 在初次渲染时点,_c 方法其实就是将 render 函数转化为 vdom 的过程
创建 vdom 的过程
3.3 再次渲染, 触发更新
还如 2.3 图, 再次触发的点即是数据变化的点
setter 中修改数据
修改完数据, 依赖通知 dep.notify()
watcher 收到通知进行_updater, 这里的 updater 是在初始化 render 时初始化给了 watcher.getter
getter 所对应的方法看调用栈还是比较好看出来
getter 对一个的方法
3.4 Patch
清楚了上面的触发点为 wathcer 的 getter 方法, 在结合如下调用栈, 可以切换下 checkbox, 查看调用栈
剩下的就是集中对比新老 vnode 的递归操作了, 这里的源码想了解得自己细看了
再次渲染的关键点
4 其他举例
4.1 Array 的重写
数组类型的响应式实现, 改写后我们可以这样对数组进行响应是设置新值了
数组正确的操作方式
- // vm.$set(this.items,1,'xxx')
- // vm.items.splice(0)
- import { def } from '../util/index'
- const arrayProto = Array.prototype
- export const arrayMethods = Object.create(arrayProto)
- const methodsToPatch = [
- 'push',
- 'pop',
- 'shift',
- 'unshift',
- 'splice',
- 'sort',
- 'reverse'
- ]
- /**
- * Intercept mutating methods and emit events
- */
- methodsToPatch.forEach(function (method) {
- // cache original method
- const original = arrayProto[method]
- def(arrayMethods, method, function mutator (...args) {
- const result = original.apply(this, args)
- const ob = this.__ob__
- let inserted
- switch (method) {
- case 'push':
- case 'unshift':
- inserted = args
- break
- case 'splice':
- inserted = args.slice(2)
- break
- }
- if (inserted) ob.observeArray(inserted)
- // notify change
- ob.dep.notify()
- return result
- })
- })
- 4.2 V-modle
我们看看语法糖, 在看源码
<input v-bind:value="message" v-on:input="message = $event.target.value" />
v-modle 指令
- function onCompositionEnd (e) {
- // prevent triggering an input event for no reason
- if (!e.target.composing) return
- e.target.composing = false
- trigger(e.target, 'input')
- }
- function trigger (el, type) {
- const e = document.createEvent('HTMLEvents')
- e.initEvent(type, true, true)
- el.dispatchEvent(e)
- }
- 4.3 nextTick
微任务的使用
- // timerFunc ---> flushCallbacks
- export function nextTick (cb?: Function, ctx?: Object) {
- let _resolve
- callbacks.push(() => {
- if (cb) {
- try {
- cb.call(ctx)
- } catch (e) {
- handleError(e, ctx, 'nextTick')
- }
- } else if (_resolve) {
- _resolve(ctx)
- }
- })
- if (!pending) {
- pending = true
- timerFunc()
- }
- // $flow-disable-line
- if (!cb && typeof Promise !== 'undefined') {
- return new Promise(resolve => {
- _resolve = resolve
- })
- }
- }
4.4 销毁
其实上诉内容知道关键点和渲染点, 就非常容易了, 在对比 vnode 时 patch 所触发销毁即可, 知道触发点继续执行销毁事件
- // patch.JS
- // destroy old node
- if (isDef(parentElm)) {
- removeVnodes([oldVnode], 0, 0)
- } else if (isDef(oldVnode.tag)) {
- invokeDestroyHook(oldVnode)
- }
通篇读下来感觉 vue 还是很小巧的, 之后再来阅读一下 vue3.0 代码看看区别
来源: https://www.qcloud.com/developer/article/1622265