Vue源码解析(1)-数据劫持与模板编译

我心飞翔 分类:javascript

Vue源码解析(1)

以下代码我已做了详细的注释以及思维导图图片版,对数据劫持以及模板编译两大块源码的核心部分进行手写,如需思维导图的可联系博主,不喜勿喷(前端小白)。

Vue源码解析.png

1.png

2.png

3.png

一.src入口代码

//index.js
import {initMixin} from './init'
import {lifecycleMixin} from './lifecycle'
import {renderMixin} from '../vdom'

function Vue(options){
  this._init(options)
}
initMixin(Vue)  // 只有执行了这个方法才能将_init方法挂载到Vue的原型上
lifecycleMixin(Vue)  // 你虽然在原型上挂载了方法 但是你得执行
renderMixin(Vue)  // 为了使_render方法生效

let vm = new Vue({
  el:"#app",
  data(){
    return {
      name:'zcl',
      age:23,
      teacher:[[1,2,3],{name:'zcl',age:22}],
      info:{a:123}
      
    }
  }
})
// 测试用例
console.log(vm.teacher[0].push(4))
console.log(vm.teacher[1].name = 'zzz')
console.log(vm.info.a=2)
console.log(vm)
 
// init.js
import {initState} from '../vue/state'
import {compileToRenderFunction} from '../compiler'
import {mountComponent} from './lifecycle'
function initMixin(Vue){
  Vue.prototype._init = function(options){ // 在Vue这个构造函数的原型上添加一个_init方法
    const vm = this   // this肯定是构造函数Vue的一个实例对象 例如vm
    vm.$options = options // 将传进来的options参数 挂载到vm.$options上
    initState(vm)  // 把整个实例都传递进去 这样想获取实例上的任何参数都比较方便

    if(vm.$options.el){
      vm.$mount(vm.$options.el)
    }
  }
  Vue.prototype.$mount = function(el){
    const vm = this,
          options = this.$options;
    el = document.querySelector(el)
    vm.$el = el
    if(!options.render){
      // 先找是否有render函数
      let template = options.template
      if(!template && el){
        // 是否有template 如果没有只能获取html
        template = el.outerHTML
      }
      const render = compileToRenderFunction(template)
      options.render = render
    }
    mountComponent(vm)  // 上树
  }
}
export {initMixin}
 
//lifecycle.js
import {patch} from '../vdom/patch'
function mountComponent(vm){
  vm._update(vm._render())
}
function lifecycleMixin(Vue){
  Vue.prototype._update = function(vnode){
    const vm = this;
    patch(vm.$el,vnode)  // 这里的vm.$el是原本html上的根节点
  }
}
export {lifecycleMixin,mountComponent}
 

二.数据劫持

//state.js
import proxyData from "./proxy"
import observe from './observe'
function initState(vm){
    var options = vm.$options
    if(options.data){
      initData(vm)
    }
}
function initData(vm){
  var data = vm.$options.data  // 获取options选项中的data
  data = vm._data = typeof data === 'function' ? data.call(vm) : data  || {} // 将data挂载到vm._data上
  // 这里因为Vue实例中的data会呈现两种形式 一种是函数和一种是对象 所以需要进行处理
  for(var key in data){
    proxyData(vm,'_data',key)  // 进行数据代理 使数据的获取方式变成简单的vm.属性的方式 
  }
  observe(vm._data)  //  进行数据劫持
}
export {initState}
 
//proxy.js
function proxyData(vm,target,key){
  Object.defineProperty(vm,key,{
    // 因为我们在使用defineProperty之后
    // 访问数据的时候 通过vm.age访问 是通过get方法返回数据的 
    get(){
      return vm[target][key]
    },
    set(newValue){
      vm[target][key] = newValue
    }
  })

}
export default proxyData
 
//observe.js
import Observer from './observer'
function observe(data){
  // 这里其实是判断是否是深层次对象 如果是就递归到底
  if(typeof data !== 'object' || data === null) return
  return new Observer(data) // 这里才是真正的数据劫持操作
}
export default observe
 
//observer.js
import defineReactiveData from './defineReactiveData'
import observeArr from './observeArr'
import { arrMethods } from './array'
function Observer(data){
  if(Array.isArray(data)){
    // 如果data是数组形式
    data.__proto__ = arrMethods  // 只有真正的[]形式才添加扩展的数组方法 让其在使用这些方法的时候 进行重新劫持
    observeArr(data)  // 虽然是数组形式但是里面还有可能还有数组或{} 例如[[],[],{},{}]
  }else{
    // 如果data是{}形式
    this.walk(data)
  }
}
Observer.prototype.walk = function(data){
  var keys = Object.keys(data)
  for(var i=0;i<keys.length;i++){
    var key = keys[i],
        value = data[key]
    defineReactiveData(data,key,value) // 对{}形式的数据进行劫持
  }
}
export default Observer
 
