网络编程 
首页 > 网络编程 > 浏览文章

浅谈vue的第一个commit分析

(编辑:jimmy 日期: 2024/5/8 浏览:3 次 )

为什么写这篇vue的分析文章?

对于天资愚钝的前端(我)来说,阅读源码是件不容易的事情,毕竟有时候看源码分析的文章都看不懂。每次看到大佬们用了1~2年的vue就能掌握原理,甚至精通源码,再看看自己用了好几年都还在基本的使用阶段,心中总是羞愧不已。如果一直满足于基本的业务开发,怕是得在初级水平一直待下去了吧。所以希望在学习源码的同时记录知识点,可以让自己的理解和记忆更加深刻,也方便将来查阅。

目录结构

本文以vue的第一次 commit a879ec06 作为分析版本

├── build
│  └── build.js        // `rollup` 打包配置
├── dist            
│  └── vue.js  
├── package.json
├── src            // vue源码目录
│  ├── compiler        // 将vue-template转化为render函数
│  │  ├── codegen.js     // 递归ast提取指令,分类attr,style,class,并生成render函数
│  │  ├── html-parser.js   // 通过正则匹配将html字符串转化为ast
│  │  ├── index.js      // compile主入口
│  │  └── text-parser.js   // 编译{{}}
│  ├── config.js       // 对于vue的全局配置文件
│  ├── index.js        // 主入口
│  ├── index.umd.js      // 未知(应该是umd格式的主入口)
│  ├── instance        // vue实例函数
│  │  └── index.js      // 包含了vue实例的初始化,compile,data代理,methods代理,watch数据,执行渲染
│  ├── observer        // 数据订阅发布的实现
│  │  ├── array.js      // 实现array变异方法,$set $remove 实现
│  │  ├── batcher.js     // watch执行队列的收集,执行
│  │  ├── dep.js       // 订阅中心实现
│  │  ├── index.js      // 数据劫持的实现,收集订阅者
│  │  └── watcher.js     // watch实现,订阅者
│  ├── util          // 工具函数
│  │  ├── component.js
│  │  ├── debug.js
│  │  ├── dom.js
│  │  ├── env.js       // nexttick实现
│  │  ├── index.js
│  │  ├── lang.js
│  │  └── options.js
│  └── vdom
│    ├── dom.js       // dom操作的封装
│    ├── h.js        // 节点数据分析(元素节点,文本节点)
│    ├── index.js      // vdom主入口
│    ├── modules      // 不同属性处理函数
│    │  ├── attrs.js    // 普通attr属性处理
│    │  ├── class.js    // class处理
│    │  ├── events.js   // event处理
│    │  ├── props.js    // props处理
│    │  └── style.js    // style处理
│    ├── patch.js      // node树的渲染,包括节点的加减更新处理,及对应attr的处理
│    └── vnode.js      // 返回最终的节点数据
└── webpack.config.js     // webpack配置

从template到html的过程分析

我们的代码是从new Vue()开始的,Vue的构造函数如下:

constructor (options) {
 // options就是我们对于vue的配置
 this.$options = options
 this._data = options.data
 // 获取元素html,即template
 const el = this._el = document.querySelector(options.el)
 // 编译模板 -> render函数
 const render = compile(getOuterHTML(el))
 this._el.innerHTML = ''
 // 实例代理data数据
 Object.keys(options.data).forEach(key => this._proxy(key))
 // 将method的this指向实例
 if (options.methods) {
  Object.keys(options.methods).forEach(key => {
   this[key] = options.methods[key].bind(this)
  })
 }
 // 数据观察
 this._ob = observe(options.data)
 this._watchers = []
 // watch数据及更新
 this._watcher = new Watcher(this, render, this._update)
 // 渲染函数
 this._update(this._watcher.value)
}

当我们初始化项目的时候,即会执行构造函数,该函数向我们展示了vue初始化的主线:编译template字符串 => 代理data数据/methods的this绑定 => 数据观察 => 建立watch及更新渲染

1. 编译template字符串

const render = compile(getOuterHTML(el))

其中compile的实现如下:

export function compile (html) {
 html = html.trim()
 // 对编译结果缓存
 const hit = cache[html]
 // parse函数在parse-html中定义,其作用是把我们获取的html字符串通过正则匹配转化为ast,输出如下 {tag: 'div', attrs: {}, children: []}
 return hit || (cache[html] = generate(parse(html)))
}

接下来看看generate函数,ast通过genElement的转化生成了构建节点html的函数,在genElement将对if for 等进行判断并转化( 指令的具体处理将在后面做分析,先关注主流程代码),最后都会执行genData函数

// 生成节点主函数
export function generate (ast) {
 const code = genElement(ast)
 // 执行code代码,并将this作为code的global对象。所以我们在template中的变量将指向为实例的属性 {{name}} -> this.name 
 return new Function (`with (this) { return $[code]}`)
}

// 解析单个节点 -> genData
function genElement (el, key) {
 let exp
 // 指令的实现,实际就是在模板编译时实现的
 if (exp = getAttr(el, 'v-for')) {
  return genFor(el, exp)
 } else if (exp = getAttr(el, 'v-if')) {
  return genIf(el, exp)
 } else if (el.tag === 'template') {
  return genChildren(el)
 } else {
  // 分别为 tag 自身属性 子节点数据
  return `__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })`
 }
}

我们可以看看在genData中都做了什么。上面的parse函数将html字符串转化为ast,而在genData中则将节点的attrs数据进一步处理,例如class -> renderClass style class props attr 分类。在这里可以看到 bind 指令的实现,即通过正则匹配 : 和 bind,如果匹配则把相应的 value值转化为 (value)的形式,而不匹配的则通过JSON.stringify()转化为字符串('value')。最后输出attrs的(key-value),在这里得到的对象是字符串形式的,例如(value)等也仅仅是将变量名,而在generate中通过new Function进一步通过(this.value)得到变量值。