//defineReactiveData.js
import defineReactiveData from './defineReactiveData'
import observeArr from './observeArr'
import { arrMethods } from './array'
function Observer(data){
  if(Array.isArray(data)){
    // 如果data是数组形式
    data.__proto__ = arrMethods  // 只有真正的[]形式才添加扩展的数组方法 让其在使用这些方法的时候 进行重新劫持
    observeArr(data)  // 虽然是数组形式但是里面还有可能还有数组或{} 例如[[],[],{},{}]
  }else{
    // 如果data是{}形式
    this.walk(data)
  }
}
Observer.prototype.walk = function(data){
  var keys = Object.keys(data)
  for(var i=0;i<keys.length;i++){
    var key = keys[i],
        value = data[key]
    defineReactiveData(data,key,value) // 对{}形式的数据进行劫持
  }
}
export default Observer
 
//config.js
var ARR_METHODS = [
  // 以下方法会对数组的数据进行改变 我们要对新增的元素进行重新劫持
  'push','pop','shift','unshift','splice','reverse','sort'
]
export {ARR_METHODS}
 
// observeArr.js
import observe from "./observe"
function observeArr(data){
  for(var i=0;i<data.length;i++){
    observe(data[i])   // 我只负责遍历出来 然后让observe去判断是否需要继续去深层递归
  }
}
export default observeArr
 
//array.js
import {ARR_METHODS} from './config'
import observeArr from './observeArr'

var originArrMethods = Array.prototype,
    arrMethods = Object.create(originArrMethods)

ARR_METHODS.map(function(m){
  arrMethods[m] = function(){ // 扩展数组原型上的方法
    var args = originArrMethods.slice.call(arguments), // 拷贝传递进来的参数也就是数据
        rt = originArrMethods[m].apply(this,args)  // 调用原型本身的方法
    
    var newArr
    switch(m){
      case 'push':
      case 'unshift':
        newArr = args;
        break;
      case 'splice':
        newArr = args.slice(2)
        break;
      default:
        break;
    }
    newArr && observeArr(newArr)
    return rt  // 数组调用方法是时候返回的东西 返不返回都可以
  }
})
export {arrMethods}
 

三.template->Ast->render

// index.js
import {parseHtmlToAst} from './astParser'
import {generate} from './generate'
function compileToRenderFunction(template){
  const ast = parseHtmlToAst(template)  // template -> ast
  // ast -> render
  const code = generate(ast)  
  const render = new Function(`with(this)return{${code}}`)
  return render
}
export {compileToRenderFunction}
 