function genData (el, key) {
 // 没有属性返回空对象
 if (!el.attrs.length) {
  return '{}'
 }
 // key
 let data = key "${ el.attrsMap['class'] || '' }"),`
 }
 // attrs
 let attrs = `attrs:{`
 let props = `props:{`
 let hasAttrs = false
 let hasProps = false
 for (let i = 0, l = el.attrs.length; i < l; i++) {
  let attr = el.attrs[i]
  let name = attr.name
  // bind属性
  if (bindRE.test(name)) {
   name = name.replace(bindRE, '')
   if (name === 'class') {
    continue
   // style处理
   } else if (name === 'style') {
    data += `style: ${ attr.value },`
   // props属性处理
   } else if (mustUsePropsRE.test(name)) {
    hasProps = true
    props += `"${ name }": (${ attr.value }),` 
   // 其他属性
   } else {
    hasAttrs = true
    attrs += `"${ name }": (${ attr.value }),`
   }
  // on指令,未实现
  } else if (onRE.test(name)) {
   name = name.replace(onRE, '')
  // 普通属性
  } else if (name !== 'class') {
   hasAttrs = true
   attrs += `"${ name }": (${ JSON.stringify(attr.value) }),`
  }
 }
 if (hasAttrs) {
  data += attrs.slice(0, -1) + '},'
 }
 if (hasProps) {
  data += props.slice(0, -1) + '},'
 }
 return data.replace(/,$/, '') + '}'
}

而对于genChildren,我们可以猜到就是对ast中的children进行遍历调用genElement,实际上在这里还包括了对文本节点的处理。

// 遍历子节点 -> genNode
function genChildren (el) {
 if (!el.children.length) {
  return 'undefined'
 }
 // 对children扁平化处理
 return '__flatten__([' + el.children.map(genNode).join(',') + '])'
}

function genNode (node) {
 if (node.tag) {
  return genElement(node)
 } else {
  return genText(node)
 }
}

// 解析{{}}
function genText (text) {
 if (text === ' ') {
  return '" "'
 } else {
  const exp = parseText(text)
  if (exp) {
   return 'String(' + escapeNewlines(exp) + ')'
  } else {
   return escapeNewlines(JSON.stringify(text))
  }
 }
}

genText处理了text及换行,在parseText函数中利用正则解析{{}},输出字符串(value)形式的字符串。

现在我们再看看__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })中__h__函数

// h 函数利用上面得到的节点数据得到 vNode对象 => 虚拟dom
export default function h (tag, b, c) {
 var data = {}, children, text, i
 if (arguments.length === 3) {
  data = b
  if (isArray(c)) { children = c }
  else if (isPrimitive(c)) { text = c }
 } else if (arguments.length === 2) {
  if (isArray(b)) { children = b }
  else if (isPrimitive(b)) { text = b }
  else { data = b }
 }
 if (isArray(children)) {
  // 子节点递归处理
  for (i = 0; i < children.length; ++i) {
   if (isPrimitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i])
  }
 }
 // svg处理
 if (tag === 'svg') {
  addNS(data, children)
 }
 // 子节点为文本节点
 return VNode(tag, data, children, text, undefined)
}

到此为止,我们分析了const render = compile(getOuterHTML(el)),从el的html字符串到render函数都是怎么处理的。

2. 代理data数据/methods的this绑定

// 实例代理data数据
Object.keys(options.data).forEach(key => this._proxy(key))
// 将method的this指向实例
if (options.methods) {
 Object.keys(options.methods).forEach(key => {
  this[key] = options.methods[key].bind(this)
 })
}

实例代理data数据的实现比较简单,就是利用了对象的setter和getter,读取this数据时返回data数据,在设置this数据时同步设置data数据

_proxy (key) {
 if (!isReserved(key)) {
  // need to store ref to self here
  // because these getter/setters might
  // be called by child scopes via
  // prototype inheritance.
  var self = this
  Object.defineProperty(self, key, {
   configurable: true,
   enumerable: true,
   get: function proxyGetter () {
    return self._data[key]
   },
   set: function proxySetter (val) {
    self._data[key] = val
   }
  })
 }
}

3. Obaerve的实现

Observe的实现原理在很多地方都有分析,主要是利用了Object.defineProperty()来建立对数据更改的订阅,在很多地方也称之为数据劫持。下面我们来学习从零开始建立这样一个数据的订阅发布体系。

从简单处开始,我们希望有个函数可以帮我们监听数据的改变,每当数据改变时执行特定回调函数

function observe(data, callback) {
 if (!data || typeof data !== 'object') {
  return
 }

 // 遍历key
 Object.keys(data).forEach((key) => {
  let value = data[key];

  // 递归遍历监听深度变化
  observe(value, callback);

  // 监听单个可以的变化
  Object.defineProperty(data, key, {
   configurable: true,
   enumerable: true,
   get() {
    return value;
   },
   set(val) {
    if (val === value) {
     return
    }

    value = val;

    // 监听新的数据
    observe(value, callback);
    
    // 数据改变的回调
    callback();
   }
  });
 });
}

// 使用observe函数监听data
const data = {};
observe(data, () => {
 console.log('data修改');
})

上面我们实现了一个简单的observe函数,只要我们将编译函数作为callback传入,那么每次数据更改时都会触发回调函数。但是我们现在不能为单独的key设置监听及回调函数,只能监听整个对象的变化执行回调。下面我们对函数进行改进,达到为某个key设置监听及回调。同时建立调度中心,让整个订阅发布模式更加清晰。

// 首先是订阅中心
class Dep {
 constructor() {
  this.subs = []; // 订阅者数组
 }

 addSub(sub) {
  // 添加订阅者
  this.subs.push(sub);
 }

 notify() {
  // 发布通知
  this.subs.forEach((sub) => {
   sub.update();
  });
 }
}

// 当前订阅者,在getter中标记
Dep.target = null;

// 订阅者
class Watch {
 constructor(express, cb) {
  this.cb = cb;
  if (typeof express === 'function') {
   this.expressFn = express;
  } else {
   this.expressFn = () => {
    return new Function(express)();
   }
  }
  
  this.get();
 }

 get() {
  // 利用Dep.target存当前订阅者
  Dep.target = this;
  // 执行表达式 -> 触发getter -> 在getter中添加订阅者
  this.expressFn();
  // 及时置空
  Dep.taget = null;
 }

 update() {
  // 更新
  this.cb();
 }

 addDep(dep) {
  // 添加订阅
  dep.addSub(this);
 }
}

// 观察者 建立观察
class Observe {
 constructor(data) {
  if (!data || typeof data !== 'object') {
   return
  }
 
  // 遍历key
  Object.keys(data).forEach((key) => {
   // key => dep 对应
   const dep = new Dep();
   let value = data[key];
 
   // 递归遍历监听深度变化
   const observe = new Observe(value);
 
   // 监听单个可以的变化
   Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
     if (Dep.target) {
      const watch = Dep.target;
      watch.addDep(dep);
     }
     return value;
    },
    set(val) {
     if (val === value) {
      return
     }
 
     value = val;
 
     // 监听新的数据
     new Observe(value);
     
     // 数据改变的回调
     dep.notify();
    }
   });
  });
 }
}

// 监听数据中某个key的更改
const data = {
 name: 'xiaoming',
 age: 26
};

const observe = new Observe(data);

const watch = new Watch('data.age', () => {
 console.log('age update');
});

data.age = 22

现在我们实现了订阅中心,订阅者,观察者。观察者监测数据的更新,订阅者通过订阅中心订阅数据的更新,当数据更新时,观察者会告诉订阅中心,订阅中心再逐个通知所有的订阅者执行更新函数。到现在为止,我们可以大概猜出vue的实现原理:

  1. 建立观察者观察data数据的更改 (new Observe)
  2. 在编译的时候,当某个代码片段或节点依赖data数据,为该节点建议订阅者,订阅data中某些数据的更新(new Watch)
  3. 当dada数据更新时,通过订阅中心通知数据更新,执行节点更新函数,新建或更新节点(dep.notify())

上面是我们对vue实现原理订阅发布模式的基本实现,及编译到更新过程的猜想,现在我们接着分析vue源码的实现:
在实例的初始化中

// ...
// 为数据建立数据观察
this._ob = observe(options.data)
this._watchers = []
// 添加订阅者 执行render 会触发 getter 订阅者订阅更新,数据改变触发 setter 订阅中心通知订阅者执行 update
this._watcher = new Watcher(this, render, this._update)
// ...

vue中数据观察的实现

// observe函数
export function observe (value, vm) {
 if (!value || typeof value !== 'object') {
  return
 }
 if (
  hasOwn(value, '__ob__') &&
  value.__ob__ instanceof Observer
 ) {
  ob = value.__ob__
 } else if (
  shouldConvert &&
  (isArray(value) || isPlainObject(value)) &&
  Object.isExtensible(value) &&
  !value._isVue
 ) {
  // 为数据建立观察者
  ob = new Observer(value)
 }
 // 存储关联的vm
 if (ob && vm) {
  ob.addVm(vm)
 }
 return ob
}

// => Observe 函数
export function Observer (value) {
 this.value = value
 // 在数组变异方法中有用
 this.dep = new Dep()
 // observer实例存在__ob__中
 def(value, '__ob__', this)
 if (isArray(value)) {
  var augment = hasProto
   "htmlcode">
let uid = 0

export default function Dep () {
 this.id = uid++
 // 订阅调度中心的watch数组
 this.subs = []
}

// 当前watch实例
Dep.target = null

// 添加订阅者
Dep.prototype.addSub = function (sub) {
 this.subs.push(sub)
}

// 移除订阅者
Dep.prototype.removeSub = function (sub) {
 this.subs.$remove(sub)
}

// 订阅
Dep.prototype.depend = function () {
 // Dep.target.addDep(this) => this.addSub(Dep.target) => this.subs.push(Dep.target)
 Dep.target.addDep(this)
}

// 通知更新
Dep.prototype.notify = function () {
 // stablize the subscriber list first
 var subs = this.subs.slice()
 for (var i = 0, l = subs.length; i < l; i++) {
  // subs[i].update() => watch.update()
  subs[i].update()
 }
}

订阅者的实现

export default function Watcher (vm, expOrFn, cb, options) {
 // mix in options
 if (options) {
  extend(this, options)
 }
 var isFn = typeof expOrFn === 'function'
 this.vm = vm
 // vm 的 _watchers 包含了所有 watch
 vm._watchers.push(this)
 this.expression = expOrFn
 this.cb = cb
 this.id = ++uid // uid for batching
 this.active = true
 this.dirty = this.lazy // for lazy watchers
 // deps 一个 watch 实例可以对应多个 dep
 this.deps = []
 this.newDeps = []
 this.depIds = Object.create(null)
 this.newDepIds = null
 this.prevError = null // for async error stacks
 // parse expression for getter/setter
 if (isFn) {
  this.getter = expOrFn
  this.setter = undefined
 } else {
  warn('vue-lite only supports watching functions.')
 }
 this.value = this.lazy
  "htmlcode">
// batch.js
var queueIndex
var queue = []
var userQueue = []
var has = {}
var circular = {}
var waiting = false
var internalQueueDepleted = false

// 重置执行池
function resetBatcherState () {
 queue = []
 userQueue = []
 // has 避免重复
 has = {}
 circular = {}
 waiting = internalQueueDepleted = false
}

// 执行执行队列
function flushBatcherQueue () {
 runBatcherQueue(queue)
 internalQueueDepleted = true
 runBatcherQueue(userQueue)
 resetBatcherState()
}

// 批量执行
function runBatcherQueue (queue) {
 for (queueIndex = 0; queueIndex < queue.length; queueIndex++) {
  var watcher = queue[queueIndex]
  var id = watcher.id
  // 执行后置为null
  has[id] = null
  watcher.run()
  // in dev build, check and stop circular updates.
  if (process.env.NODE_ENV !== 'production' && has[id] != null) {
   circular[id] = (circular[id] || 0) + 1
   if (circular[id] > config._maxUpdateCount) {
    warn(
     'You may have an infinite update loop for watcher ' +
     'with expression "' + watcher.expression + '"',
     watcher.vm
    )
    break
   }
  }
 }
}

// 添加到执行池
export function pushWatcher (watcher) {
 var id = watcher.id
 if (has[id] == null) {
  if (internalQueueDepleted && !watcher.user) {
   // an internal watcher triggered by a user watcher...
   // let's run it immediately after current user watcher is done.
   userQueue.splice(queueIndex + 1, 0, watcher)
  } else {
   // push watcher into appropriate queue
   var q = watcher.user
    "htmlcode">
// _update => createPatchFunction => patch => patchVnode => (dom api)

// vtree是通过compile函数编译的render函数执行的结果,返回了当前表示当前dom结构的对象(虚拟节点树)
_update (vtree) {
 if (!this._tree) {
  // 第一次渲染
  patch(this._el, vtree)
 } else {
  patch(this._tree, vtree)
 }
 this._tree = vtree
}

// 在处理节点时,需要针对class,props,style,attrs,events做不同处理
// 在这里注入针对不同属性的处理函数
const patch = createPatchFunction([
 _class, // makes it easy to toggle classes
 props,
 style,
 attrs,
 events
])

// => createPatchFunction返回patch函数,patch函数通过对比虚拟节点的差异,对节点进行增删更新
// 最后调用原生的dom api更新html
return function patch (oldVnode, vnode) {
 var i, elm, parent
 var insertedVnodeQueue = []
 // pre hook
 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

 if (isUndef(oldVnode.sel)) {
  oldVnode = emptyNodeAt(oldVnode)
 }

 if (sameVnode(oldVnode, vnode)) {
  // someNode can patch
  patchVnode(oldVnode, vnode, insertedVnodeQueue)
 } else {
  // 正常的不复用 remove insert
  elm = oldVnode.elm
  parent = api.parentNode(elm)

  createElm(vnode, insertedVnodeQueue)

  if (parent !== null) {
   api.insertBefore(parent, vnode.elm, api.nextSibling(elm))
   removeVnodes(parent, [oldVnode], 0, 0)
  }
 }

 for (i = 0; i < insertedVnodeQueue.length; ++i) {
  insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i])
 }

 // hook post
 for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
 return vnode
}

结尾

以上分析了vue从template 到节点渲染的大致实现,当然也有某些地方没有全面分析的地方,其中template解析为ast主要通过正则匹配实现,及节点渲染及更新的patch过程主要通过节点操作对比来实现。但是我们对编译template字符串 => 代理data数据/methods的this绑定 => 数据观察 => 建立watch及更新渲染的大致流程有了个比较完整的认知。

上一篇:vue项目或网页上实现文字转换成语音播放功能
下一篇:从零开始在vue-cli4配置自适应vw布局的实现