//astParser.js
// 匹配属性
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 匹配开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`)
// 匹配开始标签的结束标签
const startTagClose = /^\s*(\/?)>/
// 匹配真正的结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
function parseHtmlToAst(html){
let root,
currentParent,
text,
stack = [];
while(html){
let textEnd = html.indexOf('<')  // 匹配<标签判断下面一段是标签还是文本
if(text === 0){ // 如果是标签
const startTagMatch = parseStartTag() // 是否能匹配到开始标签
if(startTagMatch){
//如果匹配到了开始标签
start(startTagMatch.tagName,startTagMatch.attrs) // 处理这段已匹配的标签
continue;
}
const endTagMatch = html.match(endTag) // 是否能匹配到真正的结束标签
if(endTagMatch){
advance(endTagMatch[0].length)
end()  // 保存父子级关系
continue;
}
}
// 处理文本节点
if(textEnd>0){
text = html.substring(0,textEnd)
}
if(text){
advance(text.length)
chars(text)
}
}
function parseStartTag(){
const start = html.match(startTagOpen) // 匹配开始标签
let end,
attr;
if(start){
const match = {
tagName:start[1],
attrs:[]
}
advance(start[0].length)  // 删除这段已匹配的标签
if(!(end=html.match(startTagClose)) && (attr=html.match(attribute))){
// 如果没有匹配到开始标签的结束标签但是匹配到了属性标签 处理属性
match.attrs.push({
name:attr[1],
value:attr[3] || attr[4] || attr[5]
})
advance(attr[0].length)
}
if(end){
// 如果匹配到了开始标签的结束标签
advance(end[0].length)
return match
}
}
}
function advance(length){
html = html.substring(length)
}
function start(tagName,attrs){
const element = createASTElement(tagName,attrs) // 组装AST树
if(!root){
root = element  // 第一个节点作为根节点
}
currentParent = element // 保存当前节点为父亲节点
stack.push(element)  // 保存当前节点
}
function end(){
const element = stack.pop()
currentParent = stack[stack.length-1]
if(currentParent){
element.parent = currentParent
currentParent.children.push(element)
}
}
function chars(text){
text = text.trim()
if(text.length>0){
currentParent.children.push({
type:3,
text
})
}
}
function createASTElement(tagName,attrs){
return{
tag:tagName,
attrs,
children:[],
parent,
type:1
}
}
return root;
}
export {parseHtmlToAst}
//generate.js
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
function generate(el){
let children = getChildren(el)
return `_c('${el.tag}',${el.attrs.length>0?`${formatProps(el.attrs)}`:'undefined'}${children?`,${children}`:''})`
}
function getChildren(el){
// 获取孩子并将处理后的结果进行拼接
const children = el.children
if(children){
return children.map(child=>generateChild(child)).join(',')
}
}
function generateChild(node){
// 处理孩子
if(node.type===1){
// 如果是元素节点 则判断它是否还有孩子
return generate(node)
}else if(node.type === 3){
// 文本节点
let text = node.text
if(!defaultTagRE.test(text)){
// 纯文本
return `_v(${JSON.stringify(text)})`
}
// 处理带有差值表达式的问呗
let match,
index,
lastIndex = defaultTagRE.lastIndex = 0
let textArr = []
while(match = defaultTagRE.exec(text)){
index = match.index // 所以为插值表达式第一个{的位置
if(index>lastIndex){
// 纯文本部分
textArr.push(JSON.stringify(text.slice(lastIndex,index)))
}
// 插值表达式部分
textArr.push(`_s(${match[1].trim()})`)
lastIndex = index + match[0].length
}
if(lastIndex>text.length){
// 仍有纯文本
textArr.push(JSON.stringify(text.slice(lastIndex)))
}
return `_v(${textArr.join('+')})`
}
}
function formatProps(attrs){
// 处理属性
let attrStr = ""
for(var i=0;i<attrs.length;i++){
let attr = attrs[i]
if(attr.name === "style"){
let styleAttrs = {}
attr.value.split(';').map((styleAttr)=>{
let [key,value] = styleAttr.split(':')
styleAttrs[key] = value
})
attr.value  = styleAttrs
}
attrStr += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${attrStr.slice(0,-1)}}`  // 去除多余的逗号
}
export {generate}

四.render->vnode->上树

//index.js
import {createElement,createTextVnode} from './vnode'
function renderMixin(Vue){
Vue.prototype._c = function(){
// 创建元素虚拟节点
return createElement(...arguments)
}
Vue.prototype._v = function(text){
// 创建文本虚拟节点
return createTextVnode(text)
}
Vue.prototype._s = function(value){
// 处理_s(name)
if(value === null) return 
return typeof value === 'object' ? JSON.stringify(value) : value
}
Vue.prototype._render = function(){
render = this.$options.render
vnode = render() // 生成虚拟节点
return vnode 
}
}
export {renderMixin}
//vnode.js
function vnode(tag,props,children,text){
// 组装虚拟节点
return {
tag,props,children,text
}
}
function createElement(tag,attrs={},...children){
// 返回元素节点  元素节点是没有文本的
return vnode(tag,attrs,children)
}
function createTextVnode(text){
return vnode(undefined,undefined,undefined,text)
}
export {createElement,createTextVnode}
//patch.js
function patch(oldNode,vNode){
let el = createElement(vNode),
parentElement = oldNode.parentNode; // body
parentElement.insertBefore(el,oldNode.nextSibling)
parentElement.removeChild(oldNode)
}
function createElement(vnode){
const {tag,props,children,text} = vnode;
if(typeof tag === 'string'){
// 处理标签
vnode.el = document.createElement(tag)
updateProps(vnode)
children.map((child)=>{
// 这已经是在真实DOM里面添加节点
vnode.el.appendChild(createElement(child))
})
}else{
vnode.el = document.createTextNode(text)
}
return vnode.el
}
function updateProps(vnode){
const el = vnode.el,
newProps = vnode.props || {}
for(let key in newProps){
if(key === 'style'){ 
// 如果key为style 则代表是深层次对象 需要在进行遍历
for(let sKey in newProps.style){
el.style[sKey] = newProps.style[sKey]
}
}else if(key === 'class'){
el.className = newProps[key]
}else{
el.setAttributes(key,newProps[key])
}
}
}
export {patch}

回复

我来回复
  • 暂无回复内容